update dependencies (#6267)

Signed-off-by: hongming <coder.scala@gmail.com>
(cherry picked from commit cfebd96a1f)
This commit is contained in:
hongming
2025-03-11 14:19:32 +08:00
parent 742c1e52db
commit 39eab5ee5c
4246 changed files with 341171 additions and 131193 deletions

View File

@@ -23,5 +23,8 @@
# Tools binaries.
hack/tools/bin
# Release artifacts
tools/setup-envtest/out
junit-report.xml
/artifacts
/artifacts

View File

@@ -41,6 +41,11 @@ linters:
- whitespace
linters-settings:
govet:
enable-all: true
disable:
- fieldalignment
- shadow
importas:
no-unaliased: true
alias:
@@ -58,10 +63,6 @@ linters-settings:
# Controller Runtime
- pkg: sigs.k8s.io/controller-runtime
alias: ctrl
staticcheck:
go: "1.21"
stylecheck:
go: "1.21"
revive:
rules:
# The following rules are recommended https://github.com/mgechev/revive#recommended-configuration
@@ -105,6 +106,9 @@ issues:
- Subprocess launch(ed with variable|ing should be audited)
- (G204|G104|G307)
- "ST1000: at least one file in a package should have a package comment"
exclude-files:
- "zz_generated.*\\.go$"
- ".*conversion.*\\.go$"
exclude-rules:
- linters:
- gosec
@@ -163,8 +167,6 @@ issues:
path: _test\.go
run:
go: "1.22"
timeout: 10m
skip-files:
- "zz_generated.*\\.go$"
- ".*conversion.*\\.go$"
allow-parallel-runners: true

14
vendor/sigs.k8s.io/controller-runtime/.gomodcheck.yaml generated vendored Normal file
View File

@@ -0,0 +1,14 @@
upstreamRefs:
- k8s.io/api
- k8s.io/apiextensions-apiserver
- k8s.io/apimachinery
- k8s.io/apiserver
- k8s.io/client-go
- k8s.io/component-base
# k8s.io/klog/v2 -> conflicts with k/k deps
# k8s.io/utils -> conflicts with k/k deps
excludedModules:
# --- test dependencies:
- github.com/onsi/ginkgo/v2
- github.com/onsi/gomega

View File

@@ -4,13 +4,13 @@
**A**: Each controller should only reconcile one object type. Other
affected objects should be mapped to a single type of root object, using
the `EnqueueRequestForOwner` or `EnqueueRequestsFromMapFunc` event
the `handler.EnqueueRequestForOwner` or `handler.EnqueueRequestsFromMapFunc` event
handlers, and potentially indices. Then, your Reconcile method should
attempt to reconcile *all* state for that given root objects.
### Q: How do I have different logic in my reconciler for different types of events (e.g. create, update, delete)?
**A**: You should not. Reconcile functions should be idempotent, and
**A**: You should not. Reconcile functions should be idempotent, and
should always reconcile state by reading all the state it needs, then
writing updates. This allows your reconciler to correctly respond to
generic events, adjust to skipped or coalesced events, and easily deal

View File

@@ -24,6 +24,11 @@
SHELL:=/usr/bin/env bash
.DEFAULT_GOAL:=help
#
# Go.
#
GO_VERSION ?= 1.22.5
# Use GOPROXY environment variable if set
GOPROXY := $(shell go env GOPROXY)
ifeq ($(GOPROXY),)
@@ -34,14 +39,22 @@ export GOPROXY
# Active module mode, as we use go modules to manage dependencies
export GO111MODULE=on
# Hosts running SELinux need :z added to volume mounts
SELINUX_ENABLED := $(shell cat /sys/fs/selinux/enforce 2> /dev/null || echo 0)
ifeq ($(SELINUX_ENABLED),1)
DOCKER_VOL_OPTS?=:z
endif
# Tools.
TOOLS_DIR := hack/tools
TOOLS_BIN_DIR := $(TOOLS_DIR)/bin
TOOLS_BIN_DIR := $(abspath $(TOOLS_DIR)/bin)
GOLANGCI_LINT := $(abspath $(TOOLS_BIN_DIR)/golangci-lint)
GO_APIDIFF := $(TOOLS_BIN_DIR)/go-apidiff
CONTROLLER_GEN := $(TOOLS_BIN_DIR)/controller-gen
ENVTEST_DIR := $(abspath tools/setup-envtest)
SCRATCH_ENV_DIR := $(abspath examples/scratch-env)
GO_INSTALL := ./hack/go-install.sh
# The help will print out all targets with their descriptions organized bellow their categories. The categories are represented by `##@` and the target descriptions by `##`.
# The awk commands is responsible to read the entire set of makefiles included in this invocation, looking for lines of the file as xyz: ## something, and then pretty-format the target and help. Then, if there's a line with ##@ something, that gets pretty-printed as a category.
@@ -67,16 +80,36 @@ test-tools: ## tests the tools codebase (setup-envtest)
## Binaries
## --------------------------------------
$(GO_APIDIFF): $(TOOLS_DIR)/go.mod # Build go-apidiff from tools folder.
cd $(TOOLS_DIR) && go build -tags=tools -o bin/go-apidiff github.com/joelanford/go-apidiff
GO_APIDIFF_VER := v0.8.2
GO_APIDIFF_BIN := go-apidiff
GO_APIDIFF := $(abspath $(TOOLS_BIN_DIR)/$(GO_APIDIFF_BIN)-$(GO_APIDIFF_VER))
GO_APIDIFF_PKG := github.com/joelanford/go-apidiff
$(CONTROLLER_GEN): $(TOOLS_DIR)/go.mod # Build controller-gen from tools folder.
cd $(TOOLS_DIR) && go build -tags=tools -o bin/controller-gen sigs.k8s.io/controller-tools/cmd/controller-gen
$(GO_APIDIFF): # Build go-apidiff from tools folder.
GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(GO_APIDIFF_PKG) $(GO_APIDIFF_BIN) $(GO_APIDIFF_VER)
$(GOLANGCI_LINT): .github/workflows/golangci-lint.yml # Download golanci-lint using hack script into tools folder.
hack/ensure-golangci-lint.sh \
-b $(TOOLS_BIN_DIR) \
$(shell cat .github/workflows/golangci-lint.yml | grep "version: v" | sed 's/.*version: //')
CONTROLLER_GEN_VER := v0.14.0
CONTROLLER_GEN_BIN := controller-gen
CONTROLLER_GEN := $(abspath $(TOOLS_BIN_DIR)/$(CONTROLLER_GEN_BIN)-$(CONTROLLER_GEN_VER))
CONTROLLER_GEN_PKG := sigs.k8s.io/controller-tools/cmd/controller-gen
$(CONTROLLER_GEN): # Build controller-gen from tools folder.
GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(CONTROLLER_GEN_PKG) $(CONTROLLER_GEN_BIN) $(CONTROLLER_GEN_VER)
GOLANGCI_LINT_BIN := golangci-lint
GOLANGCI_LINT_VER := $(shell cat .github/workflows/golangci-lint.yml | grep [[:space:]]version: | sed 's/.*version: //')
GOLANGCI_LINT := $(abspath $(TOOLS_BIN_DIR)/$(GOLANGCI_LINT_BIN)-$(GOLANGCI_LINT_VER))
GOLANGCI_LINT_PKG := github.com/golangci/golangci-lint/cmd/golangci-lint
$(GOLANGCI_LINT): # Build golangci-lint from tools folder.
GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(GOLANGCI_LINT_PKG) $(GOLANGCI_LINT_BIN) $(GOLANGCI_LINT_VER)
GO_MOD_CHECK_DIR := $(abspath ./hack/tools/cmd/gomodcheck)
GO_MOD_CHECK := $(abspath $(TOOLS_BIN_DIR)/gomodcheck)
GO_MOD_CHECK_IGNORE := $(abspath .gomodcheck.yaml)
.PHONY: $(GO_MOD_CHECK)
$(GO_MOD_CHECK): # Build gomodcheck
go build -C $(GO_MOD_CHECK_DIR) -o $(GO_MOD_CHECK)
## --------------------------------------
## Linting
@@ -102,9 +135,47 @@ modules: ## Runs go mod to ensure modules are up to date.
cd $(ENVTEST_DIR); go mod tidy
cd $(SCRATCH_ENV_DIR); go mod tidy
.PHONY: generate
generate: $(CONTROLLER_GEN) ## Runs controller-gen for internal types for config file
$(CONTROLLER_GEN) object paths="./pkg/config/v1alpha1/...;./examples/configfile/custom/v1alpha1/..."
## --------------------------------------
## Release
## --------------------------------------
RELEASE_DIR := tools/setup-envtest/out
.PHONY: $(RELEASE_DIR)
$(RELEASE_DIR):
mkdir -p $(RELEASE_DIR)/
.PHONY: release
release: clean-release $(RELEASE_DIR) ## Build release.
@if ! [ -z "$$(git status --porcelain)" ]; then echo "Your local git repository contains uncommitted changes, use git clean before proceeding."; exit 1; fi
# Build binaries first.
$(MAKE) release-binaries
.PHONY: release-binaries
release-binaries: ## Build release binaries.
RELEASE_BINARY=setup-envtest-linux-amd64 GOOS=linux GOARCH=amd64 $(MAKE) release-binary
RELEASE_BINARY=setup-envtest-linux-arm64 GOOS=linux GOARCH=arm64 $(MAKE) release-binary
RELEASE_BINARY=setup-envtest-linux-ppc64le GOOS=linux GOARCH=ppc64le $(MAKE) release-binary
RELEASE_BINARY=setup-envtest-linux-s390x GOOS=linux GOARCH=s390x $(MAKE) release-binary
RELEASE_BINARY=setup-envtest-darwin-amd64 GOOS=darwin GOARCH=amd64 $(MAKE) release-binary
RELEASE_BINARY=setup-envtest-darwin-arm64 GOOS=darwin GOARCH=arm64 $(MAKE) release-binary
RELEASE_BINARY=setup-envtest-windows-amd64.exe GOOS=windows GOARCH=amd64 $(MAKE) release-binary
.PHONY: release-binary
release-binary: $(RELEASE_DIR)
docker run \
--rm \
-e CGO_ENABLED=0 \
-e GOOS=$(GOOS) \
-e GOARCH=$(GOARCH) \
-e GOCACHE=/tmp/ \
--user $$(id -u):$$(id -g) \
-v "$$(pwd):/workspace$(DOCKER_VOL_OPTS)" \
-w /workspace/tools/setup-envtest \
golang:$(GO_VERSION) \
go build -a -trimpath -ldflags "-extldflags '-static'" \
-o ./out/$(RELEASE_BINARY) ./
## --------------------------------------
## Cleanup / Verification
@@ -119,16 +190,29 @@ clean: ## Cleanup.
clean-bin: ## Remove all generated binaries.
rm -rf hack/tools/bin
.PHONY: clean-release
clean-release: ## Remove the release folder
rm -rf $(RELEASE_DIR)
.PHONY: verify-modules
verify-modules: modules ## Verify go modules are up to date
verify-modules: modules $(GO_MOD_CHECK) ## Verify go modules are up to date
@if !(git diff --quiet HEAD -- go.sum go.mod $(TOOLS_DIR)/go.mod $(TOOLS_DIR)/go.sum $(ENVTEST_DIR)/go.mod $(ENVTEST_DIR)/go.sum $(SCRATCH_ENV_DIR)/go.sum); then \
git diff; \
echo "go module files are out of date, please run 'make modules'"; exit 1; \
fi
$(GO_MOD_CHECK) $(GO_MOD_CHECK_IGNORE)
.PHONY: verify-generate
verify-generate: generate ## Verify generated files are up to date
@if !(git diff --quiet HEAD); then \
git diff; \
echo "generated files are out of date, run make generate"; exit 1; \
fi
APIDIFF_OLD_COMMIT ?= $(shell git rev-parse origin/main)
.PHONY: apidiff
verify-apidiff: $(GO_APIDIFF) ## Check for API differences
$(GO_APIDIFF) $(APIDIFF_OLD_COMMIT) --print-compatible
## --------------------------------------
## Helpers
## --------------------------------------
##@ helpers:
go-version: ## Print the go version we use to compile our binaries and images
@echo $(GO_VERSION)

View File

@@ -6,5 +6,6 @@ approvers:
- controller-runtime-approvers
reviewers:
- controller-runtime-admins
- controller-runtime-reviewers
- controller-runtime-maintainers
- controller-runtime-approvers
- controller-runtime-reviewers

View File

@@ -25,12 +25,6 @@ aliases:
- varshaprasad96
- inteon
# folks to can approve things in the directly-ported
# testing_frameworks portions of the codebase
testing-integration-approvers:
- apelisse
- hoegaarden
# folks who may have context on ancient history,
# but are no longer directly involved
controller-runtime-emeritus-maintainers:

View File

@@ -40,6 +40,25 @@ Contributors:
* [Documentation Changes](/.github/PULL_REQUEST_TEMPLATE/docs.md)
* [Test/Build/Other Changes](/.github/PULL_REQUEST_TEMPLATE/other.md)
## Compatibility
Every minor version of controller-runtime has been tested with a specific minor version of client-go. A controller-runtime minor version *may* be compatible with
other client-go minor versions, but this is by chance and neither supported nor tested. In general, we create one minor version of controller-runtime
for each minor version of client-go and other k8s.io/* dependencies.
The minimum Go version of controller-runtime is the highest minimum Go version of our Go dependencies. Usually, this will
be identical to the minimum Go version of the corresponding k8s.io/* dependencies.
Compatible k8s.io/*, client-go and minimum Go versions can be looked up in our [go.mod](go.mod) file.
| | k8s.io/*, client-go | minimum Go version |
|----------|:-------------------:|:------------------:|
| CR v0.19 | v0.31 | 1.22 |
| CR v0.18 | v0.30 | 1.22 |
| CR v0.17 | v0.29 | 1.21 |
| CR v0.16 | v0.28 | 1.20 |
| CR v0.15 | v0.27 | 1.20 |
## FAQ
See [FAQ.md](FAQ.md)
@@ -57,6 +76,7 @@ You can reach the maintainers of this project at:
- Google Group: [kubebuilder@googlegroups.com](https://groups.google.com/forum/#!forum/kubebuilder)
## Contributing
Contributions are greatly appreciated. The maintainers actively manage the issues list, and try to highlight issues suitable for newcomers.
The project follows the typical GitHub pull request model. See [CONTRIBUTING.md](CONTRIBUTING.md) for more details.
Before starting any work, please either comment on an existing issue, or file a new one.

View File

@@ -21,7 +21,6 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client/config"
cfg "sigs.k8s.io/controller-runtime/pkg/config"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/manager"
@@ -96,13 +95,6 @@ var (
// * $HOME/.kube/config if exists.
GetConfig = config.GetConfig
// ConfigFile returns the cfg.File function for deferred config file loading,
// this is passed into Options{}.From() to populate the Options fields for
// the manager.
//
// Deprecated: This is deprecated in favor of using Options directly.
ConfigFile = cfg.File
// NewControllerManagedBy returns a new controller builder that will be started by the provided Manager.
NewControllerManagedBy = builder.ControllerManagedBy
@@ -130,8 +122,8 @@ var (
// there is another OwnerReference with Controller flag set.
SetControllerReference = controllerutil.SetControllerReference
// SetupSignalHandler registered for SIGTERM and SIGINT. A stop channel is returned
// which is closed on one of these signals. If a second signal is caught, the program
// SetupSignalHandler registers for SIGTERM and SIGINT. A context is returned
// which is canceled on one of these signals. If a second signal is caught, the program
// is terminated with exit code 1.
SetupSignalHandler = signals.SetupSignalHandler

View File

@@ -19,6 +19,7 @@ package builder
import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/go-logr/logr"
@@ -30,17 +31,12 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
internalsource "sigs.k8s.io/controller-runtime/pkg/internal/source"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)
// Supporting mocking out functions for testing.
var newController = controller.New
var getGvk = apiutil.GVKForObject
// project represents other forms that we can use to
// send/receive a given resource (metadata-only, unstructured, etc).
type objectProjection int
@@ -53,20 +49,32 @@ const (
)
// Builder builds a Controller.
type Builder struct {
type Builder = TypedBuilder[reconcile.Request]
// TypedBuilder builds a Controller. The request is the request type
// that is passed to the workqueue and then to the Reconciler.
// The workqueue de-duplicates identical requests.
type TypedBuilder[request comparable] struct {
forInput ForInput
ownsInput []OwnsInput
watchesInput []WatchesInput
rawSources []source.TypedSource[request]
watchesInput []WatchesInput[request]
mgr manager.Manager
globalPredicates []predicate.Predicate
ctrl controller.Controller
ctrlOptions controller.Options
ctrl controller.TypedController[request]
ctrlOptions controller.TypedOptions[request]
name string
newController func(name string, mgr manager.Manager, options controller.TypedOptions[request]) (controller.TypedController[request], error)
}
// ControllerManagedBy returns a new controller builder that will be started by the provided Manager.
func ControllerManagedBy(m manager.Manager) *Builder {
return &Builder{mgr: m}
return TypedControllerManagedBy[reconcile.Request](m)
}
// TypedControllerManagedBy returns a new typed controller builder that will be started by the provided Manager.
func TypedControllerManagedBy[request comparable](m manager.Manager) *TypedBuilder[request] {
return &TypedBuilder[request]{mgr: m}
}
// ForInput represents the information set by the For method.
@@ -79,9 +87,10 @@ type ForInput struct {
// For defines the type of Object being *reconciled*, and configures the ControllerManagedBy to respond to create / delete /
// update events by *reconciling the object*.
//
// This is the equivalent of calling
// Watches(&source.Kind{Type: apiType}, &handler.EnqueueRequestForObject{}).
func (blder *Builder) For(object client.Object, opts ...ForOption) *Builder {
// Watches(source.Kind(cache, &Type{}, &handler.EnqueueRequestForObject{})).
func (blder *TypedBuilder[request]) For(object client.Object, opts ...ForOption) *TypedBuilder[request] {
if blder.forInput.object != nil {
blder.forInput.err = fmt.Errorf("For(...) should only be called once, could not assign multiple objects for reconciliation")
return blder
@@ -110,8 +119,8 @@ type OwnsInput struct {
// Use Owns(object, builder.MatchEveryOwner) to reconcile all owners.
//
// By default, this is the equivalent of calling
// Watches(object, handler.EnqueueRequestForOwner([...], ownerType, OnlyControllerOwner())).
func (blder *Builder) Owns(object client.Object, opts ...OwnsOption) *Builder {
// Watches(source.Kind(cache, &Type{}, handler.EnqueueRequestForOwner([...], &OwnerType{}, OnlyControllerOwner()))).
func (blder *TypedBuilder[request]) Owns(object client.Object, opts ...OwnsOption) *TypedBuilder[request] {
input := OwnsInput{object: object}
for _, opt := range opts {
opt.ApplyToOwns(&input)
@@ -121,22 +130,48 @@ func (blder *Builder) Owns(object client.Object, opts ...OwnsOption) *Builder {
return blder
}
type untypedWatchesInput interface {
setPredicates([]predicate.Predicate)
setObjectProjection(objectProjection)
}
// WatchesInput represents the information set by Watches method.
type WatchesInput struct {
src source.Source
eventHandler handler.EventHandler
type WatchesInput[request comparable] struct {
obj client.Object
handler handler.TypedEventHandler[client.Object, request]
predicates []predicate.Predicate
objectProjection objectProjection
}
func (w *WatchesInput[request]) setPredicates(predicates []predicate.Predicate) {
w.predicates = predicates
}
func (w *WatchesInput[request]) setObjectProjection(objectProjection objectProjection) {
w.objectProjection = objectProjection
}
// Watches defines the type of Object to watch, and configures the ControllerManagedBy to respond to create / delete /
// update events by *reconciling the object* with the given EventHandler.
//
// This is the equivalent of calling
// WatchesRawSource(source.Kind(cache, object), eventHandler, opts...).
func (blder *Builder) Watches(object client.Object, eventHandler handler.EventHandler, opts ...WatchesOption) *Builder {
src := source.Kind(blder.mgr.GetCache(), object)
return blder.WatchesRawSource(src, eventHandler, opts...)
// WatchesRawSource(source.Kind(cache, object, eventHandler, predicates...)).
func (blder *TypedBuilder[request]) Watches(
object client.Object,
eventHandler handler.TypedEventHandler[client.Object, request],
opts ...WatchesOption,
) *TypedBuilder[request] {
input := WatchesInput[request]{
obj: object,
handler: eventHandler,
}
for _, opt := range opts {
opt.ApplyToWatches(&input)
}
blder.watchesInput = append(blder.watchesInput, input)
return blder
}
// WatchesMetadata is the same as Watches, but forces the internal cache to only watch PartialObjectMetadata.
@@ -166,43 +201,45 @@ func (blder *Builder) Watches(object client.Object, eventHandler handler.EventHa
// In the first case, controller-runtime will create another cache for the
// concrete type on top of the metadata cache; this increases memory
// consumption and leads to race conditions as caches are not in sync.
func (blder *Builder) WatchesMetadata(object client.Object, eventHandler handler.EventHandler, opts ...WatchesOption) *Builder {
func (blder *TypedBuilder[request]) WatchesMetadata(
object client.Object,
eventHandler handler.TypedEventHandler[client.Object, request],
opts ...WatchesOption,
) *TypedBuilder[request] {
opts = append(opts, OnlyMetadata)
return blder.Watches(object, eventHandler, opts...)
}
// WatchesRawSource exposes the lower-level ControllerManagedBy Watches functions through the builder.
// Specified predicates are registered only for given source.
//
// STOP! Consider using For(...), Owns(...), Watches(...), WatchesMetadata(...) instead.
// This method is only exposed for more advanced use cases, most users should use one of the higher level functions.
func (blder *Builder) WatchesRawSource(src source.Source, eventHandler handler.EventHandler, opts ...WatchesOption) *Builder {
input := WatchesInput{src: src, eventHandler: eventHandler}
for _, opt := range opts {
opt.ApplyToWatches(&input)
}
// WatchesRawSource does not respect predicates configured through WithEventFilter.
//
// WatchesRawSource makes it possible to use typed handlers and predicates with `source.Kind` as well as custom source implementations.
func (blder *TypedBuilder[request]) WatchesRawSource(src source.TypedSource[request]) *TypedBuilder[request] {
blder.rawSources = append(blder.rawSources, src)
blder.watchesInput = append(blder.watchesInput, input)
return blder
}
// WithEventFilter sets the event filters, to filter which create/update/delete/generic events eventually
// trigger reconciliations. For example, filtering on whether the resource version has changed.
// Given predicate is added for all watched objects.
// Given predicate is added for all watched objects and thus must be able to deal with the type
// of all watched objects.
//
// Defaults to the empty list.
func (blder *Builder) WithEventFilter(p predicate.Predicate) *Builder {
func (blder *TypedBuilder[request]) WithEventFilter(p predicate.Predicate) *TypedBuilder[request] {
blder.globalPredicates = append(blder.globalPredicates, p)
return blder
}
// WithOptions overrides the controller options used in doController. Defaults to empty.
func (blder *Builder) WithOptions(options controller.Options) *Builder {
func (blder *TypedBuilder[request]) WithOptions(options controller.TypedOptions[request]) *TypedBuilder[request] {
blder.ctrlOptions = options
return blder
}
// WithLogConstructor overrides the controller options's LogConstructor.
func (blder *Builder) WithLogConstructor(logConstructor func(*reconcile.Request) logr.Logger) *Builder {
func (blder *TypedBuilder[request]) WithLogConstructor(logConstructor func(*request) logr.Logger) *TypedBuilder[request] {
blder.ctrlOptions.LogConstructor = logConstructor
return blder
}
@@ -212,19 +249,21 @@ func (blder *Builder) WithLogConstructor(logConstructor func(*reconcile.Request)
// (underscores and alphanumeric characters only).
//
// By default, controllers are named using the lowercase version of their kind.
func (blder *Builder) Named(name string) *Builder {
//
// The name must be unique as it is used to identify the controller in metrics and logs.
func (blder *TypedBuilder[request]) Named(name string) *TypedBuilder[request] {
blder.name = name
return blder
}
// Complete builds the Application Controller.
func (blder *Builder) Complete(r reconcile.Reconciler) error {
func (blder *TypedBuilder[request]) Complete(r reconcile.TypedReconciler[request]) error {
_, err := blder.Build(r)
return err
}
// Build builds the Application Controller and returns the Controller it created.
func (blder *Builder) Build(r reconcile.Reconciler) (controller.Controller, error) {
func (blder *TypedBuilder[request]) Build(r reconcile.TypedReconciler[request]) (controller.TypedController[request], error) {
if r == nil {
return nil, fmt.Errorf("must provide a non-nil Reconciler")
}
@@ -248,13 +287,13 @@ func (blder *Builder) Build(r reconcile.Reconciler) (controller.Controller, erro
return blder.ctrl, nil
}
func (blder *Builder) project(obj client.Object, proj objectProjection) (client.Object, error) {
func (blder *TypedBuilder[request]) project(obj client.Object, proj objectProjection) (client.Object, error) {
switch proj {
case projectAsNormal:
return obj, nil
case projectAsMetadata:
metaObj := &metav1.PartialObjectMetadata{}
gvk, err := getGvk(obj, blder.mgr.GetScheme())
gvk, err := apiutil.GVKForObject(obj, blder.mgr.GetScheme())
if err != nil {
return nil, fmt.Errorf("unable to determine GVK of %T for a metadata-only watch: %w", obj, err)
}
@@ -265,18 +304,24 @@ func (blder *Builder) project(obj client.Object, proj objectProjection) (client.
}
}
func (blder *Builder) doWatch() error {
func (blder *TypedBuilder[request]) doWatch() error {
// Reconcile type
if blder.forInput.object != nil {
obj, err := blder.project(blder.forInput.object, blder.forInput.objectProjection)
if err != nil {
return err
}
src := source.Kind(blder.mgr.GetCache(), obj)
hdler := &handler.EnqueueRequestForObject{}
if reflect.TypeFor[request]() != reflect.TypeOf(reconcile.Request{}) {
return fmt.Errorf("For() can only be used with reconcile.Request, got %T", *new(request))
}
var hdler handler.TypedEventHandler[client.Object, request]
reflect.ValueOf(&hdler).Elem().Set(reflect.ValueOf(&handler.EnqueueRequestForObject{}))
allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...)
allPredicates = append(allPredicates, blder.forInput.predicates...)
if err := blder.ctrl.Watch(src, hdler, allPredicates...); err != nil {
src := source.TypedKind(blder.mgr.GetCache(), obj, hdler, allPredicates...)
if err := blder.ctrl.Watch(src); err != nil {
return err
}
}
@@ -290,46 +335,49 @@ func (blder *Builder) doWatch() error {
if err != nil {
return err
}
src := source.Kind(blder.mgr.GetCache(), obj)
opts := []handler.OwnerOption{}
if !own.matchEveryOwner {
opts = append(opts, handler.OnlyControllerOwner())
}
hdler := handler.EnqueueRequestForOwner(
var hdler handler.TypedEventHandler[client.Object, request]
reflect.ValueOf(&hdler).Elem().Set(reflect.ValueOf(handler.EnqueueRequestForOwner(
blder.mgr.GetScheme(), blder.mgr.GetRESTMapper(),
blder.forInput.object,
opts...,
)
)))
allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...)
allPredicates = append(allPredicates, own.predicates...)
if err := blder.ctrl.Watch(src, hdler, allPredicates...); err != nil {
src := source.TypedKind(blder.mgr.GetCache(), obj, hdler, allPredicates...)
if err := blder.ctrl.Watch(src); err != nil {
return err
}
}
// Do the watch requests
if len(blder.watchesInput) == 0 && blder.forInput.object == nil {
return errors.New("there are no watches configured, controller will never get triggered. Use For(), Owns() or Watches() to set them up")
if len(blder.watchesInput) == 0 && blder.forInput.object == nil && len(blder.rawSources) == 0 {
return errors.New("there are no watches configured, controller will never get triggered. Use For(), Owns(), Watches() or WatchesRawSource() to set them up")
}
for _, w := range blder.watchesInput {
// If the source of this watch is of type Kind, project it.
if srcKind, ok := w.src.(*internalsource.Kind); ok {
typeForSrc, err := blder.project(srcKind.Type, w.objectProjection)
if err != nil {
return err
}
srcKind.Type = typeForSrc
projected, err := blder.project(w.obj, w.objectProjection)
if err != nil {
return fmt.Errorf("failed to project for %T: %w", w.obj, err)
}
allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...)
allPredicates = append(allPredicates, w.predicates...)
if err := blder.ctrl.Watch(w.src, w.eventHandler, allPredicates...); err != nil {
if err := blder.ctrl.Watch(source.TypedKind(blder.mgr.GetCache(), projected, w.handler, allPredicates...)); err != nil {
return err
}
}
for _, src := range blder.rawSources {
if err := blder.ctrl.Watch(src); err != nil {
return err
}
}
return nil
}
func (blder *Builder) getControllerName(gvk schema.GroupVersionKind, hasGVK bool) (string, error) {
func (blder *TypedBuilder[request]) getControllerName(gvk schema.GroupVersionKind, hasGVK bool) (string, error) {
if blder.name != "" {
return blder.name, nil
}
@@ -339,7 +387,7 @@ func (blder *Builder) getControllerName(gvk schema.GroupVersionKind, hasGVK bool
return strings.ToLower(gvk.Kind), nil
}
func (blder *Builder) doController(r reconcile.Reconciler) error {
func (blder *TypedBuilder[request]) doController(r reconcile.TypedReconciler[request]) error {
globalOpts := blder.mgr.GetControllerOptions()
ctrlOptions := blder.ctrlOptions
@@ -356,7 +404,7 @@ func (blder *Builder) doController(r reconcile.Reconciler) error {
hasGVK := blder.forInput.object != nil
if hasGVK {
var err error
gvk, err = getGvk(blder.forInput.object, blder.mgr.GetScheme())
gvk, err = apiutil.GVKForObject(blder.forInput.object, blder.mgr.GetScheme())
if err != nil {
return err
}
@@ -393,9 +441,10 @@ func (blder *Builder) doController(r reconcile.Reconciler) error {
)
}
ctrlOptions.LogConstructor = func(req *reconcile.Request) logr.Logger {
ctrlOptions.LogConstructor = func(in *request) logr.Logger {
log := log
if req != nil {
if req, ok := any(in).(*reconcile.Request); ok && req != nil {
if hasGVK {
log = log.WithValues(gvk.Kind, klog.KRef(req.Namespace, req.Name))
}
@@ -407,7 +456,11 @@ func (blder *Builder) doController(r reconcile.Reconciler) error {
}
}
if blder.newController == nil {
blder.newController = controller.NewTyped[request]
}
// Build the controller and return.
blder.ctrl, err = newController(controllerName, blder.mgr, ctrlOptions)
blder.ctrl, err = blder.newController(controllerName, blder.mgr, ctrlOptions)
return err
}

View File

@@ -37,7 +37,7 @@ type OwnsOption interface {
// WatchesOption is some configuration that modifies options for a watches request.
type WatchesOption interface {
// ApplyToWatches applies this configuration to the given watches options.
ApplyToWatches(*WatchesInput)
ApplyToWatches(untypedWatchesInput)
}
// }}}
@@ -67,8 +67,8 @@ func (w Predicates) ApplyToOwns(opts *OwnsInput) {
}
// ApplyToWatches applies this configuration to the given WatchesInput options.
func (w Predicates) ApplyToWatches(opts *WatchesInput) {
opts.predicates = w.predicates
func (w Predicates) ApplyToWatches(opts untypedWatchesInput) {
opts.setPredicates(w.predicates)
}
var _ ForOption = &Predicates{}
@@ -95,8 +95,8 @@ func (p projectAs) ApplyToOwns(opts *OwnsInput) {
}
// ApplyToWatches applies this configuration to the given WatchesInput options.
func (p projectAs) ApplyToWatches(opts *WatchesInput) {
opts.objectProjection = objectProjection(p)
func (p projectAs) ApplyToWatches(opts untypedWatchesInput) {
opts.setObjectProjection(objectProjection(p))
}
var (

View File

@@ -42,8 +42,9 @@ type WebhookBuilder struct {
gvk schema.GroupVersionKind
mgr manager.Manager
config *rest.Config
recoverPanic bool
recoverPanic *bool
logConstructor func(base logr.Logger, req *admission.Request) logr.Logger
err error
}
// WebhookManagedBy returns a new webhook builder.
@@ -57,6 +58,9 @@ func WebhookManagedBy(m manager.Manager) *WebhookBuilder {
// If the given object implements the admission.Defaulter interface, a MutatingWebhook will be wired for this type.
// If the given object implements the admission.Validator interface, a ValidatingWebhook will be wired for this type.
func (blder *WebhookBuilder) For(apiType runtime.Object) *WebhookBuilder {
if blder.apiType != nil {
blder.err = errors.New("For(...) should only be called once, could not assign multiple objects for webhook registration")
}
blder.apiType = apiType
return blder
}
@@ -80,8 +84,9 @@ func (blder *WebhookBuilder) WithLogConstructor(logConstructor func(base logr.Lo
}
// RecoverPanic indicates whether panics caused by the webhook should be recovered.
func (blder *WebhookBuilder) RecoverPanic() *WebhookBuilder {
blder.recoverPanic = true
// Defaults to true.
func (blder *WebhookBuilder) RecoverPanic(recoverPanic bool) *WebhookBuilder {
blder.recoverPanic = &recoverPanic
return blder
}
@@ -142,7 +147,7 @@ func (blder *WebhookBuilder) registerWebhooks() error {
if err != nil {
return err
}
return nil
return blder.err
}
// registerDefaultingWebhook registers a defaulting webhook if necessary.
@@ -165,10 +170,18 @@ func (blder *WebhookBuilder) registerDefaultingWebhook() {
func (blder *WebhookBuilder) getDefaultingWebhook() *admission.Webhook {
if defaulter := blder.customDefaulter; defaulter != nil {
return admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, defaulter).WithRecoverPanic(blder.recoverPanic)
w := admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, defaulter)
if blder.recoverPanic != nil {
w = w.WithRecoverPanic(*blder.recoverPanic)
}
return w
}
if defaulter, ok := blder.apiType.(admission.Defaulter); ok {
return admission.DefaultingWebhookFor(blder.mgr.GetScheme(), defaulter).WithRecoverPanic(blder.recoverPanic)
w := admission.DefaultingWebhookFor(blder.mgr.GetScheme(), defaulter)
if blder.recoverPanic != nil {
w = w.WithRecoverPanic(*blder.recoverPanic)
}
return w
}
log.Info(
"skip registering a mutating webhook, object does not implement admission.Defaulter or WithDefaulter wasn't called",
@@ -196,10 +209,18 @@ func (blder *WebhookBuilder) registerValidatingWebhook() {
func (blder *WebhookBuilder) getValidatingWebhook() *admission.Webhook {
if validator := blder.customValidator; validator != nil {
return admission.WithCustomValidator(blder.mgr.GetScheme(), blder.apiType, validator).WithRecoverPanic(blder.recoverPanic)
w := admission.WithCustomValidator(blder.mgr.GetScheme(), blder.apiType, validator)
if blder.recoverPanic != nil {
w = w.WithRecoverPanic(*blder.recoverPanic)
}
return w
}
if validator, ok := blder.apiType.(admission.Validator); ok {
return admission.ValidatingWebhookFor(blder.mgr.GetScheme(), validator).WithRecoverPanic(blder.recoverPanic)
w := admission.ValidatingWebhookFor(blder.mgr.GetScheme(), validator)
if blder.recoverPanic != nil {
w = w.WithRecoverPanic(*blder.recoverPanic)
}
return w
}
log.Info(
"skip registering a validating webhook, object does not implement admission.Validator or WithValidator wasn't called",

View File

@@ -20,6 +20,7 @@ import (
"context"
"fmt"
"net/http"
"sort"
"time"
"golang.org/x/exp/maps"
@@ -38,11 +39,9 @@ import (
"sigs.k8s.io/controller-runtime/pkg/cache/internal"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
)
var (
log = logf.RuntimeLog.WithName("object-cache")
defaultSyncPeriod = 10 * time.Hour
)
@@ -118,8 +117,8 @@ type Informer interface {
// This function is guaranteed to be idempotent and thread-safe.
RemoveEventHandler(handle toolscache.ResourceEventHandlerRegistration) error
// AddIndexers adds indexers to this store. If this is called after there is already data
// in the store, the results are undefined.
// AddIndexers adds indexers to this store. It is valid to add indexers
// after an informer was started.
AddIndexers(indexers toolscache.Indexers) error
// HasSynced return true if the informers underlying store has synced.
@@ -202,6 +201,9 @@ type Options struct {
// DefaultTransform will be used as transform for all object types
// unless there is already one set in ByObject or DefaultNamespaces.
//
// A typical usecase for this is to use TransformStripManagedFields
// to reduce the caches memory usage.
DefaultTransform toolscache.TransformFunc
// DefaultWatchErrorHandler will be used to the WatchErrorHandler which is called
@@ -221,7 +223,7 @@ type Options struct {
DefaultUnsafeDisableDeepCopy *bool
// ByObject restricts the cache's ListWatch to the desired fields per GVK at the specified object.
// object, this will fall through to Default* settings.
// If unset, this will fall through to the Default* settings.
ByObject map[client.Object]ByObject
// newInformer allows overriding of NewSharedIndexInformer for testing.
@@ -345,6 +347,20 @@ func New(cfg *rest.Config, opts Options) (Cache, error) {
return delegating, nil
}
// TransformStripManagedFields strips the managed fields of an object before it is committed to the cache.
// If you are not explicitly accessing managedFields from your code, setting this as `DefaultTransform`
// on the cache can lead to a significant reduction in memory usage.
func TransformStripManagedFields() toolscache.TransformFunc {
return func(in any) (any, error) {
// Nilcheck managed fields to avoid hitting https://github.com/kubernetes/kubernetes/issues/124337
if obj, err := meta.Accessor(in); err == nil && obj.GetManagedFields() != nil {
obj.SetManagedFields(nil)
}
return in, nil
}
}
func optionDefaultsToConfig(opts *Options) Config {
return Config{
LabelSelector: opts.DefaultLabelSelector,
@@ -418,14 +434,6 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) {
}
}
for namespace, cfg := range opts.DefaultNamespaces {
cfg = defaultConfig(cfg, optionDefaultsToConfig(&opts))
if namespace == metav1.NamespaceAll {
cfg.FieldSelector = fields.AndSelectors(appendIfNotNil(namespaceAllSelector(maps.Keys(opts.DefaultNamespaces)), cfg.FieldSelector)...)
}
opts.DefaultNamespaces[namespace] = cfg
}
for obj, byObject := range opts.ByObject {
isNamespaced, err := apiutil.IsObjectNamespaced(obj, opts.Scheme, opts.Mapper)
if err != nil {
@@ -435,7 +443,12 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) {
return opts, fmt.Errorf("type %T is not namespaced, but its ByObject.Namespaces setting is not nil", obj)
}
// Default the namespace-level configs first, because they need to use the undefaulted type-level config.
if isNamespaced && byObject.Namespaces == nil {
byObject.Namespaces = maps.Clone(opts.DefaultNamespaces)
}
// Default the namespace-level configs first, because they need to use the undefaulted type-level config
// to be able to potentially fall through to settings from DefaultNamespaces.
for namespace, config := range byObject.Namespaces {
// 1. Default from the undefaulted type-level config
config = defaultConfig(config, byObjectToConfig(byObject))
@@ -461,19 +474,35 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) {
byObject.Namespaces[namespace] = config
}
defaultedConfig := defaultConfig(byObjectToConfig(byObject), optionDefaultsToConfig(&opts))
byObject.Label = defaultedConfig.LabelSelector
byObject.Field = defaultedConfig.FieldSelector
byObject.Transform = defaultedConfig.Transform
byObject.UnsafeDisableDeepCopy = defaultedConfig.UnsafeDisableDeepCopy
if isNamespaced && byObject.Namespaces == nil {
byObject.Namespaces = opts.DefaultNamespaces
// Only default ByObject iself if it isn't namespaced or has no namespaces configured, as only
// then any of this will be honored.
if !isNamespaced || len(byObject.Namespaces) == 0 {
defaultedConfig := defaultConfig(byObjectToConfig(byObject), optionDefaultsToConfig(&opts))
byObject.Label = defaultedConfig.LabelSelector
byObject.Field = defaultedConfig.FieldSelector
byObject.Transform = defaultedConfig.Transform
byObject.UnsafeDisableDeepCopy = defaultedConfig.UnsafeDisableDeepCopy
}
opts.ByObject[obj] = byObject
}
// Default namespaces after byObject has been defaulted, otherwise a namespace without selectors
// will get the `Default` selectors, then get copied to byObject and then not get defaulted from
// byObject, as it already has selectors.
for namespace, cfg := range opts.DefaultNamespaces {
cfg = defaultConfig(cfg, optionDefaultsToConfig(&opts))
if namespace == metav1.NamespaceAll {
cfg.FieldSelector = fields.AndSelectors(
appendIfNotNil(
namespaceAllSelector(maps.Keys(opts.DefaultNamespaces)),
cfg.FieldSelector,
)...,
)
}
opts.DefaultNamespaces[namespace] = cfg
}
// Default the resync period to 10 hours if unset
if opts.SyncPeriod == nil {
opts.SyncPeriod = &defaultSyncPeriod
@@ -498,20 +527,21 @@ func defaultConfig(toDefault, defaultFrom Config) Config {
return toDefault
}
func namespaceAllSelector(namespaces []string) fields.Selector {
func namespaceAllSelector(namespaces []string) []fields.Selector {
selectors := make([]fields.Selector, 0, len(namespaces)-1)
sort.Strings(namespaces)
for _, namespace := range namespaces {
if namespace != metav1.NamespaceAll {
selectors = append(selectors, fields.OneTermNotEqualSelector("metadata.namespace", namespace))
}
}
return fields.AndSelectors(selectors...)
return selectors
}
func appendIfNotNil[T comparable](a, b T) []T {
func appendIfNotNil[T comparable](a []T, b T) []T {
if b != *new(T) {
return []T{a, b}
return append(a, b)
}
return []T{a}
return a
}

View File

@@ -36,7 +36,7 @@ import (
// CacheReader is a client.Reader.
var _ client.Reader = &CacheReader{}
// CacheReader wraps a cache.Index to implement the client.CacheReader interface for a single type.
// CacheReader wraps a cache.Index to implement the client.Reader interface for a single type.
type CacheReader struct {
// indexer is the underlying indexer wrapped by this cache.
indexer cache.Indexer

View File

@@ -18,6 +18,7 @@ package internal
import (
"context"
"errors"
"fmt"
"math/rand"
"net/http"
@@ -186,10 +187,14 @@ type Informers struct {
// Start calls Run on each of the informers and sets started to true. Blocks on the context.
// It doesn't return start because it can't return an error, and it's not a runnable directly.
func (ip *Informers) Start(ctx context.Context) error {
func() {
if err := func() error {
ip.mu.Lock()
defer ip.mu.Unlock()
if ip.started {
return errors.New("Informer already started") //nolint:stylecheck
}
// Set the context so it can be passed to informers that are added later
ip.ctx = ctx
@@ -207,7 +212,11 @@ func (ip *Informers) Start(ctx context.Context) error {
// Set started to true so we immediately start any informers added later.
ip.started = true
close(ip.startWait)
}()
return nil
}(); err != nil {
return err
}
<-ctx.Done() // Block until the context is done
ip.mu.Lock()
ip.stopped = true // Set stopped to true so we don't start any new informers

View File

@@ -163,12 +163,13 @@ func (c *multiNamespaceCache) GetInformerForKind(ctx context.Context, gvk schema
}
func (c *multiNamespaceCache) Start(ctx context.Context) error {
errs := make(chan error)
// start global cache
if c.clusterCache != nil {
go func() {
err := c.clusterCache.Start(ctx)
if err != nil {
log.Error(err, "cluster scoped cache failed to start")
errs <- fmt.Errorf("failed to start cluster-scoped cache: %w", err)
}
}()
}
@@ -177,13 +178,16 @@ func (c *multiNamespaceCache) Start(ctx context.Context) error {
for ns, cache := range c.namespaceToCache {
go func(ns string, cache Cache) {
if err := cache.Start(ctx); err != nil {
log.Error(err, "multi-namespace cache failed to start namespaced informer", "namespace", ns)
errs <- fmt.Errorf("failed to start cache for namespace %s: %w", ns, err)
}
}(ns, cache)
}
<-ctx.Done()
return nil
select {
case <-ctx.Done():
return nil
case err := <-errs:
return err
}
}
func (c *multiNamespaceCache) WaitForCacheSync(ctx context.Context) bool {

View File

@@ -173,14 +173,14 @@ func (cw *CertWatcher) ReadCertificate() error {
func (cw *CertWatcher) handleEvent(event fsnotify.Event) {
// Only care about events which may modify the contents of the file.
if !(isWrite(event) || isRemove(event) || isCreate(event)) {
if !(isWrite(event) || isRemove(event) || isCreate(event) || isChmod(event)) {
return
}
log.V(1).Info("certificate event", "event", event)
// If the file was removed, re-add the watch.
if isRemove(event) {
// If the file was removed or renamed, re-add the watch to the previous name
if isRemove(event) || isChmod(event) {
if err := cw.watcher.Add(event.Name); err != nil {
log.Error(err, "error re-watching file")
}
@@ -202,3 +202,7 @@ func isCreate(event fsnotify.Event) bool {
func isRemove(event fsnotify.Event) bool {
return event.Op.Has(fsnotify.Remove)
}
func isChmod(event fsnotify.Event) bool {
return event.Op.Has(fsnotify.Chmod)
}

View File

@@ -72,7 +72,10 @@ func IsObjectNamespaced(obj runtime.Object, scheme *runtime.Scheme, restmapper m
// IsGVKNamespaced returns true if the object having the provided
// GVK is namespace scoped.
func IsGVKNamespaced(gvk schema.GroupVersionKind, restmapper meta.RESTMapper) (bool, error) {
restmapping, err := restmapper.RESTMapping(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind})
// Fetch the RESTMapping using the complete GVK. If we exclude the Version, the Version set
// will be populated using the cached Group if available. This can lead to failures updating
// the cache with new Versions of CRDs registered at runtime.
restmapping, err := restmapper.RESTMapping(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind}, gvk.Version)
if err != nil {
return false, fmt.Errorf("failed to get restmapping: %w", err)
}

View File

@@ -50,28 +50,10 @@ type Options struct {
// Cache, if provided, is used to read objects from the cache.
Cache *CacheOptions
// WarningHandler is used to configure the warning handler responsible for
// surfacing and handling warnings messages sent by the API server.
WarningHandler WarningHandlerOptions
// DryRun instructs the client to only perform dry run requests.
DryRun *bool
}
// WarningHandlerOptions are options for configuring a
// warning handler for the client which is responsible
// for surfacing API Server warnings.
type WarningHandlerOptions struct {
// SuppressWarnings decides if the warnings from the
// API server are suppressed or surfaced in the client.
SuppressWarnings bool
// AllowDuplicateLogs does not deduplicate the to-be
// logged surfaced warnings messages. See
// log.WarningHandlerOptions for considerations
// regarding deduplication
AllowDuplicateLogs bool
}
// CacheOptions are options for creating a cache-backed client.
type CacheOptions struct {
// Reader is a cache-backed reader that will be used to read objects from the cache.
@@ -91,6 +73,12 @@ type NewClientFunc func(config *rest.Config, options Options) (Client, error)
// New returns a new Client using the provided config and Options.
//
// By default, the client surfaces warnings returned by the server. To
// suppress warnings, set config.WarningHandler = rest.NoWarnings{}. To
// define custom behavior, implement the rest.WarningHandler interface.
// See [sigs.k8s.io/controller-runtime/pkg/log.KubeAPIWarningLogger] for
// an example.
//
// The client's read behavior is determined by Options.Cache.
// If either Options.Cache or Options.Cache.Reader is nil,
// the client reads directly from the API server.
@@ -124,17 +112,12 @@ func newClient(config *rest.Config, options Options) (*client, error) {
config.UserAgent = rest.DefaultKubernetesUserAgent()
}
if !options.WarningHandler.SuppressWarnings {
// surface warnings
logger := log.Log.WithName("KubeAPIWarningLogger")
// Set a WarningHandler, the default WarningHandler
// is log.KubeAPIWarningLogger with deduplication enabled.
// See log.KubeAPIWarningLoggerOptions for considerations
// regarding deduplication.
if config.WarningHandler == nil {
// By default, we de-duplicate and surface warnings.
config.WarningHandler = log.NewKubeAPIWarningLogger(
logger,
log.Log.WithName("KubeAPIWarningLogger"),
log.KubeAPIWarningLoggerOptions{
Deduplicate: !options.WarningHandler.AllowDuplicateLogs,
Deduplicate: true,
},
)
}
@@ -523,8 +506,8 @@ func (co *SubResourceCreateOptions) ApplyOptions(opts []SubResourceCreateOption)
return co
}
// ApplyToSubresourceCreate applies the the configuration on the given create options.
func (co *SubResourceCreateOptions) ApplyToSubresourceCreate(o *SubResourceCreateOptions) {
// ApplyToSubResourceCreate applies the the configuration on the given create options.
func (co *SubResourceCreateOptions) ApplyToSubResourceCreate(o *SubResourceCreateOptions) {
co.CreateOptions.ApplyToCreate(&co.CreateOptions)
}

View File

@@ -30,7 +30,9 @@ import (
"time"
// Using v4 to match upstream
jsonpatch "github.com/evanphx/json-patch"
jsonpatch "gopkg.in/evanphx/json-patch.v4"
appsv1 "k8s.io/api/apps/v1"
autoscalingv1 "k8s.io/api/autoscaling/v1"
corev1 "k8s.io/api/core/v1"
policyv1 "k8s.io/api/policy/v1"
policyv1beta1 "k8s.io/api/policy/v1beta1"
@@ -50,6 +52,7 @@ import (
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/testing"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
@@ -65,16 +68,21 @@ type versionedTracker struct {
}
type fakeClient struct {
tracker versionedTracker
scheme *runtime.Scheme
// trackerWriteLock must be acquired before writing to
// the tracker or performing reads that affect a following
// write.
trackerWriteLock sync.Mutex
tracker versionedTracker
schemeWriteLock sync.Mutex
scheme *runtime.Scheme
restMapper meta.RESTMapper
withStatusSubresource sets.Set[schema.GroupVersionKind]
// indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK.
// The inner map maps from index name to IndexerFunc.
indexes map[schema.GroupVersionKind]map[string]client.IndexerFunc
schemeWriteLock sync.Mutex
}
var _ client.WithWatch = &fakeClient{}
@@ -83,6 +91,8 @@ const (
maxNameLength = 63
randomLength = 5
maxGeneratedNameLength = maxNameLength - randomLength
subResourceScale = "scale"
)
// NewFakeClient creates a new fake client for testing.
@@ -301,7 +311,7 @@ func (t versionedTracker) Add(obj runtime.Object) error {
return nil
}
func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error {
func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string, opts ...metav1.CreateOptions) error {
accessor, err := meta.Accessor(obj)
if err != nil {
return fmt.Errorf("failed to get accessor for object: %w", err)
@@ -320,7 +330,7 @@ func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Ob
if err != nil {
return err
}
if err := t.ObjectTracker.Create(gvr, obj, ns); err != nil {
if err := t.ObjectTracker.Create(gvr, obj, ns, opts...); err != nil {
accessor.SetResourceVersion("")
return err
}
@@ -359,24 +369,59 @@ func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (ru
return typed, nil
}
func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error {
func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string, opts ...metav1.UpdateOptions) error {
updateOpts, err := getSingleOrZeroOptions(opts)
if err != nil {
return err
}
return t.update(gvr, obj, ns, false, false, updateOpts)
}
func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Object, ns string, isStatus, deleting bool, opts metav1.UpdateOptions) error {
obj, err := t.updateObject(gvr, obj, ns, isStatus, deleting, opts.DryRun)
if err != nil {
return err
}
if obj == nil {
return nil
}
return t.ObjectTracker.Update(gvr, obj, ns, opts)
}
func (t versionedTracker) Patch(gvr schema.GroupVersionResource, obj runtime.Object, ns string, opts ...metav1.PatchOptions) error {
patchOptions, err := getSingleOrZeroOptions(opts)
if err != nil {
return err
}
isStatus := false
// We apply patches using a client-go reaction that ends up calling the trackers Update. As we can't change
// We apply patches using a client-go reaction that ends up calling the trackers Patch. As we can't change
// that reaction, we use the callstack to figure out if this originated from the status client.
if bytes.Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).statusPatch")) {
isStatus = true
}
return t.update(gvr, obj, ns, isStatus, false)
obj, err = t.updateObject(gvr, obj, ns, isStatus, false, patchOptions.DryRun)
if err != nil {
return err
}
if obj == nil {
return nil
}
return t.ObjectTracker.Patch(gvr, obj, ns, patchOptions)
}
func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Object, ns string, isStatus bool, deleting bool) error {
func (t versionedTracker) updateObject(gvr schema.GroupVersionResource, obj runtime.Object, ns string, isStatus, deleting bool, dryRun []string) (runtime.Object, error) {
accessor, err := meta.Accessor(obj)
if err != nil {
return fmt.Errorf("failed to get accessor for object: %w", err)
return nil, fmt.Errorf("failed to get accessor for object: %w", err)
}
if accessor.GetName() == "" {
return apierrors.NewInvalid(
return nil, apierrors.NewInvalid(
obj.GetObjectKind().GroupVersionKind().GroupKind(),
accessor.GetName(),
field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")})
@@ -384,7 +429,7 @@ func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Ob
gvk, err := apiutil.GVKForObject(obj, t.scheme)
if err != nil {
return err
return nil, err
}
oldObject, err := t.ObjectTracker.Get(gvr, ns, accessor.GetName())
@@ -392,65 +437,75 @@ func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Ob
// If the resource is not found and the resource allows create on update, issue a
// create instead.
if apierrors.IsNotFound(err) && allowsCreateOnUpdate(gvk) {
return t.Create(gvr, obj, ns)
return nil, t.Create(gvr, obj, ns)
}
return err
return nil, err
}
if t.withStatusSubresource.Has(gvk) {
if isStatus { // copy everything but status and metadata.ResourceVersion from original object
if err := copyStatusFrom(obj, oldObject); err != nil {
return fmt.Errorf("failed to copy non-status field for object with status subresouce: %w", err)
return nil, fmt.Errorf("failed to copy non-status field for object with status subresouce: %w", err)
}
passedRV := accessor.GetResourceVersion()
if err := copyFrom(oldObject, obj); err != nil {
return fmt.Errorf("failed to restore non-status fields: %w", err)
return nil, fmt.Errorf("failed to restore non-status fields: %w", err)
}
accessor.SetResourceVersion(passedRV)
} else { // copy status from original object
if err := copyStatusFrom(oldObject, obj); err != nil {
return fmt.Errorf("failed to copy the status for object with status subresource: %w", err)
return nil, fmt.Errorf("failed to copy the status for object with status subresource: %w", err)
}
}
} else if isStatus {
return apierrors.NewNotFound(gvr.GroupResource(), accessor.GetName())
return nil, apierrors.NewNotFound(gvr.GroupResource(), accessor.GetName())
}
oldAccessor, err := meta.Accessor(oldObject)
if err != nil {
return err
return nil, err
}
// If the new object does not have the resource version set and it allows unconditional update,
// default it to the resource version of the existing resource
if accessor.GetResourceVersion() == "" && allowsUnconditionalUpdate(gvk) {
accessor.SetResourceVersion(oldAccessor.GetResourceVersion())
if accessor.GetResourceVersion() == "" {
switch {
case allowsUnconditionalUpdate(gvk):
accessor.SetResourceVersion(oldAccessor.GetResourceVersion())
// This is needed because if the patch explicitly sets the RV to null, the client-go reaction we use
// to apply it and whose output we process here will have it unset. It is not clear why the Kubernetes
// apiserver accepts such a patch, but it does so we just copy that behavior.
// Kubernetes apiserver behavior can be checked like this:
// `kubectl patch configmap foo --patch '{"metadata":{"annotations":{"foo":"bar"},"resourceVersion":null}}' -v=9`
case bytes.
Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeClient).Patch")):
// We apply patches using a client-go reaction that ends up calling the trackers Update. As we can't change
// that reaction, we use the callstack to figure out if this originated from the "fakeClient.Patch" func.
accessor.SetResourceVersion(oldAccessor.GetResourceVersion())
}
}
if accessor.GetResourceVersion() != oldAccessor.GetResourceVersion() {
return apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), errors.New("object was modified"))
return nil, apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), errors.New("object was modified"))
}
if oldAccessor.GetResourceVersion() == "" {
oldAccessor.SetResourceVersion("0")
}
intResourceVersion, err := strconv.ParseUint(oldAccessor.GetResourceVersion(), 10, 64)
if err != nil {
return fmt.Errorf("can not convert resourceVersion %q to int: %w", oldAccessor.GetResourceVersion(), err)
return nil, fmt.Errorf("can not convert resourceVersion %q to int: %w", oldAccessor.GetResourceVersion(), err)
}
intResourceVersion++
accessor.SetResourceVersion(strconv.FormatUint(intResourceVersion, 10))
if !deleting && !deletionTimestampEqual(accessor, oldAccessor) {
return fmt.Errorf("error: Unable to edit %s: metadata.deletionTimestamp field is immutable", accessor.GetName())
return nil, fmt.Errorf("error: Unable to edit %s: metadata.deletionTimestamp field is immutable", accessor.GetName())
}
if !accessor.GetDeletionTimestamp().IsZero() && len(accessor.GetFinalizers()) == 0 {
return t.ObjectTracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName())
return nil, t.ObjectTracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName(), metav1.DeleteOptions{DryRun: dryRun})
}
obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj)
if err != nil {
return err
}
return t.ObjectTracker.Update(gvr, obj, ns)
return convertFromUnstructuredIfNecessary(t.scheme, obj)
}
func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
@@ -463,7 +518,10 @@ func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.O
return err
}
if _, isUnstructured := obj.(runtime.Unstructured); isUnstructured {
_, isUnstructured := obj.(runtime.Unstructured)
_, isPartialObject := obj.(*metav1.PartialObjectMetadata)
if isUnstructured || isPartialObject {
gvk, err := apiutil.GVKForObject(obj, c.scheme)
if err != nil {
return err
@@ -684,6 +742,8 @@ func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...clie
accessor.SetDeletionTimestamp(nil)
}
c.trackerWriteLock.Lock()
defer c.trackerWriteLock.Unlock()
return c.tracker.Create(gvr, obj, accessor.GetNamespace())
}
@@ -705,6 +765,8 @@ func (c *fakeClient) Delete(ctx context.Context, obj client.Object, opts ...clie
}
}
c.trackerWriteLock.Lock()
defer c.trackerWriteLock.Unlock()
// Check the ResourceVersion if that Precondition was specified.
if delOptions.Preconditions != nil && delOptions.Preconditions.ResourceVersion != nil {
name := accessor.GetName()
@@ -727,7 +789,7 @@ func (c *fakeClient) Delete(ctx context.Context, obj client.Object, opts ...clie
}
}
return c.deleteObject(gvr, accessor)
return c.deleteObjectLocked(gvr, accessor)
}
func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error {
@@ -745,6 +807,9 @@ func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ..
}
}
c.trackerWriteLock.Lock()
defer c.trackerWriteLock.Unlock()
gvr, _ := meta.UnsafeGuessKindToResource(gvk)
o, err := c.tracker.List(gvr, gvk, dcOptions.Namespace)
if err != nil {
@@ -764,7 +829,7 @@ func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ..
if err != nil {
return err
}
err = c.deleteObject(gvr, accessor)
err = c.deleteObjectLocked(gvr, accessor)
if err != nil {
return err
}
@@ -794,7 +859,10 @@ func (c *fakeClient) update(obj client.Object, isStatus bool, opts ...client.Upd
if err != nil {
return err
}
return c.tracker.update(gvr, obj, accessor.GetNamespace(), isStatus, false)
c.trackerWriteLock.Lock()
defer c.trackerWriteLock.Unlock()
return c.tracker.update(gvr, obj, accessor.GetNamespace(), isStatus, false, *updateOptions.AsUpdateOptions())
}
func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
@@ -829,6 +897,8 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
return err
}
c.trackerWriteLock.Lock()
defer c.trackerWriteLock.Unlock()
oldObj, err := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName())
if err != nil {
return err
@@ -1037,7 +1107,7 @@ func (c *fakeClient) SubResource(subResource string) client.SubResourceClient {
return &fakeSubResourceClient{client: c, subResource: subResource}
}
func (c *fakeClient) deleteObject(gvr schema.GroupVersionResource, accessor metav1.Object) error {
func (c *fakeClient) deleteObjectLocked(gvr schema.GroupVersionResource, accessor metav1.Object) error {
old, err := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName())
if err == nil {
oldAccessor, err := meta.Accessor(old)
@@ -1047,7 +1117,7 @@ func (c *fakeClient) deleteObject(gvr schema.GroupVersionResource, accessor meta
oldAccessor.SetDeletionTimestamp(&now)
// Call update directly with mutability parameter set to true to allow
// changes to deletionTimestamp
return c.tracker.update(gvr, old, accessor.GetNamespace(), false, true)
return c.tracker.update(gvr, old, accessor.GetNamespace(), false, true, metav1.UpdateOptions{})
}
}
}
@@ -1071,7 +1141,26 @@ type fakeSubResourceClient struct {
}
func (sw *fakeSubResourceClient) Get(ctx context.Context, obj, subResource client.Object, opts ...client.SubResourceGetOption) error {
panic("fakeSubResourceClient does not support get")
switch sw.subResource {
case subResourceScale:
// Actual client looks up resource, then extracts the scale sub-resource:
// https://github.com/kubernetes/kubernetes/blob/fb6bbc9781d11a87688c398778525c4e1dcb0f08/pkg/registry/apps/deployment/storage/storage.go#L307
if err := sw.client.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
return err
}
scale, isScale := subResource.(*autoscalingv1.Scale)
if !isScale {
return apierrors.NewBadRequest(fmt.Sprintf("expected Scale, got %t", subResource))
}
scaleOut, err := extractScale(obj)
if err != nil {
return err
}
*scale = *scaleOut
return nil
default:
return fmt.Errorf("fakeSubResourceClient does not support get for %s", sw.subResource)
}
}
func (sw *fakeSubResourceClient) Create(ctx context.Context, obj client.Object, subResource client.Object, opts ...client.SubResourceCreateOption) error {
@@ -1098,11 +1187,30 @@ func (sw *fakeSubResourceClient) Update(ctx context.Context, obj client.Object,
updateOptions := client.SubResourceUpdateOptions{}
updateOptions.ApplyOptions(opts)
body := obj
if updateOptions.SubResourceBody != nil {
body = updateOptions.SubResourceBody
switch sw.subResource {
case subResourceScale:
if err := sw.client.Get(ctx, client.ObjectKeyFromObject(obj), obj.DeepCopyObject().(client.Object)); err != nil {
return err
}
if updateOptions.SubResourceBody == nil {
return apierrors.NewBadRequest("missing SubResourceBody")
}
scale, isScale := updateOptions.SubResourceBody.(*autoscalingv1.Scale)
if !isScale {
return apierrors.NewBadRequest(fmt.Sprintf("expected Scale, got %t", updateOptions.SubResourceBody))
}
if err := applyScale(obj, scale); err != nil {
return err
}
return sw.client.update(obj, false, &updateOptions.UpdateOptions)
default:
body := obj
if updateOptions.SubResourceBody != nil {
body = updateOptions.SubResourceBody
}
return sw.client.update(body, true, &updateOptions.UpdateOptions)
}
return sw.client.update(body, true, &updateOptions.UpdateOptions)
}
func (sw *fakeSubResourceClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error {
@@ -1269,3 +1377,138 @@ func zero(x interface{}) {
res := reflect.ValueOf(x).Elem()
res.Set(reflect.Zero(res.Type()))
}
// getSingleOrZeroOptions returns the single options value in the slice, its
// zero value if the slice is empty, or an error if the slice contains more than
// one option value.
func getSingleOrZeroOptions[T any](opts []T) (opt T, err error) {
switch len(opts) {
case 0:
case 1:
opt = opts[0]
default:
err = fmt.Errorf("expected single or no options value, got %d values", len(opts))
}
return
}
func extractScale(obj client.Object) (*autoscalingv1.Scale, error) {
switch obj := obj.(type) {
case *appsv1.Deployment:
var replicas int32 = 1
if obj.Spec.Replicas != nil {
replicas = *obj.Spec.Replicas
}
var selector string
if obj.Spec.Selector != nil {
selector = obj.Spec.Selector.String()
}
return &autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{
Namespace: obj.Namespace,
Name: obj.Name,
UID: obj.UID,
ResourceVersion: obj.ResourceVersion,
CreationTimestamp: obj.CreationTimestamp,
},
Spec: autoscalingv1.ScaleSpec{
Replicas: replicas,
},
Status: autoscalingv1.ScaleStatus{
Replicas: obj.Status.Replicas,
Selector: selector,
},
}, nil
case *appsv1.ReplicaSet:
var replicas int32 = 1
if obj.Spec.Replicas != nil {
replicas = *obj.Spec.Replicas
}
var selector string
if obj.Spec.Selector != nil {
selector = obj.Spec.Selector.String()
}
return &autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{
Namespace: obj.Namespace,
Name: obj.Name,
UID: obj.UID,
ResourceVersion: obj.ResourceVersion,
CreationTimestamp: obj.CreationTimestamp,
},
Spec: autoscalingv1.ScaleSpec{
Replicas: replicas,
},
Status: autoscalingv1.ScaleStatus{
Replicas: obj.Status.Replicas,
Selector: selector,
},
}, nil
case *corev1.ReplicationController:
var replicas int32 = 1
if obj.Spec.Replicas != nil {
replicas = *obj.Spec.Replicas
}
return &autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{
Namespace: obj.Namespace,
Name: obj.Name,
UID: obj.UID,
ResourceVersion: obj.ResourceVersion,
CreationTimestamp: obj.CreationTimestamp,
},
Spec: autoscalingv1.ScaleSpec{
Replicas: replicas,
},
Status: autoscalingv1.ScaleStatus{
Replicas: obj.Status.Replicas,
Selector: labels.Set(obj.Spec.Selector).String(),
},
}, nil
case *appsv1.StatefulSet:
var replicas int32 = 1
if obj.Spec.Replicas != nil {
replicas = *obj.Spec.Replicas
}
var selector string
if obj.Spec.Selector != nil {
selector = obj.Spec.Selector.String()
}
return &autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{
Namespace: obj.Namespace,
Name: obj.Name,
UID: obj.UID,
ResourceVersion: obj.ResourceVersion,
CreationTimestamp: obj.CreationTimestamp,
},
Spec: autoscalingv1.ScaleSpec{
Replicas: replicas,
},
Status: autoscalingv1.ScaleStatus{
Replicas: obj.Status.Replicas,
Selector: selector,
},
}, nil
default:
// TODO: CRDs https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#scale-subresource
return nil, fmt.Errorf("unimplemented scale subresource for resource %T", obj)
}
}
func applyScale(obj client.Object, scale *autoscalingv1.Scale) error {
switch obj := obj.(type) {
case *appsv1.Deployment:
obj.Spec.Replicas = ptr.To(scale.Spec.Replicas)
case *appsv1.ReplicaSet:
obj.Spec.Replicas = ptr.To(scale.Spec.Replicas)
case *corev1.ReplicationController:
obj.Spec.Replicas = ptr.To(scale.Spec.Replicas)
case *appsv1.StatefulSet:
obj.Spec.Replicas = ptr.To(scale.Spec.Replicas)
default:
// TODO: CRDs https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#scale-subresource
return fmt.Errorf("unimplemented scale subresource for resource %T", obj)
}
return nil
}

View File

@@ -0,0 +1,106 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package client
import (
"context"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// WithFieldOwner wraps a Client and adds the fieldOwner as the field
// manager to all write requests from this client. If additional [FieldOwner]
// options are specified on methods of this client, the value specified here
// will be overridden.
func WithFieldOwner(c Client, fieldOwner string) Client {
return &clientWithFieldManager{
owner: fieldOwner,
c: c,
Reader: c,
}
}
type clientWithFieldManager struct {
owner string
c Client
Reader
}
func (f *clientWithFieldManager) Create(ctx context.Context, obj Object, opts ...CreateOption) error {
return f.c.Create(ctx, obj, append([]CreateOption{FieldOwner(f.owner)}, opts...)...)
}
func (f *clientWithFieldManager) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
return f.c.Update(ctx, obj, append([]UpdateOption{FieldOwner(f.owner)}, opts...)...)
}
func (f *clientWithFieldManager) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
return f.c.Patch(ctx, obj, patch, append([]PatchOption{FieldOwner(f.owner)}, opts...)...)
}
func (f *clientWithFieldManager) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error {
return f.c.Delete(ctx, obj, opts...)
}
func (f *clientWithFieldManager) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error {
return f.c.DeleteAllOf(ctx, obj, opts...)
}
func (f *clientWithFieldManager) Scheme() *runtime.Scheme { return f.c.Scheme() }
func (f *clientWithFieldManager) RESTMapper() meta.RESTMapper { return f.c.RESTMapper() }
func (f *clientWithFieldManager) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) {
return f.c.GroupVersionKindFor(obj)
}
func (f *clientWithFieldManager) IsObjectNamespaced(obj runtime.Object) (bool, error) {
return f.c.IsObjectNamespaced(obj)
}
func (f *clientWithFieldManager) Status() StatusWriter {
return &subresourceClientWithFieldOwner{
owner: f.owner,
subresourceWriter: f.c.Status(),
}
}
func (f *clientWithFieldManager) SubResource(subresource string) SubResourceClient {
c := f.c.SubResource(subresource)
return &subresourceClientWithFieldOwner{
owner: f.owner,
subresourceWriter: c,
SubResourceReader: c,
}
}
type subresourceClientWithFieldOwner struct {
owner string
subresourceWriter SubResourceWriter
SubResourceReader
}
func (f *subresourceClientWithFieldOwner) Create(ctx context.Context, obj Object, subresource Object, opts ...SubResourceCreateOption) error {
return f.subresourceWriter.Create(ctx, obj, subresource, append([]SubResourceCreateOption{FieldOwner(f.owner)}, opts...)...)
}
func (f *subresourceClientWithFieldOwner) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
return f.subresourceWriter.Update(ctx, obj, append([]SubResourceUpdateOption{FieldOwner(f.owner)}, opts...)...)
}
func (f *subresourceClientWithFieldOwner) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
return f.subresourceWriter.Patch(ctx, obj, patch, append([]SubResourcePatchOption{FieldOwner(f.owner)}, opts...)...)
}

View File

@@ -0,0 +1,106 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package client
import (
"context"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// WithFieldValidation wraps a Client and configures field validation, by
// default, for all write requests from this client. Users can override field
// validation for individual write requests.
func WithFieldValidation(c Client, validation FieldValidation) Client {
return &clientWithFieldValidation{
validation: validation,
client: c,
Reader: c,
}
}
type clientWithFieldValidation struct {
validation FieldValidation
client Client
Reader
}
func (c *clientWithFieldValidation) Create(ctx context.Context, obj Object, opts ...CreateOption) error {
return c.client.Create(ctx, obj, append([]CreateOption{c.validation}, opts...)...)
}
func (c *clientWithFieldValidation) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
return c.client.Update(ctx, obj, append([]UpdateOption{c.validation}, opts...)...)
}
func (c *clientWithFieldValidation) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
return c.client.Patch(ctx, obj, patch, append([]PatchOption{c.validation}, opts...)...)
}
func (c *clientWithFieldValidation) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error {
return c.client.Delete(ctx, obj, opts...)
}
func (c *clientWithFieldValidation) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error {
return c.client.DeleteAllOf(ctx, obj, opts...)
}
func (c *clientWithFieldValidation) Scheme() *runtime.Scheme { return c.client.Scheme() }
func (c *clientWithFieldValidation) RESTMapper() meta.RESTMapper { return c.client.RESTMapper() }
func (c *clientWithFieldValidation) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) {
return c.client.GroupVersionKindFor(obj)
}
func (c *clientWithFieldValidation) IsObjectNamespaced(obj runtime.Object) (bool, error) {
return c.client.IsObjectNamespaced(obj)
}
func (c *clientWithFieldValidation) Status() StatusWriter {
return &subresourceClientWithFieldValidation{
validation: c.validation,
subresourceWriter: c.client.Status(),
}
}
func (c *clientWithFieldValidation) SubResource(subresource string) SubResourceClient {
srClient := c.client.SubResource(subresource)
return &subresourceClientWithFieldValidation{
validation: c.validation,
subresourceWriter: srClient,
SubResourceReader: srClient,
}
}
type subresourceClientWithFieldValidation struct {
validation FieldValidation
subresourceWriter SubResourceWriter
SubResourceReader
}
func (c *subresourceClientWithFieldValidation) Create(ctx context.Context, obj Object, subresource Object, opts ...SubResourceCreateOption) error {
return c.subresourceWriter.Create(ctx, obj, subresource, append([]SubResourceCreateOption{c.validation}, opts...)...)
}
func (c *subresourceClientWithFieldValidation) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
return c.subresourceWriter.Update(ctx, obj, append([]SubResourceUpdateOption{c.validation}, opts...)...)
}
func (c *subresourceClientWithFieldValidation) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
return c.subresourceWriter.Patch(ctx, obj, patch, append([]SubResourcePatchOption{c.validation}, opts...)...)
}

View File

@@ -169,6 +169,39 @@ func (f FieldOwner) ApplyToSubResourceUpdate(opts *SubResourceUpdateOptions) {
opts.FieldManager = string(f)
}
// FieldValidation configures field validation for the given requests.
type FieldValidation string
// ApplyToPatch applies this configuration to the given patch options.
func (f FieldValidation) ApplyToPatch(opts *PatchOptions) {
opts.FieldValidation = string(f)
}
// ApplyToCreate applies this configuration to the given create options.
func (f FieldValidation) ApplyToCreate(opts *CreateOptions) {
opts.FieldValidation = string(f)
}
// ApplyToUpdate applies this configuration to the given update options.
func (f FieldValidation) ApplyToUpdate(opts *UpdateOptions) {
opts.FieldValidation = string(f)
}
// ApplyToSubResourcePatch applies this configuration to the given patch options.
func (f FieldValidation) ApplyToSubResourcePatch(opts *SubResourcePatchOptions) {
opts.FieldValidation = string(f)
}
// ApplyToSubResourceCreate applies this configuration to the given create options.
func (f FieldValidation) ApplyToSubResourceCreate(opts *SubResourceCreateOptions) {
opts.FieldValidation = string(f)
}
// ApplyToSubResourceUpdate applies this configuration to the given update options.
func (f FieldValidation) ApplyToSubResourceUpdate(opts *SubResourceUpdateOptions) {
opts.FieldValidation = string(f)
}
// }}}
// {{{ Create Options
@@ -187,6 +220,24 @@ type CreateOptions struct {
// this request. It must be set with server-side apply.
FieldManager string
// fieldValidation instructs the server on how to handle
// objects in the request (POST/PUT/PATCH) containing unknown
// or duplicate fields. Valid values are:
// - Ignore: This will ignore any unknown fields that are silently
// dropped from the object, and will ignore all but the last duplicate
// field that the decoder encounters. This is the default behavior
// prior to v1.23.
// - Warn: This will send a warning via the standard warning response
// header for each unknown field that is dropped from the object, and
// for each duplicate field that is encountered. The request will
// still succeed if there are no other errors, and will only persist
// the last of any duplicate fields. This is the default in v1.23+
// - Strict: This will fail the request with a BadRequest error if
// any unknown fields would be dropped from the object, or if any
// duplicate fields are present. The error returned from the server
// will contain all unknown and duplicate fields encountered.
FieldValidation string
// Raw represents raw CreateOptions, as passed to the API server.
Raw *metav1.CreateOptions
}
@@ -203,6 +254,7 @@ func (o *CreateOptions) AsCreateOptions() *metav1.CreateOptions {
o.Raw.DryRun = o.DryRun
o.Raw.FieldManager = o.FieldManager
o.Raw.FieldValidation = o.FieldValidation
return o.Raw
}
@@ -223,6 +275,9 @@ func (o *CreateOptions) ApplyToCreate(co *CreateOptions) {
if o.FieldManager != "" {
co.FieldManager = o.FieldManager
}
if o.FieldValidation != "" {
co.FieldValidation = o.FieldValidation
}
if o.Raw != nil {
co.Raw = o.Raw
}
@@ -679,6 +734,24 @@ type UpdateOptions struct {
// this request. It must be set with server-side apply.
FieldManager string
// fieldValidation instructs the server on how to handle
// objects in the request (POST/PUT/PATCH) containing unknown
// or duplicate fields. Valid values are:
// - Ignore: This will ignore any unknown fields that are silently
// dropped from the object, and will ignore all but the last duplicate
// field that the decoder encounters. This is the default behavior
// prior to v1.23.
// - Warn: This will send a warning via the standard warning response
// header for each unknown field that is dropped from the object, and
// for each duplicate field that is encountered. The request will
// still succeed if there are no other errors, and will only persist
// the last of any duplicate fields. This is the default in v1.23+
// - Strict: This will fail the request with a BadRequest error if
// any unknown fields would be dropped from the object, or if any
// duplicate fields are present. The error returned from the server
// will contain all unknown and duplicate fields encountered.
FieldValidation string
// Raw represents raw UpdateOptions, as passed to the API server.
Raw *metav1.UpdateOptions
}
@@ -695,6 +768,7 @@ func (o *UpdateOptions) AsUpdateOptions() *metav1.UpdateOptions {
o.Raw.DryRun = o.DryRun
o.Raw.FieldManager = o.FieldManager
o.Raw.FieldValidation = o.FieldValidation
return o.Raw
}
@@ -717,6 +791,9 @@ func (o *UpdateOptions) ApplyToUpdate(uo *UpdateOptions) {
if o.FieldManager != "" {
uo.FieldManager = o.FieldManager
}
if o.FieldValidation != "" {
uo.FieldValidation = o.FieldValidation
}
if o.Raw != nil {
uo.Raw = o.Raw
}
@@ -745,6 +822,24 @@ type PatchOptions struct {
// this request. It must be set with server-side apply.
FieldManager string
// fieldValidation instructs the server on how to handle
// objects in the request (POST/PUT/PATCH) containing unknown
// or duplicate fields. Valid values are:
// - Ignore: This will ignore any unknown fields that are silently
// dropped from the object, and will ignore all but the last duplicate
// field that the decoder encounters. This is the default behavior
// prior to v1.23.
// - Warn: This will send a warning via the standard warning response
// header for each unknown field that is dropped from the object, and
// for each duplicate field that is encountered. The request will
// still succeed if there are no other errors, and will only persist
// the last of any duplicate fields. This is the default in v1.23+
// - Strict: This will fail the request with a BadRequest error if
// any unknown fields would be dropped from the object, or if any
// duplicate fields are present. The error returned from the server
// will contain all unknown and duplicate fields encountered.
FieldValidation string
// Raw represents raw PatchOptions, as passed to the API server.
Raw *metav1.PatchOptions
}
@@ -771,6 +866,7 @@ func (o *PatchOptions) AsPatchOptions() *metav1.PatchOptions {
o.Raw.DryRun = o.DryRun
o.Raw.Force = o.Force
o.Raw.FieldManager = o.FieldManager
o.Raw.FieldValidation = o.FieldValidation
return o.Raw
}
@@ -787,6 +883,9 @@ func (o *PatchOptions) ApplyToPatch(po *PatchOptions) {
if o.FieldManager != "" {
po.FieldManager = o.FieldManager
}
if o.FieldValidation != "" {
po.FieldValidation = o.FieldValidation
}
if o.Raw != nil {
po.Raw = o.Raw
}

View File

@@ -1,112 +0,0 @@
/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
import (
"fmt"
"os"
"sync"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"sigs.k8s.io/controller-runtime/pkg/config/v1alpha1"
)
// ControllerManagerConfiguration defines the functions necessary to parse a config file
// and to configure the Options struct for the ctrl.Manager.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
type ControllerManagerConfiguration interface {
runtime.Object
// Complete returns the versioned configuration
Complete() (v1alpha1.ControllerManagerConfigurationSpec, error)
}
// DeferredFileLoader is used to configure the decoder for loading controller
// runtime component config types.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
type DeferredFileLoader struct {
ControllerManagerConfiguration
path string
scheme *runtime.Scheme
once sync.Once
err error
}
// File will set up the deferred file loader for the configuration
// this will also configure the defaults for the loader if nothing is
//
// Defaults:
// * Path: "./config.yaml"
// * Kind: GenericControllerManagerConfiguration
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
func File() *DeferredFileLoader {
scheme := runtime.NewScheme()
utilruntime.Must(v1alpha1.AddToScheme(scheme))
return &DeferredFileLoader{
path: "./config.yaml",
ControllerManagerConfiguration: &v1alpha1.ControllerManagerConfiguration{},
scheme: scheme,
}
}
// Complete will use sync.Once to set the scheme.
func (d *DeferredFileLoader) Complete() (v1alpha1.ControllerManagerConfigurationSpec, error) {
d.once.Do(d.loadFile)
if d.err != nil {
return v1alpha1.ControllerManagerConfigurationSpec{}, d.err
}
return d.ControllerManagerConfiguration.Complete()
}
// AtPath will set the path to load the file for the decoder.
func (d *DeferredFileLoader) AtPath(path string) *DeferredFileLoader {
d.path = path
return d
}
// OfKind will set the type to be used for decoding the file into.
func (d *DeferredFileLoader) OfKind(obj ControllerManagerConfiguration) *DeferredFileLoader {
d.ControllerManagerConfiguration = obj
return d
}
// loadFile is used from the mutex.Once to load the file.
func (d *DeferredFileLoader) loadFile() {
if d.scheme == nil {
d.err = fmt.Errorf("scheme not supplied to controller configuration loader")
return
}
content, err := os.ReadFile(d.path)
if err != nil {
d.err = fmt.Errorf("could not read file at %s", d.path)
return
}
codecs := serializer.NewCodecFactory(d.scheme)
// Regardless of if the bytes are of any external version,
// it will be read successfully and converted into the internal version
if err = runtime.DecodeInto(codecs.UniversalDecoder(), content, d.ControllerManagerConfiguration); err != nil {
d.err = fmt.Errorf("could not decode file into runtime.Object")
}
}

View File

@@ -20,6 +20,12 @@ import "time"
// Controller contains configuration options for a controller.
type Controller struct {
// SkipNameValidation allows skipping the name validation that ensures that every controller name is unique.
// Unique controller names are important to get unique metrics and logs for a controller.
// Can be overwritten for a controller via the SkipNameValidation setting on the controller.
// Defaults to false if SkipNameValidation setting on controller and Manager are unset.
SkipNameValidation *bool
// GroupKindConcurrency is a map from a Kind to the number of concurrent reconciliation
// allowed for that controller.
//
@@ -40,7 +46,8 @@ type Controller struct {
CacheSyncTimeout time.Duration
// RecoverPanic indicates whether the panic caused by reconcile should be recovered.
// Defaults to the Controller.RecoverPanic setting from the Manager if unset.
// Can be overwritten for a controller via the RecoverPanic setting on the controller.
// Defaults to true if RecoverPanic setting on controller and Manager are unset.
RecoverPanic *bool
// NeedLeaderElection indicates whether the controller needs to use leader election.

View File

@@ -1,19 +0,0 @@
/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package config contains functionality for interacting with
// configuration for controller-runtime components.
package config

View File

@@ -1,22 +0,0 @@
/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package v1alpha1 provides the ControllerManagerConfiguration used for
// configuring ctrl.Manager
// +kubebuilder:object:generate=true
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
package v1alpha1

View File

@@ -1,43 +0,0 @@
/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
// GroupVersion is group version used to register these objects.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
GroupVersion = schema.GroupVersion{Group: "controller-runtime.sigs.k8s.io", Version: "v1alpha1"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
AddToScheme = SchemeBuilder.AddToScheme
)
func init() {
SchemeBuilder.Register(&ControllerManagerConfiguration{})
}

View File

@@ -1,179 +0,0 @@
/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
configv1alpha1 "k8s.io/component-base/config/v1alpha1"
)
// ControllerManagerConfigurationSpec defines the desired state of GenericControllerManagerConfiguration.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
type ControllerManagerConfigurationSpec struct {
// SyncPeriod determines the minimum frequency at which watched resources are
// reconciled. A lower period will correct entropy more quickly, but reduce
// responsiveness to change if there are many watched resources. Change this
// value only if you know what you are doing. Defaults to 10 hours if unset.
// there will a 10 percent jitter between the SyncPeriod of all controllers
// so that all controllers will not send list requests simultaneously.
// +optional
SyncPeriod *metav1.Duration `json:"syncPeriod,omitempty"`
// LeaderElection is the LeaderElection config to be used when configuring
// the manager.Manager leader election
// +optional
LeaderElection *configv1alpha1.LeaderElectionConfiguration `json:"leaderElection,omitempty"`
// CacheNamespace if specified restricts the manager's cache to watch objects in
// the desired namespace Defaults to all namespaces
//
// Note: If a namespace is specified, controllers can still Watch for a
// cluster-scoped resource (e.g Node). For namespaced resources the cache
// will only hold objects from the desired namespace.
// +optional
CacheNamespace string `json:"cacheNamespace,omitempty"`
// GracefulShutdownTimeout is the duration given to runnable to stop before the manager actually returns on stop.
// To disable graceful shutdown, set to time.Duration(0)
// To use graceful shutdown without timeout, set to a negative duration, e.G. time.Duration(-1)
// The graceful shutdown is skipped for safety reasons in case the leader election lease is lost.
GracefulShutdownTimeout *metav1.Duration `json:"gracefulShutDown,omitempty"`
// Controller contains global configuration options for controllers
// registered within this manager.
// +optional
Controller *ControllerConfigurationSpec `json:"controller,omitempty"`
// Metrics contains the controller metrics configuration
// +optional
Metrics ControllerMetrics `json:"metrics,omitempty"`
// Health contains the controller health configuration
// +optional
Health ControllerHealth `json:"health,omitempty"`
// Webhook contains the controllers webhook configuration
// +optional
Webhook ControllerWebhook `json:"webhook,omitempty"`
}
// ControllerConfigurationSpec defines the global configuration for
// controllers registered with the manager.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
//
// Deprecated: Controller global configuration can now be set at the manager level,
// using the manager.Options.Controller field.
type ControllerConfigurationSpec struct {
// GroupKindConcurrency is a map from a Kind to the number of concurrent reconciliation
// allowed for that controller.
//
// When a controller is registered within this manager using the builder utilities,
// users have to specify the type the controller reconciles in the For(...) call.
// If the object's kind passed matches one of the keys in this map, the concurrency
// for that controller is set to the number specified.
//
// The key is expected to be consistent in form with GroupKind.String(),
// e.g. ReplicaSet in apps group (regardless of version) would be `ReplicaSet.apps`.
//
// +optional
GroupKindConcurrency map[string]int `json:"groupKindConcurrency,omitempty"`
// CacheSyncTimeout refers to the time limit set to wait for syncing caches.
// Defaults to 2 minutes if not set.
// +optional
CacheSyncTimeout *time.Duration `json:"cacheSyncTimeout,omitempty"`
// RecoverPanic indicates if panics should be recovered.
// +optional
RecoverPanic *bool `json:"recoverPanic,omitempty"`
}
// ControllerMetrics defines the metrics configs.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
type ControllerMetrics struct {
// BindAddress is the TCP address that the controller should bind to
// for serving prometheus metrics.
// It can be set to "0" to disable the metrics serving.
// +optional
BindAddress string `json:"bindAddress,omitempty"`
}
// ControllerHealth defines the health configs.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
type ControllerHealth struct {
// HealthProbeBindAddress is the TCP address that the controller should bind to
// for serving health probes
// It can be set to "0" or "" to disable serving the health probe.
// +optional
HealthProbeBindAddress string `json:"healthProbeBindAddress,omitempty"`
// ReadinessEndpointName, defaults to "readyz"
// +optional
ReadinessEndpointName string `json:"readinessEndpointName,omitempty"`
// LivenessEndpointName, defaults to "healthz"
// +optional
LivenessEndpointName string `json:"livenessEndpointName,omitempty"`
}
// ControllerWebhook defines the webhook server for the controller.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
type ControllerWebhook struct {
// Port is the port that the webhook server serves at.
// It is used to set webhook.Server.Port.
// +optional
Port *int `json:"port,omitempty"`
// Host is the hostname that the webhook server binds to.
// It is used to set webhook.Server.Host.
// +optional
Host string `json:"host,omitempty"`
// CertDir is the directory that contains the server key and certificate.
// if not set, webhook server would look up the server key and certificate in
// {TempDir}/k8s-webhook-server/serving-certs. The server key and certificate
// must be named tls.key and tls.crt, respectively.
// +optional
CertDir string `json:"certDir,omitempty"`
}
// +kubebuilder:object:root=true
// ControllerManagerConfiguration is the Schema for the GenericControllerManagerConfigurations API.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
type ControllerManagerConfiguration struct {
metav1.TypeMeta `json:",inline"`
// ControllerManagerConfiguration returns the contfigurations for controllers
ControllerManagerConfigurationSpec `json:",inline"`
}
// Complete returns the configuration for controller-runtime.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
func (c *ControllerManagerConfigurationSpec) Complete() (ControllerManagerConfigurationSpec, error) {
return *c, nil
}

View File

@@ -1,157 +0,0 @@
//go:build !ignore_autogenerated
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
"k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
configv1alpha1 "k8s.io/component-base/config/v1alpha1"
timex "time"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ControllerConfigurationSpec) DeepCopyInto(out *ControllerConfigurationSpec) {
*out = *in
if in.GroupKindConcurrency != nil {
in, out := &in.GroupKindConcurrency, &out.GroupKindConcurrency
*out = make(map[string]int, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.CacheSyncTimeout != nil {
in, out := &in.CacheSyncTimeout, &out.CacheSyncTimeout
*out = new(timex.Duration)
**out = **in
}
if in.RecoverPanic != nil {
in, out := &in.RecoverPanic, &out.RecoverPanic
*out = new(bool)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerConfigurationSpec.
func (in *ControllerConfigurationSpec) DeepCopy() *ControllerConfigurationSpec {
if in == nil {
return nil
}
out := new(ControllerConfigurationSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ControllerHealth) DeepCopyInto(out *ControllerHealth) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerHealth.
func (in *ControllerHealth) DeepCopy() *ControllerHealth {
if in == nil {
return nil
}
out := new(ControllerHealth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ControllerManagerConfiguration) DeepCopyInto(out *ControllerManagerConfiguration) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ControllerManagerConfigurationSpec.DeepCopyInto(&out.ControllerManagerConfigurationSpec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerManagerConfiguration.
func (in *ControllerManagerConfiguration) DeepCopy() *ControllerManagerConfiguration {
if in == nil {
return nil
}
out := new(ControllerManagerConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ControllerManagerConfiguration) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ControllerManagerConfigurationSpec) DeepCopyInto(out *ControllerManagerConfigurationSpec) {
*out = *in
if in.SyncPeriod != nil {
in, out := &in.SyncPeriod, &out.SyncPeriod
*out = new(v1.Duration)
**out = **in
}
if in.LeaderElection != nil {
in, out := &in.LeaderElection, &out.LeaderElection
*out = new(configv1alpha1.LeaderElectionConfiguration)
(*in).DeepCopyInto(*out)
}
if in.GracefulShutdownTimeout != nil {
in, out := &in.GracefulShutdownTimeout, &out.GracefulShutdownTimeout
*out = new(v1.Duration)
**out = **in
}
if in.Controller != nil {
in, out := &in.Controller, &out.Controller
*out = new(ControllerConfigurationSpec)
(*in).DeepCopyInto(*out)
}
out.Metrics = in.Metrics
out.Health = in.Health
in.Webhook.DeepCopyInto(&out.Webhook)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerManagerConfigurationSpec.
func (in *ControllerManagerConfigurationSpec) DeepCopy() *ControllerManagerConfigurationSpec {
if in == nil {
return nil
}
out := new(ControllerManagerConfigurationSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ControllerMetrics) DeepCopyInto(out *ControllerMetrics) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerMetrics.
func (in *ControllerMetrics) DeepCopy() *ControllerMetrics {
if in == nil {
return nil
}
out := new(ControllerMetrics)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ControllerWebhook) DeepCopyInto(out *ControllerWebhook) {
*out = *in
if in.Port != nil {
in, out := &in.Port, &out.Port
*out = new(int)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerWebhook.
func (in *ControllerWebhook) DeepCopy() *ControllerWebhook {
if in == nil {
return nil
}
out := new(ControllerWebhook)
in.DeepCopyInto(out)
return out
}

View File

@@ -25,17 +25,23 @@ import (
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/internal/controller"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/ratelimiter"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)
// Options are the arguments for creating a new Controller.
type Options struct {
type Options = TypedOptions[reconcile.Request]
// TypedOptions are the arguments for creating a new Controller.
type TypedOptions[request comparable] struct {
// SkipNameValidation allows skipping the name validation that ensures that every controller name is unique.
// Unique controller names are important to get unique metrics and logs for a controller.
// Defaults to the Controller.SkipNameValidation setting from the Manager if unset.
// Defaults to false if Controller.SkipNameValidation setting from the Manager is also unset.
SkipNameValidation *bool
// MaxConcurrentReconciles is the maximum number of concurrent Reconciles which can be run. Defaults to 1.
MaxConcurrentReconciles int
@@ -45,6 +51,7 @@ type Options struct {
// RecoverPanic indicates whether the panic caused by reconcile should be recovered.
// Defaults to the Controller.RecoverPanic setting from the Manager if unset.
// Defaults to true if Controller.RecoverPanic setting from the Manager is also unset.
RecoverPanic *bool
// NeedLeaderElection indicates whether the controller needs to use leader election.
@@ -52,33 +59,43 @@ type Options struct {
NeedLeaderElection *bool
// Reconciler reconciles an object
Reconciler reconcile.Reconciler
Reconciler reconcile.TypedReconciler[request]
// RateLimiter is used to limit how frequently requests may be queued.
// Defaults to MaxOfRateLimiter which has both overall and per-item rate limiting.
// The overall is a token bucket and the per-item is exponential.
RateLimiter ratelimiter.RateLimiter
RateLimiter workqueue.TypedRateLimiter[request]
// NewQueue constructs the queue for this controller once the controller is ready to start.
// With NewQueue a custom queue implementation can be used, e.g. a priority queue to prioritize with which
// priority/order objects are reconciled (e.g. to reconcile objects with changes first).
// This is a func because the standard Kubernetes work queues start themselves immediately, which
// leads to goroutine leaks if something calls controller.New repeatedly.
// The NewQueue func gets the controller name and the RateLimiter option (defaulted if necessary) passed in.
// NewQueue defaults to NewRateLimitingQueueWithConfig.
//
// NOTE: LOW LEVEL PRIMITIVE!
// Only use a custom NewQueue if you know what you are doing.
NewQueue func(controllerName string, rateLimiter workqueue.TypedRateLimiter[request]) workqueue.TypedRateLimitingInterface[request]
// LogConstructor is used to construct a logger used for this controller and passed
// to each reconciliation via the context field.
LogConstructor func(request *reconcile.Request) logr.Logger
LogConstructor func(request *request) logr.Logger
}
// Controller implements a Kubernetes API. A Controller manages a work queue fed reconcile.Requests
// from source.Sources. Work is performed through the reconcile.Reconciler for each enqueued item.
// Work typically is reads and writes Kubernetes objects to make the system state match the state specified
// in the object Spec.
type Controller interface {
// Reconciler is called to reconcile an object by Namespace/Name
reconcile.Reconciler
type Controller = TypedController[reconcile.Request]
// Watch takes events provided by a Source and uses the EventHandler to
// enqueue reconcile.Requests in response to the events.
//
// Watch may be provided one or more Predicates to filter events before
// they are given to the EventHandler. Events will be passed to the
// EventHandler if all provided Predicates evaluate to true.
Watch(src source.Source, eventhandler handler.EventHandler, predicates ...predicate.Predicate) error
// TypedController implements an API.
type TypedController[request comparable] interface {
// Reconciler is called to reconcile an object by Namespace/Name
reconcile.TypedReconciler[request]
// Watch watches the provided Source.
Watch(src source.TypedSource[request]) error
// Start starts the controller. Start blocks until the context is closed or a
// controller has an error starting.
@@ -90,8 +107,17 @@ type Controller interface {
// New returns a new Controller registered with the Manager. The Manager will ensure that shared Caches have
// been synced before the Controller is Started.
//
// The name must be unique as it is used to identify the controller in metrics and logs.
func New(name string, mgr manager.Manager, options Options) (Controller, error) {
c, err := NewUnmanaged(name, mgr, options)
return NewTyped(name, mgr, options)
}
// NewTyped returns a new typed controller registered with the Manager,
//
// The name must be unique as it is used to identify the controller in metrics and logs.
func NewTyped[request comparable](name string, mgr manager.Manager, options TypedOptions[request]) (TypedController[request], error) {
c, err := NewTypedUnmanaged(name, mgr, options)
if err != nil {
return nil, err
}
@@ -102,7 +128,16 @@ func New(name string, mgr manager.Manager, options Options) (Controller, error)
// NewUnmanaged returns a new controller without adding it to the manager. The
// caller is responsible for starting the returned controller.
//
// The name must be unique as it is used to identify the controller in metrics and logs.
func NewUnmanaged(name string, mgr manager.Manager, options Options) (Controller, error) {
return NewTypedUnmanaged(name, mgr, options)
}
// NewTypedUnmanaged returns a new typed controller without adding it to the manager.
//
// The name must be unique as it is used to identify the controller in metrics and logs.
func NewTypedUnmanaged[request comparable](name string, mgr manager.Manager, options TypedOptions[request]) (TypedController[request], error) {
if options.Reconciler == nil {
return nil, fmt.Errorf("must specify Reconciler")
}
@@ -111,13 +146,23 @@ func NewUnmanaged(name string, mgr manager.Manager, options Options) (Controller
return nil, fmt.Errorf("must specify Name for Controller")
}
if options.SkipNameValidation == nil {
options.SkipNameValidation = mgr.GetControllerOptions().SkipNameValidation
}
if options.SkipNameValidation == nil || !*options.SkipNameValidation {
if err := checkName(name); err != nil {
return nil, err
}
}
if options.LogConstructor == nil {
log := mgr.GetLogger().WithValues(
"controller", name,
)
options.LogConstructor = func(req *reconcile.Request) logr.Logger {
options.LogConstructor = func(in *request) logr.Logger {
log := log
if req != nil {
if req, ok := any(in).(*reconcile.Request); ok && req != nil {
log = log.WithValues(
"object", klog.KRef(req.Namespace, req.Name),
"namespace", req.Namespace, "name", req.Name,
@@ -144,7 +189,15 @@ func NewUnmanaged(name string, mgr manager.Manager, options Options) (Controller
}
if options.RateLimiter == nil {
options.RateLimiter = workqueue.DefaultControllerRateLimiter()
options.RateLimiter = workqueue.DefaultTypedControllerRateLimiter[request]()
}
if options.NewQueue == nil {
options.NewQueue = func(controllerName string, rateLimiter workqueue.TypedRateLimiter[request]) workqueue.TypedRateLimitingInterface[request] {
return workqueue.NewTypedRateLimitingQueueWithConfig(rateLimiter, workqueue.TypedRateLimitingQueueConfig[request]{
Name: controllerName,
})
}
}
if options.RecoverPanic == nil {
@@ -156,13 +209,10 @@ func NewUnmanaged(name string, mgr manager.Manager, options Options) (Controller
}
// Create controller with dependencies set
return &controller.Controller{
Do: options.Reconciler,
MakeQueue: func() workqueue.RateLimitingInterface {
return workqueue.NewRateLimitingQueueWithConfig(options.RateLimiter, workqueue.RateLimitingQueueConfig{
Name: name,
})
},
return &controller.Controller[request]{
Do: options.Reconciler,
RateLimiter: options.RateLimiter,
NewQueue: options.NewQueue,
MaxConcurrentReconciles: options.MaxConcurrentReconciles,
CacheSyncTimeout: options.CacheSyncTimeout,
Name: name,

View File

@@ -23,6 +23,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
var _ runtime.Object = &ErrorType{}
@@ -36,23 +37,27 @@ func (ErrorType) GetObjectKind() schema.ObjectKind { return nil }
// DeepCopyObject implements runtime.Object.
func (ErrorType) DeepCopyObject() runtime.Object { return nil }
var _ workqueue.RateLimitingInterface = &Queue{}
var _ workqueue.TypedRateLimitingInterface[reconcile.Request] = &Queue{}
// Queue implements a RateLimiting queue as a non-ratelimited queue for testing.
// This helps testing by having functions that use a RateLimiting queue synchronously add items to the queue.
type Queue struct {
workqueue.Interface
type Queue = TypedQueue[reconcile.Request]
// TypedQueue implements a RateLimiting queue as a non-ratelimited queue for testing.
// This helps testing by having functions that use a RateLimiting queue synchronously add items to the queue.
type TypedQueue[request comparable] struct {
workqueue.TypedInterface[request]
AddedRateLimitedLock sync.Mutex
AddedRatelimited []any
}
// AddAfter implements RateLimitingInterface.
func (q *Queue) AddAfter(item interface{}, duration time.Duration) {
func (q *TypedQueue[request]) AddAfter(item request, duration time.Duration) {
q.Add(item)
}
// AddRateLimited implements RateLimitingInterface. TODO(community): Implement this.
func (q *Queue) AddRateLimited(item interface{}) {
func (q *TypedQueue[request]) AddRateLimited(item request) {
q.AddedRateLimitedLock.Lock()
q.AddedRatelimited = append(q.AddedRatelimited, item)
q.AddedRateLimitedLock.Unlock()
@@ -60,9 +65,9 @@ func (q *Queue) AddRateLimited(item interface{}) {
}
// Forget implements RateLimitingInterface. TODO(community): Implement this.
func (q *Queue) Forget(item interface{}) {}
func (q *TypedQueue[request]) Forget(item request) {}
// NumRequeues implements RateLimitingInterface. TODO(community): Implement this.
func (q *Queue) NumRequeues(item interface{}) int {
func (q *TypedQueue[request]) NumRequeues(item request) int {
return 0
}

View File

@@ -52,12 +52,22 @@ func newAlreadyOwnedError(obj metav1.Object, owner metav1.OwnerReference) *Alrea
}
}
// OwnerReferenceOption is a function that can modify a `metav1.OwnerReference`.
type OwnerReferenceOption func(*metav1.OwnerReference)
// WithBlockOwnerDeletion allows configuring the BlockOwnerDeletion field on the `metav1.OwnerReference`.
func WithBlockOwnerDeletion(blockOwnerDeletion bool) OwnerReferenceOption {
return func(ref *metav1.OwnerReference) {
ref.BlockOwnerDeletion = &blockOwnerDeletion
}
}
// SetControllerReference sets owner as a Controller OwnerReference on controlled.
// This is used for garbage collection of the controlled object and for
// reconciling the owner object on changes to controlled (with a Watch + EnqueueRequestForOwner).
// Since only one OwnerReference can be a controller, it returns an error if
// there is another OwnerReference with Controller flag set.
func SetControllerReference(owner, controlled metav1.Object, scheme *runtime.Scheme) error {
func SetControllerReference(owner, controlled metav1.Object, scheme *runtime.Scheme, opts ...OwnerReferenceOption) error {
// Validate the owner.
ro, ok := owner.(runtime.Object)
if !ok {
@@ -80,6 +90,9 @@ func SetControllerReference(owner, controlled metav1.Object, scheme *runtime.Sch
BlockOwnerDeletion: ptr.To(true),
Controller: ptr.To(true),
}
for _, opt := range opts {
opt(&ref)
}
// Return early with an error if the object is already controlled.
if existing := metav1.GetControllerOf(controlled); existing != nil && !referSameObject(*existing, ref) {
@@ -94,7 +107,7 @@ func SetControllerReference(owner, controlled metav1.Object, scheme *runtime.Sch
// SetOwnerReference is a helper method to make sure the given object contains an object reference to the object provided.
// This allows you to declare that owner has a dependency on the object without specifying it as a controller.
// If a reference to the same object already exists, it'll be overwritten with the newly provided version.
func SetOwnerReference(owner, object metav1.Object, scheme *runtime.Scheme) error {
func SetOwnerReference(owner, object metav1.Object, scheme *runtime.Scheme, opts ...OwnerReferenceOption) error {
// Validate the owner.
ro, ok := owner.(runtime.Object)
if !ok {
@@ -115,6 +128,9 @@ func SetOwnerReference(owner, object metav1.Object, scheme *runtime.Scheme) erro
UID: owner.GetUID(),
Name: owner.GetName(),
}
for _, opt := range opts {
opt(&ref)
}
// Update owner references and return.
upsertOwnerRef(ref, object)

View File

@@ -14,9 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/*
Package ratelimiter defines rate limiters used by Controllers to limit how frequently requests may be queued.
package controller
Typical rate limiters that can be used are implemented in client-go's workqueue package.
*/
package ratelimiter
import (
"fmt"
"sync"
"k8s.io/apimachinery/pkg/util/sets"
)
var nameLock sync.Mutex
var usedNames sets.Set[string]
func checkName(name string) error {
nameLock.Lock()
defer nameLock.Unlock()
if usedNames == nil {
usedNames = sets.Set[string]{}
}
if usedNames.Has(name) {
return fmt.Errorf("controller with name %s already exists. Controller names must be unique to avoid multiple controllers reporting to the same metric", name)
}
usedNames.Insert(name)
return nil
}

View File

@@ -17,15 +17,20 @@ limitations under the License.
package envtest
import (
"context"
"fmt"
"os"
"strings"
"time"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"
logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
@@ -265,6 +270,12 @@ func (te *Environment) Start() (*rest.Config, error) {
te.Scheme = scheme.Scheme
}
// If we are bringing etcd up for the first time, it can take some time for the
// default namespace to actually be created and seen as available to the apiserver
if err := te.waitForDefaultNamespace(te.Config); err != nil {
return nil, fmt.Errorf("default namespace didn't register within deadline: %w", err)
}
// Call PrepWithoutInstalling to setup certificates first
// and have them available to patch CRD conversion webhook as well.
if err := te.WebhookInstallOptions.PrepWithoutInstalling(); err != nil {
@@ -322,6 +333,20 @@ func (te *Environment) startControlPlane() error {
return nil
}
func (te *Environment) waitForDefaultNamespace(config *rest.Config) error {
cs, err := client.New(config, client.Options{})
if err != nil {
return fmt.Errorf("unable to create client: %w", err)
}
// It shouldn't take longer than 5s for the default namespace to be brought up in etcd
return wait.PollUntilContextTimeout(context.TODO(), time.Millisecond*50, time.Second*5, true, func(ctx context.Context) (bool, error) {
if err = cs.Get(ctx, types.NamespacedName{Name: "default"}, &corev1.Namespace{}); err != nil {
return false, nil //nolint:nilerr
}
return true, nil
})
}
func (te *Environment) defaultTimeouts() error {
var err error
if te.ControlPlaneStartTimeout == 0 {

View File

@@ -18,38 +18,55 @@ package event
import "sigs.k8s.io/controller-runtime/pkg/client"
// CreateEvent is an event where a Kubernetes object was created. CreateEvent should be generated
// CreateEvent is an event where a Kubernetes object was created. CreateEvent should be generated
// by a source.Source and transformed into a reconcile.Request by a handler.EventHandler.
type CreateEvent = TypedCreateEvent[client.Object]
// UpdateEvent is an event where a Kubernetes object was updated. UpdateEvent should be generated
// by a source.Source and transformed into a reconcile.Request by an handler.EventHandler.
type CreateEvent struct {
type UpdateEvent = TypedUpdateEvent[client.Object]
// DeleteEvent is an event where a Kubernetes object was deleted. DeleteEvent should be generated
// by a source.Source and transformed into a reconcile.Request by an handler.EventHandler.
type DeleteEvent = TypedDeleteEvent[client.Object]
// GenericEvent is an event where the operation type is unknown (e.g. polling or event originating outside the cluster).
// GenericEvent should be generated by a source.Source and transformed into a reconcile.Request by an
// handler.EventHandler.
type GenericEvent = TypedGenericEvent[client.Object]
// TypedCreateEvent is an event where a Kubernetes object was created. TypedCreateEvent should be generated
// by a source.Source and transformed into a reconcile.Request by an handler.TypedEventHandler.
type TypedCreateEvent[object any] struct {
// Object is the object from the event
Object client.Object
Object object
}
// UpdateEvent is an event where a Kubernetes object was updated. UpdateEvent should be generated
// by a source.Source and transformed into a reconcile.Request by an handler.EventHandler.
type UpdateEvent struct {
// TypedUpdateEvent is an event where a Kubernetes object was updated. TypedUpdateEvent should be generated
// by a source.Source and transformed into a reconcile.Request by an handler.TypedEventHandler.
type TypedUpdateEvent[object any] struct {
// ObjectOld is the object from the event
ObjectOld client.Object
ObjectOld object
// ObjectNew is the object from the event
ObjectNew client.Object
ObjectNew object
}
// DeleteEvent is an event where a Kubernetes object was deleted. DeleteEvent should be generated
// by a source.Source and transformed into a reconcile.Request by an handler.EventHandler.
type DeleteEvent struct {
// TypedDeleteEvent is an event where a Kubernetes object was deleted. TypedDeleteEvent should be generated
// by a source.Source and transformed into a reconcile.Request by an handler.TypedEventHandler.
type TypedDeleteEvent[object any] struct {
// Object is the object from the event
Object client.Object
Object object
// DeleteStateUnknown is true if the Delete event was missed but we identified the object
// as having been deleted.
DeleteStateUnknown bool
}
// GenericEvent is an event where the operation type is unknown (e.g. polling or event originating outside the cluster).
// GenericEvent should be generated by a source.Source and transformed into a reconcile.Request by an
// handler.EventHandler.
type GenericEvent struct {
// TypedGenericEvent is an event where the operation type is unknown (e.g. polling or event originating outside the cluster).
// TypedGenericEvent should be generated by a source.Source and transformed into a reconcile.Request by an
// handler.TypedEventHandler.
type TypedGenericEvent[object any] struct {
// Object is the object from the event
Object client.Object
Object object
}

View File

@@ -18,9 +18,11 @@ package handler
import (
"context"
"reflect"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
@@ -33,13 +35,20 @@ type empty struct{}
var _ EventHandler = &EnqueueRequestForObject{}
// EnqueueRequestForObject enqueues a Request containing the Name and Namespace of the object that is the source of the Event.
// (e.g. the created / deleted / updated objects Name and Namespace). handler.EnqueueRequestForObject is used by almost all
// (e.g. the created / deleted / updated objects Name and Namespace). handler.EnqueueRequestForObject is used by almost all
// Controllers that have associated Resources (e.g. CRDs) to reconcile the associated Resource.
type EnqueueRequestForObject struct{}
type EnqueueRequestForObject = TypedEnqueueRequestForObject[client.Object]
// TypedEnqueueRequestForObject enqueues a Request containing the Name and Namespace of the object that is the source of the Event.
// (e.g. the created / deleted / updated objects Name and Namespace). handler.TypedEnqueueRequestForObject is used by almost all
// Controllers that have associated Resources (e.g. CRDs) to reconcile the associated Resource.
//
// TypedEnqueueRequestForObject is experimental and subject to future change.
type TypedEnqueueRequestForObject[object client.Object] struct{}
// Create implements EventHandler.
func (e *EnqueueRequestForObject) Create(ctx context.Context, evt event.CreateEvent, q workqueue.RateLimitingInterface) {
if evt.Object == nil {
func (e *TypedEnqueueRequestForObject[T]) Create(ctx context.Context, evt event.TypedCreateEvent[T], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
if isNil(evt.Object) {
enqueueLog.Error(nil, "CreateEvent received with no metadata", "event", evt)
return
}
@@ -50,14 +59,14 @@ func (e *EnqueueRequestForObject) Create(ctx context.Context, evt event.CreateEv
}
// Update implements EventHandler.
func (e *EnqueueRequestForObject) Update(ctx context.Context, evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
func (e *TypedEnqueueRequestForObject[T]) Update(ctx context.Context, evt event.TypedUpdateEvent[T], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
switch {
case evt.ObjectNew != nil:
case !isNil(evt.ObjectNew):
q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
Name: evt.ObjectNew.GetName(),
Namespace: evt.ObjectNew.GetNamespace(),
}})
case evt.ObjectOld != nil:
case !isNil(evt.ObjectOld):
q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
Name: evt.ObjectOld.GetName(),
Namespace: evt.ObjectOld.GetNamespace(),
@@ -68,8 +77,8 @@ func (e *EnqueueRequestForObject) Update(ctx context.Context, evt event.UpdateEv
}
// Delete implements EventHandler.
func (e *EnqueueRequestForObject) Delete(ctx context.Context, evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
if evt.Object == nil {
func (e *TypedEnqueueRequestForObject[T]) Delete(ctx context.Context, evt event.TypedDeleteEvent[T], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
if isNil(evt.Object) {
enqueueLog.Error(nil, "DeleteEvent received with no metadata", "event", evt)
return
}
@@ -80,8 +89,8 @@ func (e *EnqueueRequestForObject) Delete(ctx context.Context, evt event.DeleteEv
}
// Generic implements EventHandler.
func (e *EnqueueRequestForObject) Generic(ctx context.Context, evt event.GenericEvent, q workqueue.RateLimitingInterface) {
if evt.Object == nil {
func (e *TypedEnqueueRequestForObject[T]) Generic(ctx context.Context, evt event.TypedGenericEvent[T], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
if isNil(evt.Object) {
enqueueLog.Error(nil, "GenericEvent received with no metadata", "event", evt)
return
}
@@ -90,3 +99,15 @@ func (e *EnqueueRequestForObject) Generic(ctx context.Context, evt event.Generic
Namespace: evt.Object.GetNamespace(),
}})
}
func isNil(arg any) bool {
if v := reflect.ValueOf(arg); !v.IsValid() || ((v.Kind() == reflect.Ptr ||
v.Kind() == reflect.Interface ||
v.Kind() == reflect.Slice ||
v.Kind() == reflect.Map ||
v.Kind() == reflect.Chan ||
v.Kind() == reflect.Func) && v.IsNil()) {
return true
}
return false
}

View File

@@ -27,7 +27,13 @@ import (
// MapFunc is the signature required for enqueueing requests from a generic function.
// This type is usually used with EnqueueRequestsFromMapFunc when registering an event handler.
type MapFunc func(context.Context, client.Object) []reconcile.Request
type MapFunc = TypedMapFunc[client.Object, reconcile.Request]
// TypedMapFunc is the signature required for enqueueing requests from a generic function.
// This type is usually used with EnqueueRequestsFromTypedMapFunc when registering an event handler.
//
// TypedMapFunc is experimental and subject to future change.
type TypedMapFunc[object any, request comparable] func(context.Context, object) []request
// EnqueueRequestsFromMapFunc enqueues Requests by running a transformation function that outputs a collection
// of reconcile.Requests on each Event. The reconcile.Requests may be for an arbitrary set of objects
@@ -40,45 +46,77 @@ type MapFunc func(context.Context, client.Object) []reconcile.Request
// For UpdateEvents which contain both a new and old object, the transformation function is run on both
// objects and both sets of Requests are enqueue.
func EnqueueRequestsFromMapFunc(fn MapFunc) EventHandler {
return &enqueueRequestsFromMapFunc{
return TypedEnqueueRequestsFromMapFunc(fn)
}
// TypedEnqueueRequestsFromMapFunc enqueues Requests by running a transformation function that outputs a collection
// of reconcile.Requests on each Event. The reconcile.Requests may be for an arbitrary set of objects
// defined by some user specified transformation of the source Event. (e.g. trigger Reconciler for a set of objects
// in response to a cluster resize event caused by adding or deleting a Node)
//
// TypedEnqueueRequestsFromMapFunc is frequently used to fan-out updates from one object to one or more other
// objects of a differing type.
//
// For TypedUpdateEvents which contain both a new and old object, the transformation function is run on both
// objects and both sets of Requests are enqueue.
//
// TypedEnqueueRequestsFromMapFunc is experimental and subject to future change.
func TypedEnqueueRequestsFromMapFunc[object any, request comparable](fn TypedMapFunc[object, request]) TypedEventHandler[object, request] {
return &enqueueRequestsFromMapFunc[object, request]{
toRequests: fn,
}
}
var _ EventHandler = &enqueueRequestsFromMapFunc{}
var _ EventHandler = &enqueueRequestsFromMapFunc[client.Object, reconcile.Request]{}
type enqueueRequestsFromMapFunc struct {
type enqueueRequestsFromMapFunc[object any, request comparable] struct {
// Mapper transforms the argument into a slice of keys to be reconciled
toRequests MapFunc
toRequests TypedMapFunc[object, request]
}
// Create implements EventHandler.
func (e *enqueueRequestsFromMapFunc) Create(ctx context.Context, evt event.CreateEvent, q workqueue.RateLimitingInterface) {
reqs := map[reconcile.Request]empty{}
func (e *enqueueRequestsFromMapFunc[object, request]) Create(
ctx context.Context,
evt event.TypedCreateEvent[object],
q workqueue.TypedRateLimitingInterface[request],
) {
reqs := map[request]empty{}
e.mapAndEnqueue(ctx, q, evt.Object, reqs)
}
// Update implements EventHandler.
func (e *enqueueRequestsFromMapFunc) Update(ctx context.Context, evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
reqs := map[reconcile.Request]empty{}
func (e *enqueueRequestsFromMapFunc[object, request]) Update(
ctx context.Context,
evt event.TypedUpdateEvent[object],
q workqueue.TypedRateLimitingInterface[request],
) {
reqs := map[request]empty{}
e.mapAndEnqueue(ctx, q, evt.ObjectOld, reqs)
e.mapAndEnqueue(ctx, q, evt.ObjectNew, reqs)
}
// Delete implements EventHandler.
func (e *enqueueRequestsFromMapFunc) Delete(ctx context.Context, evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
reqs := map[reconcile.Request]empty{}
func (e *enqueueRequestsFromMapFunc[object, request]) Delete(
ctx context.Context,
evt event.TypedDeleteEvent[object],
q workqueue.TypedRateLimitingInterface[request],
) {
reqs := map[request]empty{}
e.mapAndEnqueue(ctx, q, evt.Object, reqs)
}
// Generic implements EventHandler.
func (e *enqueueRequestsFromMapFunc) Generic(ctx context.Context, evt event.GenericEvent, q workqueue.RateLimitingInterface) {
reqs := map[reconcile.Request]empty{}
func (e *enqueueRequestsFromMapFunc[object, request]) Generic(
ctx context.Context,
evt event.TypedGenericEvent[object],
q workqueue.TypedRateLimitingInterface[request],
) {
reqs := map[request]empty{}
e.mapAndEnqueue(ctx, q, evt.Object, reqs)
}
func (e *enqueueRequestsFromMapFunc) mapAndEnqueue(ctx context.Context, q workqueue.RateLimitingInterface, object client.Object, reqs map[reconcile.Request]empty) {
for _, req := range e.toRequests(ctx, object) {
func (e *enqueueRequestsFromMapFunc[object, request]) mapAndEnqueue(ctx context.Context, q workqueue.TypedRateLimitingInterface[request], o object, reqs map[request]empty) {
for _, req := range e.toRequests(ctx, o) {
_, ok := reqs[req]
if !ok {
q.Add(req)

View File

@@ -32,12 +32,12 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
var _ EventHandler = &enqueueRequestForOwner{}
var _ EventHandler = &enqueueRequestForOwner[client.Object]{}
var log = logf.RuntimeLog.WithName("eventhandler").WithName("enqueueRequestForOwner")
// OwnerOption modifies an EnqueueRequestForOwner EventHandler.
type OwnerOption func(e *enqueueRequestForOwner)
type OwnerOption func(e enqueueRequestForOwnerInterface)
// EnqueueRequestForOwner enqueues Requests for the Owners of an object. E.g. the object that created
// the object that was the source of the Event.
@@ -48,7 +48,21 @@ type OwnerOption func(e *enqueueRequestForOwner)
//
// - a handler.enqueueRequestForOwner EventHandler with an OwnerType of ReplicaSet and OnlyControllerOwner set to true.
func EnqueueRequestForOwner(scheme *runtime.Scheme, mapper meta.RESTMapper, ownerType client.Object, opts ...OwnerOption) EventHandler {
e := &enqueueRequestForOwner{
return TypedEnqueueRequestForOwner[client.Object](scheme, mapper, ownerType, opts...)
}
// TypedEnqueueRequestForOwner enqueues Requests for the Owners of an object. E.g. the object that created
// the object that was the source of the Event.
//
// If a ReplicaSet creates Pods, users may reconcile the ReplicaSet in response to Pod Events using:
//
// - a source.Kind Source with Type of Pod.
//
// - a handler.typedEnqueueRequestForOwner EventHandler with an OwnerType of ReplicaSet and OnlyControllerOwner set to true.
//
// TypedEnqueueRequestForOwner is experimental and subject to future change.
func TypedEnqueueRequestForOwner[object client.Object](scheme *runtime.Scheme, mapper meta.RESTMapper, ownerType client.Object, opts ...OwnerOption) TypedEventHandler[object, reconcile.Request] {
e := &enqueueRequestForOwner[object]{
ownerType: ownerType,
mapper: mapper,
}
@@ -63,12 +77,16 @@ func EnqueueRequestForOwner(scheme *runtime.Scheme, mapper meta.RESTMapper, owne
// OnlyControllerOwner if provided will only look at the first OwnerReference with Controller: true.
func OnlyControllerOwner() OwnerOption {
return func(e *enqueueRequestForOwner) {
e.isController = true
return func(e enqueueRequestForOwnerInterface) {
e.setIsController(true)
}
}
type enqueueRequestForOwner struct {
type enqueueRequestForOwnerInterface interface {
setIsController(bool)
}
type enqueueRequestForOwner[object client.Object] struct {
// ownerType is the type of the Owner object to look for in OwnerReferences. Only Group and Kind are compared.
ownerType runtime.Object
@@ -82,8 +100,12 @@ type enqueueRequestForOwner struct {
mapper meta.RESTMapper
}
func (e *enqueueRequestForOwner[object]) setIsController(isController bool) {
e.isController = isController
}
// Create implements EventHandler.
func (e *enqueueRequestForOwner) Create(ctx context.Context, evt event.CreateEvent, q workqueue.RateLimitingInterface) {
func (e *enqueueRequestForOwner[object]) Create(ctx context.Context, evt event.TypedCreateEvent[object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
reqs := map[reconcile.Request]empty{}
e.getOwnerReconcileRequest(evt.Object, reqs)
for req := range reqs {
@@ -92,7 +114,7 @@ func (e *enqueueRequestForOwner) Create(ctx context.Context, evt event.CreateEve
}
// Update implements EventHandler.
func (e *enqueueRequestForOwner) Update(ctx context.Context, evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
func (e *enqueueRequestForOwner[object]) Update(ctx context.Context, evt event.TypedUpdateEvent[object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
reqs := map[reconcile.Request]empty{}
e.getOwnerReconcileRequest(evt.ObjectOld, reqs)
e.getOwnerReconcileRequest(evt.ObjectNew, reqs)
@@ -102,7 +124,7 @@ func (e *enqueueRequestForOwner) Update(ctx context.Context, evt event.UpdateEve
}
// Delete implements EventHandler.
func (e *enqueueRequestForOwner) Delete(ctx context.Context, evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
func (e *enqueueRequestForOwner[object]) Delete(ctx context.Context, evt event.TypedDeleteEvent[object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
reqs := map[reconcile.Request]empty{}
e.getOwnerReconcileRequest(evt.Object, reqs)
for req := range reqs {
@@ -111,7 +133,7 @@ func (e *enqueueRequestForOwner) Delete(ctx context.Context, evt event.DeleteEve
}
// Generic implements EventHandler.
func (e *enqueueRequestForOwner) Generic(ctx context.Context, evt event.GenericEvent, q workqueue.RateLimitingInterface) {
func (e *enqueueRequestForOwner[object]) Generic(ctx context.Context, evt event.TypedGenericEvent[object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
reqs := map[reconcile.Request]empty{}
e.getOwnerReconcileRequest(evt.Object, reqs)
for req := range reqs {
@@ -121,7 +143,7 @@ func (e *enqueueRequestForOwner) Generic(ctx context.Context, evt event.GenericE
// parseOwnerTypeGroupKind parses the OwnerType into a Group and Kind and caches the result. Returns false
// if the OwnerType could not be parsed using the scheme.
func (e *enqueueRequestForOwner) parseOwnerTypeGroupKind(scheme *runtime.Scheme) error {
func (e *enqueueRequestForOwner[object]) parseOwnerTypeGroupKind(scheme *runtime.Scheme) error {
// Get the kinds of the type
kinds, _, err := scheme.ObjectKinds(e.ownerType)
if err != nil {
@@ -141,10 +163,10 @@ func (e *enqueueRequestForOwner) parseOwnerTypeGroupKind(scheme *runtime.Scheme)
// getOwnerReconcileRequest looks at object and builds a map of reconcile.Request to reconcile
// owners of object that match e.OwnerType.
func (e *enqueueRequestForOwner) getOwnerReconcileRequest(object metav1.Object, result map[reconcile.Request]empty) {
func (e *enqueueRequestForOwner[object]) getOwnerReconcileRequest(obj metav1.Object, result map[reconcile.Request]empty) {
// Iterate through the OwnerReferences looking for a match on Group and Kind against what was requested
// by the user
for _, ref := range e.getOwnersReferences(object) {
for _, ref := range e.getOwnersReferences(obj) {
// Parse the Group out of the OwnerReference to compare it to what was parsed out of the requested OwnerType
refGV, err := schema.ParseGroupVersion(ref.APIVersion)
if err != nil {
@@ -170,7 +192,7 @@ func (e *enqueueRequestForOwner) getOwnerReconcileRequest(object metav1.Object,
return
}
if mapping.Scope.Name() != meta.RESTScopeNameRoot {
request.Namespace = object.GetNamespace()
request.Namespace = obj.GetNamespace()
}
result[request] = empty{}
@@ -181,17 +203,17 @@ func (e *enqueueRequestForOwner) getOwnerReconcileRequest(object metav1.Object,
// getOwnersReferences returns the OwnerReferences for an object as specified by the enqueueRequestForOwner
// - if IsController is true: only take the Controller OwnerReference (if found)
// - if IsController is false: take all OwnerReferences.
func (e *enqueueRequestForOwner) getOwnersReferences(object metav1.Object) []metav1.OwnerReference {
if object == nil {
func (e *enqueueRequestForOwner[object]) getOwnersReferences(obj metav1.Object) []metav1.OwnerReference {
if obj == nil {
return nil
}
// If not filtered as Controller only, then use all the OwnerReferences
if !e.isController {
return object.GetOwnerReferences()
return obj.GetOwnerReferences()
}
// If filtered to a Controller, only take the Controller OwnerReference
if ownerRef := metav1.GetControllerOf(object); ownerRef != nil {
if ownerRef := metav1.GetControllerOf(obj); ownerRef != nil {
return []metav1.OwnerReference{*ownerRef}
}
// No Controller OwnerReference found

View File

@@ -20,12 +20,14 @@ import (
"context"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// EventHandler enqueues reconcile.Requests in response to events (e.g. Pod Create). EventHandlers map an Event
// for one object to trigger Reconciles for either the same object or different objects - e.g. if there is an
// Event for object with type Foo (using source.KindSource) then reconcile one or more object(s) with type Bar.
// Event for object with type Foo (using source.Kind) then reconcile one or more object(s) with type Bar.
//
// Identical reconcile.Requests will be batched together through the queuing mechanism before reconcile is called.
//
@@ -41,65 +43,92 @@ import (
//
// Unless you are implementing your own EventHandler, you can ignore the functions on the EventHandler interface.
// Most users shouldn't need to implement their own EventHandler.
type EventHandler interface {
type EventHandler = TypedEventHandler[client.Object, reconcile.Request]
// TypedEventHandler enqueues reconcile.Requests in response to events (e.g. Pod Create). TypedEventHandlers map an Event
// for one object to trigger Reconciles for either the same object or different objects - e.g. if there is an
// Event for object with type Foo (using source.Kind) then reconcile one or more object(s) with type Bar.
//
// Identical reconcile.Requests will be batched together through the queuing mechanism before reconcile is called.
//
// * Use TypedEnqueueRequestForObject to reconcile the object the event is for
// - do this for events for the type the Controller Reconciles. (e.g. Deployment for a Deployment Controller)
//
// * Use TypedEnqueueRequestForOwner to reconcile the owner of the object the event is for
// - do this for events for the types the Controller creates. (e.g. ReplicaSets created by a Deployment Controller)
//
// * Use TypedEnqueueRequestsFromMapFunc to transform an event for an object to a reconcile of an object
// of a different type - do this for events for types the Controller may be interested in, but doesn't create.
// (e.g. If Foo responds to cluster size events, map Node events to Foo objects.)
//
// Unless you are implementing your own TypedEventHandler, you can ignore the functions on the TypedEventHandler interface.
// Most users shouldn't need to implement their own TypedEventHandler.
//
// TypedEventHandler is experimental and subject to future change.
type TypedEventHandler[object any, request comparable] interface {
// Create is called in response to a create event - e.g. Pod Creation.
Create(context.Context, event.CreateEvent, workqueue.RateLimitingInterface)
Create(context.Context, event.TypedCreateEvent[object], workqueue.TypedRateLimitingInterface[request])
// Update is called in response to an update event - e.g. Pod Updated.
Update(context.Context, event.UpdateEvent, workqueue.RateLimitingInterface)
Update(context.Context, event.TypedUpdateEvent[object], workqueue.TypedRateLimitingInterface[request])
// Delete is called in response to a delete event - e.g. Pod Deleted.
Delete(context.Context, event.DeleteEvent, workqueue.RateLimitingInterface)
Delete(context.Context, event.TypedDeleteEvent[object], workqueue.TypedRateLimitingInterface[request])
// Generic is called in response to an event of an unknown type or a synthetic event triggered as a cron or
// external trigger request - e.g. reconcile Autoscaling, or a Webhook.
Generic(context.Context, event.GenericEvent, workqueue.RateLimitingInterface)
Generic(context.Context, event.TypedGenericEvent[object], workqueue.TypedRateLimitingInterface[request])
}
var _ EventHandler = Funcs{}
// Funcs implements EventHandler.
type Funcs struct {
// Funcs implements eventhandler.
type Funcs = TypedFuncs[client.Object, reconcile.Request]
// TypedFuncs implements eventhandler.
//
// TypedFuncs is experimental and subject to future change.
type TypedFuncs[object any, request comparable] struct {
// Create is called in response to an add event. Defaults to no-op.
// RateLimitingInterface is used to enqueue reconcile.Requests.
CreateFunc func(context.Context, event.CreateEvent, workqueue.RateLimitingInterface)
CreateFunc func(context.Context, event.TypedCreateEvent[object], workqueue.TypedRateLimitingInterface[request])
// Update is called in response to an update event. Defaults to no-op.
// RateLimitingInterface is used to enqueue reconcile.Requests.
UpdateFunc func(context.Context, event.UpdateEvent, workqueue.RateLimitingInterface)
UpdateFunc func(context.Context, event.TypedUpdateEvent[object], workqueue.TypedRateLimitingInterface[request])
// Delete is called in response to a delete event. Defaults to no-op.
// RateLimitingInterface is used to enqueue reconcile.Requests.
DeleteFunc func(context.Context, event.DeleteEvent, workqueue.RateLimitingInterface)
DeleteFunc func(context.Context, event.TypedDeleteEvent[object], workqueue.TypedRateLimitingInterface[request])
// GenericFunc is called in response to a generic event. Defaults to no-op.
// RateLimitingInterface is used to enqueue reconcile.Requests.
GenericFunc func(context.Context, event.GenericEvent, workqueue.RateLimitingInterface)
GenericFunc func(context.Context, event.TypedGenericEvent[object], workqueue.TypedRateLimitingInterface[request])
}
// Create implements EventHandler.
func (h Funcs) Create(ctx context.Context, e event.CreateEvent, q workqueue.RateLimitingInterface) {
func (h TypedFuncs[object, request]) Create(ctx context.Context, e event.TypedCreateEvent[object], q workqueue.TypedRateLimitingInterface[request]) {
if h.CreateFunc != nil {
h.CreateFunc(ctx, e, q)
}
}
// Delete implements EventHandler.
func (h Funcs) Delete(ctx context.Context, e event.DeleteEvent, q workqueue.RateLimitingInterface) {
func (h TypedFuncs[object, request]) Delete(ctx context.Context, e event.TypedDeleteEvent[object], q workqueue.TypedRateLimitingInterface[request]) {
if h.DeleteFunc != nil {
h.DeleteFunc(ctx, e, q)
}
}
// Update implements EventHandler.
func (h Funcs) Update(ctx context.Context, e event.UpdateEvent, q workqueue.RateLimitingInterface) {
func (h TypedFuncs[object, request]) Update(ctx context.Context, e event.TypedUpdateEvent[object], q workqueue.TypedRateLimitingInterface[request]) {
if h.UpdateFunc != nil {
h.UpdateFunc(ctx, e, q)
}
}
// Generic implements EventHandler.
func (h Funcs) Generic(ctx context.Context, e event.GenericEvent, q workqueue.RateLimitingInterface) {
func (h TypedFuncs[object, request]) Generic(ctx context.Context, e event.TypedGenericEvent[object], q workqueue.TypedRateLimitingInterface[request]) {
if h.GenericFunc != nil {
h.GenericFunc(ctx, e, q)
}

View File

@@ -29,16 +29,14 @@ import (
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/handler"
ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)
// Controller implements controller.Controller.
type Controller struct {
type Controller[request comparable] struct {
// Name is used to uniquely identify a Controller in tracing, logging and monitoring. Name is required.
Name string
@@ -48,16 +46,19 @@ type Controller struct {
// Reconciler is a function that can be called at any time with the Name / Namespace of an object and
// ensures that the state of the system matches the state specified in the object.
// Defaults to the DefaultReconcileFunc.
Do reconcile.Reconciler
Do reconcile.TypedReconciler[request]
// MakeQueue constructs the queue for this controller once the controller is ready to start.
// This exists because the standard Kubernetes workqueues start themselves immediately, which
// RateLimiter is used to limit how frequently requests may be queued into the work queue.
RateLimiter workqueue.TypedRateLimiter[request]
// NewQueue constructs the queue for this controller once the controller is ready to start.
// This is a func because the standard Kubernetes work queues start themselves immediately, which
// leads to goroutine leaks if something calls controller.New repeatedly.
MakeQueue func() workqueue.RateLimitingInterface
NewQueue func(controllerName string, rateLimiter workqueue.TypedRateLimiter[request]) workqueue.TypedRateLimitingInterface[request]
// Queue is an listeningQueue that listens for events from Informers and adds object keys to
// the Queue for processing
Queue workqueue.RateLimitingInterface
Queue workqueue.TypedRateLimitingInterface[request]
// mu is used to synchronize Controller setup
mu sync.Mutex
@@ -77,35 +78,31 @@ type Controller struct {
CacheSyncTimeout time.Duration
// startWatches maintains a list of sources, handlers, and predicates to start when the controller is started.
startWatches []watchDescription
startWatches []source.TypedSource[request]
// LogConstructor is used to construct a logger to then log messages to users during reconciliation,
// or for example when a watch is started.
// Note: LogConstructor has to be able to handle nil requests as we are also using it
// outside the context of a reconciliation.
LogConstructor func(request *reconcile.Request) logr.Logger
LogConstructor func(request *request) logr.Logger
// RecoverPanic indicates whether the panic caused by reconcile should be recovered.
// Defaults to true.
RecoverPanic *bool
// LeaderElected indicates whether the controller is leader elected or always running.
LeaderElected *bool
}
// watchDescription contains all the information necessary to start a watch.
type watchDescription struct {
src source.Source
handler handler.EventHandler
predicates []predicate.Predicate
}
// Reconcile implements reconcile.Reconciler.
func (c *Controller) Reconcile(ctx context.Context, req reconcile.Request) (_ reconcile.Result, err error) {
func (c *Controller[request]) Reconcile(ctx context.Context, req request) (_ reconcile.Result, err error) {
defer func() {
if r := recover(); r != nil {
if c.RecoverPanic != nil && *c.RecoverPanic {
ctrlmetrics.ReconcilePanics.WithLabelValues(c.Name).Inc()
if c.RecoverPanic == nil || *c.RecoverPanic {
for _, fn := range utilruntime.PanicHandlers {
fn(r)
fn(ctx, r)
}
err = fmt.Errorf("panic: %v [recovered]", r)
return
@@ -120,7 +117,7 @@ func (c *Controller) Reconcile(ctx context.Context, req reconcile.Request) (_ re
}
// Watch implements controller.Controller.
func (c *Controller) Watch(src source.Source, evthdler handler.EventHandler, prct ...predicate.Predicate) error {
func (c *Controller[request]) Watch(src source.TypedSource[request]) error {
c.mu.Lock()
defer c.mu.Unlock()
@@ -128,16 +125,16 @@ func (c *Controller) Watch(src source.Source, evthdler handler.EventHandler, prc
//
// These watches are going to be held on the controller struct until the manager or user calls Start(...).
if !c.Started {
c.startWatches = append(c.startWatches, watchDescription{src: src, handler: evthdler, predicates: prct})
c.startWatches = append(c.startWatches, src)
return nil
}
c.LogConstructor(nil).Info("Starting EventSource", "source", src)
return src.Start(c.ctx, evthdler, c.Queue, prct...)
return src.Start(c.ctx, c.Queue)
}
// NeedLeaderElection implements the manager.LeaderElectionRunnable interface.
func (c *Controller) NeedLeaderElection() bool {
func (c *Controller[request]) NeedLeaderElection() bool {
if c.LeaderElected == nil {
return true
}
@@ -145,7 +142,7 @@ func (c *Controller) NeedLeaderElection() bool {
}
// Start implements controller.Controller.
func (c *Controller) Start(ctx context.Context) error {
func (c *Controller[request]) Start(ctx context.Context) error {
// use an IIFE to get proper lock handling
// but lock outside to get proper handling of the queue shutdown
c.mu.Lock()
@@ -158,7 +155,7 @@ func (c *Controller) Start(ctx context.Context) error {
// Set the internal context.
c.ctx = ctx
c.Queue = c.MakeQueue()
c.Queue = c.NewQueue(c.Name, c.RateLimiter)
go func() {
<-ctx.Done()
c.Queue.ShutDown()
@@ -175,9 +172,9 @@ func (c *Controller) Start(ctx context.Context) error {
// caches to sync so that they have a chance to register their intendeded
// caches.
for _, watch := range c.startWatches {
c.LogConstructor(nil).Info("Starting EventSource", "source", fmt.Sprintf("%s", watch.src))
c.LogConstructor(nil).Info("Starting EventSource", "source", fmt.Sprintf("%s", watch))
if err := watch.src.Start(ctx, watch.handler, c.Queue, watch.predicates...); err != nil {
if err := watch.Start(ctx, c.Queue); err != nil {
return err
}
}
@@ -186,7 +183,7 @@ func (c *Controller) Start(ctx context.Context) error {
c.LogConstructor(nil).Info("Starting Controller")
for _, watch := range c.startWatches {
syncingSource, ok := watch.src.(source.SyncingSource)
syncingSource, ok := watch.(source.SyncingSource)
if !ok {
continue
}
@@ -245,7 +242,7 @@ func (c *Controller) Start(ctx context.Context) error {
// processNextWorkItem will read a single work item off the workqueue and
// attempt to process it, by calling the reconcileHandler.
func (c *Controller) processNextWorkItem(ctx context.Context) bool {
func (c *Controller[request]) processNextWorkItem(ctx context.Context) bool {
obj, shutdown := c.Queue.Get()
if shutdown {
// Stop working
@@ -274,35 +271,25 @@ const (
labelSuccess = "success"
)
func (c *Controller) initMetrics() {
ctrlmetrics.ActiveWorkers.WithLabelValues(c.Name).Set(0)
ctrlmetrics.ReconcileErrors.WithLabelValues(c.Name).Add(0)
func (c *Controller[request]) initMetrics() {
ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, labelError).Add(0)
ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, labelRequeueAfter).Add(0)
ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, labelRequeue).Add(0)
ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, labelSuccess).Add(0)
ctrlmetrics.ReconcileErrors.WithLabelValues(c.Name).Add(0)
ctrlmetrics.TerminalReconcileErrors.WithLabelValues(c.Name).Add(0)
ctrlmetrics.ReconcilePanics.WithLabelValues(c.Name).Add(0)
ctrlmetrics.WorkerCount.WithLabelValues(c.Name).Set(float64(c.MaxConcurrentReconciles))
ctrlmetrics.ActiveWorkers.WithLabelValues(c.Name).Set(0)
}
func (c *Controller) reconcileHandler(ctx context.Context, obj interface{}) {
func (c *Controller[request]) reconcileHandler(ctx context.Context, req request) {
// Update metrics after processing each item
reconcileStartTS := time.Now()
defer func() {
c.updateMetrics(time.Since(reconcileStartTS))
}()
// Make sure that the object is a valid request.
req, ok := obj.(reconcile.Request)
if !ok {
// As the item in the workqueue is actually invalid, we call
// Forget here else we'd go into a loop of attempting to
// process a work item that is invalid.
c.Queue.Forget(obj)
c.LogConstructor(nil).Error(nil, "Queue item was not a Request", "type", fmt.Sprintf("%T", obj), "value", obj)
// Return true, don't take a break
return
}
log := c.LogConstructor(&req)
reconcileID := uuid.NewUUID()
@@ -333,7 +320,7 @@ func (c *Controller) reconcileHandler(ctx context.Context, obj interface{}) {
// along with a non-nil error. But this is intended as
// We need to drive to stable reconcile loops before queuing due
// to result.RequestAfter
c.Queue.Forget(obj)
c.Queue.Forget(req)
c.Queue.AddAfter(req, result.RequeueAfter)
ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, labelRequeueAfter).Inc()
case result.Requeue:
@@ -344,18 +331,18 @@ func (c *Controller) reconcileHandler(ctx context.Context, obj interface{}) {
log.V(5).Info("Reconcile successful")
// Finally, if no error occurs we Forget this item so it does not
// get queued again until another change happens.
c.Queue.Forget(obj)
c.Queue.Forget(req)
ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, labelSuccess).Inc()
}
}
// GetLogger returns this controller's logger.
func (c *Controller) GetLogger() logr.Logger {
func (c *Controller[request]) GetLogger() logr.Logger {
return c.LogConstructor(nil)
}
// updateMetrics updates prometheus metrics within the controller.
func (c *Controller) updateMetrics(reconcileTime time.Duration) {
func (c *Controller[request]) updateMetrics(reconcileTime time.Duration) {
ctrlmetrics.ReconcileTime.WithLabelValues(c.Name).Observe(reconcileTime.Seconds())
}

View File

@@ -46,6 +46,13 @@ var (
Help: "Total number of terminal reconciliation errors per controller",
}, []string{"controller"})
// ReconcilePanics is a prometheus counter metrics which holds the total
// number of panics from the Reconciler.
ReconcilePanics = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "controller_runtime_reconcile_panics_total",
Help: "Total number of reconciliation panics per controller",
}, []string{"controller"})
// ReconcileTime is a prometheus metric which keeps track of the duration
// of reconciliations.
ReconcileTime = prometheus.NewHistogramVec(prometheus.HistogramOpts{
@@ -75,6 +82,7 @@ func init() {
ReconcileTotal,
ReconcileErrors,
TerminalReconcileErrors,
ReconcilePanics,
ReconcileTime,
WorkerCount,
ActiveWorkers,

View File

@@ -33,8 +33,12 @@ import (
var log = logf.RuntimeLog.WithName("source").WithName("EventHandler")
// NewEventHandler creates a new EventHandler.
func NewEventHandler(ctx context.Context, queue workqueue.RateLimitingInterface, handler handler.EventHandler, predicates []predicate.Predicate) *EventHandler {
return &EventHandler{
func NewEventHandler[object client.Object, request comparable](
ctx context.Context,
queue workqueue.TypedRateLimitingInterface[request],
handler handler.TypedEventHandler[object, request],
predicates []predicate.TypedPredicate[object]) *EventHandler[object, request] {
return &EventHandler[object, request]{
ctx: ctx,
handler: handler,
queue: queue,
@@ -43,19 +47,19 @@ func NewEventHandler(ctx context.Context, queue workqueue.RateLimitingInterface,
}
// EventHandler adapts a handler.EventHandler interface to a cache.ResourceEventHandler interface.
type EventHandler struct {
type EventHandler[object client.Object, request comparable] struct {
// ctx stores the context that created the event handler
// that is used to propagate cancellation signals to each handler function.
ctx context.Context
handler handler.EventHandler
queue workqueue.RateLimitingInterface
predicates []predicate.Predicate
handler handler.TypedEventHandler[object, request]
queue workqueue.TypedRateLimitingInterface[request]
predicates []predicate.TypedPredicate[object]
}
// HandlerFuncs converts EventHandler to a ResourceEventHandlerFuncs
// TODO: switch to ResourceEventHandlerDetailedFuncs with client-go 1.27
func (e *EventHandler) HandlerFuncs() cache.ResourceEventHandlerFuncs {
func (e *EventHandler[object, request]) HandlerFuncs() cache.ResourceEventHandlerFuncs {
return cache.ResourceEventHandlerFuncs{
AddFunc: e.OnAdd,
UpdateFunc: e.OnUpdate,
@@ -64,11 +68,11 @@ func (e *EventHandler) HandlerFuncs() cache.ResourceEventHandlerFuncs {
}
// OnAdd creates CreateEvent and calls Create on EventHandler.
func (e *EventHandler) OnAdd(obj interface{}) {
c := event.CreateEvent{}
func (e *EventHandler[object, request]) OnAdd(obj interface{}) {
c := event.TypedCreateEvent[object]{}
// Pull Object out of the object
if o, ok := obj.(client.Object); ok {
if o, ok := obj.(object); ok {
c.Object = o
} else {
log.Error(nil, "OnAdd missing Object",
@@ -89,10 +93,10 @@ func (e *EventHandler) OnAdd(obj interface{}) {
}
// OnUpdate creates UpdateEvent and calls Update on EventHandler.
func (e *EventHandler) OnUpdate(oldObj, newObj interface{}) {
u := event.UpdateEvent{}
func (e *EventHandler[object, request]) OnUpdate(oldObj, newObj interface{}) {
u := event.TypedUpdateEvent[object]{}
if o, ok := oldObj.(client.Object); ok {
if o, ok := oldObj.(object); ok {
u.ObjectOld = o
} else {
log.Error(nil, "OnUpdate missing ObjectOld",
@@ -101,7 +105,7 @@ func (e *EventHandler) OnUpdate(oldObj, newObj interface{}) {
}
// Pull Object out of the object
if o, ok := newObj.(client.Object); ok {
if o, ok := newObj.(object); ok {
u.ObjectNew = o
} else {
log.Error(nil, "OnUpdate missing ObjectNew",
@@ -122,8 +126,8 @@ func (e *EventHandler) OnUpdate(oldObj, newObj interface{}) {
}
// OnDelete creates DeleteEvent and calls Delete on EventHandler.
func (e *EventHandler) OnDelete(obj interface{}) {
d := event.DeleteEvent{}
func (e *EventHandler[object, request]) OnDelete(obj interface{}) {
d := event.TypedDeleteEvent[object]{}
// Deal with tombstone events by pulling the object out. Tombstone events wrap the object in a
// DeleteFinalStateUnknown struct, so the object needs to be pulled out.
@@ -149,7 +153,7 @@ func (e *EventHandler) OnDelete(obj interface{}) {
}
// Pull Object out of the object
if o, ok := obj.(client.Object); ok {
if o, ok := obj.(object); ok {
d.Object = o
} else {
log.Error(nil, "OnDelete missing Object",

View File

@@ -4,12 +4,14 @@ import (
"context"
"errors"
"fmt"
"reflect"
"time"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
@@ -17,34 +19,40 @@ import (
)
// Kind is used to provide a source of events originating inside the cluster from Watches (e.g. Pod Create).
type Kind struct {
type Kind[object client.Object, request comparable] struct {
// Type is the type of object to watch. e.g. &v1.Pod{}
Type client.Object
Type object
// Cache used to watch APIs
Cache cache.Cache
// started may contain an error if one was encountered during startup. If its closed and does not
Handler handler.TypedEventHandler[object, request]
Predicates []predicate.TypedPredicate[object]
// startedErr may contain an error if one was encountered during startup. If its closed and does not
// contain an error, startup and syncing finished.
started chan error
startedErr chan error
startCancel func()
}
// Start is internal and should be called only by the Controller to register an EventHandler with the Informer
// to enqueue reconcile.Requests.
func (ks *Kind) Start(ctx context.Context, handler handler.EventHandler, queue workqueue.RateLimitingInterface,
prct ...predicate.Predicate) error {
if ks.Type == nil {
func (ks *Kind[object, request]) Start(ctx context.Context, queue workqueue.TypedRateLimitingInterface[request]) error {
if isNil(ks.Type) {
return fmt.Errorf("must create Kind with a non-nil object")
}
if ks.Cache == nil {
if isNil(ks.Cache) {
return fmt.Errorf("must create Kind with a non-nil cache")
}
if isNil(ks.Handler) {
return errors.New("must create Kind with non-nil handler")
}
// cache.GetInformer will block until its context is cancelled if the cache was already started and it can not
// sync that informer (most commonly due to RBAC issues).
ctx, ks.startCancel = context.WithCancel(ctx)
ks.started = make(chan error)
ks.startedErr = make(chan error)
go func() {
var (
i cache.Informer
@@ -72,30 +80,30 @@ func (ks *Kind) Start(ctx context.Context, handler handler.EventHandler, queue w
return true, nil
}); err != nil {
if lastErr != nil {
ks.started <- fmt.Errorf("failed to get informer from cache: %w", lastErr)
ks.startedErr <- fmt.Errorf("failed to get informer from cache: %w", lastErr)
return
}
ks.started <- err
ks.startedErr <- err
return
}
_, err := i.AddEventHandler(NewEventHandler(ctx, queue, handler, prct).HandlerFuncs())
_, err := i.AddEventHandler(NewEventHandler(ctx, queue, ks.Handler, ks.Predicates).HandlerFuncs())
if err != nil {
ks.started <- err
ks.startedErr <- err
return
}
if !ks.Cache.WaitForCacheSync(ctx) {
// Would be great to return something more informative here
ks.started <- errors.New("cache did not sync")
ks.startedErr <- errors.New("cache did not sync")
}
close(ks.started)
close(ks.startedErr)
}()
return nil
}
func (ks *Kind) String() string {
if ks.Type != nil {
func (ks *Kind[object, request]) String() string {
if !isNil(ks.Type) {
return fmt.Sprintf("kind source: %T", ks.Type)
}
return "kind source: unknown type"
@@ -103,9 +111,9 @@ func (ks *Kind) String() string {
// WaitForSync implements SyncingSource to allow controllers to wait with starting
// workers until the cache is synced.
func (ks *Kind) WaitForSync(ctx context.Context) error {
func (ks *Kind[object, request]) WaitForSync(ctx context.Context) error {
select {
case err := <-ks.started:
case err := <-ks.startedErr:
return err
case <-ctx.Done():
ks.startCancel()
@@ -115,3 +123,15 @@ func (ks *Kind) WaitForSync(ctx context.Context) error {
return fmt.Errorf("timed out waiting for cache to be synced for Kind %T", ks.Type)
}
}
func isNil(arg any) bool {
if v := reflect.ValueOf(arg); !v.IsValid() || ((v.Kind() == reflect.Ptr ||
v.Kind() == reflect.Interface ||
v.Kind() == reflect.Slice ||
v.Kind() == reflect.Map ||
v.Kind() == reflect.Chan ||
v.Kind() == reflect.Func) && v.IsNil()) {
return true
}
return false
}

View File

@@ -179,6 +179,24 @@ func (cm *controllerManager) add(r Runnable) error {
return cm.runnables.Add(r)
}
// AddMetricsServerExtraHandler adds extra handler served on path to the http server that serves metrics.
func (cm *controllerManager) AddMetricsServerExtraHandler(path string, handler http.Handler) error {
cm.Lock()
defer cm.Unlock()
if cm.started {
return fmt.Errorf("unable to add new metrics handler because metrics endpoint has already been created")
}
if cm.metricsServer == nil {
cm.GetLogger().Info("warn: metrics server is currently disabled, registering extra handler will be ignored", "path", path)
return nil
}
if err := cm.metricsServer.AddExtraHandler(path, handler); err != nil {
return err
}
cm.logger.V(2).Info("Registering metrics http server extra handler", "path", path)
return nil
}
// AddHealthzCheck allows you to add Healthz checker.
func (cm *controllerManager) AddHealthzCheck(name string, check healthz.Checker) error {
cm.Lock()
@@ -284,9 +302,8 @@ func (cm *controllerManager) addHealthProbeServer() error {
mux.Handle(cm.livenessEndpointName+"/", http.StripPrefix(cm.livenessEndpointName, cm.healthzHandler))
}
return cm.add(&server{
Kind: "health probe",
Log: cm.logger,
return cm.add(&Server{
Name: "health probe",
Server: srv,
Listener: cm.healthProbeListener,
})
@@ -302,9 +319,8 @@ func (cm *controllerManager) addPprofServer() error {
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
return cm.add(&server{
Kind: "pprof",
Log: cm.logger,
return cm.add(&Server{
Name: "pprof",
Server: srv,
Listener: cm.pprofListener,
})
@@ -335,6 +351,16 @@ func (cm *controllerManager) Start(ctx context.Context) (err error) {
// Initialize the internal context.
cm.internalCtx, cm.internalCancel = context.WithCancel(ctx)
// Leader elector must be created before defer that contains engageStopProcedure function
// https://github.com/kubernetes-sigs/controller-runtime/issues/2873
var leaderElector *leaderelection.LeaderElector
if cm.resourceLock != nil {
leaderElector, err = cm.initLeaderElector()
if err != nil {
return fmt.Errorf("failed during initialization leader election process: %w", err)
}
}
// This chan indicates that stop is complete, in other words all runnables have returned or timeout on stop request
stopComplete := make(chan struct{})
defer close(stopComplete)
@@ -384,14 +410,13 @@ func (cm *controllerManager) Start(ctx context.Context) (err error) {
}
}
// First start any internal HTTP servers, which includes health probes, metrics and profiling if enabled.
// First start any HTTP servers, which includes health probes, metrics and profiling if enabled.
//
// WARNING: Internal HTTP servers MUST start before any cache is populated, otherwise it would block
// conversion webhooks to be ready for serving which make the cache never get ready.
if err := cm.runnables.HTTPServers.Start(cm.internalCtx); err != nil {
if err != nil {
return fmt.Errorf("failed to start HTTP servers: %w", err)
}
// WARNING: HTTPServers includes the health probes, which MUST start before any cache is populated, otherwise
// it would block conversion webhooks to be ready for serving which make the cache never get ready.
logCtx := logr.NewContext(cm.internalCtx, cm.logger)
if err := cm.runnables.HTTPServers.Start(logCtx); err != nil {
return fmt.Errorf("failed to start HTTP servers: %w", err)
}
// Start any webhook servers, which includes conversion, validation, and defaulting
@@ -401,42 +426,39 @@ func (cm *controllerManager) Start(ctx context.Context) (err error) {
// between conversion webhooks and the cache sync (usually initial list) which causes the webhooks
// to never start because no cache can be populated.
if err := cm.runnables.Webhooks.Start(cm.internalCtx); err != nil {
if err != nil {
return fmt.Errorf("failed to start webhooks: %w", err)
}
return fmt.Errorf("failed to start webhooks: %w", err)
}
// Start and wait for caches.
if err := cm.runnables.Caches.Start(cm.internalCtx); err != nil {
if err != nil {
return fmt.Errorf("failed to start caches: %w", err)
}
return fmt.Errorf("failed to start caches: %w", err)
}
// Start the non-leaderelection Runnables after the cache has synced.
if err := cm.runnables.Others.Start(cm.internalCtx); err != nil {
if err != nil {
return fmt.Errorf("failed to start other runnables: %w", err)
}
return fmt.Errorf("failed to start other runnables: %w", err)
}
// Start the leader election and all required runnables.
{
ctx, cancel := context.WithCancel(context.Background())
cm.leaderElectionCancel = cancel
go func() {
if cm.resourceLock != nil {
if err := cm.startLeaderElection(ctx); err != nil {
cm.errChan <- err
}
} else {
if leaderElector != nil {
// Start the leader elector process
go func() {
leaderElector.Run(ctx)
<-ctx.Done()
close(cm.leaderElectionStopped)
}()
} else {
go func() {
// Treat not having leader election enabled the same as being elected.
if err := cm.startLeaderElectionRunnables(); err != nil {
cm.errChan <- err
}
close(cm.elected)
}
}()
}()
}
}
ready = true
@@ -485,8 +507,8 @@ func (cm *controllerManager) engageStopProcedure(stopComplete <-chan struct{}) e
cm.internalCancel()
})
select {
case err, ok := <-cm.errChan:
if ok {
case err := <-cm.errChan:
if !errors.Is(err, context.Canceled) {
cm.logger.Error(err, "error received after stop sequence was engaged")
}
case <-stopComplete:
@@ -518,6 +540,8 @@ func (cm *controllerManager) engageStopProcedure(stopComplete <-chan struct{}) e
// Stop all the leader election runnables, which includes reconcilers.
cm.logger.Info("Stopping and waiting for leader election runnables")
// Prevent leader election when shutting down a non-elected manager
cm.runnables.LeaderElection.startOnce.Do(func() {})
cm.runnables.LeaderElection.StopAndWait(cm.shutdownCtx)
// Stop the caches before the leader election runnables, this is an important
@@ -553,12 +577,8 @@ func (cm *controllerManager) engageStopProcedure(stopComplete <-chan struct{}) e
return nil
}
func (cm *controllerManager) startLeaderElectionRunnables() error {
return cm.runnables.LeaderElection.Start(cm.internalCtx)
}
func (cm *controllerManager) startLeaderElection(ctx context.Context) (err error) {
l, err := leaderelection.NewLeaderElector(leaderelection.LeaderElectionConfig{
func (cm *controllerManager) initLeaderElector() (*leaderelection.LeaderElector, error) {
leaderElector, err := leaderelection.NewLeaderElector(leaderelection.LeaderElectionConfig{
Lock: cm.resourceLock,
LeaseDuration: cm.leaseDuration,
RenewDeadline: cm.renewDeadline,
@@ -588,16 +608,14 @@ func (cm *controllerManager) startLeaderElection(ctx context.Context) (err error
Name: cm.leaderElectionID,
})
if err != nil {
return err
return nil, err
}
// Start the leader elector process
go func() {
l.Run(ctx)
<-ctx.Done()
close(cm.leaderElectionStopped)
}()
return nil
return leaderElector, nil
}
func (cm *controllerManager) startLeaderElectionRunnables() error {
return cm.runnables.LeaderElection.Start(cm.internalCtx)
}
func (cm *controllerManager) Elected() <-chan struct{} {

View File

@@ -22,14 +22,12 @@ import (
"fmt"
"net"
"net/http"
"reflect"
"time"
"github.com/go-logr/logr"
coordinationv1 "k8s.io/api/coordination/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/leaderelection/resourcelock"
@@ -41,7 +39,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/cluster"
"sigs.k8s.io/controller-runtime/pkg/config"
"sigs.k8s.io/controller-runtime/pkg/config/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/healthz"
intrec "sigs.k8s.io/controller-runtime/pkg/internal/recorder"
"sigs.k8s.io/controller-runtime/pkg/leaderelection"
@@ -67,6 +64,15 @@ type Manager interface {
// election was configured.
Elected() <-chan struct{}
// AddMetricsServerExtraHandler adds an extra handler served on path to the http server that serves metrics.
// Might be useful to register some diagnostic endpoints e.g. pprof.
//
// Note that these endpoints are meant to be sensitive and shouldn't be exposed publicly.
//
// If the simple path -> handler mapping offered here is not enough,
// a new http server/listener should be added as Runnable to the manager via Add method.
AddMetricsServerExtraHandler(path string, handler http.Handler) error
// AddHealthzCheck allows you to add Healthz checker
AddHealthzCheck(name string, check healthz.Checker) error
@@ -438,126 +444,6 @@ func New(config *rest.Config, options Options) (Manager, error) {
}, nil
}
// AndFrom will use a supplied type and convert to Options
// any options already set on Options will be ignored, this is used to allow
// cli flags to override anything specified in the config file.
//
// Deprecated: This function has been deprecated and will be removed in a future release,
// The Component Configuration package has been unmaintained for over a year and is no longer
// actively developed. Users should migrate to their own configuration format
// and configure Manager.Options directly.
// See https://github.com/kubernetes-sigs/controller-runtime/issues/895
// for more information, feedback, and comments.
func (o Options) AndFrom(loader config.ControllerManagerConfiguration) (Options, error) {
newObj, err := loader.Complete()
if err != nil {
return o, err
}
o = o.setLeaderElectionConfig(newObj)
if o.Cache.SyncPeriod == nil && newObj.SyncPeriod != nil {
o.Cache.SyncPeriod = &newObj.SyncPeriod.Duration
}
if len(o.Cache.DefaultNamespaces) == 0 && newObj.CacheNamespace != "" {
o.Cache.DefaultNamespaces = map[string]cache.Config{newObj.CacheNamespace: {}}
}
if o.Metrics.BindAddress == "" && newObj.Metrics.BindAddress != "" {
o.Metrics.BindAddress = newObj.Metrics.BindAddress
}
if o.HealthProbeBindAddress == "" && newObj.Health.HealthProbeBindAddress != "" {
o.HealthProbeBindAddress = newObj.Health.HealthProbeBindAddress
}
if o.ReadinessEndpointName == "" && newObj.Health.ReadinessEndpointName != "" {
o.ReadinessEndpointName = newObj.Health.ReadinessEndpointName
}
if o.LivenessEndpointName == "" && newObj.Health.LivenessEndpointName != "" {
o.LivenessEndpointName = newObj.Health.LivenessEndpointName
}
if o.WebhookServer == nil {
port := 0
if newObj.Webhook.Port != nil {
port = *newObj.Webhook.Port
}
o.WebhookServer = webhook.NewServer(webhook.Options{
Port: port,
Host: newObj.Webhook.Host,
CertDir: newObj.Webhook.CertDir,
})
}
if newObj.Controller != nil {
if o.Controller.CacheSyncTimeout == 0 && newObj.Controller.CacheSyncTimeout != nil {
o.Controller.CacheSyncTimeout = *newObj.Controller.CacheSyncTimeout
}
if len(o.Controller.GroupKindConcurrency) == 0 && len(newObj.Controller.GroupKindConcurrency) > 0 {
o.Controller.GroupKindConcurrency = newObj.Controller.GroupKindConcurrency
}
}
return o, nil
}
// AndFromOrDie will use options.AndFrom() and will panic if there are errors.
//
// Deprecated: This function has been deprecated and will be removed in a future release,
// The Component Configuration package has been unmaintained for over a year and is no longer
// actively developed. Users should migrate to their own configuration format
// and configure Manager.Options directly.
// See https://github.com/kubernetes-sigs/controller-runtime/issues/895
// for more information, feedback, and comments.
func (o Options) AndFromOrDie(loader config.ControllerManagerConfiguration) Options {
o, err := o.AndFrom(loader)
if err != nil {
panic(fmt.Sprintf("could not parse config file: %v", err))
}
return o
}
func (o Options) setLeaderElectionConfig(obj v1alpha1.ControllerManagerConfigurationSpec) Options {
if obj.LeaderElection == nil {
// The source does not have any configuration; noop
return o
}
if !o.LeaderElection && obj.LeaderElection.LeaderElect != nil {
o.LeaderElection = *obj.LeaderElection.LeaderElect
}
if o.LeaderElectionResourceLock == "" && obj.LeaderElection.ResourceLock != "" {
o.LeaderElectionResourceLock = obj.LeaderElection.ResourceLock
}
if o.LeaderElectionNamespace == "" && obj.LeaderElection.ResourceNamespace != "" {
o.LeaderElectionNamespace = obj.LeaderElection.ResourceNamespace
}
if o.LeaderElectionID == "" && obj.LeaderElection.ResourceName != "" {
o.LeaderElectionID = obj.LeaderElection.ResourceName
}
if o.LeaseDuration == nil && !reflect.DeepEqual(obj.LeaderElection.LeaseDuration, metav1.Duration{}) {
o.LeaseDuration = &obj.LeaderElection.LeaseDuration.Duration
}
if o.RenewDeadline == nil && !reflect.DeepEqual(obj.LeaderElection.RenewDeadline, metav1.Duration{}) {
o.RenewDeadline = &obj.LeaderElection.RenewDeadline.Duration
}
if o.RetryPeriod == nil && !reflect.DeepEqual(obj.LeaderElection.RetryPeriod, metav1.Duration{}) {
o.RetryPeriod = &obj.LeaderElection.RetryPeriod.Duration
}
return o
}
// defaultHealthProbeListener creates the default health probes listener bound to the given address.
func defaultHealthProbeListener(addr string) (net.Listener, error) {
if addr == "" || addr == "0" {

View File

@@ -54,7 +54,10 @@ func newRunnables(baseContext BaseContextFunc, errChan chan error) *runnables {
// The runnables added after Start are started directly.
func (r *runnables) Add(fn Runnable) error {
switch runnable := fn.(type) {
case *server:
case *Server:
if runnable.NeedLeaderElection() {
return r.LeaderElection.Add(fn, nil)
}
return r.HTTPServers.Add(fn, nil)
case hasCache:
return r.Caches.Add(fn, func(ctx context.Context) bool {
@@ -263,6 +266,15 @@ func (r *runnableGroup) Add(rn Runnable, ready runnableCheck) error {
r.start.Unlock()
}
// Recheck if we're stopped and hold the readlock, given that the stop and start can be called
// at the same time, we can end up in a situation where the runnable is added
// after the group is stopped and the channel is closed.
r.stop.RLock()
defer r.stop.RUnlock()
if r.stopped {
return errRunnableGroupStopped
}
// Enqueue the runnable.
r.ch <- readyRunnable
return nil
@@ -272,7 +284,11 @@ func (r *runnableGroup) Add(rn Runnable, ready runnableCheck) error {
func (r *runnableGroup) StopAndWait(ctx context.Context) {
r.stopOnce.Do(func() {
// Close the reconciler channel once we're done.
defer close(r.ch)
defer func() {
r.stop.Lock()
close(r.ch)
r.stop.Unlock()
}()
_ = r.Start(ctx)
r.stop.Lock()

View File

@@ -21,34 +21,67 @@ import (
"errors"
"net"
"net/http"
"time"
"github.com/go-logr/logr"
crlog "sigs.k8s.io/controller-runtime/pkg/log"
)
// server is a general purpose HTTP server Runnable for a manager
// to serve some internal handlers such as health probes, metrics and profiling.
type server struct {
Kind string
Log logr.Logger
Server *http.Server
var (
_ Runnable = (*Server)(nil)
_ LeaderElectionRunnable = (*Server)(nil)
)
// Server is a general purpose HTTP server Runnable for a manager.
// It is used to serve some internal handlers for health probes and profiling,
// but it can also be used to run custom servers.
type Server struct {
// Name is an optional string that describes the purpose of the server. It is used in logs to distinguish
// among multiple servers.
Name string
// Server is the HTTP server to run. It is required.
Server *http.Server
// Listener is an optional listener to use. If not set, the server start a listener using the server.Addr.
// Using a listener is useful when the port reservation needs to happen in advance of this runnable starting.
Listener net.Listener
// OnlyServeWhenLeader is an optional bool that indicates that the server should only be started when the manager is the leader.
OnlyServeWhenLeader bool
// ShutdownTimeout is an optional duration that indicates how long to wait for the server to shutdown gracefully. If not set,
// the server will wait indefinitely for all connections to close.
ShutdownTimeout *time.Duration
}
func (s *server) Start(ctx context.Context) error {
log := s.Log.WithValues("kind", s.Kind, "addr", s.Listener.Addr())
// Start starts the server. It will block until the server is stopped or an error occurs.
func (s *Server) Start(ctx context.Context) error {
log := crlog.FromContext(ctx)
if s.Name != "" {
log = log.WithValues("name", s.Name)
}
log = log.WithValues("addr", s.addr())
serverShutdown := make(chan struct{})
go func() {
<-ctx.Done()
log.Info("shutting down server")
if err := s.Server.Shutdown(context.Background()); err != nil {
shutdownCtx := context.Background()
if s.ShutdownTimeout != nil {
var shutdownCancel context.CancelFunc
shutdownCtx, shutdownCancel = context.WithTimeout(context.Background(), *s.ShutdownTimeout)
defer shutdownCancel()
}
if err := s.Server.Shutdown(shutdownCtx); err != nil {
log.Error(err, "error shutting down server")
}
close(serverShutdown)
}()
log.Info("starting server")
if err := s.Server.Serve(s.Listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
if err := s.serve(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
@@ -56,6 +89,21 @@ func (s *server) Start(ctx context.Context) error {
return nil
}
func (s *server) NeedLeaderElection() bool {
return false
// NeedLeaderElection returns true if the server should only be started when the manager is the leader.
func (s *Server) NeedLeaderElection() bool {
return s.OnlyServeWhenLeader
}
func (s *Server) addr() string {
if s.Listener != nil {
return s.Listener.Addr().String()
}
return s.Server.Addr
}
func (s *Server) serve() error {
if s.Listener != nil {
return s.Server.Serve(s.Listener)
}
return s.Server.ListenAndServe()
}

View File

@@ -14,6 +14,11 @@ var (
Name: "leader_election_master_status",
Help: "Gauge of if the reporting system is master of the relevant lease, 0 indicates backup, 1 indicates master. 'name' is the string used to identify the lease. Please make sure to group by name.",
}, []string{"name"})
leaderSlowpathCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "leader_election_slowpath_total",
Help: "Total number of slow path exercised in renewing leader leases. 'name' is the string used to identify the lease. Please make sure to group by name.",
}, []string{"name"})
)
func init() {
@@ -23,18 +28,20 @@ func init() {
type leaderelectionMetricsProvider struct{}
func (leaderelectionMetricsProvider) NewLeaderMetric() leaderelection.SwitchMetric {
return &switchAdapter{gauge: leaderGauge}
func (leaderelectionMetricsProvider) NewLeaderMetric() leaderelection.LeaderMetric {
return leaderElectionPrometheusAdapter{}
}
type switchAdapter struct {
gauge *prometheus.GaugeVec
type leaderElectionPrometheusAdapter struct{}
func (s leaderElectionPrometheusAdapter) On(name string) {
leaderGauge.WithLabelValues(name).Set(1.0)
}
func (s *switchAdapter) On(name string) {
s.gauge.WithLabelValues(name).Set(1.0)
func (s leaderElectionPrometheusAdapter) Off(name string) {
leaderGauge.WithLabelValues(name).Set(0.0)
}
func (s *switchAdapter) Off(name string) {
s.gauge.WithLabelValues(name).Set(0.0)
func (leaderElectionPrometheusAdapter) SlowpathExercised(name string) {
leaderSlowpathCounter.WithLabelValues(name).Inc()
}

View File

@@ -46,6 +46,9 @@ var DefaultBindAddress = ":8080"
// Server is a server that serves metrics.
type Server interface {
// AddExtraHandler adds extra handler served on path to the http server that serves metrics.
AddExtraHandler(path string, handler http.Handler) error
// NeedLeaderElection implements the LeaderElectionRunnable interface, which indicates
// the metrics server doesn't need leader election.
NeedLeaderElection() bool
@@ -101,6 +104,9 @@ type Options struct {
// TLSOpts is used to allow configuring the TLS config used for the server.
// This also allows providing a certificate via GetCertificate.
TLSOpts []func(*tls.Config)
// ListenConfig contains options for listening to an address on the metric server.
ListenConfig net.ListenConfig
}
// Filter is a func that is added around metrics and extra handlers on the metrics server.
@@ -179,6 +185,23 @@ func (*defaultServer) NeedLeaderElection() bool {
return false
}
// AddExtraHandler adds extra handler served on path to the http server that serves metrics.
func (s *defaultServer) AddExtraHandler(path string, handler http.Handler) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.options.ExtraHandlers == nil {
s.options.ExtraHandlers = make(map[string]http.Handler)
}
if path == defaultMetricsEndpoint {
return fmt.Errorf("overriding builtin %s endpoint is not allowed", defaultMetricsEndpoint)
}
if _, found := s.options.ExtraHandlers[path]; found {
return fmt.Errorf("can't register extra handler by duplicate path %q on metrics http server", path)
}
s.options.ExtraHandlers[path] = handler
return nil
}
// Start runs the server.
// It will install the metrics related resources depend on the server configuration.
func (s *defaultServer) Start(ctx context.Context) error {
@@ -249,7 +272,7 @@ func (s *defaultServer) Start(ctx context.Context) error {
func (s *defaultServer) createListener(ctx context.Context, log logr.Logger) (net.Listener, error) {
if !s.options.SecureServing {
return net.Listen("tcp", s.options.BindAddress)
return s.options.ListenConfig.Listen(ctx, "tcp", s.options.BindAddress)
}
cfg := &tls.Config{ //nolint:gosec
@@ -302,7 +325,12 @@ func (s *defaultServer) createListener(ctx context.Context, log logr.Logger) (ne
cfg.Certificates = []tls.Certificate{keyPair}
}
return tls.Listen("tcp", s.options.BindAddress, cfg)
l, err := s.options.ListenConfig.Listen(ctx, "tcp", s.options.BindAddress)
if err != nil {
return nil, err
}
return tls.NewListener(l, cfg), nil
}
func (s *defaultServer) GetBindAddr() string {

View File

@@ -42,27 +42,27 @@ var (
Subsystem: WorkQueueSubsystem,
Name: DepthKey,
Help: "Current depth of workqueue",
}, []string{"name"})
}, []string{"name", "controller"})
adds = prometheus.NewCounterVec(prometheus.CounterOpts{
Subsystem: WorkQueueSubsystem,
Name: AddsKey,
Help: "Total number of adds handled by workqueue",
}, []string{"name"})
}, []string{"name", "controller"})
latency = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Subsystem: WorkQueueSubsystem,
Name: QueueLatencyKey,
Help: "How long in seconds an item stays in workqueue before being requested",
Buckets: prometheus.ExponentialBuckets(10e-9, 10, 12),
}, []string{"name"})
}, []string{"name", "controller"})
workDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Subsystem: WorkQueueSubsystem,
Name: WorkDurationKey,
Help: "How long in seconds processing an item from workqueue takes.",
Buckets: prometheus.ExponentialBuckets(10e-9, 10, 12),
}, []string{"name"})
}, []string{"name", "controller"})
unfinished = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Subsystem: WorkQueueSubsystem,
@@ -71,20 +71,20 @@ var (
"is in progress and hasn't been observed by work_duration. Large " +
"values indicate stuck threads. One can deduce the number of stuck " +
"threads by observing the rate at which this increases.",
}, []string{"name"})
}, []string{"name", "controller"})
longestRunningProcessor = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Subsystem: WorkQueueSubsystem,
Name: LongestRunningProcessorKey,
Help: "How many seconds has the longest running " +
"processor for workqueue been running.",
}, []string{"name"})
}, []string{"name", "controller"})
retries = prometheus.NewCounterVec(prometheus.CounterOpts{
Subsystem: WorkQueueSubsystem,
Name: RetriesKey,
Help: "Total number of retries handled by workqueue",
}, []string{"name"})
}, []string{"name", "controller"})
)
func init() {
@@ -102,29 +102,29 @@ func init() {
type workqueueMetricsProvider struct{}
func (workqueueMetricsProvider) NewDepthMetric(name string) workqueue.GaugeMetric {
return depth.WithLabelValues(name)
return depth.WithLabelValues(name, name)
}
func (workqueueMetricsProvider) NewAddsMetric(name string) workqueue.CounterMetric {
return adds.WithLabelValues(name)
return adds.WithLabelValues(name, name)
}
func (workqueueMetricsProvider) NewLatencyMetric(name string) workqueue.HistogramMetric {
return latency.WithLabelValues(name)
return latency.WithLabelValues(name, name)
}
func (workqueueMetricsProvider) NewWorkDurationMetric(name string) workqueue.HistogramMetric {
return workDuration.WithLabelValues(name)
return workDuration.WithLabelValues(name, name)
}
func (workqueueMetricsProvider) NewUnfinishedWorkSecondsMetric(name string) workqueue.SettableGaugeMetric {
return unfinished.WithLabelValues(name)
return unfinished.WithLabelValues(name, name)
}
func (workqueueMetricsProvider) NewLongestRunningProcessorSecondsMetric(name string) workqueue.SettableGaugeMetric {
return longestRunningProcessor.WithLabelValues(name)
return longestRunningProcessor.WithLabelValues(name, name)
}
func (workqueueMetricsProvider) NewRetriesMetric(name string) workqueue.CounterMetric {
return retries.WithLabelValues(name)
return retries.WithLabelValues(name, name)
}

View File

@@ -17,6 +17,7 @@ limitations under the License.
package predicate
import (
"maps"
"reflect"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -29,45 +30,51 @@ import (
var log = logf.RuntimeLog.WithName("predicate").WithName("eventFilters")
// Predicate filters events before enqueuing the keys.
type Predicate interface {
type Predicate = TypedPredicate[client.Object]
// TypedPredicate filters events before enqueuing the keys.
type TypedPredicate[object any] interface {
// Create returns true if the Create event should be processed
Create(event.CreateEvent) bool
Create(event.TypedCreateEvent[object]) bool
// Delete returns true if the Delete event should be processed
Delete(event.DeleteEvent) bool
Delete(event.TypedDeleteEvent[object]) bool
// Update returns true if the Update event should be processed
Update(event.UpdateEvent) bool
Update(event.TypedUpdateEvent[object]) bool
// Generic returns true if the Generic event should be processed
Generic(event.GenericEvent) bool
Generic(event.TypedGenericEvent[object]) bool
}
var _ Predicate = Funcs{}
var _ Predicate = ResourceVersionChangedPredicate{}
var _ Predicate = GenerationChangedPredicate{}
var _ Predicate = AnnotationChangedPredicate{}
var _ Predicate = or{}
var _ Predicate = and{}
var _ Predicate = not{}
var _ Predicate = or[client.Object]{}
var _ Predicate = and[client.Object]{}
var _ Predicate = not[client.Object]{}
// Funcs is a function that implements Predicate.
type Funcs struct {
type Funcs = TypedFuncs[client.Object]
// TypedFuncs is a function that implements TypedPredicate.
type TypedFuncs[object any] struct {
// Create returns true if the Create event should be processed
CreateFunc func(event.CreateEvent) bool
CreateFunc func(event.TypedCreateEvent[object]) bool
// Delete returns true if the Delete event should be processed
DeleteFunc func(event.DeleteEvent) bool
DeleteFunc func(event.TypedDeleteEvent[object]) bool
// Update returns true if the Update event should be processed
UpdateFunc func(event.UpdateEvent) bool
UpdateFunc func(event.TypedUpdateEvent[object]) bool
// Generic returns true if the Generic event should be processed
GenericFunc func(event.GenericEvent) bool
GenericFunc func(event.TypedGenericEvent[object]) bool
}
// Create implements Predicate.
func (p Funcs) Create(e event.CreateEvent) bool {
func (p TypedFuncs[object]) Create(e event.TypedCreateEvent[object]) bool {
if p.CreateFunc != nil {
return p.CreateFunc(e)
}
@@ -75,7 +82,7 @@ func (p Funcs) Create(e event.CreateEvent) bool {
}
// Delete implements Predicate.
func (p Funcs) Delete(e event.DeleteEvent) bool {
func (p TypedFuncs[object]) Delete(e event.TypedDeleteEvent[object]) bool {
if p.DeleteFunc != nil {
return p.DeleteFunc(e)
}
@@ -83,7 +90,7 @@ func (p Funcs) Delete(e event.DeleteEvent) bool {
}
// Update implements Predicate.
func (p Funcs) Update(e event.UpdateEvent) bool {
func (p TypedFuncs[object]) Update(e event.TypedUpdateEvent[object]) bool {
if p.UpdateFunc != nil {
return p.UpdateFunc(e)
}
@@ -91,7 +98,7 @@ func (p Funcs) Update(e event.UpdateEvent) bool {
}
// Generic implements Predicate.
func (p Funcs) Generic(e event.GenericEvent) bool {
func (p TypedFuncs[object]) Generic(e event.TypedGenericEvent[object]) bool {
if p.GenericFunc != nil {
return p.GenericFunc(e)
}
@@ -118,18 +125,41 @@ func NewPredicateFuncs(filter func(object client.Object) bool) Funcs {
}
}
// NewTypedPredicateFuncs returns a predicate funcs that applies the given filter function
// on CREATE, UPDATE, DELETE and GENERIC events. For UPDATE events, the filter is applied
// to the new object.
func NewTypedPredicateFuncs[object any](filter func(object object) bool) TypedFuncs[object] {
return TypedFuncs[object]{
CreateFunc: func(e event.TypedCreateEvent[object]) bool {
return filter(e.Object)
},
UpdateFunc: func(e event.TypedUpdateEvent[object]) bool {
return filter(e.ObjectNew)
},
DeleteFunc: func(e event.TypedDeleteEvent[object]) bool {
return filter(e.Object)
},
GenericFunc: func(e event.TypedGenericEvent[object]) bool {
return filter(e.Object)
},
}
}
// ResourceVersionChangedPredicate implements a default update predicate function on resource version change.
type ResourceVersionChangedPredicate struct {
Funcs
type ResourceVersionChangedPredicate = TypedResourceVersionChangedPredicate[client.Object]
// TypedResourceVersionChangedPredicate implements a default update predicate function on resource version change.
type TypedResourceVersionChangedPredicate[T metav1.Object] struct {
TypedFuncs[T]
}
// Update implements default UpdateEvent filter for validating resource version change.
func (ResourceVersionChangedPredicate) Update(e event.UpdateEvent) bool {
if e.ObjectOld == nil {
func (TypedResourceVersionChangedPredicate[T]) Update(e event.TypedUpdateEvent[T]) bool {
if isNil(e.ObjectOld) {
log.Error(nil, "Update event has no old object to update", "event", e)
return false
}
if e.ObjectNew == nil {
if isNil(e.ObjectNew) {
log.Error(nil, "Update event has no new object to update", "event", e)
return false
}
@@ -153,17 +183,35 @@ func (ResourceVersionChangedPredicate) Update(e event.UpdateEvent) bool {
//
// * With this predicate, any update events with writes only to the status field will not be reconciled.
// So in the event that the status block is overwritten or wiped by someone else the controller will not self-correct to restore the correct status.
type GenerationChangedPredicate struct {
Funcs
type GenerationChangedPredicate = TypedGenerationChangedPredicate[client.Object]
// TypedGenerationChangedPredicate implements a default update predicate function on Generation change.
//
// This predicate will skip update events that have no change in the object's metadata.generation field.
// The metadata.generation field of an object is incremented by the API server when writes are made to the spec field of an object.
// This allows a controller to ignore update events where the spec is unchanged, and only the metadata and/or status fields are changed.
//
// For CustomResource objects the Generation is only incremented when the status subresource is enabled.
//
// Caveats:
//
// * The assumption that the Generation is incremented only on writing to the spec does not hold for all APIs.
// E.g For Deployment objects the Generation is also incremented on writes to the metadata.annotations field.
// For object types other than CustomResources be sure to verify which fields will trigger a Generation increment when they are written to.
//
// * With this predicate, any update events with writes only to the status field will not be reconciled.
// So in the event that the status block is overwritten or wiped by someone else the controller will not self-correct to restore the correct status.
type TypedGenerationChangedPredicate[object metav1.Object] struct {
TypedFuncs[object]
}
// Update implements default UpdateEvent filter for validating generation change.
func (GenerationChangedPredicate) Update(e event.UpdateEvent) bool {
if e.ObjectOld == nil {
func (TypedGenerationChangedPredicate[object]) Update(e event.TypedUpdateEvent[object]) bool {
if isNil(e.ObjectOld) {
log.Error(nil, "Update event has no old object to update", "event", e)
return false
}
if e.ObjectNew == nil {
if isNil(e.ObjectNew) {
log.Error(nil, "Update event has no new object for update", "event", e)
return false
}
@@ -183,22 +231,25 @@ func (GenerationChangedPredicate) Update(e event.UpdateEvent) bool {
//
// This is mostly useful for controllers that needs to trigger both when the resource's generation is incremented
// (i.e., when the resource' .spec changes), or an annotation changes (e.g., for a staging/alpha API).
type AnnotationChangedPredicate struct {
Funcs
type AnnotationChangedPredicate = TypedAnnotationChangedPredicate[client.Object]
// TypedAnnotationChangedPredicate implements a default update predicate function on annotation change.
type TypedAnnotationChangedPredicate[object metav1.Object] struct {
TypedFuncs[object]
}
// Update implements default UpdateEvent filter for validating annotation change.
func (AnnotationChangedPredicate) Update(e event.UpdateEvent) bool {
if e.ObjectOld == nil {
func (TypedAnnotationChangedPredicate[object]) Update(e event.TypedUpdateEvent[object]) bool {
if isNil(e.ObjectOld) {
log.Error(nil, "Update event has no old object to update", "event", e)
return false
}
if e.ObjectNew == nil {
if isNil(e.ObjectNew) {
log.Error(nil, "Update event has no new object for update", "event", e)
return false
}
return !reflect.DeepEqual(e.ObjectNew.GetAnnotations(), e.ObjectOld.GetAnnotations())
return !maps.Equal(e.ObjectNew.GetAnnotations(), e.ObjectOld.GetAnnotations())
}
// LabelChangedPredicate implements a default update predicate function on label change.
@@ -214,34 +265,37 @@ func (AnnotationChangedPredicate) Update(e event.UpdateEvent) bool {
//
// This will be helpful when object's labels is carrying some extra specification information beyond object's spec,
// and the controller will be triggered if any valid spec change (not only in spec, but also in labels) happens.
type LabelChangedPredicate struct {
Funcs
type LabelChangedPredicate = TypedLabelChangedPredicate[client.Object]
// TypedLabelChangedPredicate implements a default update predicate function on label change.
type TypedLabelChangedPredicate[object metav1.Object] struct {
TypedFuncs[object]
}
// Update implements default UpdateEvent filter for checking label change.
func (LabelChangedPredicate) Update(e event.UpdateEvent) bool {
if e.ObjectOld == nil {
func (TypedLabelChangedPredicate[object]) Update(e event.TypedUpdateEvent[object]) bool {
if isNil(e.ObjectOld) {
log.Error(nil, "Update event has no old object to update", "event", e)
return false
}
if e.ObjectNew == nil {
if isNil(e.ObjectNew) {
log.Error(nil, "Update event has no new object for update", "event", e)
return false
}
return !reflect.DeepEqual(e.ObjectNew.GetLabels(), e.ObjectOld.GetLabels())
return !maps.Equal(e.ObjectNew.GetLabels(), e.ObjectOld.GetLabels())
}
// And returns a composite predicate that implements a logical AND of the predicates passed to it.
func And(predicates ...Predicate) Predicate {
return and{predicates}
func And[object any](predicates ...TypedPredicate[object]) TypedPredicate[object] {
return and[object]{predicates}
}
type and struct {
predicates []Predicate
type and[object any] struct {
predicates []TypedPredicate[object]
}
func (a and) Create(e event.CreateEvent) bool {
func (a and[object]) Create(e event.TypedCreateEvent[object]) bool {
for _, p := range a.predicates {
if !p.Create(e) {
return false
@@ -250,7 +304,7 @@ func (a and) Create(e event.CreateEvent) bool {
return true
}
func (a and) Update(e event.UpdateEvent) bool {
func (a and[object]) Update(e event.TypedUpdateEvent[object]) bool {
for _, p := range a.predicates {
if !p.Update(e) {
return false
@@ -259,7 +313,7 @@ func (a and) Update(e event.UpdateEvent) bool {
return true
}
func (a and) Delete(e event.DeleteEvent) bool {
func (a and[object]) Delete(e event.TypedDeleteEvent[object]) bool {
for _, p := range a.predicates {
if !p.Delete(e) {
return false
@@ -268,7 +322,7 @@ func (a and) Delete(e event.DeleteEvent) bool {
return true
}
func (a and) Generic(e event.GenericEvent) bool {
func (a and[object]) Generic(e event.TypedGenericEvent[object]) bool {
for _, p := range a.predicates {
if !p.Generic(e) {
return false
@@ -278,15 +332,15 @@ func (a and) Generic(e event.GenericEvent) bool {
}
// Or returns a composite predicate that implements a logical OR of the predicates passed to it.
func Or(predicates ...Predicate) Predicate {
return or{predicates}
func Or[object any](predicates ...TypedPredicate[object]) TypedPredicate[object] {
return or[object]{predicates}
}
type or struct {
predicates []Predicate
type or[object any] struct {
predicates []TypedPredicate[object]
}
func (o or) Create(e event.CreateEvent) bool {
func (o or[object]) Create(e event.TypedCreateEvent[object]) bool {
for _, p := range o.predicates {
if p.Create(e) {
return true
@@ -295,7 +349,7 @@ func (o or) Create(e event.CreateEvent) bool {
return false
}
func (o or) Update(e event.UpdateEvent) bool {
func (o or[object]) Update(e event.TypedUpdateEvent[object]) bool {
for _, p := range o.predicates {
if p.Update(e) {
return true
@@ -304,7 +358,7 @@ func (o or) Update(e event.UpdateEvent) bool {
return false
}
func (o or) Delete(e event.DeleteEvent) bool {
func (o or[object]) Delete(e event.TypedDeleteEvent[object]) bool {
for _, p := range o.predicates {
if p.Delete(e) {
return true
@@ -313,7 +367,7 @@ func (o or) Delete(e event.DeleteEvent) bool {
return false
}
func (o or) Generic(e event.GenericEvent) bool {
func (o or[object]) Generic(e event.TypedGenericEvent[object]) bool {
for _, p := range o.predicates {
if p.Generic(e) {
return true
@@ -323,27 +377,27 @@ func (o or) Generic(e event.GenericEvent) bool {
}
// Not returns a predicate that implements a logical NOT of the predicate passed to it.
func Not(predicate Predicate) Predicate {
return not{predicate}
func Not[object any](predicate TypedPredicate[object]) TypedPredicate[object] {
return not[object]{predicate}
}
type not struct {
predicate Predicate
type not[object any] struct {
predicate TypedPredicate[object]
}
func (n not) Create(e event.CreateEvent) bool {
func (n not[object]) Create(e event.TypedCreateEvent[object]) bool {
return !n.predicate.Create(e)
}
func (n not) Update(e event.UpdateEvent) bool {
func (n not[object]) Update(e event.TypedUpdateEvent[object]) bool {
return !n.predicate.Update(e)
}
func (n not) Delete(e event.DeleteEvent) bool {
func (n not[object]) Delete(e event.TypedDeleteEvent[object]) bool {
return !n.predicate.Delete(e)
}
func (n not) Generic(e event.GenericEvent) bool {
func (n not[object]) Generic(e event.TypedGenericEvent[object]) bool {
return !n.predicate.Generic(e)
}
@@ -358,3 +412,15 @@ func LabelSelectorPredicate(s metav1.LabelSelector) (Predicate, error) {
return selector.Matches(labels.Set(o.GetLabels()))
}), nil
}
func isNil(arg any) bool {
if v := reflect.ValueOf(arg); !v.IsValid() || ((v.Kind() == reflect.Ptr ||
v.Kind() == reflect.Interface ||
v.Kind() == reflect.Slice ||
v.Kind() == reflect.Map ||
v.Kind() == reflect.Chan ||
v.Kind() == reflect.Func) && v.IsNil()) {
return true
}
return false
}

View File

@@ -1,30 +0,0 @@
/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ratelimiter
import "time"
// RateLimiter is an identical interface of client-go workqueue RateLimiter.
type RateLimiter interface {
// When gets an item and gets to decide how long that item should wait
When(item interface{}) time.Duration
// Forget indicates that an item is finished being retried. Doesn't matter whether its for perm failing
// or for success, we'll stop tracking it
Forget(item interface{})
// NumRequeues returns back how many failures the item has had
NumRequeues(item interface{}) int
}

View File

@@ -89,7 +89,14 @@ driven by actual cluster state read from the apiserver or a local cache.
For example if responding to a Pod Delete Event, the Request won't contain that a Pod was deleted,
instead the reconcile function observes this when reading the cluster state and seeing the Pod as missing.
*/
type Reconciler interface {
type Reconciler = TypedReconciler[Request]
// TypedReconciler implements an API for a specific Resource by Creating, Updating or Deleting Kubernetes
// objects, or by making changes to systems external to the cluster (e.g. cloudproviders, github, etc).
//
// The request type is what event handlers put into the workqueue. The workqueue then de-duplicates identical
// requests.
type TypedReconciler[request comparable] interface {
// Reconcile performs a full reconciliation for the object referred to by the Request.
//
// If the returned error is non-nil, the Result is ignored and the request will be
@@ -101,40 +108,45 @@ type Reconciler interface {
//
// If the error is nil and result.RequeueAfter is zero and result.Requeue is true, the request
// will be requeued using exponential backoff.
Reconcile(context.Context, Request) (Result, error)
Reconcile(context.Context, request) (Result, error)
}
// Func is a function that implements the reconcile interface.
type Func func(context.Context, Request) (Result, error)
type Func = TypedFunc[Request]
// TypedFunc is a function that implements the reconcile interface.
type TypedFunc[request comparable] func(context.Context, request) (Result, error)
var _ Reconciler = Func(nil)
// Reconcile implements Reconciler.
func (r Func) Reconcile(ctx context.Context, o Request) (Result, error) { return r(ctx, o) }
func (r TypedFunc[request]) Reconcile(ctx context.Context, req request) (Result, error) {
return r(ctx, req)
}
// ObjectReconciler is a specialized version of Reconciler that acts on instances of client.Object. Each reconciliation
// event gets the associated object from Kubernetes before passing it to Reconcile. An ObjectReconciler can be used in
// Builder.Complete by calling AsReconciler. See Reconciler for more details.
type ObjectReconciler[T client.Object] interface {
Reconcile(context.Context, T) (Result, error)
type ObjectReconciler[object client.Object] interface {
Reconcile(context.Context, object) (Result, error)
}
// AsReconciler creates a Reconciler based on the given ObjectReconciler.
func AsReconciler[T client.Object](client client.Client, rec ObjectReconciler[T]) Reconciler {
return &objectReconcilerAdapter[T]{
func AsReconciler[object client.Object](client client.Client, rec ObjectReconciler[object]) Reconciler {
return &objectReconcilerAdapter[object]{
objReconciler: rec,
client: client,
}
}
type objectReconcilerAdapter[T client.Object] struct {
objReconciler ObjectReconciler[T]
type objectReconcilerAdapter[object client.Object] struct {
objReconciler ObjectReconciler[object]
client client.Client
}
// Reconcile implements Reconciler.
func (a *objectReconcilerAdapter[T]) Reconcile(ctx context.Context, req Request) (Result, error) {
o := reflect.New(reflect.TypeOf(*new(T)).Elem()).Interface().(T)
func (a *objectReconcilerAdapter[object]) Reconcile(ctx context.Context, req Request) (Result, error) {
o := reflect.New(reflect.TypeOf(*new(object)).Elem()).Interface().(object)
if err := a.client.Get(ctx, req.NamespacedName, o); err != nil {
return Result{}, client.IgnoreNotFound(err)
}

View File

@@ -18,94 +18,174 @@ package source
import (
"context"
"errors"
"fmt"
"sync"
"k8s.io/client-go/util/workqueue"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
internal "sigs.k8s.io/controller-runtime/pkg/internal/source"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)
const (
// defaultBufferSize is the default number of event notifications that can be buffered.
defaultBufferSize = 1024
)
// Source is a source of events (eh.g. Create, Update, Delete operations on Kubernetes Objects, Webhook callbacks, etc)
// Source is a source of events (e.g. Create, Update, Delete operations on Kubernetes Objects, Webhook callbacks, etc)
// which should be processed by event.EventHandlers to enqueue reconcile.Requests.
//
// * Use Kind for events originating in the cluster (e.g. Pod Create, Pod Update, Deployment Update).
//
// * Use Channel for events originating outside the cluster (eh.g. GitHub Webhook callback, Polling external urls).
// * Use Channel for events originating outside the cluster (e.g. GitHub Webhook callback, Polling external urls).
//
// Users may build their own Source implementations.
type Source interface {
// Start is internal and should be called only by the Controller to register an EventHandler with the Informer
// to enqueue reconcile.Requests.
Start(context.Context, handler.EventHandler, workqueue.RateLimitingInterface, ...predicate.Predicate) error
type Source = TypedSource[reconcile.Request]
// TypedSource is a generic source of events (e.g. Create, Update, Delete operations on Kubernetes Objects, Webhook callbacks, etc)
// which should be processed by event.EventHandlers to enqueue a request.
//
// * Use Kind for events originating in the cluster (e.g. Pod Create, Pod Update, Deployment Update).
//
// * Use Channel for events originating outside the cluster (e.g. GitHub Webhook callback, Polling external urls).
//
// Users may build their own Source implementations.
type TypedSource[request comparable] interface {
// Start is internal and should be called only by the Controller to start the source.
// Start must be non-blocking.
Start(context.Context, workqueue.TypedRateLimitingInterface[request]) error
}
// SyncingSource is a source that needs syncing prior to being usable. The controller
// will call its WaitForSync prior to starting workers.
type SyncingSource interface {
Source
type SyncingSource = TypedSyncingSource[reconcile.Request]
// TypedSyncingSource is a source that needs syncing prior to being usable. The controller
// will call its WaitForSync prior to starting workers.
type TypedSyncingSource[request comparable] interface {
TypedSource[request]
WaitForSync(ctx context.Context) error
}
// Kind creates a KindSource with the given cache provider.
func Kind(cache cache.Cache, object client.Object) SyncingSource {
return &internal.Kind{Type: object, Cache: cache}
func Kind[object client.Object](
cache cache.Cache,
obj object,
handler handler.TypedEventHandler[object, reconcile.Request],
predicates ...predicate.TypedPredicate[object],
) SyncingSource {
return TypedKind(cache, obj, handler, predicates...)
}
var _ Source = &Channel{}
// TypedKind creates a KindSource with the given cache provider.
func TypedKind[object client.Object, request comparable](
cache cache.Cache,
obj object,
handler handler.TypedEventHandler[object, request],
predicates ...predicate.TypedPredicate[object],
) TypedSyncingSource[request] {
return &internal.Kind[object, request]{
Type: obj,
Cache: cache,
Handler: handler,
Predicates: predicates,
}
}
var _ Source = &channel[string, reconcile.Request]{}
// ChannelOpt allows to configure a source.Channel.
type ChannelOpt[object any, request comparable] func(*channel[object, request])
// WithPredicates adds the configured predicates to a source.Channel.
func WithPredicates[object any, request comparable](p ...predicate.TypedPredicate[object]) ChannelOpt[object, request] {
return func(c *channel[object, request]) {
c.predicates = append(c.predicates, p...)
}
}
// WithBufferSize configures the buffer size for a source.Channel. By
// default, the buffer size is 1024.
func WithBufferSize[object any, request comparable](bufferSize int) ChannelOpt[object, request] {
return func(c *channel[object, request]) {
c.bufferSize = &bufferSize
}
}
// Channel is used to provide a source of events originating outside the cluster
// (e.g. GitHub Webhook callback). Channel requires the user to wire the external
// source (eh.g. http handler) to write GenericEvents to the underlying channel.
type Channel struct {
// source (e.g. http handler) to write GenericEvents to the underlying channel.
func Channel[object any](
source <-chan event.TypedGenericEvent[object],
handler handler.TypedEventHandler[object, reconcile.Request],
opts ...ChannelOpt[object, reconcile.Request],
) Source {
return TypedChannel[object, reconcile.Request](source, handler, opts...)
}
// TypedChannel is used to provide a source of events originating outside the cluster
// (e.g. GitHub Webhook callback). Channel requires the user to wire the external
// source (e.g. http handler) to write GenericEvents to the underlying channel.
func TypedChannel[object any, request comparable](
source <-chan event.TypedGenericEvent[object],
handler handler.TypedEventHandler[object, request],
opts ...ChannelOpt[object, request],
) TypedSource[request] {
c := &channel[object, request]{
source: source,
handler: handler,
}
for _, opt := range opts {
opt(c)
}
return c
}
type channel[object any, request comparable] struct {
// once ensures the event distribution goroutine will be performed only once
once sync.Once
// Source is the source channel to fetch GenericEvents
Source <-chan event.GenericEvent
// source is the source channel to fetch GenericEvents
source <-chan event.TypedGenericEvent[object]
handler handler.TypedEventHandler[object, request]
predicates []predicate.TypedPredicate[object]
bufferSize *int
// dest is the destination channels of the added event handlers
dest []chan event.GenericEvent
// DestBufferSize is the specified buffer size of dest channels.
// Default to 1024 if not specified.
DestBufferSize int
dest []chan event.TypedGenericEvent[object]
// destLock is to ensure the destination channels are safely added/removed
destLock sync.Mutex
}
func (cs *Channel) String() string {
func (cs *channel[object, request]) String() string {
return fmt.Sprintf("channel source: %p", cs)
}
// Start implements Source and should only be called by the Controller.
func (cs *Channel) Start(
func (cs *channel[object, request]) Start(
ctx context.Context,
handler handler.EventHandler,
queue workqueue.RateLimitingInterface,
prct ...predicate.Predicate) error {
queue workqueue.TypedRateLimitingInterface[request],
) error {
// Source should have been specified by the user.
if cs.Source == nil {
if cs.source == nil {
return fmt.Errorf("must specify Channel.Source")
}
// use default value if DestBufferSize not specified
if cs.DestBufferSize == 0 {
cs.DestBufferSize = defaultBufferSize
if cs.handler == nil {
return errors.New("must specify Channel.Handler")
}
dst := make(chan event.GenericEvent, cs.DestBufferSize)
if cs.bufferSize == nil {
cs.bufferSize = ptr.To(1024)
}
dst := make(chan event.TypedGenericEvent[object], *cs.bufferSize)
cs.destLock.Lock()
cs.dest = append(cs.dest, dst)
@@ -119,7 +199,7 @@ func (cs *Channel) Start(
go func() {
for evt := range dst {
shouldHandle := true
for _, p := range prct {
for _, p := range cs.predicates {
if !p.Generic(evt) {
shouldHandle = false
break
@@ -130,7 +210,7 @@ func (cs *Channel) Start(
func() {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
handler.Generic(ctx, evt, queue)
cs.handler.Generic(ctx, evt, queue)
}()
}
}
@@ -139,7 +219,7 @@ func (cs *Channel) Start(
return nil
}
func (cs *Channel) doStop() {
func (cs *channel[object, request]) doStop() {
cs.destLock.Lock()
defer cs.destLock.Unlock()
@@ -148,7 +228,7 @@ func (cs *Channel) doStop() {
}
}
func (cs *Channel) distribute(evt event.GenericEvent) {
func (cs *channel[object, request]) distribute(evt event.TypedGenericEvent[object]) {
cs.destLock.Lock()
defer cs.destLock.Unlock()
@@ -162,14 +242,14 @@ func (cs *Channel) distribute(evt event.GenericEvent) {
}
}
func (cs *Channel) syncLoop(ctx context.Context) {
func (cs *channel[object, request]) syncLoop(ctx context.Context) {
for {
select {
case <-ctx.Done():
// Close destination channels
cs.doStop()
return
case evt, stillOpen := <-cs.Source:
case evt, stillOpen := <-cs.source:
if !stillOpen {
// if the source channel is closed, we're never gonna get
// anything more on it, so stop & bail
@@ -184,21 +264,25 @@ func (cs *Channel) syncLoop(ctx context.Context) {
// Informer is used to provide a source of events originating inside the cluster from Watches (e.g. Pod Create).
type Informer struct {
// Informer is the controller-runtime Informer
Informer cache.Informer
Informer cache.Informer
Handler handler.EventHandler
Predicates []predicate.Predicate
}
var _ Source = &Informer{}
// Start is internal and should be called only by the Controller to register an EventHandler with the Informer
// to enqueue reconcile.Requests.
func (is *Informer) Start(ctx context.Context, handler handler.EventHandler, queue workqueue.RateLimitingInterface,
prct ...predicate.Predicate) error {
func (is *Informer) Start(ctx context.Context, queue workqueue.TypedRateLimitingInterface[reconcile.Request]) error {
// Informer should have been specified by the user.
if is.Informer == nil {
return fmt.Errorf("must specify Informer.Informer")
}
if is.Handler == nil {
return errors.New("must specify Informer.Handler")
}
_, err := is.Informer.AddEventHandler(internal.NewEventHandler(ctx, queue, handler, prct).HandlerFuncs())
_, err := is.Informer.AddEventHandler(internal.NewEventHandler(ctx, queue, is.Handler, is.Predicates).HandlerFuncs())
if err != nil {
return err
}
@@ -212,14 +296,16 @@ func (is *Informer) String() string {
var _ Source = Func(nil)
// Func is a function that implements Source.
type Func func(context.Context, handler.EventHandler, workqueue.RateLimitingInterface, ...predicate.Predicate) error
type Func = TypedFunc[reconcile.Request]
// TypedFunc is a function that implements Source.
type TypedFunc[request comparable] func(context.Context, workqueue.TypedRateLimitingInterface[request]) error
// Start implements Source.
func (f Func) Start(ctx context.Context, evt handler.EventHandler, queue workqueue.RateLimitingInterface,
pr ...predicate.Predicate) error {
return f(ctx, evt, queue, pr...)
func (f TypedFunc[request]) Start(ctx context.Context, queue workqueue.TypedRateLimitingInterface[request]) error {
return f(ctx, queue)
}
func (f Func) String() string {
func (f TypedFunc[request]) String() string {
return fmt.Sprintf("func source: %p", f)
}

View File

@@ -26,22 +26,35 @@ import (
// Decoder knows how to decode the contents of an admission
// request into a concrete object.
type Decoder struct {
type Decoder interface {
// Decode decodes the inlined object in the AdmissionRequest into the passed-in runtime.Object.
// If you want decode the OldObject in the AdmissionRequest, use DecodeRaw.
// It errors out if req.Object.Raw is empty i.e. containing 0 raw bytes.
Decode(req Request, into runtime.Object) error
// DecodeRaw decodes a RawExtension object into the passed-in runtime.Object.
// It errors out if rawObj is empty i.e. containing 0 raw bytes.
DecodeRaw(rawObj runtime.RawExtension, into runtime.Object) error
}
// decoder knows how to decode the contents of an admission
// request into a concrete object.
type decoder struct {
codecs serializer.CodecFactory
}
// NewDecoder creates a Decoder given the runtime.Scheme.
func NewDecoder(scheme *runtime.Scheme) *Decoder {
// NewDecoder creates a decoder given the runtime.Scheme.
func NewDecoder(scheme *runtime.Scheme) Decoder {
if scheme == nil {
panic("scheme should never be nil")
}
return &Decoder{codecs: serializer.NewCodecFactory(scheme)}
return &decoder{codecs: serializer.NewCodecFactory(scheme)}
}
// Decode decodes the inlined object in the AdmissionRequest into the passed-in runtime.Object.
// If you want decode the OldObject in the AdmissionRequest, use DecodeRaw.
// It errors out if req.Object.Raw is empty i.e. containing 0 raw bytes.
func (d *Decoder) Decode(req Request, into runtime.Object) error {
func (d *decoder) Decode(req Request, into runtime.Object) error {
// we error out if rawObj is an empty object.
if len(req.Object.Raw) == 0 {
return fmt.Errorf("there is no content to decode")
@@ -51,7 +64,7 @@ func (d *Decoder) Decode(req Request, into runtime.Object) error {
// DecodeRaw decodes a RawExtension object into the passed-in runtime.Object.
// It errors out if rawObj is empty i.e. containing 0 raw bytes.
func (d *Decoder) DecodeRaw(rawObj runtime.RawExtension, into runtime.Object) error {
func (d *decoder) DecodeRaw(rawObj runtime.RawExtension, into runtime.Object) error {
// NB(directxman12): there's a bug/weird interaction between decoders and
// the API server where the API server doesn't send a GVK on the embedded
// objects, which means the unstructured decoder refuses to decode. It

View File

@@ -43,7 +43,7 @@ func DefaultingWebhookFor(scheme *runtime.Scheme, defaulter Defaulter) *Webhook
type mutatingHandler struct {
defaulter Defaulter
decoder *Decoder
decoder Decoder
}
// Handle handles admission requests.

View File

@@ -43,7 +43,7 @@ func WithCustomDefaulter(scheme *runtime.Scheme, obj runtime.Object, defaulter C
type defaulterForType struct {
defaulter CustomDefaulter
object runtime.Object
decoder *Decoder
decoder Decoder
}
// Handle handles admission requests.

View File

@@ -0,0 +1,39 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"sigs.k8s.io/controller-runtime/pkg/metrics"
)
var (
// WebhookPanics is a prometheus counter metrics which holds the total
// number of panics from webhooks.
WebhookPanics = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "controller_runtime_webhook_panics_total",
Help: "Total number of webhook panics",
}, []string{})
)
func init() {
metrics.Registry.MustRegister(
WebhookPanics,
)
// Init metric.
WebhookPanics.WithLabelValues().Add(0)
}

View File

@@ -63,7 +63,7 @@ func ValidatingWebhookFor(scheme *runtime.Scheme, validator Validator) *Webhook
type validatingHandler struct {
validator Validator
decoder *Decoder
decoder Decoder
}
// Handle handles admission requests.

View File

@@ -56,7 +56,7 @@ func WithCustomValidator(scheme *runtime.Scheme, obj runtime.Object, validator C
type validatorForType struct {
validator CustomValidator
object runtime.Object
decoder *Decoder
decoder Decoder
}
// Handle handles admission requests.

View File

@@ -30,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/util/json"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/klog/v2"
admissionmetrics "sigs.k8s.io/controller-runtime/pkg/webhook/admission/metrics"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics"
@@ -123,7 +124,8 @@ type Webhook struct {
Handler Handler
// RecoverPanic indicates whether the panic caused by webhook should be recovered.
RecoverPanic bool
// Defaults to true.
RecoverPanic *bool
// WithContextFunc will allow you to take the http.Request.Context() and
// add any additional information such as passing the request path or
@@ -141,8 +143,9 @@ type Webhook struct {
}
// WithRecoverPanic takes a bool flag which indicates whether the panic caused by webhook should be recovered.
// Defaults to true.
func (wh *Webhook) WithRecoverPanic(recoverPanic bool) *Webhook {
wh.RecoverPanic = recoverPanic
wh.RecoverPanic = &recoverPanic
return wh
}
@@ -151,17 +154,26 @@ func (wh *Webhook) WithRecoverPanic(recoverPanic bool) *Webhook {
// If the webhook is validating type, it delegates the AdmissionRequest to each handler and
// deny the request if anyone denies.
func (wh *Webhook) Handle(ctx context.Context, req Request) (response Response) {
if wh.RecoverPanic {
defer func() {
if r := recover(); r != nil {
defer func() {
if r := recover(); r != nil {
admissionmetrics.WebhookPanics.WithLabelValues().Inc()
if wh.RecoverPanic == nil || *wh.RecoverPanic {
for _, fn := range utilruntime.PanicHandlers {
fn(r)
fn(ctx, r)
}
response = Errored(http.StatusInternalServerError, fmt.Errorf("panic: %v [recovered]", r))
// Note: We explicitly have to set the response UID. Usually that is done via resp.Complete below,
// but if we encounter a panic in wh.Handler.Handle we are never going to reach resp.Complete.
response.UID = req.UID
return
}
}()
}
log := logf.FromContext(ctx)
log.Info(fmt.Sprintf("Observed a panic in webhook: %v", r))
panic(r)
}
}()
reqLog := wh.getLogger(&req)
ctx = logf.IntoContext(ctx, reqLog)
@@ -169,7 +181,10 @@ func (wh *Webhook) Handle(ctx context.Context, req Request) (response Response)
resp := wh.Handler.Handle(ctx, req)
if err := resp.Complete(req); err != nil {
reqLog.Error(err, "unable to encode response")
return Errored(http.StatusInternalServerError, errUnableToEncodeResponse)
resp := Errored(http.StatusInternalServerError, errUnableToEncodeResponse)
// Note: We explicitly have to set the response UID. Usually that is done via resp.Complete.
resp.UID = req.UID
return resp
}
return resp