From e74cb9d3334c169406ec6614f3a0ea51ab8dff1f Mon Sep 17 00:00:00 2001 From: Bryan Cox Date: Fri, 5 Jun 2026 07:37:40 -0400 Subject: [PATCH 1/4] chore: remove controller-runtime v0.19.7 pin, bump NTO to release-4.20 Remove the replace directive that pinned controller-runtime to v0.19.7. The pin was added when webhook.Validator deprecation in v0.20 broke vendored dependencies, but all CAPI providers and HyperShift code have since migrated to CustomValidator/CustomDefaulter. Bump cluster-node-tuning-operator to release-4.20 branch which uses the new webhook.CustomValidator interface compatible with v0.22. This also pulls in k8s.io/kubelet v0.33.3 (from v0.32.2). Signed-off-by: Bryan Cox Commit-Message-Assisted-by: Claude (via Claude Code) --- go.mod | 9 +++------ go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index ed9b4a7de2fc..9f6766e6c333 100644 --- a/go.mod +++ b/go.mod @@ -66,9 +66,9 @@ require ( github.com/openshift/cloud-credential-operator v0.0.0-20250225003505-216fd1a30ec3 github.com/openshift/cluster-api-provider-agent/api v0.0.0-20260120122324-898e638ec7d1 github.com/openshift/cluster-autoscaler-operator v0.0.1-0.20241204142113-43631b045675 - github.com/openshift/cluster-node-tuning-operator v0.0.0-20250225115807-f166846b7256 + github.com/openshift/cluster-node-tuning-operator v0.0.0-20260527203700-73801cc280b3 github.com/openshift/custom-resource-status v1.1.3-0.20220503160415-f2fdb4999d87 - github.com/openshift/hypershift/api v0.0.0-20240604072534-cd2d5291e2b7 + github.com/openshift/hypershift/api v0.0.0-20250724113115-0281b49cc465 github.com/openshift/library-go v0.0.0-20251204132909-8814e976a023 github.com/openshift/multi-operator-manager v0.0.0-20260112172834-b64ebc8c627b github.com/operator-framework/api v0.37.0 @@ -301,7 +301,7 @@ require ( k8s.io/csi-translation-lib v0.35.0 // indirect k8s.io/kms v0.35.1 // indirect k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect - k8s.io/kubelet v0.32.2 // indirect + k8s.io/kubelet v0.33.3 // indirect kubevirt.io/controller-lifecycle-operator-sdk/api v0.2.4 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect @@ -319,9 +319,6 @@ replace github.com/k-orc/openstack-resource-controller => sigs.k8s.io/cluster-ap // CVE-2025-30204 replace github.com/golang-jwt/jwt/v4 => github.com/golang-jwt/jwt/v4 v4.5.2 -// webhook.Validator deprecation in v0.20 breaks everything, conversion is nontrivial -replace sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.19.7 - // Use our openshift version of karpenter instead of upstream replace github.com/aws/karpenter-provider-aws => github.com/openshift/aws-karpenter-provider-aws v0.0.0-20260311064431-f0be9c72e5bf diff --git a/go.sum b/go.sum index 4bf47a495004..4825c1328e58 100644 --- a/go.sum +++ b/go.sum @@ -615,8 +615,8 @@ github.com/openshift/cluster-api-provider-agent/api v0.0.0-20260120122324-898e63 github.com/openshift/cluster-api-provider-agent/api v0.0.0-20260120122324-898e638ec7d1/go.mod h1:/hEmyZ903PeqntDSxLgZcje96/2lC3PmvsnUABQpR8Q= github.com/openshift/cluster-autoscaler-operator v0.0.1-0.20241204142113-43631b045675 h1:hPFyXtaR42wqKKGMVP4G7M2vmp5iBxBqKetMguh4Td0= github.com/openshift/cluster-autoscaler-operator v0.0.1-0.20241204142113-43631b045675/go.mod h1:0tGCwMCgKq7KhJWDGr6Tsqqb6Sk3epz/b6tfFDFK1Ug= -github.com/openshift/cluster-node-tuning-operator v0.0.0-20250225115807-f166846b7256 h1:YLqKXRH7DTukM1sxLOi++uUe7R2m0ivK7yqf0X6emok= -github.com/openshift/cluster-node-tuning-operator v0.0.0-20250225115807-f166846b7256/go.mod h1:nMuHN1oKtpWkQlV1jwqMYV0UfF6wWb+e3TazrWTbNSY= +github.com/openshift/cluster-node-tuning-operator v0.0.0-20260527203700-73801cc280b3 h1:oZXGbNRqAqe+mPEvC2GP4fWtkkn5isB6G+PyxpNKaEQ= +github.com/openshift/cluster-node-tuning-operator v0.0.0-20260527203700-73801cc280b3/go.mod h1:8aR3IFSv4aq7edM3U8UDpWHUFP/nKQneNPoiheWRsYU= github.com/openshift/custom-resource-status v1.1.3-0.20220503160415-f2fdb4999d87 h1:cHyxR+Y8rAMT6m1jQCaYGRwikqahI0OjjUDhFNf3ySQ= github.com/openshift/custom-resource-status v1.1.3-0.20220503160415-f2fdb4999d87/go.mod h1:DB/Mf2oTeiAmVVX1gN+NEqweonAPY0TKUwADizj8+ZA= github.com/openshift/kubernetes-sigs-karpenter v0.0.0-20260310165629-67e201b559d5 h1:vkWMN8h47VGeqtES4jvRDfWWDqPIGTMtm2ltcg8GcuA= @@ -1056,8 +1056,8 @@ k8s.io/kube-scheduler v0.35.1 h1:xhF7M/4Hclq69IAG6K6qW2Y2P3jf9btRqccon3hKz9s= k8s.io/kube-scheduler v0.35.1/go.mod h1:6wg+wyqGBuT93PRNmk7b/xPvYZ28K4JmUfWgeIz/JAU= k8s.io/kubectl v0.35.1 h1:zP3Er8C5i1dcAFUMh9Eva0kVvZHptXIn/+8NtRWMxwg= k8s.io/kubectl v0.35.1/go.mod h1:cQ2uAPs5IO/kx8R5s5J3Ihv3VCYwrx0obCXum0CvnXo= -k8s.io/kubelet v0.32.2 h1:WFTSYdt3BB1aTApDuKNI16x/4MYqqX8WBBBBh3KupDg= -k8s.io/kubelet v0.32.2/go.mod h1:cC1ms5RS+lu0ckVr6AviCQXHLSPKEBC3D5oaCBdTGkI= +k8s.io/kubelet v0.33.3 h1:Cvy8+7Lq9saZds2ib7YBXbKvkMMJu3f5mzucmhSIJno= +k8s.io/kubelet v0.33.3/go.mod h1:Q1Cfr6VQq1m9v9XsE/mDmhTxPdN6NPU6Ug0e6mAqi58= k8s.io/pod-security-admission v0.35.1 h1:Ra7QA/mTXVabzzgQAe36trllpQdGSvwuq9pdnXsIqoI= k8s.io/pod-security-admission v0.35.1/go.mod h1:J2OnqW+rNItdl6XZeySa4m2nDqrZ+nBpk1Mr6Vf9M/U= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= @@ -1088,8 +1088,8 @@ sigs.k8s.io/cluster-api-provider-openstack v0.13.3 h1:qBPgWG1E3wiiK8+VR5a3jnKGTy sigs.k8s.io/cluster-api-provider-openstack v0.13.3/go.mod h1:McuqrgmgsbK4yaC1HKqJ7dxKnOERmXQedUUL15sqelQ= sigs.k8s.io/cluster-api-provider-openstack/orc v0.0.0-20250113192833-e4f56a2b4f32 h1:AkFSgi+dAnPLtg+SXjtCf3rDzjI/mskDiJ1PWHZoRno= sigs.k8s.io/cluster-api-provider-openstack/orc v0.0.0-20250113192833-e4f56a2b4f32/go.mod h1:hQOMZZjzuAt9pdLaE/tj5ByDTVNBkqNaP10ijnqNZfU= -sigs.k8s.io/controller-runtime v0.19.7 h1:DLABZfMr20A+AwCZOHhcbcu+TqBXnJZaVBri9K3EO48= -sigs.k8s.io/controller-runtime v0.19.7/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= +sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= +sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= From 046f53c726ff1d2ef5e3ed1dafcd2cec77c57e4e Mon Sep 17 00:00:00 2001 From: Bryan Cox Date: Fri, 5 Jun 2026 07:37:46 -0400 Subject: [PATCH 2/4] chore(api): revendor controller-runtime v0.22.4, NTO and kubelet Revendor after removing the controller-runtime v0.19.7 replace directive. Updates: - sigs.k8s.io/controller-runtime v0.19.7 -> v0.22.4 - cluster-node-tuning-operator to release-4.20 branch - k8s.io/kubelet v0.32.2 -> v0.33.3 - golang.org/x/exp/maps added as new NTO dependency Regenerated test fixtures with updated resourceVersion values. Signed-off-by: Bryan Cox Commit-Message-Assisted-by: Claude (via Claude Code) --- .../zz_fixture_TestReconcileComponents.yaml | 2 +- .../zz_fixture_TestReconcileComponents.yaml | 2 +- .../zz_fixture_TestReconcileComponents.yaml | 2 +- .../zz_fixture_TestReconcileComponents.yaml | 2 +- .../v2/performanceprofile_types.go | 4 +- .../v2/performanceprofile_validation.go | 64 +- .../v2/performanceprofile_webhook.go | 3 +- .../pkg/apis/tuned/v1/tuned_types.go | 31 +- .../apis/tuned/v1/zz_generated.deepcopy.go | 19 +- vendor/golang.org/x/exp/maps/maps.go | 86 --- vendor/k8s.io/kubelet/config/v1beta1/doc.go | 2 +- vendor/k8s.io/kubelet/config/v1beta1/types.go | 77 ++- .../config/v1beta1/zz_generated.deepcopy.go | 36 ++ vendor/modules.txt | 19 +- .../controller-runtime/.golangci.yml | 294 ++++----- .../controller-runtime/.gomodcheck.yaml | 3 + .../sigs.k8s.io/controller-runtime/Makefile | 8 +- .../controller-runtime/OWNERS_ALIASES | 6 +- .../sigs.k8s.io/controller-runtime/README.md | 11 +- .../controller-runtime/VERSIONING.md | 10 + .../sigs.k8s.io/controller-runtime/alias.go | 6 + vendor/sigs.k8s.io/controller-runtime/doc.go | 2 +- .../controller-runtime/pkg/builder/webhook.go | 131 ++-- .../controller-runtime/pkg/cache/cache.go | 123 +++- .../pkg/cache/delegating_by_gvk_cache.go | 7 +- .../pkg/cache/internal/cache_reader.go | 17 +- .../pkg/cache/internal/informers.go | 66 +- .../pkg/cache/multi_namespace_cache.go | 46 +- .../pkg/certwatcher/certwatcher.go | 25 +- .../pkg/client/apiutil/apimachinery.go | 20 +- .../pkg/client/apiutil/restmapper.go | 137 +++-- .../pkg/client/applyconfigurations.go | 75 +++ .../controller-runtime/pkg/client/client.go | 26 +- .../pkg/client/client_rest_resources.go | 105 +++- .../pkg/client/config/config.go | 16 +- .../controller-runtime/pkg/client/dryrun.go | 4 + .../pkg/client/fake/client.go | 575 +++++++++++++++--- .../controller-runtime/pkg/client/fake/doc.go | 2 +- .../pkg/client/fake/typeconverter.go | 60 ++ .../pkg/client/fieldowner.go | 4 + .../pkg/client/fieldvalidation.go | 4 + .../pkg/client/interceptor/intercept.go | 9 + .../pkg/client/interfaces.go | 17 +- .../pkg/client/namespaced_client.go | 56 +- .../controller-runtime/pkg/client/options.go | 127 +++- .../controller-runtime/pkg/client/patch.go | 5 + .../pkg/client/typed_client.go | 57 +- .../pkg/client/unstructured_client.go | 57 +- .../controller-runtime/pkg/cluster/cluster.go | 18 +- .../pkg/config/controller.go | 40 +- .../pkg/controller/controller.go | 129 +++- .../controllerutil/controllerutil.go | 76 ++- .../controller-runtime/pkg/controller/name.go | 2 +- .../pkg/controller/priorityqueue/metrics.go | 172 ++++++ .../controller/priorityqueue/priorityqueue.go | 462 ++++++++++++++ .../pkg/envtest/binaries.go | 387 ++++++++++++ .../controller-runtime/pkg/envtest/crd.go | 10 +- .../controller-runtime/pkg/envtest/server.go | 79 ++- .../controller-runtime/pkg/envtest/webhook.go | 12 +- .../controller-runtime/pkg/event/event.go | 3 + .../controller-runtime/pkg/handler/enqueue.go | 19 +- .../pkg/handler/enqueue_mapped.go | 45 +- .../pkg/handler/enqueue_owner.go | 2 +- .../pkg/handler/eventhandler.go | 116 +++- .../pkg/internal/controller/controller.go | 298 +++++++-- .../internal/controller/metrics/metrics.go | 9 +- .../pkg/internal/metrics/workqueue.go | 170 ++++++ .../pkg/internal/source/event_handler.go | 18 +- .../pkg/internal/source/kind.go | 16 +- .../testing/controlplane/apiserver.go | 18 +- .../pkg/internal/testing/controlplane/auth.go | 2 +- .../pkg/internal/testing/controlplane/etcd.go | 4 + .../pkg/internal/testing/process/process.go | 2 +- .../pkg/leaderelection/leader_election.go | 32 +- .../pkg/log/warning_handler.go | 27 +- .../controller-runtime/pkg/log/zap/flags.go | 1 + .../pkg/log/zap/kube_helpers.go | 1 + .../controller-runtime/pkg/log/zap/zap.go | 4 +- .../pkg/manager/internal.go | 17 + .../controller-runtime/pkg/manager/manager.go | 25 +- .../pkg/manager/runnable_group.go | 60 +- .../controller-runtime/pkg/manager/server.go | 2 +- .../pkg/metrics/server/server.go | 2 +- .../pkg/metrics/workqueue.go | 101 --- .../pkg/predicate/predicate.go | 31 +- .../pkg/reconcile/reconcile.go | 12 +- .../controller-runtime/pkg/source/source.go | 8 +- .../pkg/webhook/admission/defaulter.go | 84 --- .../pkg/webhook/admission/defaulter_custom.go | 83 ++- .../pkg/webhook/admission/multi.go | 6 + .../pkg/webhook/admission/validator.go | 127 ---- .../pkg/webhook/admission/validator_custom.go | 3 + .../controller-runtime/pkg/webhook/alias.go | 8 - .../pkg/webhook/conversion/conversion.go | 21 +- .../pkg/webhook/conversion/metrics/metrics.go | 39 ++ .../pkg/webhook/internal/metrics/metrics.go | 8 +- .../controller-runtime/pkg/webhook/server.go | 4 +- 97 files changed, 4052 insertions(+), 1223 deletions(-) delete mode 100644 vendor/golang.org/x/exp/maps/maps.go create mode 100644 vendor/sigs.k8s.io/controller-runtime/pkg/client/applyconfigurations.go create mode 100644 vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/typeconverter.go create mode 100644 vendor/sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue/metrics.go create mode 100644 vendor/sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue/priorityqueue.go create mode 100644 vendor/sigs.k8s.io/controller-runtime/pkg/envtest/binaries.go create mode 100644 vendor/sigs.k8s.io/controller-runtime/pkg/internal/metrics/workqueue.go delete mode 100644 vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/defaulter.go delete mode 100644 vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/validator.go create mode 100644 vendor/sigs.k8s.io/controller-runtime/pkg/webhook/conversion/metrics/metrics.go diff --git a/hypershift-operator/controllers/hostedcluster/testdata/capi-provider/zz_fixture_TestReconcileComponents.yaml b/hypershift-operator/controllers/hostedcluster/testdata/capi-provider/zz_fixture_TestReconcileComponents.yaml index bef8e2986811..fe399d3b5d97 100644 --- a/hypershift-operator/controllers/hostedcluster/testdata/capi-provider/zz_fixture_TestReconcileComponents.yaml +++ b/hypershift-operator/controllers/hostedcluster/testdata/capi-provider/zz_fixture_TestReconcileComponents.yaml @@ -7,7 +7,7 @@ metadata: hypershift.openshift.io/managed-by: control-plane-operator name: capi-provider namespace: hcp-namespace - resourceVersion: "1" + resourceVersion: "2" spec: replicas: 1 revisionHistoryLimit: 2 diff --git a/hypershift-operator/controllers/hostedcluster/testdata/cluster-api/zz_fixture_TestReconcileComponents.yaml b/hypershift-operator/controllers/hostedcluster/testdata/cluster-api/zz_fixture_TestReconcileComponents.yaml index 41854ee0f0a0..ca6ee942b800 100644 --- a/hypershift-operator/controllers/hostedcluster/testdata/cluster-api/zz_fixture_TestReconcileComponents.yaml +++ b/hypershift-operator/controllers/hostedcluster/testdata/cluster-api/zz_fixture_TestReconcileComponents.yaml @@ -7,7 +7,7 @@ metadata: hypershift.openshift.io/managed-by: control-plane-operator name: cluster-api namespace: hcp-namespace - resourceVersion: "1" + resourceVersion: "2" spec: replicas: 1 revisionHistoryLimit: 2 diff --git a/hypershift-operator/controllers/hostedcluster/testdata/control-plane-operator/zz_fixture_TestReconcileComponents.yaml b/hypershift-operator/controllers/hostedcluster/testdata/control-plane-operator/zz_fixture_TestReconcileComponents.yaml index a2499aca7d07..315e980c37db 100644 --- a/hypershift-operator/controllers/hostedcluster/testdata/control-plane-operator/zz_fixture_TestReconcileComponents.yaml +++ b/hypershift-operator/controllers/hostedcluster/testdata/control-plane-operator/zz_fixture_TestReconcileComponents.yaml @@ -7,7 +7,7 @@ metadata: hypershift.openshift.io/managed-by: control-plane-operator name: control-plane-operator namespace: hcp-namespace - resourceVersion: "1" + resourceVersion: "2" spec: replicas: 1 revisionHistoryLimit: 2 diff --git a/hypershift-operator/controllers/hostedcluster/testdata/karpenter-operator/zz_fixture_TestReconcileComponents.yaml b/hypershift-operator/controllers/hostedcluster/testdata/karpenter-operator/zz_fixture_TestReconcileComponents.yaml index 4e761c91a2bb..9d0380f3bd67 100644 --- a/hypershift-operator/controllers/hostedcluster/testdata/karpenter-operator/zz_fixture_TestReconcileComponents.yaml +++ b/hypershift-operator/controllers/hostedcluster/testdata/karpenter-operator/zz_fixture_TestReconcileComponents.yaml @@ -6,7 +6,7 @@ metadata: hypershift.openshift.io/managed-by: control-plane-operator name: karpenter-operator namespace: hcp-namespace - resourceVersion: "1" + resourceVersion: "2" spec: replicas: 1 revisionHistoryLimit: 2 diff --git a/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/performanceprofile/v2/performanceprofile_types.go b/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/performanceprofile/v2/performanceprofile_types.go index d95a855f0c8b..98e8e6c4ee34 100644 --- a/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/performanceprofile/v2/performanceprofile_types.go +++ b/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/performanceprofile/v2/performanceprofile_types.go @@ -30,8 +30,8 @@ const PerformanceProfilePauseAnnotation = "performance.openshift.io/pause-reconc // network devices by including physical interfaces from netdev-rps rule. const PerformanceProfileEnablePhysicalRpsAnnotation = "performance.openshift.io/enable-physical-dev-rps" -// PerformanceProfileEnableRpsAnnotation is an emergancy annotation -// that ignores the removal of all RPS settings when realtime workload hint is explicitly set to false. +// PerformanceProfileEnableRpsAnnotation enables RPS settings which are now disabled by default. +// Valid values: "true", "enable" (to enable), "false", "disable" (to disable). const PerformanceProfileEnableRpsAnnotation = "performance.openshift.io/enable-rps" // PerformanceProfileSpec defines the desired state of PerformanceProfile. diff --git a/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/performanceprofile/v2/performanceprofile_validation.go b/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/performanceprofile/v2/performanceprofile_validation.go index bb945512cd51..b00e28933a79 100644 --- a/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/performanceprofile/v2/performanceprofile_validation.go +++ b/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/performanceprofile/v2/performanceprofile_validation.go @@ -81,18 +81,26 @@ var x86ValidKernelPageSizes = []string{ var validatorContext = context.TODO() -// ValidateCreate implements webhook.Validator so a webhook will be registered for the type -func (r *PerformanceProfile) ValidateCreate() (admission.Warnings, error) { - klog.Infof("Create validation for the performance profile %q", r.Name) +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type +func (r *PerformanceProfile) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + profile, ok := obj.(*PerformanceProfile) + if !ok { + return admission.Warnings{}, fmt.Errorf("expected PerformanceProfile, got %T", obj) + } + klog.Infof("Create validation for the performance profile %q", profile.Name) - return r.validateCreateOrUpdate() + return profile.validateCreateOrUpdate() } -// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type -func (r *PerformanceProfile) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { - klog.Infof("Update validation for the performance profile %q", r.Name) +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type +func (r *PerformanceProfile) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + profile, ok := newObj.(*PerformanceProfile) + if !ok { + return admission.Warnings{}, fmt.Errorf("expected PerformanceProfile, got %T", newObj) + } + klog.Infof("Update validation for the performance profile %q", profile.Name) - return r.validateCreateOrUpdate() + return profile.validateCreateOrUpdate() } func (r *PerformanceProfile) validateCreateOrUpdate() (admission.Warnings, error) { @@ -118,9 +126,13 @@ func (r *PerformanceProfile) validateCreateOrUpdate() (admission.Warnings, error r.Name, allErrs) } -// ValidateDelete implements webhook.Validator so a webhook will be registered for the type -func (r *PerformanceProfile) ValidateDelete() (admission.Warnings, error) { - klog.Infof("Delete validation for the performance profile %q", r.Name) +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type +func (r *PerformanceProfile) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + profile, ok := obj.(*PerformanceProfile) + if !ok { + return admission.Warnings{}, fmt.Errorf("expected PerformanceProfile, got %T", obj) + } + klog.Infof("Delete validation for the performance profile %q", profile.Name) // TODO(user): fill in your validation logic upon object deletion. return admission.Warnings{}, nil @@ -396,7 +408,7 @@ func (r *PerformanceProfile) validateHugePages(nodes corev1.NodeList) field.Erro if r.Spec.HugePages.DefaultHugePagesSize != nil { defaultSize := *r.Spec.HugePages.DefaultHugePagesSize errField := "spec.hugepages.defaultHugepagesSize" - errMsg := "hugepages default size should be equal to one of" + errMsg := fmt.Sprintf("The compatible default huge page sizes for the selected kernel page size %s are:", kernelPageSize) docsRef := "https://docs.kernel.org/mm/vmemmap_dedup.html" if x86 && !slices.Contains(x86ValidHugepagesSizes, string(defaultSize)) { allErrs = append( @@ -413,8 +425,14 @@ func (r *PerformanceProfile) validateHugePages(nodes corev1.NodeList) field.Erro field.Invalid( field.NewPath(errField), r.Spec.HugePages.DefaultHugePagesSize, - fmt.Sprintf("%s %v. doc reference=%s", errMsg, aarch64HugePagesByKernelPageSize[kernelPageSize], docsRef), - ), + fmt.Sprintf( + "%s %v. doc reference=%s. "+ + "In case you are trying to define a default hugepage size that requires a different kernel page size, "+ + "please set the needed kernel page size under the spec.kernelPageSize field.", + errMsg, + aarch64HugePagesByKernelPageSize[kernelPageSize], + docsRef, + )), ) } else if !x86 && !aarch64 && !hugepagesSizes.Has(string(defaultSize)) { allErrs = append( @@ -430,14 +448,14 @@ func (r *PerformanceProfile) validateHugePages(nodes corev1.NodeList) field.Erro for i, page := range r.Spec.HugePages.Pages { errField := "spec.hugepages.pages" - errMsg := "the page size should be equal to one of" + errMsg := fmt.Sprintf("The compatible huge page sizes for the selected kernel page size %s are:", kernelPageSize) docsRef := "https://docs.kernel.org/mm/vmemmap_dedup.html" if x86 && !slices.Contains(x86ValidHugepagesSizes, string(page.Size)) { allErrs = append( allErrs, field.Invalid( field.NewPath(errField), - r.Spec.HugePages.Pages, + page.Size, fmt.Sprintf("%s %v. doc reference=%s", errMsg, x86ValidHugepagesSizes, docsRef), ), ) @@ -446,16 +464,22 @@ func (r *PerformanceProfile) validateHugePages(nodes corev1.NodeList) field.Erro allErrs, field.Invalid( field.NewPath(errField), - r.Spec.HugePages.Pages, - fmt.Sprintf("%s %v. doc reference=%s", errMsg, aarch64HugePagesByKernelPageSize[(kernelPageSize)], docsRef), - ), + page.Size, + fmt.Sprintf( + "%s %v. doc reference=%s. "+ + "In case you are trying to define a hugepage that requires a different kernel page size, "+ + "please set the needed kernel page size under the spec.kernelPageSize field.", + errMsg, + aarch64HugePagesByKernelPageSize[kernelPageSize], + docsRef, + )), ) } else if !x86 && !aarch64 && !hugepagesSizes.Has(string(page.Size)) { allErrs = append( allErrs, field.Invalid( field.NewPath(errField), - r.Spec.HugePages.Pages, + page.Size, fmt.Sprintf("%s %v. doc reference=%s", errMsg, hugepagesSizes, docsRef), ), ) diff --git a/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/performanceprofile/v2/performanceprofile_webhook.go b/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/performanceprofile/v2/performanceprofile_webhook.go index 3f6ebba34a39..62bef40b8ff9 100644 --- a/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/performanceprofile/v2/performanceprofile_webhook.go +++ b/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/performanceprofile/v2/performanceprofile_webhook.go @@ -6,7 +6,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" ) -var _ webhook.Validator = &PerformanceProfile{} +var _ webhook.CustomValidator = &PerformanceProfile{} // we need this variable only because our validate methods should have access to the client var validatorClient client.Client @@ -19,5 +19,6 @@ func (r *PerformanceProfile) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(r). + WithValidator(r). Complete() } diff --git a/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/tuned/v1/tuned_types.go b/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/tuned/v1/tuned_types.go index f5b8a50d462d..38a4ca7feaa2 100644 --- a/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/tuned/v1/tuned_types.go +++ b/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/tuned/v1/tuned_types.go @@ -34,6 +34,7 @@ const ( ///////////////////////////////////////////////////////////////////////////////// // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:subresource:status // Tuned is a collection of rules that allows cluster-wide deployment // of node-level sysctls and more flexibility to add custom tuning @@ -134,8 +135,18 @@ type TuneDConfig struct { // TunedStatus is the status for a Tuned resource. type TunedStatus struct { + // conditions represents the state of the Tuned profile + // +patchMergeKey=type + // +patchStrategy=merge + // +optional + Conditions []StatusCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } +const ( + // Tuned CR was validated and no problems with it were found. + TunedValid ConditionType = "Valid" +) + // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // TunedList is a list of Tuned resources. @@ -192,16 +203,20 @@ type ProfileStatus struct { // +patchMergeKey=type // +patchStrategy=merge // +optional - Conditions []ProfileStatusCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + Conditions []StatusCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + // If set, this represents the .metadata.generation that the conditions were set based upon. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,1,opt,name=observedGeneration"` } -// ProfileStatusCondition represents a partial state of the per-node Profile application. +// StatusCondition represents a partial state of the per-node Profile application. // +k8s:deepcopy-gen=true -type ProfileStatusCondition struct { +type StatusCondition struct { // type specifies the aspect reported by this condition. // +kubebuilder:validation:Required // +required - Type ProfileConditionType `json:"type"` + Type ConditionType `json:"type"` // status of the condition, one of True, False, Unknown. // +kubebuilder:validation:Required @@ -223,18 +238,18 @@ type ProfileStatusCondition struct { Message string `json:"message,omitempty"` } -// ProfileConditionType is an aspect of Tuned daemon profile application state. -type ProfileConditionType string +// ConditionType is an aspect of Tuned daemon profile application state. +type ConditionType string const ( // ProfileApplied indicates that the Tuned daemon has successfully applied // the selected profile. - TunedProfileApplied ProfileConditionType = "Applied" + TunedProfileApplied ConditionType = "Applied" // TunedDegraded indicates the Tuned daemon issued errors during profile // application. To conclude the profile application was successful, // both TunedProfileApplied and TunedDegraded need to be queried. - TunedDegraded ProfileConditionType = "Degraded" + TunedDegraded ConditionType = "Degraded" ) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/tuned/v1/zz_generated.deepcopy.go b/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/tuned/v1/zz_generated.deepcopy.go index 0f3954f707ed..900ec6ca59fa 100644 --- a/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/tuned/v1/zz_generated.deepcopy.go +++ b/vendor/github.com/openshift/cluster-node-tuning-operator/pkg/apis/tuned/v1/zz_generated.deepcopy.go @@ -148,7 +148,7 @@ func (in *ProfileStatus) DeepCopyInto(out *ProfileStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]ProfileStatusCondition, len(*in)) + *out = make([]StatusCondition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -167,18 +167,18 @@ func (in *ProfileStatus) DeepCopy() *ProfileStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ProfileStatusCondition) DeepCopyInto(out *ProfileStatusCondition) { +func (in *StatusCondition) DeepCopyInto(out *StatusCondition) { *out = *in in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProfileStatusCondition. -func (in *ProfileStatusCondition) DeepCopy() *ProfileStatusCondition { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StatusCondition. +func (in *StatusCondition) DeepCopy() *StatusCondition { if in == nil { return nil } - out := new(ProfileStatusCondition) + out := new(StatusCondition) in.DeepCopyInto(out) return out } @@ -210,7 +210,7 @@ func (in *Tuned) DeepCopyInto(out *Tuned) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -403,6 +403,13 @@ func (in *TunedSpec) DeepCopy() *TunedSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TunedStatus) DeepCopyInto(out *TunedStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]StatusCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/vendor/golang.org/x/exp/maps/maps.go b/vendor/golang.org/x/exp/maps/maps.go deleted file mode 100644 index 4a9747ef40a4..000000000000 --- a/vendor/golang.org/x/exp/maps/maps.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package maps defines various functions useful with maps of any type. -package maps - -import "maps" - -// Keys returns the keys of the map m. -// The keys will be in an indeterminate order. -// -// The simplest true equivalent using the standard library is: -// -// slices.AppendSeq(make([]K, 0, len(m)), maps.Keys(m)) -func Keys[M ~map[K]V, K comparable, V any](m M) []K { - - r := make([]K, 0, len(m)) - for k := range m { - r = append(r, k) - } - return r -} - -// Values returns the values of the map m. -// The values will be in an indeterminate order. -// -// The simplest true equivalent using the standard library is: -// -// slices.AppendSeq(make([]V, 0, len(m)), maps.Values(m)) -func Values[M ~map[K]V, K comparable, V any](m M) []V { - - r := make([]V, 0, len(m)) - for _, v := range m { - r = append(r, v) - } - return r -} - -// Equal reports whether two maps contain the same key/value pairs. -// Values are compared using ==. -// -//go:fix inline -func Equal[M1, M2 ~map[K]V, K, V comparable](m1 M1, m2 M2) bool { - return maps.Equal(m1, m2) -} - -// EqualFunc is like Equal, but compares values using eq. -// Keys are still compared with ==. -// -//go:fix inline -func EqualFunc[M1 ~map[K]V1, M2 ~map[K]V2, K comparable, V1, V2 any](m1 M1, m2 M2, eq func(V1, V2) bool) bool { - return maps.EqualFunc(m1, m2, eq) -} - -// Clear removes all entries from m, leaving it empty. -// -//go:fix inline -func Clear[M ~map[K]V, K comparable, V any](m M) { - clear(m) -} - -// Clone returns a copy of m. This is a shallow clone: -// the new keys and values are set using ordinary assignment. -// -//go:fix inline -func Clone[M ~map[K]V, K comparable, V any](m M) M { - return maps.Clone(m) -} - -// Copy copies all key/value pairs in src adding them to dst. -// When a key in src is already present in dst, -// the value in dst will be overwritten by the value associated -// with the key in src. -// -//go:fix inline -func Copy[M1 ~map[K]V, M2 ~map[K]V, K comparable, V any](dst M1, src M2) { - maps.Copy(dst, src) -} - -// DeleteFunc deletes any key/value pairs from m for which del returns true. -// -//go:fix inline -func DeleteFunc[M ~map[K]V, K comparable, V any](m M, del func(K, V) bool) { - maps.DeleteFunc(m, del) -} diff --git a/vendor/k8s.io/kubelet/config/v1beta1/doc.go b/vendor/k8s.io/kubelet/config/v1beta1/doc.go index 5644377d6c88..0edffa4b691d 100644 --- a/vendor/k8s.io/kubelet/config/v1beta1/doc.go +++ b/vendor/k8s.io/kubelet/config/v1beta1/doc.go @@ -18,4 +18,4 @@ limitations under the License. // +k8s:openapi-gen=true // +groupName=kubelet.config.k8s.io -package v1beta1 // import "k8s.io/kubelet/config/v1beta1" +package v1beta1 diff --git a/vendor/k8s.io/kubelet/config/v1beta1/types.go b/vendor/k8s.io/kubelet/config/v1beta1/types.go index 9fe75d41b0c1..474c5c0c91e6 100644 --- a/vendor/k8s.io/kubelet/config/v1beta1/types.go +++ b/vendor/k8s.io/kubelet/config/v1beta1/types.go @@ -83,6 +83,25 @@ const ( StaticMemoryManagerPolicy = "Static" ) +// ImagePullCredentialsVerificationPolicy is an enum for the policy that is enforced +// when pod is requesting an image that appears on the system +type ImagePullCredentialsVerificationPolicy string + +const ( + // NeverVerify will never require credential verification for images that + // already exist on the node + NeverVerify ImagePullCredentialsVerificationPolicy = "NeverVerify" + // NeverVerifyPreloadedImages does not require credential verification for images + // pulled outside the kubelet process + NeverVerifyPreloadedImages ImagePullCredentialsVerificationPolicy = "NeverVerifyPreloadedImages" + // NeverVerifyAllowlistedImages does not require credential verification for + // a list of images that were pulled outside the kubelet process + NeverVerifyAllowlistedImages ImagePullCredentialsVerificationPolicy = "NeverVerifyAllowlistedImages" + // AlwaysVerify requires credential verification for accessing any image on the + // node irregardless how it was pulled + AlwaysVerify ImagePullCredentialsVerificationPolicy = "AlwaysVerify" +) + // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // KubeletConfiguration contains the configuration for the Kubelet @@ -210,6 +229,28 @@ type KubeletConfiguration struct { // Default: 10 // +optional RegistryBurst int32 `json:"registryBurst,omitempty"` + // imagePullCredentialsVerificationPolicy determines how credentials should be + // verified when pod requests an image that is already present on the node: + // - NeverVerify + // - anyone on a node can use any image present on the node + // - NeverVerifyPreloadedImages + // - images that were pulled to the node by something else than the kubelet + // can be used without reverifying pull credentials + // - NeverVerifyAllowlistedImages + // - like "NeverVerifyPreloadedImages" but only node images from + // `preloadedImagesVerificationAllowlist` don't require reverification + // - AlwaysVerify + // - all images require credential reverification + // +optional + ImagePullCredentialsVerificationPolicy ImagePullCredentialsVerificationPolicy `json:"imagePullCredentialsVerificationPolicy,omitempty"` + // preloadedImagesVerificationAllowlist specifies a list of images that are + // exempted from credential reverification for the "NeverVerifyAllowlistedImages" + // `imagePullCredentialsVerificationPolicy`. + // The list accepts a full path segment wildcard suffix "/*". + // Only use image specs without an image tag or digest. + // +optional + // +listType=set + PreloadedImagesVerificationAllowlist []string `json:"preloadedImagesVerificationAllowlist,omitempty"` // eventRecordQPS is the maximum event creations per second. If 0, there // is no limit enforced. The value cannot be a negative number. // Default: 50 @@ -350,7 +391,6 @@ type KubeletConfiguration struct { // +optional CgroupDriver string `json:"cgroupDriver,omitempty"` // cpuManagerPolicy is the name of the policy to use. - // Requires the CPUManager feature gate to be enabled. // Default: "None" // +optional CPUManagerPolicy string `json:"cpuManagerPolicy,omitempty"` @@ -365,12 +405,10 @@ type KubeletConfiguration struct { SingleProcessOOMKill *bool `json:"singleProcessOOMKill,omitempty"` // cpuManagerPolicyOptions is a set of key=value which allows to set extra options // to fine tune the behaviour of the cpu manager policies. - // Requires both the "CPUManager" and "CPUManagerPolicyOptions" feature gates to be enabled. // Default: nil // +optional CPUManagerPolicyOptions map[string]string `json:"cpuManagerPolicyOptions,omitempty"` // cpuManagerReconcilePeriod is the reconciliation period for the CPU Manager. - // Requires the CPUManager feature gate to be enabled. // Default: "10s" // +optional CPUManagerReconcilePeriod metav1.Duration `json:"cpuManagerReconcilePeriod,omitempty"` @@ -530,6 +568,7 @@ type KubeletConfiguration struct { EvictionSoftGracePeriod map[string]string `json:"evictionSoftGracePeriod,omitempty"` // evictionPressureTransitionPeriod is the duration for which the kubelet has to wait // before transitioning out of an eviction pressure condition. + // A duration of 0s will be converted to the default value of 5m // Default: "5m" // +optional EvictionPressureTransitionPeriod metav1.Duration `json:"evictionPressureTransitionPeriod,omitempty"` @@ -546,6 +585,16 @@ type KubeletConfiguration struct { // Default: nil // +optional EvictionMinimumReclaim map[string]string `json:"evictionMinimumReclaim,omitempty"` + // mergeDefaultEvictionSettings indicates that defaults for the evictionHard, evictionSoft, evictionSoftGracePeriod, and evictionMinimumReclaim + // fields should be merged into values specified for those fields in this configuration. + // Signals specified in this configuration take precedence. + // Signals not specified in this configuration inherit their defaults. + // If false, and if any signal is specified in this configuration then other signals that + // are not specified in this configuration will be set to 0. + // It applies to merging the fields for which the default exists, and currently only evictionHard has default values. + // Default: false + // +optional + MergeDefaultEvictionSettings *bool `json:"mergeDefaultEvictionSettings,omitempty"` // podsPerCore is the maximum number of pods per core. Cannot exceed maxPods. // The value must be a non-negative integer. // If 0, there is no limit on the number of Pods. @@ -872,6 +921,11 @@ type KubeletConfiguration struct { // Default: false // +optional FailCgroupV1 *bool `json:"failCgroupV1,omitempty"` + + // UserNamespaces contains User Namespace configurations. + // +featureGate=UserNamespaceSupport + // +optional + UserNamespaces *UserNamespaces `json:"userNamespaces,omitempty"` } type KubeletAuthorizationMode string @@ -1003,7 +1057,7 @@ type CredentialProviderConfig struct { // Multiple providers may match against a single image, in which case credentials // from all providers will be returned to the kubelet. If multiple providers are called // for a single image, the results are combined. If providers return overlapping - // auth keys, the value from the provider earlier in this list is used. + // auth keys, the value from the provider earlier in this list is attempted first. Providers []CredentialProvider `json:"providers"` } @@ -1013,6 +1067,7 @@ type CredentialProvider struct { // name is the required name of the credential provider. It must match the name of the // provider executable as seen by the kubelet. The executable must be in the kubelet's // bin directory (set by the --image-credential-provider-bin-dir flag). + // Required to be unique across all providers. Name string `json:"name"` // matchImages is a required list of strings used to match against images in order to @@ -1066,3 +1121,17 @@ type ExecEnvVar struct { Name string `json:"name"` Value string `json:"value"` } + +// UserNamespaces contains User Namespace configurations. +type UserNamespaces struct { + // IDsPerPod is the mapping length of UIDs and GIDs. + // The length must be a multiple of 65536, and must be less than 1<<32. + // On non-linux such as windows, only null / absent is allowed. + // + // Changing the value may require recreating all containers on the node. + // + // Default: 65536 + // +featureGate=UserNamespaceSupport + // +optional + IDsPerPod *int64 `json:"idsPerPod,omitempty"` +} diff --git a/vendor/k8s.io/kubelet/config/v1beta1/zz_generated.deepcopy.go b/vendor/k8s.io/kubelet/config/v1beta1/zz_generated.deepcopy.go index 89dda0dfa17c..ac0ba7096bc2 100644 --- a/vendor/k8s.io/kubelet/config/v1beta1/zz_generated.deepcopy.go +++ b/vendor/k8s.io/kubelet/config/v1beta1/zz_generated.deepcopy.go @@ -229,6 +229,11 @@ func (in *KubeletConfiguration) DeepCopyInto(out *KubeletConfiguration) { *out = new(int32) **out = **in } + if in.PreloadedImagesVerificationAllowlist != nil { + in, out := &in.PreloadedImagesVerificationAllowlist, &out.PreloadedImagesVerificationAllowlist + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.EventRecordQPS != nil { in, out := &in.EventRecordQPS, &out.EventRecordQPS *out = new(int32) @@ -372,6 +377,11 @@ func (in *KubeletConfiguration) DeepCopyInto(out *KubeletConfiguration) { (*out)[key] = val } } + if in.MergeDefaultEvictionSettings != nil { + in, out := &in.MergeDefaultEvictionSettings, &out.MergeDefaultEvictionSettings + *out = new(bool) + **out = **in + } if in.EnableControllerAttachDetach != nil { in, out := &in.EnableControllerAttachDetach, &out.EnableControllerAttachDetach *out = new(bool) @@ -517,6 +527,11 @@ func (in *KubeletConfiguration) DeepCopyInto(out *KubeletConfiguration) { *out = new(bool) **out = **in } + if in.UserNamespaces != nil { + in, out := &in.UserNamespaces, &out.UserNamespaces + *out = new(UserNamespaces) + (*in).DeepCopyInto(*out) + } return } @@ -674,3 +689,24 @@ func (in *ShutdownGracePeriodByPodPriority) DeepCopy() *ShutdownGracePeriodByPod in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserNamespaces) DeepCopyInto(out *UserNamespaces) { + *out = *in + if in.IDsPerPod != nil { + in, out := &in.IDsPerPod, &out.IDsPerPod + *out = new(int64) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserNamespaces. +func (in *UserNamespaces) DeepCopy() *UserNamespaces { + if in == nil { + return nil + } + out := new(UserNamespaces) + in.DeepCopyInto(out) + return out +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 984535e5c1e6..5c8b8c880d96 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1117,8 +1117,8 @@ github.com/openshift/cluster-api-provider-agent/api/v1beta1 ## explicit; go 1.22.0 github.com/openshift/cluster-autoscaler-operator/pkg/apis/autoscaling/v1 github.com/openshift/cluster-autoscaler-operator/pkg/apis/autoscaling/v1beta1 -# github.com/openshift/cluster-node-tuning-operator v0.0.0-20250225115807-f166846b7256 -## explicit; go 1.22.0 +# github.com/openshift/cluster-node-tuning-operator v0.0.0-20260527203700-73801cc280b3 +## explicit; go 1.24.0 github.com/openshift/cluster-node-tuning-operator/pkg/apis/performanceprofile/v1 github.com/openshift/cluster-node-tuning-operator/pkg/apis/performanceprofile/v2 github.com/openshift/cluster-node-tuning-operator/pkg/apis/tuned @@ -1127,7 +1127,7 @@ github.com/openshift/cluster-node-tuning-operator/pkg/performanceprofile/control # github.com/openshift/custom-resource-status v1.1.3-0.20220503160415-f2fdb4999d87 ## explicit; go 1.12 github.com/openshift/custom-resource-status/conditions/v1 -# github.com/openshift/hypershift/api v0.0.0-20240604072534-cd2d5291e2b7 => ./api +# github.com/openshift/hypershift/api v0.0.0-20250724113115-0281b49cc465 => ./api ## explicit; go 1.25.7 github.com/openshift/hypershift/api/auditlogpersistence github.com/openshift/hypershift/api/auditlogpersistence/v1alpha1 @@ -1465,7 +1465,6 @@ golang.org/x/crypto/ssh golang.org/x/crypto/ssh/internal/bcrypt_pbkdf # golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 ## explicit; go 1.24.0 -golang.org/x/exp/maps golang.org/x/exp/slices # golang.org/x/mod v0.35.0 ## explicit; go 1.25.0 @@ -2528,8 +2527,8 @@ k8s.io/kubectl/pkg/util/openapi k8s.io/kubectl/pkg/util/templates k8s.io/kubectl/pkg/util/term k8s.io/kubectl/pkg/validation -# k8s.io/kubelet v0.32.2 -## explicit; go 1.23.0 +# k8s.io/kubelet v0.33.3 +## explicit; go 1.24.0 k8s.io/kubelet/config/v1beta1 # k8s.io/pod-security-admission v0.35.1 ## explicit; go 1.25.0 @@ -2626,8 +2625,8 @@ sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha1 sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1 sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/errors sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/optional -# sigs.k8s.io/controller-runtime v0.22.4 => sigs.k8s.io/controller-runtime v0.19.7 -## explicit; go 1.22.0 +# sigs.k8s.io/controller-runtime v0.22.4 +## explicit; go 1.24.0 sigs.k8s.io/controller-runtime sigs.k8s.io/controller-runtime/pkg/builder sigs.k8s.io/controller-runtime/pkg/cache @@ -2643,6 +2642,7 @@ sigs.k8s.io/controller-runtime/pkg/cluster sigs.k8s.io/controller-runtime/pkg/config sigs.k8s.io/controller-runtime/pkg/controller sigs.k8s.io/controller-runtime/pkg/controller/controllerutil +sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue sigs.k8s.io/controller-runtime/pkg/conversion sigs.k8s.io/controller-runtime/pkg/envtest sigs.k8s.io/controller-runtime/pkg/event @@ -2654,6 +2654,7 @@ sigs.k8s.io/controller-runtime/pkg/internal/field/selector sigs.k8s.io/controller-runtime/pkg/internal/flock sigs.k8s.io/controller-runtime/pkg/internal/httpserver sigs.k8s.io/controller-runtime/pkg/internal/log +sigs.k8s.io/controller-runtime/pkg/internal/metrics sigs.k8s.io/controller-runtime/pkg/internal/objectutil sigs.k8s.io/controller-runtime/pkg/internal/recorder sigs.k8s.io/controller-runtime/pkg/internal/source @@ -2678,6 +2679,7 @@ sigs.k8s.io/controller-runtime/pkg/webhook sigs.k8s.io/controller-runtime/pkg/webhook/admission sigs.k8s.io/controller-runtime/pkg/webhook/admission/metrics sigs.k8s.io/controller-runtime/pkg/webhook/conversion +sigs.k8s.io/controller-runtime/pkg/webhook/conversion/metrics sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics # sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 ## explicit; go 1.23 @@ -2801,6 +2803,5 @@ sigs.k8s.io/yaml/kyaml # github.com/openshift/hypershift/api => ./api # github.com/k-orc/openstack-resource-controller => sigs.k8s.io/cluster-api-provider-openstack/orc v0.0.0-20250113192833-e4f56a2b4f32 # github.com/golang-jwt/jwt/v4 => github.com/golang-jwt/jwt/v4 v4.5.2 -# sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.19.7 # github.com/aws/karpenter-provider-aws => github.com/openshift/aws-karpenter-provider-aws v0.0.0-20260311064431-f0be9c72e5bf # sigs.k8s.io/karpenter => github.com/openshift/kubernetes-sigs-karpenter v0.0.0-20260310165629-67e201b559d5 diff --git a/vendor/sigs.k8s.io/controller-runtime/.golangci.yml b/vendor/sigs.k8s.io/controller-runtime/.golangci.yml index 4c43665e2bfd..1741432a013d 100644 --- a/vendor/sigs.k8s.io/controller-runtime/.golangci.yml +++ b/vendor/sigs.k8s.io/controller-runtime/.golangci.yml @@ -1,26 +1,28 @@ +version: "2" +run: + go: "1.24" + timeout: 10m + allow-parallel-runners: true linters: - disable-all: true + default: none enable: - asasalint - asciicheck - bidichk - bodyclose + - copyloopvar - dogsled - dupl - errcheck - errchkjson - errorlint - exhaustive - - exportloopref + - forbidigo - ginkgolinter - goconst - gocritic - gocyclo - - gofmt - - goimports - goprintffuncname - - gosec - - gosimple - govet - importas - ineffassign @@ -32,141 +34,161 @@ linters: - prealloc - revive - staticcheck - - stylecheck - tagliatelle - - typecheck - unconvert - unparam - unused - whitespace - -linters-settings: - govet: - enable-all: true - disable: - - fieldalignment - - shadow - importas: - no-unaliased: true - alias: - # Kubernetes - - pkg: k8s.io/api/core/v1 - alias: corev1 - - pkg: k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1 - alias: apiextensionsv1 - - pkg: k8s.io/apimachinery/pkg/apis/meta/v1 - alias: metav1 - - pkg: k8s.io/apimachinery/pkg/api/errors - alias: apierrors - - pkg: k8s.io/apimachinery/pkg/util/errors - alias: kerrors - # Controller Runtime - - pkg: sigs.k8s.io/controller-runtime - alias: ctrl - revive: + settings: + forbidigo: + forbid: + - pattern: context.Background + msg: Use ginkgos SpecContext or go testings t.Context instead + - pattern: context.TODO + msg: Use ginkgos SpecContext or go testings t.Context instead + govet: + disable: + - fieldalignment + - shadow + enable-all: true + importas: + alias: + - pkg: k8s.io/api/core/v1 + alias: corev1 + - pkg: k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1 + alias: apiextensionsv1 + - pkg: k8s.io/apimachinery/pkg/apis/meta/v1 + alias: metav1 + - pkg: k8s.io/apimachinery/pkg/api/errors + alias: apierrors + - pkg: k8s.io/apimachinery/pkg/util/errors + alias: kerrors + - pkg: sigs.k8s.io/controller-runtime + alias: ctrl + no-unaliased: true + revive: + rules: + # The following rules are recommended https://github.com/mgechev/revive#recommended-configuration + - name: blank-imports + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + - name: error-return + - name: error-strings + - name: error-naming + - name: exported + - name: if-return + - name: increment-decrement + - name: var-naming + - name: var-declaration + - name: range + - name: receiver-naming + - name: time-naming + - name: unexported-return + - name: indent-error-flow + - name: errorf + - name: superfluous-else + - name: unreachable-code + - name: redefines-builtin-id + # + # Rules in addition to the recommended configuration above. + # + - name: bool-literal-in-expr + - name: constant-logical-expr + exclusions: + generated: strict + paths: + - zz_generated.*\.go$ + - .*conversion.*\.go$ rules: - # The following rules are recommended https://github.com/mgechev/revive#recommended-configuration - - name: blank-imports - - name: context-as-argument - - name: context-keys-type - - name: dot-imports - - name: error-return - - name: error-strings - - name: error-naming - - name: exported - - name: if-return - - name: increment-decrement - - name: var-naming - - name: var-declaration - - name: range - - name: receiver-naming - - name: time-naming - - name: unexported-return - - name: indent-error-flow - - name: errorf - - name: superfluous-else - - name: unreachable-code - - name: redefines-builtin-id - # - # Rules in addition to the recommended configuration above. - # - - name: bool-literal-in-expr - - name: constant-logical-expr - + - linters: + - forbidigo + path-except: _test\.go + - linters: + - gosec + text: 'G108: Profiling endpoint is automatically exposed on /debug/pprof' + - linters: + - revive + text: 'exported: exported method .*\.(Reconcile|SetupWithManager|SetupWebhookWithManager) should have comment or be unexported' + - linters: + - errcheck + text: Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked + - linters: + - staticcheck + text: 'SA1019: .*The component config package has been deprecated and will be removed in a future release.' + # With Go 1.16, the new embed directive can be used with an un-named import, + # revive (previously, golint) only allows these to be imported in a main.go, which wouldn't work for us. + # This directive allows the embed package to be imported with an underscore everywhere. + - linters: + - revive + source: _ "embed" + # Exclude some packages or code to require comments, for example test code, or fake clients. + - linters: + - revive + text: exported (method|function|type|const) (.+) should have comment or be unexported + source: (func|type).*Fake.* + - linters: + - revive + path: fake_\.go + text: exported (method|function|type|const) (.+) should have comment or be unexported + # Disable unparam "always receives" which might not be really + # useful when building libraries. + - linters: + - unparam + text: always receives + # Dot imports for gomega and ginkgo are allowed + # within test files. + - path: _test\.go + text: should not use dot imports + - path: _test\.go + text: cyclomatic complexity + - path: _test\.go + text: 'G107: Potential HTTP request made with variable url' + # Append should be able to assign to a different var/slice. + - linters: + - gocritic + text: 'appendAssign: append result not assigned to the same slice' + - linters: + - gocritic + text: 'singleCaseSwitch: should rewrite switch statement to if statement' + # It considers all file access to a filename that comes from a variable problematic, + # which is naiv at best. + - linters: + - gosec + text: 'G304: Potential file inclusion via variable' + - linters: + - dupl + path: _test\.go + - linters: + - revive + path: .*/internal/.* + - linters: + - unused + # Seems to incorrectly trigger on the two implementations that are only + # used through an interface and not directly..? + # Likely same issue as https://github.com/dominikh/go-tools/issues/1616 + path: pkg/controller/priorityqueue/metrics\.go + # The following are being worked on to remove their exclusion. This list should be reduced or go away all together over time. + # If it is decided they will not be addressed they should be moved above this comment. + - path: (.+)\.go$ + text: Subprocess launch(ed with variable|ing should be audited) + - linters: + - gosec + path: (.+)\.go$ + text: (G204|G104|G307) + - linters: + - staticcheck + path: (.+)\.go$ + text: (ST1000|QF1008) issues: - max-same-issues: 0 max-issues-per-linter: 0 - # We are disabling default golangci exclusions because we want to help reviewers to focus on reviewing the most relevant - # changes in PRs and avoid nitpicking. - exclude-use-default: false - # List of regexps of issue texts to exclude, empty list by default. - exclude: - # The following are being worked on to remove their exclusion. This list should be reduced or go away all together over time. - # If it is decided they will not be addressed they should be moved above this comment. - - 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 - text: "G108: Profiling endpoint is automatically exposed on /debug/pprof" - - linters: - - revive - text: "exported: exported method .*\\.(Reconcile|SetupWithManager|SetupWebhookWithManager) should have comment or be unexported" - - linters: - - errcheck - text: Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked - - linters: - - staticcheck - text: "SA1019: .*The component config package has been deprecated and will be removed in a future release." - # With Go 1.16, the new embed directive can be used with an un-named import, - # revive (previously, golint) only allows these to be imported in a main.go, which wouldn't work for us. - # This directive allows the embed package to be imported with an underscore everywhere. - - linters: - - revive - source: _ "embed" - # Exclude some packages or code to require comments, for example test code, or fake clients. - - linters: - - revive - text: exported (method|function|type|const) (.+) should have comment or be unexported - source: (func|type).*Fake.* - - linters: - - revive - text: exported (method|function|type|const) (.+) should have comment or be unexported - path: fake_\.go - # Disable unparam "always receives" which might not be really - # useful when building libraries. - - linters: - - unparam - text: always receives - # Dot imports for gomega and ginkgo are allowed - # within test files. - - path: _test\.go - text: should not use dot imports - - path: _test\.go - text: cyclomatic complexity - - path: _test\.go - text: "G107: Potential HTTP request made with variable url" - # Append should be able to assign to a different var/slice. - - linters: - - gocritic - text: "appendAssign: append result not assigned to the same slice" - - linters: - - gocritic - text: "singleCaseSwitch: should rewrite switch statement to if statement" - # It considers all file access to a filename that comes from a variable problematic, - # which is naiv at best. - - linters: - - gosec - text: "G304: Potential file inclusion via variable" - - linters: - - dupl - path: _test\.go - -run: - go: "1.22" - timeout: 10m - allow-parallel-runners: true + max-same-issues: 0 +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: strict + paths: + - zz_generated.*\.go$ + - .*conversion.*\.go$ diff --git a/vendor/sigs.k8s.io/controller-runtime/.gomodcheck.yaml b/vendor/sigs.k8s.io/controller-runtime/.gomodcheck.yaml index 75c5261fde59..3608de331dda 100644 --- a/vendor/sigs.k8s.io/controller-runtime/.gomodcheck.yaml +++ b/vendor/sigs.k8s.io/controller-runtime/.gomodcheck.yaml @@ -12,3 +12,6 @@ excludedModules: # --- test dependencies: - github.com/onsi/ginkgo/v2 - github.com/onsi/gomega + + # --- We want a newer version with generics support for this + - github.com/google/btree diff --git a/vendor/sigs.k8s.io/controller-runtime/Makefile b/vendor/sigs.k8s.io/controller-runtime/Makefile index 9d92b9773037..b8e9cfa877fe 100644 --- a/vendor/sigs.k8s.io/controller-runtime/Makefile +++ b/vendor/sigs.k8s.io/controller-runtime/Makefile @@ -27,7 +27,7 @@ SHELL:=/usr/bin/env bash # # Go. # -GO_VERSION ?= 1.22.5 +GO_VERSION ?= 1.24.0 # Use GOPROXY environment variable if set GOPROXY := $(shell go env GOPROXY) @@ -88,7 +88,7 @@ GO_APIDIFF_PKG := github.com/joelanford/go-apidiff $(GO_APIDIFF): # Build go-apidiff from tools folder. GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(GO_APIDIFF_PKG) $(GO_APIDIFF_BIN) $(GO_APIDIFF_VER) -CONTROLLER_GEN_VER := v0.14.0 +CONTROLLER_GEN_VER := v0.17.1 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 @@ -99,7 +99,7 @@ $(CONTROLLER_GEN): # Build controller-gen from tools folder. 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_PKG := github.com/golangci/golangci-lint/v2/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) @@ -174,7 +174,7 @@ release-binary: $(RELEASE_DIR) -v "$$(pwd):/workspace$(DOCKER_VOL_OPTS)" \ -w /workspace/tools/setup-envtest \ golang:$(GO_VERSION) \ - go build -a -trimpath -ldflags "-extldflags '-static'" \ + go build -a -trimpath -ldflags "-X 'sigs.k8s.io/controller-runtime/tools/setup-envtest/version.version=$(RELEASE_TAG)' -extldflags '-static'" \ -o ./out/$(RELEASE_BINARY) ./ ## -------------------------------------- diff --git a/vendor/sigs.k8s.io/controller-runtime/OWNERS_ALIASES b/vendor/sigs.k8s.io/controller-runtime/OWNERS_ALIASES index e465c3d5b028..47bf6eedf3c5 100644 --- a/vendor/sigs.k8s.io/controller-runtime/OWNERS_ALIASES +++ b/vendor/sigs.k8s.io/controller-runtime/OWNERS_ALIASES @@ -4,8 +4,10 @@ aliases: # active folks who can be contacted to perform admin-related # tasks on the repo, or otherwise approve any PRS. controller-runtime-admins: - - vincepri + - alvaroaleman - joelanford + - sbueringer + - vincepri # non-admin folks who have write-access and can approve any PRs in the repo controller-runtime-maintainers: @@ -24,6 +26,8 @@ aliases: controller-runtime-reviewers: - varshaprasad96 - inteon + - JoelSpeed + - troy0820 # folks who may have context on ancient history, # but are no longer directly involved diff --git a/vendor/sigs.k8s.io/controller-runtime/README.md b/vendor/sigs.k8s.io/controller-runtime/README.md index 7b4f345044f0..54bacad42ec5 100644 --- a/vendor/sigs.k8s.io/controller-runtime/README.md +++ b/vendor/sigs.k8s.io/controller-runtime/README.md @@ -25,9 +25,9 @@ The full documentation can be found at [VERSIONING.md](VERSIONING.md), but TL;DR Users: -- We follow [Semantic Versioning (semver)](https://semver.org) -- Use releases with your dependency management to ensure that you get compatible code -- The main branch contains all the latest code, some of which may break compatibility (so "normal" `go get` is not recommended) +- We stick to a zero major version +- We publish a minor version for each Kubernetes minor release and allow breaking changes between minor versions +- We publish patch versions as needed and we don't allow breaking changes in them Contributors: @@ -53,6 +53,8 @@ Compatible k8s.io/*, client-go and minimum Go versions can be looked up in our [ | | k8s.io/*, client-go | minimum Go version | |----------|:-------------------:|:------------------:| +| CR v0.21 | v0.33 | 1.24 | +| CR v0.20 | v0.32 | 1.23 | | CR v0.19 | v0.31 | 1.22 | | CR v0.18 | v0.30 | 1.22 | | CR v0.17 | v0.29 | 1.21 | @@ -67,9 +69,6 @@ See [FAQ.md](FAQ.md) Learn how to engage with the Kubernetes community on the [community page](http://kubernetes.io/community/). -controller-runtime is a subproject of the [kubebuilder](https://github.com/kubernetes-sigs/kubebuilder) project -in sig apimachinery. - You can reach the maintainers of this project at: - Slack channel: [#controller-runtime](https://kubernetes.slack.com/archives/C02MRBMN00Z) diff --git a/vendor/sigs.k8s.io/controller-runtime/VERSIONING.md b/vendor/sigs.k8s.io/controller-runtime/VERSIONING.md index 2c0f2f9b2d47..7ad6b142cc47 100644 --- a/vendor/sigs.k8s.io/controller-runtime/VERSIONING.md +++ b/vendor/sigs.k8s.io/controller-runtime/VERSIONING.md @@ -7,6 +7,16 @@ For the purposes of the aforementioned guidelines, controller-runtime counts as a "library project", but otherwise follows the guidelines exactly. +We stick to a major version of zero and create a minor version for +each Kubernetes minor version and we allow breaking changes in our +minor versions. We create patch releases as needed and don't allow +breaking changes in them. + +Publishing a non-zero major version is pointless for us, as the k8s.io/* +libraries we heavily depend on do breaking changes but use the same +versioning scheme as described above. Consequently, a project can only +ever depend on one controller-runtime version. + [guidelines]: https://sigs.k8s.io/kubebuilder-release-tools/VERSIONING.md ## Compatibility and Release Support diff --git a/vendor/sigs.k8s.io/controller-runtime/alias.go b/vendor/sigs.k8s.io/controller-runtime/alias.go index 3e1ccdcf08c4..01ba012dcc2f 100644 --- a/vendor/sigs.k8s.io/controller-runtime/alias.go +++ b/vendor/sigs.k8s.io/controller-runtime/alias.go @@ -77,6 +77,9 @@ var ( // If --kubeconfig is set, will use the kubeconfig file at that location. Otherwise will assume running // in cluster and use the cluster provided kubeconfig. // + // The returned `*rest.Config` has client-side ratelimting disabled as we can rely on API priority and + // fairness. Set its QPS to a value equal or bigger than 0 to re-enable it. + // // Will log an error and exit if there is an error creating the rest.Config. GetConfigOrDie = config.GetConfigOrDie @@ -84,6 +87,9 @@ var ( // If --kubeconfig is set, will use the kubeconfig file at that location. Otherwise will assume running // in cluster and use the cluster provided kubeconfig. // + // The returned `*rest.Config` has client-side ratelimting disabled as we can rely on API priority and + // fairness. Set its QPS to a value equal or bigger than 0 to re-enable it. + // // Config precedence // // * --kubeconfig flag pointing at a file diff --git a/vendor/sigs.k8s.io/controller-runtime/doc.go b/vendor/sigs.k8s.io/controller-runtime/doc.go index 0319bc3ff866..75d1d908c50d 100644 --- a/vendor/sigs.k8s.io/controller-runtime/doc.go +++ b/vendor/sigs.k8s.io/controller-runtime/doc.go @@ -87,7 +87,7 @@ limitations under the License. // during writes (nor does it promise sequential create/get coherence), and code // should not assume a get immediately following a create/update will return // the updated resource. Caches may also have indexes, which can be created via -// a FieldIndexer (pkg/client) obtained from the manager. Indexes can used to +// a FieldIndexer (pkg/client) obtained from the manager. Indexes can be used to // quickly and easily look up all objects with certain fields set. Reconcilers // may retrieve event recorders (pkg/recorder) to emit events using the // manager. diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/builder/webhook.go b/vendor/sigs.k8s.io/controller-runtime/pkg/builder/webhook.go index 81d8f7405634..6263f030a03c 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/builder/webhook.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/builder/webhook.go @@ -20,6 +20,7 @@ import ( "errors" "net/http" "net/url" + "regexp" "strings" "github.com/go-logr/logr" @@ -36,15 +37,19 @@ import ( // WebhookBuilder builds a Webhook. type WebhookBuilder struct { - apiType runtime.Object - customDefaulter admission.CustomDefaulter - customValidator admission.CustomValidator - gvk schema.GroupVersionKind - mgr manager.Manager - config *rest.Config - recoverPanic *bool - logConstructor func(base logr.Logger, req *admission.Request) logr.Logger - err error + apiType runtime.Object + customDefaulter admission.CustomDefaulter + customDefaulterOpts []admission.DefaulterOption + customValidator admission.CustomValidator + customPath string + customValidatorCustomPath string + customDefaulterCustomPath string + gvk schema.GroupVersionKind + mgr manager.Manager + config *rest.Config + recoverPanic *bool + logConstructor func(base logr.Logger, req *admission.Request) logr.Logger + err error } // WebhookManagedBy returns a new webhook builder. @@ -65,9 +70,11 @@ func (blder *WebhookBuilder) For(apiType runtime.Object) *WebhookBuilder { return blder } -// WithDefaulter takes an admission.CustomDefaulter interface, a MutatingWebhook will be wired for this type. -func (blder *WebhookBuilder) WithDefaulter(defaulter admission.CustomDefaulter) *WebhookBuilder { +// WithDefaulter takes an admission.CustomDefaulter interface, a MutatingWebhook with the provided opts (admission.DefaulterOption) +// will be wired for this type. +func (blder *WebhookBuilder) WithDefaulter(defaulter admission.CustomDefaulter, opts ...admission.DefaulterOption) *WebhookBuilder { blder.customDefaulter = defaulter + blder.customDefaulterOpts = opts return blder } @@ -90,6 +97,27 @@ func (blder *WebhookBuilder) RecoverPanic(recoverPanic bool) *WebhookBuilder { return blder } +// WithCustomPath overrides the webhook's default path by the customPath +// +// Deprecated: WithCustomPath should not be used anymore. +// Please use WithValidatorCustomPath or WithDefaulterCustomPath instead. +func (blder *WebhookBuilder) WithCustomPath(customPath string) *WebhookBuilder { + blder.customPath = customPath + return blder +} + +// WithValidatorCustomPath overrides the path of the Validator. +func (blder *WebhookBuilder) WithValidatorCustomPath(customPath string) *WebhookBuilder { + blder.customValidatorCustomPath = customPath + return blder +} + +// WithDefaulterCustomPath overrides the path of the Defaulter. +func (blder *WebhookBuilder) WithDefaulterCustomPath(customPath string) *WebhookBuilder { + blder.customDefaulterCustomPath = customPath + return blder +} + // Complete builds the webhook. func (blder *WebhookBuilder) Complete() error { // Set the Config @@ -128,6 +156,10 @@ func (blder *WebhookBuilder) setLogConstructor() { } } +func (blder *WebhookBuilder) isThereCustomPathConflict() bool { + return (blder.customPath != "" && blder.customDefaulter != nil && blder.customValidator != nil) || (blder.customPath != "" && blder.customDefaulterCustomPath != "") || (blder.customPath != "" && blder.customValidatorCustomPath != "") +} + func (blder *WebhookBuilder) registerWebhooks() error { typ, err := blder.getType() if err != nil { @@ -139,9 +171,27 @@ func (blder *WebhookBuilder) registerWebhooks() error { return err } + if blder.isThereCustomPathConflict() { + return errors.New("only one of CustomDefaulter or CustomValidator should be set when using WithCustomPath. Otherwise, WithDefaulterCustomPath() and WithValidatorCustomPath() should be used") + } + if blder.customPath != "" { + // isThereCustomPathConflict() already checks for potential conflicts. + // Since we are sure that only one of customDefaulter or customValidator will be used, + // we can set both customDefaulterCustomPath and validatingCustomPath. + blder.customDefaulterCustomPath = blder.customPath + blder.customValidatorCustomPath = blder.customPath + } + // Register webhook(s) for type - blder.registerDefaultingWebhook() - blder.registerValidatingWebhook() + err = blder.registerDefaultingWebhook() + if err != nil { + return err + } + + err = blder.registerValidatingWebhook() + if err != nil { + return err + } err = blder.registerConversionWebhook() if err != nil { @@ -151,11 +201,18 @@ func (blder *WebhookBuilder) registerWebhooks() error { } // registerDefaultingWebhook registers a defaulting webhook if necessary. -func (blder *WebhookBuilder) registerDefaultingWebhook() { +func (blder *WebhookBuilder) registerDefaultingWebhook() error { mwh := blder.getDefaultingWebhook() if mwh != nil { mwh.LogConstructor = blder.logConstructor path := generateMutatePath(blder.gvk) + if blder.customDefaulterCustomPath != "" { + generatedCustomPath, err := generateCustomPath(blder.customDefaulterCustomPath) + if err != nil { + return err + } + path = generatedCustomPath + } // Checking if the path is already registered. // If so, just skip it. @@ -166,35 +223,34 @@ func (blder *WebhookBuilder) registerDefaultingWebhook() { blder.mgr.GetWebhookServer().Register(path, mwh) } } + + return nil } func (blder *WebhookBuilder) getDefaultingWebhook() *admission.Webhook { if defaulter := blder.customDefaulter; defaulter != nil { - w := admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, defaulter) + w := admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, defaulter, blder.customDefaulterOpts...) if blder.recoverPanic != nil { w = w.WithRecoverPanic(*blder.recoverPanic) } return w } - if defaulter, ok := blder.apiType.(admission.Defaulter); ok { - 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", - "GVK", blder.gvk) return nil } // registerValidatingWebhook registers a validating webhook if necessary. -func (blder *WebhookBuilder) registerValidatingWebhook() { +func (blder *WebhookBuilder) registerValidatingWebhook() error { vwh := blder.getValidatingWebhook() if vwh != nil { vwh.LogConstructor = blder.logConstructor path := generateValidatePath(blder.gvk) + if blder.customValidatorCustomPath != "" { + generatedCustomPath, err := generateCustomPath(blder.customValidatorCustomPath) + if err != nil { + return err + } + path = generatedCustomPath + } // Checking if the path is already registered. // If so, just skip it. @@ -205,6 +261,8 @@ func (blder *WebhookBuilder) registerValidatingWebhook() { blder.mgr.GetWebhookServer().Register(path, vwh) } } + + return nil } func (blder *WebhookBuilder) getValidatingWebhook() *admission.Webhook { @@ -215,16 +273,6 @@ func (blder *WebhookBuilder) getValidatingWebhook() *admission.Webhook { } return w } - if validator, ok := blder.apiType.(admission.Validator); ok { - 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", - "GVK", blder.gvk) return nil } @@ -271,3 +319,14 @@ func generateValidatePath(gvk schema.GroupVersionKind) string { return "/validate-" + strings.ReplaceAll(gvk.Group, ".", "-") + "-" + gvk.Version + "-" + strings.ToLower(gvk.Kind) } + +const webhookPathStringValidation = `^((/[a-zA-Z0-9-_]+)+|/)$` + +var validWebhookPathRegex = regexp.MustCompile(webhookPathStringValidation) + +func generateCustomPath(customPath string) (string, error) { + if !validWebhookPathRegex.MatchString(customPath) { + return "", errors.New("customPath \"" + customPath + "\" does not match this regex: " + webhookPathStringValidation) + } + return customPath, nil +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/cache.go b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/cache.go index 9c7bf2325542..a94ec6cc3210 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/cache.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/cache.go @@ -19,11 +19,12 @@ package cache import ( "context" "fmt" + "maps" "net/http" + "slices" "sort" "time" - "golang.org/x/exp/maps" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -112,6 +113,10 @@ type Informer interface { // the handler again and an error if the handler cannot be added. AddEventHandlerWithResyncPeriod(handler toolscache.ResourceEventHandler, resyncPeriod time.Duration) (toolscache.ResourceEventHandlerRegistration, error) + // AddEventHandlerWithOptions is a variant of AddEventHandlerWithResyncPeriod where + // all optional parameters are passed in as a struct. + AddEventHandlerWithOptions(handler toolscache.ResourceEventHandler, options toolscache.HandlerOptions) (toolscache.ResourceEventHandlerRegistration, error) + // RemoveEventHandler removes a previously added event handler given by // its registration handle. // This function is guaranteed to be idempotent and thread-safe. @@ -167,6 +172,15 @@ type Options struct { // is "done" with an object, and would otherwise not requeue it, i.e., we // recommend the `Reconcile` function return `reconcile.Result{RequeueAfter: t}`, // instead of `reconcile.Result{}`. + // + // SyncPeriod will locally trigger an artificial Update event with the same + // object in both ObjectOld and ObjectNew for everything that is in the + // cache. + // + // Predicates or Handlers that expect ObjectOld and ObjectNew to be different + // (such as GenerationChangedPredicate) will filter out this event, preventing + // it from triggering a reconciliation. + // SyncPeriod does not sync between the local cache and the server. SyncPeriod *time.Duration // ReaderFailOnMissingInformer configures the cache to return a ErrResourceNotCached error when a user @@ -206,11 +220,11 @@ type Options struct { // to reduce the caches memory usage. DefaultTransform toolscache.TransformFunc - // DefaultWatchErrorHandler will be used to the WatchErrorHandler which is called + // DefaultWatchErrorHandler will be used to set the WatchErrorHandler which is called // whenever ListAndWatch drops the connection with an error. // // After calling this handler, the informer will backoff and retry. - DefaultWatchErrorHandler toolscache.WatchErrorHandler + DefaultWatchErrorHandler toolscache.WatchErrorHandlerWithContext // DefaultUnsafeDisableDeepCopy is the default for UnsafeDisableDeepCopy // for everything that doesn't specify this. @@ -231,15 +245,16 @@ type Options struct { // This will be used for all object types, unless it is set in ByObject or // DefaultNamespaces. // - // Defaults to false. + // Defaults to true. DefaultEnableWatchBookmarks *bool // ByObject restricts the cache's ListWatch to the desired fields per GVK at the specified object. // If unset, this will fall through to the Default* settings. ByObject map[client.Object]ByObject - // newInformer allows overriding of NewSharedIndexInformer for testing. - newInformer *func(toolscache.ListerWatcher, runtime.Object, time.Duration, toolscache.Indexers) toolscache.SharedIndexInformer + // NewInformer allows overriding of NewSharedIndexInformer, for example for testing + // or if someone wants to write their own Informer. + NewInformer func(toolscache.ListerWatcher, runtime.Object, time.Duration, toolscache.Indexers) toolscache.SharedIndexInformer } // ByObject offers more fine-grained control over the cache's ListWatch by object. @@ -291,8 +306,44 @@ type ByObject struct { // assume bookmarks are returned at any specific interval, nor may they // assume the server will send any BOOKMARK event during a session. // - // Defaults to false. + // Defaults to true. EnableWatchBookmarks *bool + + // 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. + // + // This applies to all controllers. + // + // A period sync happens for two reasons: + // 1. To insure against a bug in the controller that causes an object to not + // be requeued, when it otherwise should be requeued. + // 2. To insure against an unknown bug in controller-runtime, or its dependencies, + // that causes an object to not be requeued, when it otherwise should be + // requeued, or to be removed from the queue, when it otherwise should not + // be removed. + // + // If you want + // 1. to insure against missed watch events, or + // 2. to poll services that cannot be watched, + // then we recommend that, instead of changing the default period, the + // controller requeue, with a constant duration `t`, whenever the controller + // is "done" with an object, and would otherwise not requeue it, i.e., we + // recommend the `Reconcile` function return `reconcile.Result{RequeueAfter: t}`, + // instead of `reconcile.Result{}`. + // + // SyncPeriod will locally trigger an artificial Update event with the same + // object in both ObjectOld and ObjectNew for everything that is in the + // cache. + // + // Predicates or Handlers that expect ObjectOld and ObjectNew to be different + // (such as GenerationChangedPredicate) will filter out this event, preventing + // it from triggering a reconciliation. + // SyncPeriod does not sync between the local cache and the server. + SyncPeriod *time.Duration } // Config describes all potential options for a given watch. @@ -326,8 +377,44 @@ type Config struct { // assume bookmarks are returned at any specific interval, nor may they // assume the server will send any BOOKMARK event during a session. // - // Defaults to false. + // Defaults to true. EnableWatchBookmarks *bool + + // 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. + // + // This applies to all controllers. + // + // A period sync happens for two reasons: + // 1. To insure against a bug in the controller that causes an object to not + // be requeued, when it otherwise should be requeued. + // 2. To insure against an unknown bug in controller-runtime, or its dependencies, + // that causes an object to not be requeued, when it otherwise should be + // requeued, or to be removed from the queue, when it otherwise should not + // be removed. + // + // If you want + // 1. to insure against missed watch events, or + // 2. to poll services that cannot be watched, + // then we recommend that, instead of changing the default period, the + // controller requeue, with a constant duration `t`, whenever the controller + // is "done" with an object, and would otherwise not requeue it, i.e., we + // recommend the `Reconcile` function return `reconcile.Result{RequeueAfter: t}`, + // instead of `reconcile.Result{}`. + // + // SyncPeriod will locally trigger an artificial Update event with the same + // object in both ObjectOld and ObjectNew for everything that is in the + // cache. + // + // Predicates or Handlers that expect ObjectOld and ObjectNew to be different + // (such as GenerationChangedPredicate) will filter out this event, preventing + // it from triggering a reconciliation. + // SyncPeriod does not sync between the local cache and the server. + SyncPeriod *time.Duration } // NewCacheFunc - Function for creating a new cache from the options and a rest config. @@ -398,6 +485,7 @@ func optionDefaultsToConfig(opts *Options) Config { Transform: opts.DefaultTransform, UnsafeDisableDeepCopy: opts.DefaultUnsafeDisableDeepCopy, EnableWatchBookmarks: opts.DefaultEnableWatchBookmarks, + SyncPeriod: opts.SyncPeriod, } } @@ -408,6 +496,7 @@ func byObjectToConfig(byObject ByObject) Config { Transform: byObject.Transform, UnsafeDisableDeepCopy: byObject.UnsafeDisableDeepCopy, EnableWatchBookmarks: byObject.EnableWatchBookmarks, + SyncPeriod: byObject.SyncPeriod, } } @@ -421,7 +510,7 @@ func newCache(restConfig *rest.Config, opts Options) newCacheFunc { HTTPClient: opts.HTTPClient, Scheme: opts.Scheme, Mapper: opts.Mapper, - ResyncPeriod: *opts.SyncPeriod, + ResyncPeriod: ptr.Deref(config.SyncPeriod, defaultSyncPeriod), Namespace: namespace, Selector: internal.Selector{ Label: config.LabelSelector, @@ -430,8 +519,8 @@ func newCache(restConfig *rest.Config, opts Options) newCacheFunc { Transform: config.Transform, WatchErrorHandler: opts.DefaultWatchErrorHandler, UnsafeDisableDeepCopy: ptr.Deref(config.UnsafeDisableDeepCopy, false), - EnableWatchBookmarks: ptr.Deref(config.EnableWatchBookmarks, false), - NewInformer: opts.newInformer, + EnableWatchBookmarks: ptr.Deref(config.EnableWatchBookmarks, true), + NewInformer: opts.NewInformer, }), readerFailOnMissingInformer: opts.ReaderFailOnMissingInformer, } @@ -501,7 +590,7 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) { if namespace == metav1.NamespaceAll { config.FieldSelector = fields.AndSelectors( appendIfNotNil( - namespaceAllSelector(maps.Keys(byObject.Namespaces)), + namespaceAllSelector(slices.Collect(maps.Keys(byObject.Namespaces))), config.FieldSelector, )..., ) @@ -519,6 +608,7 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) { byObject.Transform = defaultedConfig.Transform byObject.UnsafeDisableDeepCopy = defaultedConfig.UnsafeDisableDeepCopy byObject.EnableWatchBookmarks = defaultedConfig.EnableWatchBookmarks + byObject.SyncPeriod = defaultedConfig.SyncPeriod } opts.ByObject[obj] = byObject @@ -532,7 +622,7 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) { if namespace == metav1.NamespaceAll { cfg.FieldSelector = fields.AndSelectors( appendIfNotNil( - namespaceAllSelector(maps.Keys(opts.DefaultNamespaces)), + namespaceAllSelector(slices.Collect(maps.Keys(opts.DefaultNamespaces))), cfg.FieldSelector, )..., ) @@ -540,10 +630,6 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) { opts.DefaultNamespaces[namespace] = cfg } - // Default the resync period to 10 hours if unset - if opts.SyncPeriod == nil { - opts.SyncPeriod = &defaultSyncPeriod - } return opts, nil } @@ -563,6 +649,9 @@ func defaultConfig(toDefault, defaultFrom Config) Config { if toDefault.EnableWatchBookmarks == nil { toDefault.EnableWatchBookmarks = defaultFrom.EnableWatchBookmarks } + if toDefault.SyncPeriod == nil { + toDefault.SyncPeriod = defaultFrom.SyncPeriod + } return toDefault } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/delegating_by_gvk_cache.go b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/delegating_by_gvk_cache.go index 4db8208a63fa..46bd243c66b2 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/delegating_by_gvk_cache.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/delegating_by_gvk_cache.go @@ -18,10 +18,11 @@ package cache import ( "context" + "maps" + "slices" "strings" "sync" - "golang.org/x/exp/maps" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" @@ -73,7 +74,7 @@ func (dbt *delegatingByGVKCache) GetInformerForKind(ctx context.Context, gvk sch } func (dbt *delegatingByGVKCache) Start(ctx context.Context) error { - allCaches := maps.Values(dbt.caches) + allCaches := slices.Collect(maps.Values(dbt.caches)) allCaches = append(allCaches, dbt.defaultCache) wg := &sync.WaitGroup{} @@ -100,7 +101,7 @@ func (dbt *delegatingByGVKCache) Start(ctx context.Context) error { func (dbt *delegatingByGVKCache) WaitForCacheSync(ctx context.Context) bool { synced := true - for _, cache := range append(maps.Values(dbt.caches), dbt.defaultCache) { + for _, cache := range append(slices.Collect(maps.Values(dbt.caches)), dbt.defaultCache) { if !cache.WaitForCacheSync(ctx) { synced = false } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/internal/cache_reader.go b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/internal/cache_reader.go index 81ee960b7384..eb6b5448559f 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/internal/cache_reader.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/internal/cache_reader.go @@ -54,7 +54,10 @@ type CacheReader struct { } // Get checks the indexer for the object and writes a copy of it if found. -func (c *CacheReader) Get(_ context.Context, key client.ObjectKey, out client.Object, _ ...client.GetOption) error { +func (c *CacheReader) Get(_ context.Context, key client.ObjectKey, out client.Object, opts ...client.GetOption) error { + getOpts := client.GetOptions{} + getOpts.ApplyOptions(opts) + if c.scopeName == apimeta.RESTScopeNameRoot { key.Namespace = "" } @@ -81,7 +84,7 @@ func (c *CacheReader) Get(_ context.Context, key client.ObjectKey, out client.Ob return fmt.Errorf("cache contained %T, which is not an Object", obj) } - if c.disableDeepCopy { + if c.disableDeepCopy || (getOpts.UnsafeDisableDeepCopy != nil && *getOpts.UnsafeDisableDeepCopy) { // skip deep copy which might be unsafe // you must DeepCopy any object before mutating it outside } else { @@ -97,7 +100,7 @@ func (c *CacheReader) Get(_ context.Context, key client.ObjectKey, out client.Ob return fmt.Errorf("cache had type %s, but %s was asked for", objVal.Type(), outVal.Type()) } reflect.Indirect(outVal).Set(reflect.Indirect(objVal)) - if !c.disableDeepCopy { + if !c.disableDeepCopy && (getOpts.UnsafeDisableDeepCopy == nil || !*getOpts.UnsafeDisableDeepCopy) { out.GetObjectKind().SetGroupVersionKind(c.groupVersionKind) } @@ -174,7 +177,13 @@ func (c *CacheReader) List(_ context.Context, out client.ObjectList, opts ...cli } runtimeObjs = append(runtimeObjs, outObj) } - return apimeta.SetList(out, runtimeObjs) + + if err := apimeta.SetList(out, runtimeObjs); err != nil { + return err + } + + out.SetContinue("continue-not-supported") + return nil } func byIndexes(indexer cache.Indexer, requires fields.Requirements, namespace string) ([]interface{}, error) { diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/internal/informers.go b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/internal/informers.go index a40382d6f378..f216be0d9e53 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/internal/informers.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/internal/informers.go @@ -25,21 +25,26 @@ import ( "sync" "time" + "github.com/go-logr/logr" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/dynamic" "k8s.io/client-go/metadata" "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + logf "sigs.k8s.io/controller-runtime/pkg/internal/log" "sigs.k8s.io/controller-runtime/pkg/internal/syncs" ) +var log = logf.RuntimeLog.WithName("cache") + // InformersOpts configures an InformerMap. type InformersOpts struct { HTTPClient *http.Client @@ -47,19 +52,19 @@ type InformersOpts struct { Mapper meta.RESTMapper ResyncPeriod time.Duration Namespace string - NewInformer *func(cache.ListerWatcher, runtime.Object, time.Duration, cache.Indexers) cache.SharedIndexInformer + NewInformer func(cache.ListerWatcher, runtime.Object, time.Duration, cache.Indexers) cache.SharedIndexInformer Selector Selector Transform cache.TransformFunc UnsafeDisableDeepCopy bool EnableWatchBookmarks bool - WatchErrorHandler cache.WatchErrorHandler + WatchErrorHandler cache.WatchErrorHandlerWithContext } // NewInformers creates a new InformersMap that can create informers under the hood. func NewInformers(config *rest.Config, options *InformersOpts) *Informers { newInformer := cache.NewSharedIndexInformer if options.NewInformer != nil { - newInformer = *options.NewInformer + newInformer = options.NewInformer } return &Informers{ config: config, @@ -105,7 +110,8 @@ func (c *Cache) Start(stop <-chan struct{}) { // Stop on either the whole map stopping or just this informer being removed. internalStop, cancel := syncs.MergeChans(stop, c.stop) defer cancel() - c.Informer.Run(internalStop) + // Convert the stop channel to a context and then add the logger. + c.Informer.RunWithContext(logr.NewContext(wait.ContextForChannel(internalStop), log)) } type tracker struct { @@ -181,10 +187,10 @@ type Informers struct { // NewInformer allows overriding of the shared index informer constructor for testing. newInformer func(cache.ListerWatcher, runtime.Object, time.Duration, cache.Indexers) cache.SharedIndexInformer - // WatchErrorHandler allows the shared index informer's + // watchErrorHandler allows the shared index informer's // watchErrorHandler to be set by overriding the options // or to use the default watchErrorHandler - watchErrorHandler cache.WatchErrorHandler + watchErrorHandler cache.WatchErrorHandlerWithContext } // Start calls Run on each of the informers and sets started to true. Blocks on the context. @@ -195,7 +201,7 @@ func (ip *Informers) Start(ctx context.Context) error { defer ip.mu.Unlock() if ip.started { - return errors.New("Informer already started") //nolint:stylecheck + return errors.New("informer already started") //nolint:stylecheck } // Set the context so it can be passed to informers that are added later @@ -359,16 +365,16 @@ func (ip *Informers) addInformerToMap(gvk schema.GroupVersionKind, obj runtime.O return nil, false, err } sharedIndexInformer := ip.newInformer(&cache.ListWatch{ - ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { + ListWithContextFunc: func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { ip.selector.ApplyToList(&opts) - return listWatcher.ListFunc(opts) + return listWatcher.ListWithContextFunc(ctx, opts) }, - WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) { + WatchFuncWithContext: func(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { opts.Watch = true // Watch needs to be set to true separately opts.AllowWatchBookmarks = ip.enableWatchBookmarks ip.selector.ApplyToList(&opts) - return listWatcher.WatchFunc(opts) + return listWatcher.WatchFuncWithContext(ctx, opts) }, }, obj, calculateResyncPeriod(ip.resync), cache.Indexers{ cache.NamespaceIndex: cache.MetaNamespaceIndexFunc, @@ -376,7 +382,7 @@ func (ip *Informers) addInformerToMap(gvk schema.GroupVersionKind, obj runtime.O // Set WatchErrorHandler on SharedIndexInformer if set if ip.watchErrorHandler != nil { - if err := sharedIndexInformer.SetWatchErrorHandler(ip.watchErrorHandler); err != nil { + if err := sharedIndexInformer.SetWatchErrorHandlerWithContext(ip.watchErrorHandler); err != nil { return nil, false, err } } @@ -441,21 +447,21 @@ func (ip *Informers) makeListWatcher(gvk schema.GroupVersionKind, obj runtime.Ob } resources := dynamicClient.Resource(mapping.Resource) return &cache.ListWatch{ - ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { + ListWithContextFunc: func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { if namespace != "" { - return resources.Namespace(namespace).List(ip.ctx, opts) + return resources.Namespace(namespace).List(ctx, opts) } - return resources.List(ip.ctx, opts) + return resources.List(ctx, opts) }, // Setup the watch function - WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) { + WatchFuncWithContext: func(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { opts.Watch = true // Watch needs to be set to true separately opts.AllowWatchBookmarks = ip.enableWatchBookmarks if namespace != "" { - return resources.Namespace(namespace).Watch(ip.ctx, opts) + return resources.Namespace(namespace).Watch(ctx, opts) } - return resources.Watch(ip.ctx, opts) + return resources.Watch(ctx, opts) }, }, nil // @@ -475,15 +481,15 @@ func (ip *Informers) makeListWatcher(gvk schema.GroupVersionKind, obj runtime.Ob resources := metadataClient.Resource(mapping.Resource) return &cache.ListWatch{ - ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { + ListWithContextFunc: func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { var ( list *metav1.PartialObjectMetadataList err error ) if namespace != "" { - list, err = resources.Namespace(namespace).List(ip.ctx, opts) + list, err = resources.Namespace(namespace).List(ctx, opts) } else { - list, err = resources.List(ip.ctx, opts) + list, err = resources.List(ctx, opts) } if list != nil { for i := range list.Items { @@ -493,14 +499,14 @@ func (ip *Informers) makeListWatcher(gvk schema.GroupVersionKind, obj runtime.Ob return list, err }, // Setup the watch function - WatchFunc: func(opts metav1.ListOptions) (watcher watch.Interface, err error) { + WatchFuncWithContext: func(ctx context.Context, opts metav1.ListOptions) (watcher watch.Interface, err error) { opts.Watch = true // Watch needs to be set to true separately opts.AllowWatchBookmarks = ip.enableWatchBookmarks if namespace != "" { - watcher, err = resources.Namespace(namespace).Watch(ip.ctx, opts) + watcher, err = resources.Namespace(namespace).Watch(ctx, opts) } else { - watcher, err = resources.Watch(ip.ctx, opts) + watcher, err = resources.Watch(ctx, opts) } if err != nil { return nil, err @@ -512,7 +518,7 @@ func (ip *Informers) makeListWatcher(gvk schema.GroupVersionKind, obj runtime.Ob // Structured. // default: - client, err := apiutil.RESTClientForGVK(gvk, false, ip.config, ip.codecs, ip.httpClient) + client, err := apiutil.RESTClientForGVK(gvk, false, false, ip.config, ip.codecs, ip.httpClient) if err != nil { return nil, err } @@ -522,7 +528,7 @@ func (ip *Informers) makeListWatcher(gvk schema.GroupVersionKind, obj runtime.Ob return nil, err } return &cache.ListWatch{ - ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { + ListWithContextFunc: func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { // Build the request. req := client.Get().Resource(mapping.Resource.Resource).VersionedParams(&opts, ip.paramCodec) if namespace != "" { @@ -531,13 +537,13 @@ func (ip *Informers) makeListWatcher(gvk schema.GroupVersionKind, obj runtime.Ob // Create the resulting object, and execute the request. res := listObj.DeepCopyObject() - if err := req.Do(ip.ctx).Into(res); err != nil { + if err := req.Do(ctx).Into(res); err != nil { return nil, err } return res, nil }, // Setup the watch function - WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) { + WatchFuncWithContext: func(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { opts.Watch = true // Watch needs to be set to true separately opts.AllowWatchBookmarks = ip.enableWatchBookmarks @@ -547,7 +553,7 @@ func (ip *Informers) makeListWatcher(gvk schema.GroupVersionKind, obj runtime.Ob req.Namespace(namespace) } // Call the watch. - return req.Watch(ip.ctx) + return req.Watch(ctx) }, }, nil } @@ -585,7 +591,7 @@ func newGVKFixupWatcher(gvk schema.GroupVersionKind, watcher watch.Interface) wa // hammer the apiserver with list requests simultaneously. func calculateResyncPeriod(resync time.Duration) time.Duration { // the factor will fall into [0.9, 1.1) - factor := rand.Float64()/5.0 + 0.9 //nolint:gosec + factor := rand.Float64()/5.0 + 0.9 return time.Duration(float64(resync.Nanoseconds()) * factor) } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/multi_namespace_cache.go b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/multi_namespace_cache.go index aeeeb66937b9..d7d7b0e7c2f6 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/multi_namespace_cache.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/multi_namespace_cache.go @@ -249,6 +249,10 @@ func (c *multiNamespaceCache) List(ctx context.Context, list client.ObjectList, listOpts := client.ListOptions{} listOpts.ApplyOptions(opts) + if listOpts.Continue != "" { + return fmt.Errorf("continue list option is not supported by the cache") + } + isNamespaced, err := apiutil.IsObjectNamespaced(list, c.Scheme, c.RESTMapper) if err != nil { return err @@ -275,10 +279,7 @@ func (c *multiNamespaceCache) List(ctx context.Context, list client.ObjectList, return err } - allItems, err := apimeta.ExtractList(list) - if err != nil { - return err - } + allItems := []runtime.Object{} limitSet := listOpts.Limit > 0 @@ -316,7 +317,12 @@ func (c *multiNamespaceCache) List(ctx context.Context, list client.ObjectList, } listAccessor.SetResourceVersion(resourceVersion) - return apimeta.SetList(list, allItems) + if err := apimeta.SetList(list, allItems); err != nil { + return err + } + + list.SetContinue("continue-not-supported") + return nil } // multiNamespaceInformer knows how to handle interacting with the underlying informer across multiple namespaces. @@ -328,18 +334,11 @@ type handlerRegistration struct { handles map[string]toolscache.ResourceEventHandlerRegistration } -type syncer interface { - HasSynced() bool -} - // HasSynced asserts that the handler has been called for the full initial state of the informer. -// This uses syncer to be compatible between client-go 1.27+ and older versions when the interface changed. func (h handlerRegistration) HasSynced() bool { - for _, reg := range h.handles { - if s, ok := reg.(syncer); ok { - if !s.HasSynced() { - return false - } + for _, h := range h.handles { + if !h.HasSynced() { + return false } } return true @@ -381,6 +380,23 @@ func (i *multiNamespaceInformer) AddEventHandlerWithResyncPeriod(handler toolsca return handles, nil } +// AddEventHandlerWithOptions adds the handler with options to each namespaced informer. +func (i *multiNamespaceInformer) AddEventHandlerWithOptions(handler toolscache.ResourceEventHandler, options toolscache.HandlerOptions) (toolscache.ResourceEventHandlerRegistration, error) { + handles := handlerRegistration{ + handles: make(map[string]toolscache.ResourceEventHandlerRegistration, len(i.namespaceToInformer)), + } + + for ns, informer := range i.namespaceToInformer { + registration, err := informer.AddEventHandlerWithOptions(handler, options) + if err != nil { + return nil, err + } + handles.handles[ns] = registration + } + + return handles, nil +} + // RemoveEventHandler removes a previously added event handler given by its registration handle. func (i *multiNamespaceInformer) RemoveEventHandler(h toolscache.ResourceEventHandlerRegistration) error { handles, ok := h.(handlerRegistration) diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/certwatcher/certwatcher.go b/vendor/sigs.k8s.io/controller-runtime/pkg/certwatcher/certwatcher.go index c32324098234..2362d020b839 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/certwatcher/certwatcher.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/certwatcher/certwatcher.go @@ -26,6 +26,7 @@ import ( "time" "github.com/fsnotify/fsnotify" + "github.com/go-logr/logr" kerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" @@ -47,6 +48,7 @@ type CertWatcher struct { currentCert *tls.Certificate watcher *fsnotify.Watcher interval time.Duration + log logr.Logger certPath string keyPath string @@ -65,6 +67,7 @@ func New(certPath, keyPath string) (*CertWatcher, error) { certPath: certPath, keyPath: keyPath, interval: defaultWatchInterval, + log: log.WithValues("cert", certPath, "key", keyPath), } // Initial read of certificate and key. @@ -130,14 +133,14 @@ func (cw *CertWatcher) Start(ctx context.Context) error { ticker := time.NewTicker(cw.interval) defer ticker.Stop() - log.Info("Starting certificate poll+watcher", "interval", cw.interval) + cw.log.Info("Starting certificate poll+watcher", "interval", cw.interval) for { select { case <-ctx.Done(): return cw.watcher.Close() case <-ticker.C: if err := cw.ReadCertificate(); err != nil { - log.Error(err, "failed read certificate") + cw.log.Error(err, "failed read certificate") } } } @@ -160,7 +163,7 @@ func (cw *CertWatcher) Watch() { return } - log.Error(err, "certificate watch error") + cw.log.Error(err, "certificate watch error") } } } @@ -174,7 +177,7 @@ func (cw *CertWatcher) updateCachedCertificate(cert *tls.Certificate, keyPEMBloc if cw.currentCert != nil && bytes.Equal(cw.currentCert.Certificate[0], cert.Certificate[0]) && bytes.Equal(cw.cachedKeyPEMBlock, keyPEMBlock) { - log.V(7).Info("certificate already cached") + cw.log.V(7).Info("certificate already cached") return false } cw.currentCert = cert @@ -208,7 +211,7 @@ func (cw *CertWatcher) ReadCertificate() error { return nil } - log.Info("Updated current TLS certificate") + cw.log.Info("Updated current TLS certificate") // If a callback is registered, invoke it with the new certificate. cw.RLock() @@ -229,14 +232,20 @@ func (cw *CertWatcher) handleEvent(event fsnotify.Event) { case event.Op.Has(fsnotify.Chmod), event.Op.Has(fsnotify.Remove): // If the file was removed or renamed, re-add the watch to the previous name if err := cw.watcher.Add(event.Name); err != nil { - log.Error(err, "error re-watching file") + cw.log.Error(err, "error re-watching file") } default: return } - log.V(1).Info("certificate event", "event", event) + cw.log.V(1).Info("certificate event", "event", event) if err := cw.ReadCertificate(); err != nil { - log.Error(err, "error re-reading certificate") + cw.log.Error(err, "error re-reading certificate") } } + +// NeedLeaderElection indicates that the cert-manager +// does not need leader election. +func (cw *CertWatcher) NeedLeaderElection() bool { + return false +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/apiutil/apimachinery.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/apiutil/apimachinery.go index 1d4ce264c9bc..b132cb2d4d67 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/apiutil/apimachinery.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/apiutil/apimachinery.go @@ -161,15 +161,27 @@ func GVKForObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersi // RESTClientForGVK constructs a new rest.Interface capable of accessing the resource associated // with the given GroupVersionKind. The REST client will be configured to use the negotiated serializer from // baseConfig, if set, otherwise a default serializer will be set. -func RESTClientForGVK(gvk schema.GroupVersionKind, isUnstructured bool, baseConfig *rest.Config, codecs serializer.CodecFactory, httpClient *http.Client) (rest.Interface, error) { +func RESTClientForGVK( + gvk schema.GroupVersionKind, + forceDisableProtoBuf bool, + isUnstructured bool, + baseConfig *rest.Config, + codecs serializer.CodecFactory, + httpClient *http.Client, +) (rest.Interface, error) { if httpClient == nil { return nil, fmt.Errorf("httpClient must not be nil, consider using rest.HTTPClientFor(c) to create a client") } - return rest.RESTClientForConfigAndClient(createRestConfig(gvk, isUnstructured, baseConfig, codecs), httpClient) + return rest.RESTClientForConfigAndClient(createRestConfig(gvk, forceDisableProtoBuf, isUnstructured, baseConfig, codecs), httpClient) } // createRestConfig copies the base config and updates needed fields for a new rest config. -func createRestConfig(gvk schema.GroupVersionKind, isUnstructured bool, baseConfig *rest.Config, codecs serializer.CodecFactory) *rest.Config { +func createRestConfig(gvk schema.GroupVersionKind, + forceDisableProtoBuf bool, + isUnstructured bool, + baseConfig *rest.Config, + codecs serializer.CodecFactory, +) *rest.Config { gv := gvk.GroupVersion() cfg := rest.CopyConfig(baseConfig) @@ -183,7 +195,7 @@ func createRestConfig(gvk schema.GroupVersionKind, isUnstructured bool, baseConf cfg.UserAgent = rest.DefaultKubernetesUserAgent() } // TODO(FillZpp): In the long run, we want to check discovery or something to make sure that this is actually true. - if cfg.ContentType == "" && !isUnstructured { + if cfg.ContentType == "" && !forceDisableProtoBuf { protobufSchemeLock.RLock() if protobufScheme.Recognizes(gvk) { cfg.ContentType = runtime.ContentTypeProtobuf diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/apiutil/restmapper.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/apiutil/restmapper.go index 927be22b4e70..7a7a0d114551 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/apiutil/restmapper.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/apiutil/restmapper.go @@ -28,6 +28,7 @@ import ( "k8s.io/client-go/discovery" "k8s.io/client-go/rest" "k8s.io/client-go/restmapper" + "k8s.io/utils/ptr" ) // NewDynamicRESTMapper returns a dynamic RESTMapper for cfg. The dynamic @@ -41,6 +42,7 @@ func NewDynamicRESTMapper(cfg *rest.Config, httpClient *http.Client) (meta.RESTM if err != nil { return nil, err } + return &mapper{ mapper: restmapper.NewDiscoveryRESTMapper([]*restmapper.APIGroupResources{}), client: client, @@ -53,11 +55,15 @@ func NewDynamicRESTMapper(cfg *rest.Config, httpClient *http.Client) (meta.RESTM // client for discovery information to do REST mappings. type mapper struct { mapper meta.RESTMapper - client discovery.DiscoveryInterface + client discovery.AggregatedDiscoveryInterface knownGroups map[string]*restmapper.APIGroupResources apiGroups map[string]*metav1.APIGroup + initialDiscoveryDone bool + // mutex to provide thread-safe mapper reloading. + // It protects all fields in the mapper as well as methods + // that have the `Locked` suffix. mu sync.RWMutex } @@ -159,28 +165,42 @@ func (m *mapper) addKnownGroupAndReload(groupName string, versions ...string) er versions = nil } + m.mu.Lock() + defer m.mu.Unlock() // If no specific versions are set by user, we will scan all available ones for the API group. // This operation requires 2 requests: /api and /apis, but only once. For all subsequent calls // this data will be taken from cache. - if len(versions) == 0 { - apiGroup, err := m.findAPIGroupByName(groupName) + // + // We always run this once, because if the server supports aggregated discovery, this will + // load everything with two api calls which we assume is overall cheaper. + if len(versions) == 0 || !m.initialDiscoveryDone { + apiGroup, didAggregatedDiscovery, err := m.findAPIGroupByNameAndMaybeAggregatedDiscoveryLocked(groupName) if err != nil { return err } - if apiGroup != nil { + if apiGroup != nil && len(versions) == 0 { for _, version := range apiGroup.Versions { versions = append(versions, version.Version) } } - } - - m.mu.Lock() - defer m.mu.Unlock() - // Create or fetch group resources from cache. - groupResources := &restmapper.APIGroupResources{ - Group: metav1.APIGroup{Name: groupName}, - VersionedResources: make(map[string][]metav1.APIResource), + // No need to do anything further if aggregatedDiscovery is supported and we did a lookup + if didAggregatedDiscovery { + failedGroups := make(map[schema.GroupVersion]error) + for _, version := range versions { + if m.knownGroups[groupName] == nil || m.knownGroups[groupName].VersionedResources[version] == nil { + failedGroups[schema.GroupVersion{Group: groupName, Version: version}] = &meta.NoResourceMatchError{ + PartialResource: schema.GroupVersionResource{ + Group: groupName, + Version: version, + }} + } + } + if len(failedGroups) > 0 { + return ptr.To(ErrResourceDiscoveryFailed(failedGroups)) + } + return nil + } } // Update information for group resources about versioned resources. @@ -194,13 +214,26 @@ func (m *mapper) addKnownGroupAndReload(groupName string, versions ...string) er return fmt.Errorf("failed to get API group resources: %w", err) } - if _, ok := m.knownGroups[groupName]; ok { - groupResources = m.knownGroups[groupName] - } + m.addGroupVersionResourcesToCacheAndReloadLocked(groupVersionResources) + return nil +} +// addGroupVersionResourcesToCacheAndReloadLocked does what the name suggests. The mutex must be held when +// calling it. +func (m *mapper) addGroupVersionResourcesToCacheAndReloadLocked(gvr map[schema.GroupVersion]*metav1.APIResourceList) { // Update information for group resources about the API group by adding new versions. - // Ignore the versions that are already registered. - for groupVersion, resources := range groupVersionResources { + // Ignore the versions that are already registered + for groupVersion, resources := range gvr { + var groupResources *restmapper.APIGroupResources + if _, ok := m.knownGroups[groupVersion.Group]; ok { + groupResources = m.knownGroups[groupVersion.Group] + } else { + groupResources = &restmapper.APIGroupResources{ + Group: metav1.APIGroup{Name: groupVersion.Group}, + VersionedResources: make(map[string][]metav1.APIResource), + } + } + version := groupVersion.Version groupResources.VersionedResources[version] = resources.APIResources @@ -213,61 +246,65 @@ func (m *mapper) addKnownGroupAndReload(groupName string, versions ...string) er } if !found { - groupResources.Group.Versions = append(groupResources.Group.Versions, metav1.GroupVersionForDiscovery{ - GroupVersion: metav1.GroupVersion{Group: groupName, Version: version}.String(), + gv := metav1.GroupVersionForDiscovery{ + GroupVersion: metav1.GroupVersion{Group: groupVersion.Group, Version: version}.String(), Version: version, - }) + } + + // Prepend if preferred version, else append. The upstream DiscoveryRestMappper assumes + // the first version is the preferred one: https://github.com/kubernetes/kubernetes/blob/ef54ac803b712137871c1a1f8d635d50e69ffa6c/staging/src/k8s.io/apimachinery/pkg/api/meta/restmapper.go#L458-L461 + if group, ok := m.apiGroups[groupVersion.Group]; ok && group.PreferredVersion.Version == version { + groupResources.Group.Versions = append([]metav1.GroupVersionForDiscovery{gv}, groupResources.Group.Versions...) + } else { + groupResources.Group.Versions = append(groupResources.Group.Versions, gv) + } } - } - // Update data in the cache. - m.knownGroups[groupName] = groupResources + // Update data in the cache. + m.knownGroups[groupVersion.Group] = groupResources + } - // Finally, update the group with received information and regenerate the mapper. + // Finally, reload the mapper. updatedGroupResources := make([]*restmapper.APIGroupResources, 0, len(m.knownGroups)) for _, agr := range m.knownGroups { updatedGroupResources = append(updatedGroupResources, agr) } m.mapper = restmapper.NewDiscoveryRESTMapper(updatedGroupResources) - return nil } -// findAPIGroupByNameLocked returns API group by its name. -func (m *mapper) findAPIGroupByName(groupName string) (*metav1.APIGroup, error) { - // Looking in the cache first. - { - m.mu.RLock() - group, ok := m.apiGroups[groupName] - m.mu.RUnlock() - if ok { - return group, nil - } +// findAPIGroupByNameAndMaybeAggregatedDiscoveryLocked tries to find the passed apiGroup. +// If the server supports aggregated discovery, it will always perform that. +func (m *mapper) findAPIGroupByNameAndMaybeAggregatedDiscoveryLocked(groupName string) (_ *metav1.APIGroup, didAggregatedDiscovery bool, _ error) { + // Looking in the cache first + group, ok := m.apiGroups[groupName] + if ok { + return group, false, nil } // Update the cache if nothing was found. - apiGroups, err := m.client.ServerGroups() + apiGroups, maybeResources, _, err := m.client.GroupsAndMaybeResources() if err != nil { - return nil, fmt.Errorf("failed to get server groups: %w", err) + return nil, false, fmt.Errorf("failed to get server groups: %w", err) } if len(apiGroups.Groups) == 0 { - return nil, fmt.Errorf("received an empty API groups list") + return nil, false, fmt.Errorf("received an empty API groups list") } - m.mu.Lock() + m.initialDiscoveryDone = true for i := range apiGroups.Groups { group := &apiGroups.Groups[i] m.apiGroups[group.Name] = group } - m.mu.Unlock() + if len(maybeResources) > 0 { + didAggregatedDiscovery = true + m.addGroupVersionResourcesToCacheAndReloadLocked(maybeResources) + } // Looking in the cache again. - m.mu.RLock() - defer m.mu.RUnlock() - // Don't return an error here if the API group is not present. // The reloaded RESTMapper will take care of returning a NoMatchError. - return m.apiGroups[groupName], nil + return m.apiGroups[groupName], didAggregatedDiscovery, nil } // fetchGroupVersionResourcesLocked fetches the resources for the specified group and its versions. @@ -283,10 +320,10 @@ func (m *mapper) fetchGroupVersionResourcesLocked(groupName string, versions ... if apierrors.IsNotFound(err) { // If the version is not found, we remove the group from the cache // so it gets refreshed on the next call. - if m.isAPIGroupCached(groupVersion) { + if m.isAPIGroupCachedLocked(groupVersion) { delete(m.apiGroups, groupName) } - if m.isGroupVersionCached(groupVersion) { + if m.isGroupVersionCachedLocked(groupVersion) { delete(m.knownGroups, groupName) } continue @@ -308,8 +345,8 @@ func (m *mapper) fetchGroupVersionResourcesLocked(groupName string, versions ... return groupVersionResources, nil } -// isGroupVersionCached checks if a version for a group is cached in the known groups cache. -func (m *mapper) isGroupVersionCached(gv schema.GroupVersion) bool { +// isGroupVersionCachedLocked checks if a version for a group is cached in the known groups cache. +func (m *mapper) isGroupVersionCachedLocked(gv schema.GroupVersion) bool { if cachedGroup, ok := m.knownGroups[gv.Group]; ok { _, cached := cachedGroup.VersionedResources[gv.Version] return cached @@ -318,8 +355,8 @@ func (m *mapper) isGroupVersionCached(gv schema.GroupVersion) bool { return false } -// isAPIGroupCached checks if a version for a group is cached in the api groups cache. -func (m *mapper) isAPIGroupCached(gv schema.GroupVersion) bool { +// isAPIGroupCachedLocked checks if a version for a group is cached in the api groups cache. +func (m *mapper) isAPIGroupCachedLocked(gv schema.GroupVersion) bool { cachedGroup, ok := m.apiGroups[gv.Group] if !ok { return false diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/applyconfigurations.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/applyconfigurations.go new file mode 100644 index 000000000000..97192050f9d3 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/applyconfigurations.go @@ -0,0 +1,75 @@ +/* +Copyright 2025 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 ( + "fmt" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" +) + +type unstructuredApplyConfiguration struct { + *unstructured.Unstructured +} + +func (u *unstructuredApplyConfiguration) IsApplyConfiguration() {} + +// ApplyConfigurationFromUnstructured creates a runtime.ApplyConfiguration from an *unstructured.Unstructured object. +// +// Do not use Unstructured objects here that were generated from API objects, as its impossible to tell +// if a zero value was explicitly set. +func ApplyConfigurationFromUnstructured(u *unstructured.Unstructured) runtime.ApplyConfiguration { + return &unstructuredApplyConfiguration{Unstructured: u} +} + +type applyconfigurationRuntimeObject struct { + runtime.ApplyConfiguration +} + +func (a *applyconfigurationRuntimeObject) GetObjectKind() schema.ObjectKind { + return a +} + +func (a *applyconfigurationRuntimeObject) GroupVersionKind() schema.GroupVersionKind { + return schema.GroupVersionKind{} +} + +func (a *applyconfigurationRuntimeObject) SetGroupVersionKind(gvk schema.GroupVersionKind) {} + +func (a *applyconfigurationRuntimeObject) DeepCopyObject() runtime.Object { + panic("applyconfigurationRuntimeObject does not support DeepCopyObject") +} + +func runtimeObjectFromApplyConfiguration(ac runtime.ApplyConfiguration) runtime.Object { + return &applyconfigurationRuntimeObject{ApplyConfiguration: ac} +} + +func gvkFromApplyConfiguration(ac applyConfiguration) (schema.GroupVersionKind, error) { + var gvk schema.GroupVersionKind + gv, err := schema.ParseGroupVersion(ptr.Deref(ac.GetAPIVersion(), "")) + if err != nil { + return gvk, fmt.Errorf("failed to parse %q as GroupVersion: %w", ptr.Deref(ac.GetAPIVersion(), ""), err) + } + gvk.Group = gv.Group + gvk.Version = gv.Version + gvk.Kind = ptr.Deref(ac.GetKind(), "") + + return gvk, nil +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/client.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/client.go index fe9862b814b9..e9f731453b4f 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/client.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/client.go @@ -74,8 +74,8 @@ 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. +// suppress warnings, set config.WarningHandlerWithContext = rest.NoWarnings{}. To +// define custom behavior, implement the rest.WarningHandlerWithContext interface. // See [sigs.k8s.io/controller-runtime/pkg/log.KubeAPIWarningLogger] for // an example. // @@ -112,12 +112,11 @@ func newClient(config *rest.Config, options Options) (*client, error) { config.UserAgent = rest.DefaultKubernetesUserAgent() } - if config.WarningHandler == nil { - // By default, we de-duplicate and surface warnings. - config.WarningHandler = log.NewKubeAPIWarningLogger( - log.Log.WithName("KubeAPIWarningLogger"), + if config.WarningHandler == nil && config.WarningHandlerWithContext == nil { + // By default, we surface warnings. + config.WarningHandlerWithContext = log.NewKubeAPIWarningLogger( log.KubeAPIWarningLoggerOptions{ - Deduplicate: true, + Deduplicate: false, }, ) } @@ -152,8 +151,7 @@ func newClient(config *rest.Config, options Options) (*client, error) { mapper: options.Mapper, codecs: serializer.NewCodecFactory(options.Scheme), - structuredResourceByType: make(map[schema.GroupVersionKind]*resourceMeta), - unstructuredResourceByType: make(map[schema.GroupVersionKind]*resourceMeta), + resourceByType: make(map[cacheKey]*resourceMeta), } rawMetaClient, err := metadata.NewForConfigAndClient(metadata.ConfigFor(config), options.HTTPClient) @@ -330,6 +328,16 @@ func (c *client) Patch(ctx context.Context, obj Object, patch Patch, opts ...Pat } } +func (c *client) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error { + switch obj := obj.(type) { + case *unstructuredApplyConfiguration: + defer c.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind()) + return c.unstructuredClient.Apply(ctx, obj, opts...) + default: + return c.typedClient.Apply(ctx, obj, opts...) + } +} + // Get implements client.Client. func (c *client) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { if isUncached, err := c.shouldBypassCache(obj); err != nil { diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/client_rest_resources.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/client_rest_resources.go index 2d07879520b4..d75d685cbbb7 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/client_rest_resources.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/client_rest_resources.go @@ -17,16 +17,17 @@ limitations under the License. package client import ( + "fmt" "net/http" "strings" "sync" "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/client-go/rest" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" ) @@ -47,22 +48,30 @@ type clientRestResources struct { // codecs are used to create a REST client for a gvk codecs serializer.CodecFactory - // structuredResourceByType stores structured type metadata - structuredResourceByType map[schema.GroupVersionKind]*resourceMeta - // unstructuredResourceByType stores unstructured type metadata - unstructuredResourceByType map[schema.GroupVersionKind]*resourceMeta - mu sync.RWMutex + // resourceByType stores type metadata + resourceByType map[cacheKey]*resourceMeta + + mu sync.RWMutex +} + +type cacheKey struct { + gvk schema.GroupVersionKind + forceDisableProtoBuf bool } // newResource maps obj to a Kubernetes Resource and constructs a client for that Resource. // If the object is a list, the resource represents the item's type instead. -func (c *clientRestResources) newResource(gvk schema.GroupVersionKind, isList, isUnstructured bool) (*resourceMeta, error) { +func (c *clientRestResources) newResource(gvk schema.GroupVersionKind, + isList bool, + forceDisableProtoBuf bool, + isUnstructured bool, +) (*resourceMeta, error) { if strings.HasSuffix(gvk.Kind, "List") && isList { // if this was a list, treat it as a request for the item's resource gvk.Kind = gvk.Kind[:len(gvk.Kind)-4] } - client, err := apiutil.RESTClientForGVK(gvk, isUnstructured, c.config, c.codecs, c.httpClient) + client, err := apiutil.RESTClientForGVK(gvk, forceDisableProtoBuf, isUnstructured, c.config, c.codecs, c.httpClient) if err != nil { return nil, err } @@ -73,52 +82,96 @@ func (c *clientRestResources) newResource(gvk schema.GroupVersionKind, isList, i return &resourceMeta{Interface: client, mapping: mapping, gvk: gvk}, nil } +type applyConfiguration interface { + GetName() *string + GetNamespace() *string + GetKind() *string + GetAPIVersion() *string +} + // getResource returns the resource meta information for the given type of object. // If the object is a list, the resource represents the item's type instead. -func (c *clientRestResources) getResource(obj runtime.Object) (*resourceMeta, error) { - gvk, err := apiutil.GVKForObject(obj, c.scheme) - if err != nil { - return nil, err +func (c *clientRestResources) getResource(obj any) (*resourceMeta, error) { + var gvk schema.GroupVersionKind + var err error + var isApplyConfiguration bool + switch o := obj.(type) { + case runtime.Object: + gvk, err = apiutil.GVKForObject(o, c.scheme) + if err != nil { + return nil, err + } + case runtime.ApplyConfiguration: + ac, ok := o.(applyConfiguration) + if !ok { + return nil, fmt.Errorf("%T is a runtime.ApplyConfiguration but not an applyConfiguration", o) + } + gvk, err = gvkFromApplyConfiguration(ac) + if err != nil { + return nil, err + } + isApplyConfiguration = true + default: + return nil, fmt.Errorf("bug: %T is neither a runtime.Object nor a runtime.ApplyConfiguration", o) } _, isUnstructured := obj.(runtime.Unstructured) + forceDisableProtoBuf := isUnstructured || isApplyConfiguration // It's better to do creation work twice than to not let multiple // people make requests at once c.mu.RLock() - resourceByType := c.structuredResourceByType - if isUnstructured { - resourceByType = c.unstructuredResourceByType - } - r, known := resourceByType[gvk] + + cacheKey := cacheKey{gvk: gvk, forceDisableProtoBuf: forceDisableProtoBuf} + + r, known := c.resourceByType[cacheKey] + c.mu.RUnlock() if known { return r, nil } + var isList bool + if runtimeObject, ok := obj.(runtime.Object); ok && meta.IsListType(runtimeObject) { + isList = true + } + // Initialize a new Client c.mu.Lock() defer c.mu.Unlock() - r, err = c.newResource(gvk, meta.IsListType(obj), isUnstructured) + r, err = c.newResource(gvk, isList, forceDisableProtoBuf, isUnstructured) if err != nil { return nil, err } - resourceByType[gvk] = r + c.resourceByType[cacheKey] = r return r, err } // getObjMeta returns objMeta containing both type and object metadata and state. -func (c *clientRestResources) getObjMeta(obj runtime.Object) (*objMeta, error) { +func (c *clientRestResources) getObjMeta(obj any) (*objMeta, error) { r, err := c.getResource(obj) if err != nil { return nil, err } - m, err := meta.Accessor(obj) - if err != nil { - return nil, err + objMeta := &objMeta{resourceMeta: r} + + switch o := obj.(type) { + case runtime.Object: + m, err := meta.Accessor(obj) + if err != nil { + return nil, err + } + objMeta.namespace = m.GetNamespace() + objMeta.name = m.GetName() + case applyConfiguration: + objMeta.namespace = ptr.Deref(o.GetNamespace(), "") + objMeta.name = ptr.Deref(o.GetName(), "") + default: + return nil, fmt.Errorf("object %T is neither a runtime.Object nor a runtime.ApplyConfiguration", obj) } - return &objMeta{resourceMeta: r, Object: m}, err + + return objMeta, nil } // resourceMeta stores state for a Kubernetes type. @@ -146,6 +199,6 @@ type objMeta struct { // resourceMeta contains type information for the object *resourceMeta - // Object contains meta data for the object instance - metav1.Object + namespace string + name string } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/config/config.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/config/config.go index 5f0a6d4b1dd1..70389dfa90f4 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/config/config.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/config/config.go @@ -61,6 +61,9 @@ func RegisterFlags(fs *flag.FlagSet) { // If --kubeconfig is set, will use the kubeconfig file at that location. Otherwise will assume running // in cluster and use the cluster provided kubeconfig. // +// The returned `*rest.Config` has client-side ratelimting disabled as we can rely on API priority and +// fairness. Set its QPS to a value equal or bigger than 0 to re-enable it. +// // It also applies saner defaults for QPS and burst based on the Kubernetes // controller manager defaults (20 QPS, 30 burst) // @@ -81,6 +84,9 @@ func GetConfig() (*rest.Config, error) { // If --kubeconfig is set, will use the kubeconfig file at that location. Otherwise will assume running // in cluster and use the cluster provided kubeconfig. // +// The returned `*rest.Config` has client-side ratelimting disabled as we can rely on API priority and +// fairness. Set its QPS to a value equal or bigger than 0 to re-enable it. +// // It also applies saner defaults for QPS and burst based on the Kubernetes // controller manager defaults (20 QPS, 30 burst) // @@ -99,10 +105,9 @@ func GetConfigWithContext(context string) (*rest.Config, error) { return nil, err } if cfg.QPS == 0.0 { - cfg.QPS = 20.0 - } - if cfg.Burst == 0 { - cfg.Burst = 30 + // Disable client-side ratelimer by default, we can rely on + // API priority and fairness + cfg.QPS = -1 } return cfg, nil } @@ -170,6 +175,9 @@ func loadConfigWithContext(apiServerURL string, loader clientcmd.ClientConfigLoa // If --kubeconfig is set, will use the kubeconfig file at that location. Otherwise will assume running // in cluster and use the cluster provided kubeconfig. // +// The returned `*rest.Config` has client-side ratelimting disabled as we can rely on API priority and +// fairness. Set its QPS to a value equal or bigger than 0 to re-enable it. +// // Will log an error and exit if there is an error creating the rest.Config. func GetConfigOrDie() *rest.Config { config, err := GetConfig() diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/dryrun.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/dryrun.go index bbcdd3832153..a185860d33da 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/dryrun.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/dryrun.go @@ -82,6 +82,10 @@ func (c *dryRunClient) Patch(ctx context.Context, obj Object, patch Patch, opts return c.client.Patch(ctx, obj, patch, append(opts, DryRunAll)...) } +func (c *dryRunClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error { + return c.client.Apply(ctx, obj, append(opts, DryRunAll)...) +} + // Get implements client.Client. func (c *dryRunClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { return c.client.Get(ctx, key, obj, opts...) diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go index e8818152337c..f88a44edd2c4 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go @@ -19,7 +19,6 @@ package fake import ( "bytes" "context" - "encoding/json" "errors" "fmt" "reflect" @@ -29,9 +28,23 @@ import ( "sync" "time" - // Using v4 to match upstream + /* + Stick with gopkg.in/evanphx/json-patch.v4 here to match + upstream Kubernetes code and avoid breaking changes introduced in v5. + - Kubernetes itself remains on json-patch v4 to avoid compatibility issues + tied to v5’s stricter RFC6902 compliance. + - The fake client code is adapted from client-go’s testing fixture, which also + relies on json-patch v4. + See: + https://github.com/kubernetes/kubernetes/pull/91622 (discussion of why K8s + stays on v4) + https://github.com/kubernetes/kubernetes/pull/120326 (v5.6.0+incompatible + missing a critical fix) + */ + jsonpatch "gopkg.in/evanphx/json-patch.v4" appsv1 "k8s.io/api/apps/v1" + authenticationv1 "k8s.io/api/authentication/v1" autoscalingv1 "k8s.io/api/autoscaling/v1" corev1 "k8s.io/api/core/v1" policyv1 "k8s.io/api/policy/v1" @@ -40,16 +53,21 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/apis/meta/v1/validation" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/json" + "k8s.io/apimachinery/pkg/util/managedfields" utilrand "k8s.io/apimachinery/pkg/util/rand" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/watch" + clientgoapplyconfigurations "k8s.io/client-go/applyconfigurations" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/testing" "k8s.io/utils/ptr" @@ -63,8 +81,9 @@ import ( type versionedTracker struct { testing.ObjectTracker - scheme *runtime.Scheme - withStatusSubresource sets.Set[schema.GroupVersionKind] + scheme *runtime.Scheme + withStatusSubresource sets.Set[schema.GroupVersionKind] + usesFieldManagedObjectTracker bool } type fakeClient struct { @@ -83,6 +102,10 @@ type fakeClient struct { // 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 + // indexesLock must be held when accessing indexes. + indexesLock sync.RWMutex + + returnManagedFields bool } var _ client.WithWatch = &fakeClient{} @@ -116,6 +139,9 @@ type ClientBuilder struct { withStatusSubresource []client.Object objectTracker testing.ObjectTracker interceptorFuncs *interceptor.Funcs + typeConverters []managedfields.TypeConverter + returnManagedFields bool + isBuilt bool // indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK. // The inner map maps from index name to IndexerFunc. @@ -157,6 +183,8 @@ func (f *ClientBuilder) WithRuntimeObjects(initRuntimeObjs ...runtime.Object) *C } // WithObjectTracker can be optionally used to initialize this fake client with testing.ObjectTracker. +// Setting this is incompatible with setting WithTypeConverters, as they are a setting on the +// tracker. func (f *ClientBuilder) WithObjectTracker(ot testing.ObjectTracker) *ClientBuilder { f.objectTracker = ot return f @@ -213,8 +241,36 @@ func (f *ClientBuilder) WithInterceptorFuncs(interceptorFuncs interceptor.Funcs) return f } +// WithTypeConverters sets the type converters for the fake client. The list is ordered and the first +// non-erroring converter is used. A type converter must be provided for all types the client is used +// for, otherwise it will error. +// +// This setting is incompatible with WithObjectTracker, as the type converters are a setting on the tracker. +// +// If unset, this defaults to: +// * clientgoapplyconfigurations.NewTypeConverter(scheme.Scheme), +// * managedfields.NewDeducedTypeConverter(), +// +// Be aware that the behavior of the `NewDeducedTypeConverter` might not match the behavior of the +// Kubernetes APIServer, it is recommended to provide a type converter for your types. TypeConverters +// are generated along with ApplyConfigurations. +func (f *ClientBuilder) WithTypeConverters(typeConverters ...managedfields.TypeConverter) *ClientBuilder { + f.typeConverters = append(f.typeConverters, typeConverters...) + return f +} + +// WithReturnManagedFields configures the fake client to return managedFields +// on objects. +func (f *ClientBuilder) WithReturnManagedFields() *ClientBuilder { + f.returnManagedFields = true + return f +} + // Build builds and returns a new fake client. func (f *ClientBuilder) Build() client.WithWatch { + if f.isBuilt { + panic("Build() must not be called multiple times when creating a ClientBuilder") + } if f.scheme == nil { f.scheme = scheme.Scheme } @@ -222,8 +278,6 @@ func (f *ClientBuilder) Build() client.WithWatch { f.restMapper = meta.NewDefaultRESTMapper([]schema.GroupVersion{}) } - var tracker versionedTracker - withStatusSubResource := sets.New(inTreeResourcesWithStatus()...) for _, o := range f.withStatusSubresource { gvk, err := apiutil.GVKForObject(o, f.scheme) @@ -233,10 +287,36 @@ func (f *ClientBuilder) Build() client.WithWatch { withStatusSubResource.Insert(gvk) } + if f.objectTracker != nil && len(f.typeConverters) > 0 { + panic(errors.New("WithObjectTracker and WithTypeConverters are incompatible")) + } + + var usesFieldManagedObjectTracker bool if f.objectTracker == nil { - tracker = versionedTracker{ObjectTracker: testing.NewObjectTracker(f.scheme, scheme.Codecs.UniversalDecoder()), scheme: f.scheme, withStatusSubresource: withStatusSubResource} - } else { - tracker = versionedTracker{ObjectTracker: f.objectTracker, scheme: f.scheme, withStatusSubresource: withStatusSubResource} + if len(f.typeConverters) == 0 { + // Use corresponding scheme to ensure the converter error + // for types it can't handle. + clientGoScheme := runtime.NewScheme() + if err := scheme.AddToScheme(clientGoScheme); err != nil { + panic(fmt.Sprintf("failed to construct client-go scheme: %v", err)) + } + f.typeConverters = []managedfields.TypeConverter{ + clientgoapplyconfigurations.NewTypeConverter(clientGoScheme), + managedfields.NewDeducedTypeConverter(), + } + } + f.objectTracker = testing.NewFieldManagedObjectTracker( + f.scheme, + serializer.NewCodecFactory(f.scheme).UniversalDecoder(), + multiTypeConverter{upstream: f.typeConverters}, + ) + usesFieldManagedObjectTracker = true + } + tracker := versionedTracker{ + ObjectTracker: f.objectTracker, + scheme: f.scheme, + withStatusSubresource: withStatusSubResource, + usesFieldManagedObjectTracker: usesFieldManagedObjectTracker, } for _, obj := range f.initObject { @@ -261,12 +341,14 @@ func (f *ClientBuilder) Build() client.WithWatch { restMapper: f.restMapper, indexes: f.indexes, withStatusSubresource: withStatusSubResource, + returnManagedFields: f.returnManagedFields, } if f.interceptorFuncs != nil { result = interceptor.NewClient(result, *f.interceptorFuncs) } + f.isBuilt = true return result } @@ -303,6 +385,16 @@ func (t versionedTracker) Add(obj runtime.Object) error { if err != nil { return err } + + // If the fieldManager can not decode fields, it will just silently clear them. This is pretty + // much guaranteed not to be what someone that initializes a fake client with objects that + // have them set wants, so validate them here. + // Ref https://github.com/kubernetes/kubernetes/blob/a956ef4862993b825bcd524a19260192ff1da72d/staging/src/k8s.io/apimachinery/pkg/util/managedfields/internal/fieldmanager.go#L105 + if t.usesFieldManagedObjectTracker { + if err := managedfields.ValidateManagedFields(accessor.GetManagedFields()); err != nil { + return fmt.Errorf("invalid managedFields on %T: %w", obj, err) + } + } if err := t.ObjectTracker.Add(obj); err != nil { return err } @@ -317,8 +409,9 @@ func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Ob return fmt.Errorf("failed to get accessor for object: %w", err) } if accessor.GetName() == "" { + gvk, _ := apiutil.GVKForObject(obj, t.scheme) return apierrors.NewInvalid( - obj.GetObjectKind().GroupVersionKind().GroupKind(), + gvk.GroupKind(), accessor.GetName(), field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")}) } @@ -357,6 +450,9 @@ func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (ru if err != nil { return nil, fmt.Errorf("scheme recognizes %s but failed to produce an object for it: %w", gvk, err) } + if _, isTypedUnstructured := typed.(runtime.Unstructured); isTypedUnstructured { + return o, nil + } unstructuredSerialized, err := json.Marshal(u) if err != nil { @@ -379,7 +475,11 @@ func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Ob } 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) + gvk, err := apiutil.GVKForObject(obj, t.scheme) + if err != nil { + return err + } + obj, err = t.updateObject(gvr, obj, ns, isStatus, deleting, opts.DryRun) if err != nil { return err } @@ -387,6 +487,10 @@ func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Ob return nil } + if u, unstructured := obj.(*unstructured.Unstructured); unstructured { + u.SetGroupVersionKind(gvk) + } + return t.ObjectTracker.Update(gvr, obj, ns, opts) } @@ -396,12 +500,9 @@ func (t versionedTracker) Patch(gvr schema.GroupVersionResource, obj runtime.Obj return err } - isStatus := false // 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 - } + isStatus := bytes.Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).statusPatch")) obj, err = t.updateObject(gvr, obj, ns, isStatus, false, patchOptions.DryRun) if err != nil { @@ -421,8 +522,9 @@ func (t versionedTracker) updateObject(gvr schema.GroupVersionResource, obj runt } if accessor.GetName() == "" { + gvk, _ := apiutil.GVKForObject(obj, t.scheme) return nil, apierrors.NewInvalid( - obj.GetObjectKind().GroupVersionKind().GroupKind(), + gvk.GroupKind(), accessor.GetName(), field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")}) } @@ -509,42 +611,60 @@ func (t versionedTracker) updateObject(gvr schema.GroupVersionResource, obj runt } func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + if err := c.addToSchemeIfUnknownAndUnstructuredOrPartial(obj); err != nil { + return err + } + c.schemeLock.RLock() defer c.schemeLock.RUnlock() gvr, err := getGVRFromObject(obj, c.scheme) if err != nil { return err } + gvk, err := apiutil.GVKForObject(obj, c.scheme) + if err != nil { + return err + } o, err := c.tracker.Get(gvr, key.Namespace, key.Name) if err != nil { return err } - _, isUnstructured := obj.(runtime.Unstructured) - _, isPartialObject := obj.(*metav1.PartialObjectMetadata) - - if isUnstructured || isPartialObject { - gvk, err := apiutil.GVKForObject(obj, c.scheme) - if err != nil { - return err - } - ta, err := meta.TypeAccessor(o) - if err != nil { - return err - } - ta.SetKind(gvk.Kind) - ta.SetAPIVersion(gvk.GroupVersion().String()) + ta, err := meta.TypeAccessor(o) + if err != nil { + return err } + // If the final object is unstructuctured, the json + // representation must contain GVK or the apimachinery + // json serializer will error out. + ta.SetAPIVersion(gvk.GroupVersion().String()) + ta.SetKind(gvk.Kind) + j, err := json.Marshal(o) if err != nil { return err } zero(obj) - return json.Unmarshal(j, obj) + if err := json.Unmarshal(j, obj); err != nil { + return err + } + + if !c.returnManagedFields { + obj.SetManagedFields(nil) + } + + return ensureTypeMeta(obj, gvk) } func (c *fakeClient) Watch(ctx context.Context, list client.ObjectList, opts ...client.ListOption) (watch.Interface, error) { + if err := c.addToSchemeIfUnknownAndUnstructuredOrPartial(list); err != nil { + return nil, err + } + + c.schemeLock.RLock() + defer c.schemeLock.RUnlock() + gvk, err := apiutil.GVKForObject(list, c.scheme) if err != nil { return nil, err @@ -560,6 +680,10 @@ func (c *fakeClient) Watch(ctx context.Context, list client.ObjectList, opts ... } func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...client.ListOption) error { + if err := c.addToSchemeIfUnknownAndUnstructuredOrPartial(obj); err != nil { + return err + } + c.schemeLock.RLock() defer c.schemeLock.RUnlock() gvk, err := apiutil.GVKForObject(obj, c.scheme) @@ -567,11 +691,12 @@ func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...cl return err } - originalKind := gvk.Kind - + originalGVK := gvk gvk.Kind = strings.TrimSuffix(gvk.Kind, "List") + listGVK := gvk + listGVK.Kind += "List" - if _, isUnstructuredList := obj.(runtime.Unstructured); isUnstructuredList && !c.scheme.Recognizes(gvk) { + if _, isUnstructuredList := obj.(runtime.Unstructured); isUnstructuredList && !c.scheme.Recognizes(listGVK) { // We need to register the ListKind with UnstructuredList: // https://github.com/kubernetes/kubernetes/blob/7b2776b89fb1be28d4e9203bdeec079be903c103/staging/src/k8s.io/client-go/dynamic/fake/simple.go#L44-L51 c.schemeLock.RUnlock() @@ -590,35 +715,40 @@ func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...cl return err } - if _, isUnstructured := obj.(runtime.Unstructured); isUnstructured { - ta, err := meta.TypeAccessor(o) - if err != nil { - return err - } - ta.SetKind(originalKind) - ta.SetAPIVersion(gvk.GroupVersion().String()) - } - j, err := json.Marshal(o) if err != nil { return err } zero(obj) - if err := json.Unmarshal(j, obj); err != nil { + if err := ensureTypeMeta(obj, originalGVK); err != nil { return err } - - if listOpts.LabelSelector == nil && listOpts.FieldSelector == nil { - return nil + objCopy := obj.DeepCopyObject().(client.ObjectList) + if err := json.Unmarshal(j, objCopy); err != nil { + return err } - // If we're here, either a label or field selector are specified (or both), so before we return - // the list we must filter it. If both selectors are set, they are ANDed. - objs, err := meta.ExtractList(obj) + objs, err := meta.ExtractList(objCopy) if err != nil { return err } + for _, o := range objs { + if err := ensureTypeMeta(o, gvk); err != nil { + return err + } + + if !c.returnManagedFields { + o.(metav1.Object).SetManagedFields(nil) + } + } + + if listOpts.LabelSelector == nil && listOpts.FieldSelector == nil { + return meta.SetList(obj, objs) + } + + // If we're here, either a label or field selector are specified (or both), so before we return + // the list we must filter it. If both selectors are set, they are ANDed. filteredList, err := c.filterList(objs, gvk, listOpts.LabelSelector, listOpts.FieldSelector) if err != nil { return err @@ -653,10 +783,11 @@ func (c *fakeClient) filterList(list []runtime.Object, gvk schema.GroupVersionKi func (c *fakeClient) filterWithFields(list []runtime.Object, gvk schema.GroupVersionKind, fs fields.Selector) ([]runtime.Object, error) { requiresExact := selector.RequiresExactMatch(fs) if !requiresExact { - return nil, fmt.Errorf("field selector %s is not in one of the two supported forms \"key==val\" or \"key=val\"", - fs) + return nil, fmt.Errorf(`field selector %s is not in one of the two supported forms "key==val" or "key=val"`, fs) } + c.indexesLock.RLock() + defer c.indexesLock.RUnlock() // Field selection is mimicked via indexes, so there's no sane answer this function can give // if there are no indexes registered for the GroupVersionKind of the objects in the list. indexes := c.indexes[gvk] @@ -718,8 +849,13 @@ func (c *fakeClient) IsObjectNamespaced(obj runtime.Object) (bool, error) { } func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { + if err := c.addToSchemeIfUnknownAndUnstructuredOrPartial(obj); err != nil { + return err + } + c.schemeLock.RLock() defer c.schemeLock.RUnlock() + createOptions := &client.CreateOptions{} createOptions.ApplyOptions(opts) @@ -750,14 +886,35 @@ func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...clie accessor.SetDeletionTimestamp(nil) } + gvk, err := apiutil.GVKForObject(obj, c.scheme) + if err != nil { + return err + } + c.trackerWriteLock.Lock() defer c.trackerWriteLock.Unlock() - return c.tracker.Create(gvr, obj, accessor.GetNamespace()) + + if err := c.tracker.Create(gvr, obj, accessor.GetNamespace(), *createOptions.AsCreateOptions()); err != nil { + // The managed fields tracker sets gvk even on errors + _ = ensureTypeMeta(obj, gvk) + return err + } + + if !c.returnManagedFields { + obj.SetManagedFields(nil) + } + + return ensureTypeMeta(obj, gvk) } func (c *fakeClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { + if err := c.addToSchemeIfUnknownAndUnstructuredOrPartial(obj); err != nil { + return err + } + c.schemeLock.RLock() defer c.schemeLock.RUnlock() + gvr, err := getGVRFromObject(obj, c.scheme) if err != nil { return err @@ -803,8 +960,13 @@ func (c *fakeClient) Delete(ctx context.Context, obj client.Object, opts ...clie } func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { + if err := c.addToSchemeIfUnknownAndUnstructuredOrPartial(obj); err != nil { + return err + } + c.schemeLock.RLock() defer c.schemeLock.RUnlock() + gvk, err := apiutil.GVKForObject(obj, c.scheme) if err != nil { return err @@ -854,8 +1016,13 @@ func (c *fakeClient) Update(ctx context.Context, obj client.Object, opts ...clie } func (c *fakeClient) update(obj client.Object, isStatus bool, opts ...client.UpdateOption) error { + if err := c.addToSchemeIfUnknownAndUnstructuredOrPartial(obj); err != nil { + return err + } + c.schemeLock.RLock() defer c.schemeLock.RUnlock() + updateOptions := &client.UpdateOptions{} updateOptions.ApplyOptions(opts) @@ -869,6 +1036,10 @@ func (c *fakeClient) update(obj client.Object, isStatus bool, opts ...client.Upd if err != nil { return err } + gvk, err := apiutil.GVKForObject(obj, c.scheme) + if err != nil { + return err + } accessor, err := meta.Accessor(obj) if err != nil { return err @@ -876,19 +1047,100 @@ func (c *fakeClient) update(obj client.Object, isStatus bool, opts ...client.Upd c.trackerWriteLock.Lock() defer c.trackerWriteLock.Unlock() - return c.tracker.update(gvr, obj, accessor.GetNamespace(), isStatus, false, *updateOptions.AsUpdateOptions()) + + // Retain managed fields + // We can ignore all errors here since update will fail if we encounter an error. + obj.SetManagedFields(nil) + current, _ := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName()) + if currentMetaObj, ok := current.(metav1.Object); ok { + obj.SetManagedFields(currentMetaObj.GetManagedFields()) + } + + if err := c.tracker.update(gvr, obj, accessor.GetNamespace(), isStatus, false, *updateOptions.AsUpdateOptions()); err != nil { + return err + } + + if !c.returnManagedFields { + obj.SetManagedFields(nil) + } + + return ensureTypeMeta(obj, gvk) } func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { return c.patch(obj, patch, opts...) } +func (c *fakeClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.ApplyOption) error { + applyOpts := &client.ApplyOptions{} + applyOpts.ApplyOptions(opts) + + data, err := json.Marshal(obj) + if err != nil { + return fmt.Errorf("failed to marshal apply configuration: %w", err) + } + + u := &unstructured.Unstructured{} + if err := json.Unmarshal(data, u); err != nil { + return fmt.Errorf("failed to unmarshal apply configuration: %w", err) + } + + applyPatch := &fakeApplyPatch{} + + patchOpts := &client.PatchOptions{} + patchOpts.Raw = applyOpts.AsPatchOptions() + + if err := c.patch(u, applyPatch, patchOpts); err != nil { + return err + } + + acJSON, err := json.Marshal(u) + if err != nil { + return fmt.Errorf("failed to marshal patched object: %w", err) + } + + // We have to zero the object in case it contained a status and there is a + // status subresource. If its the private `unstructuredApplyConfiguration` + // we can not zero all of it, as that will cause the embedded Unstructured + // to be nil which then causes a NPD in the json.Unmarshal below. + switch reflect.TypeOf(obj).String() { + case "*client.unstructuredApplyConfiguration": + zero(reflect.ValueOf(obj).Elem().FieldByName("Unstructured").Interface()) + default: + zero(obj) + } + if err := json.Unmarshal(acJSON, obj); err != nil { + return fmt.Errorf("failed to unmarshal patched object: %w", err) + } + + return nil +} + +type fakeApplyPatch struct{} + +func (p *fakeApplyPatch) Type() types.PatchType { + return types.ApplyPatchType +} + +func (p *fakeApplyPatch) Data(obj client.Object) ([]byte, error) { + return json.Marshal(obj) +} + func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client.PatchOption) error { - c.schemeLock.RLock() - defer c.schemeLock.RUnlock() + if err := c.addToSchemeIfUnknownAndUnstructuredOrPartial(obj); err != nil { + return err + } + patchOptions := &client.PatchOptions{} patchOptions.ApplyOptions(opts) + if errs := validation.ValidatePatchOptions(patchOptions.AsPatchOptions(), patch.Type()); len(errs) > 0 { + return apierrors.NewInvalid(schema.GroupKind{Group: "meta.k8s.io", Kind: "PatchOptions"}, "", errs) + } + + c.schemeLock.RLock() + defer c.schemeLock.RUnlock() + for _, dryRunOpt := range patchOptions.DryRun { if dryRunOpt == metav1.DryRunAll { return nil @@ -899,51 +1151,77 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client if err != nil { return err } - accessor, err := meta.Accessor(obj) - if err != nil { - return err - } - data, err := patch.Data(obj) + gvk, err := apiutil.GVKForObject(obj, c.scheme) if err != nil { return err } - - gvk, err := apiutil.GVKForObject(obj, c.scheme) + accessor, err := meta.Accessor(obj) if err != nil { return err } + var isApplyCreate bool c.trackerWriteLock.Lock() defer c.trackerWriteLock.Unlock() oldObj, err := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName()) if err != nil { - return err + if !apierrors.IsNotFound(err) || patch.Type() != types.ApplyPatchType { + return err + } + oldObj = &unstructured.Unstructured{} + isApplyCreate = true } oldAccessor, err := meta.Accessor(oldObj) if err != nil { return err } - // Apply patch without updating object. - // To remain in accordance with the behavior of k8s api behavior, - // a patch must not allow for changes to the deletionTimestamp of an object. - // The reaction() function applies the patch to the object and calls Update(), - // whereas dryPatch() replicates this behavior but skips the call to Update(). - // This ensures that the patch may be rejected if a deletionTimestamp is modified, prior - // to updating the object. - action := testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), data) - o, err := dryPatch(action, c.tracker) - if err != nil { - return err + // SSA deletionTimestamp updates are silently ignored + if patch.Type() == types.ApplyPatchType && !isApplyCreate { + obj.SetDeletionTimestamp(oldAccessor.GetDeletionTimestamp()) } - newObj, err := meta.Accessor(o) + + data, err := patch.Data(obj) if err != nil { return err } - // Validate that deletionTimestamp has not been changed - if !deletionTimestampEqual(newObj, oldAccessor) { - return fmt.Errorf("rejected patch, metadata.deletionTimestamp immutable") + action := testing.NewPatchActionWithOptions( + gvr, + accessor.GetNamespace(), + accessor.GetName(), + patch.Type(), + data, + *patchOptions.AsPatchOptions(), + ) + + // Apply is implemented in the tracker and calling it has side-effects + // such as bumping RV and updating managedFields timestamps, hence we + // can not dry-run it. Luckily, the only validation we use it for + // doesn't apply to SSA - Creating objects with non-nil deletionTimestamp + // through SSA is possible and updating the deletionTimestamp is valid, + // but has no effect. + if patch.Type() != types.ApplyPatchType { + // Apply patch without updating object. + // To remain in accordance with the behavior of k8s api behavior, + // a patch must not allow for changes to the deletionTimestamp of an object. + // The reaction() function applies the patch to the object and calls Update(), + // whereas dryPatch() replicates this behavior but skips the call to Update(). + // This ensures that the patch may be rejected if a deletionTimestamp is modified, prior + // to updating the object. + o, err := dryPatch(action, c.tracker) + if err != nil { + return err + } + newObj, err := meta.Accessor(o) + if err != nil { + return err + } + + // Validate that deletionTimestamp has not been changed + if !deletionTimestampEqual(newObj, oldAccessor) { + return fmt.Errorf("rejected patch, metadata.deletionTimestamp immutable") + } } reaction := testing.ObjectReaction(c.tracker) @@ -955,21 +1233,28 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client panic("tracker could not handle patch method") } - if _, isUnstructured := obj.(runtime.Unstructured); isUnstructured { - ta, err := meta.TypeAccessor(o) - if err != nil { - return err - } - ta.SetKind(gvk.Kind) - ta.SetAPIVersion(gvk.GroupVersion().String()) + ta, err := meta.TypeAccessor(o) + if err != nil { + return err } + ta.SetAPIVersion(gvk.GroupVersion().String()) + ta.SetKind(gvk.Kind) + j, err := json.Marshal(o) if err != nil { return err } zero(obj) - return json.Unmarshal(j, obj) + if err := json.Unmarshal(j, obj); err != nil { + return err + } + + if !c.returnManagedFields { + obj.SetManagedFields(nil) + } + + return ensureTypeMeta(obj, gvk) } // Applying a patch results in a deletionTimestamp that is truncated to the nearest second. @@ -997,6 +1282,9 @@ func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (ru obj, err := tracker.Get(gvr, ns, action.GetName()) if err != nil { + if apierrors.IsNotFound(err) && action.GetPatchType() == types.ApplyPatchType { + return &unstructured.Unstructured{}, nil + } return nil, err } @@ -1041,8 +1329,10 @@ func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (ru if err = json.Unmarshal(mergedByte, obj); err != nil { return nil, err } + case types.ApplyCBORPatchType: + return nil, errors.New("apply CBOR patches are not supported in the fake client") case types.ApplyPatchType: - return nil, errors.New("apply patches are not supported in the fake client. Follow https://github.com/kubernetes/kubernetes/issues/115598 for the current status") + return nil, errors.New("bug in controller-runtime: should not end up in dryPatch for SSA") default: return nil, fmt.Errorf("%s PatchType is not supported", action.GetPatchType()) } @@ -1050,19 +1340,19 @@ func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (ru } // copyStatusFrom copies the status from old into new -func copyStatusFrom(old, new runtime.Object) error { +func copyStatusFrom(old, n runtime.Object) error { oldMapStringAny, err := toMapStringAny(old) if err != nil { return fmt.Errorf("failed to convert old to *unstructured.Unstructured: %w", err) } - newMapStringAny, err := toMapStringAny(new) + newMapStringAny, err := toMapStringAny(n) if err != nil { return fmt.Errorf("failed to convert new to *unststructured.Unstructured: %w", err) } newMapStringAny["status"] = oldMapStringAny["status"] - if err := fromMapStringAny(newMapStringAny, new); err != nil { + if err := fromMapStringAny(newMapStringAny, n); err != nil { return fmt.Errorf("failed to convert back from map[string]any: %w", err) } @@ -1070,12 +1360,12 @@ func copyStatusFrom(old, new runtime.Object) error { } // copyFrom copies from old into new -func copyFrom(old, new runtime.Object) error { +func copyFrom(old, n runtime.Object) error { oldMapStringAny, err := toMapStringAny(old) if err != nil { return fmt.Errorf("failed to convert old to *unstructured.Unstructured: %w", err) } - if err := fromMapStringAny(oldMapStringAny, new); err != nil { + if err := fromMapStringAny(oldMapStringAny, n); err != nil { return fmt.Errorf("failed to convert back from map[string]any: %w", err) } @@ -1138,7 +1428,7 @@ func (c *fakeClient) deleteObjectLocked(gvr schema.GroupVersionResource, accesso } } - //TODO: implement propagation + // TODO: implement propagation return c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName()) } @@ -1166,7 +1456,7 @@ func (sw *fakeSubResourceClient) Get(ctx context.Context, obj, subResource clien } scale, isScale := subResource.(*autoscalingv1.Scale) if !isScale { - return apierrors.NewBadRequest(fmt.Sprintf("expected Scale, got %t", subResource)) + return apierrors.NewBadRequest(fmt.Sprintf("expected Scale, got %T", subResource)) } scaleOut, err := extractScale(obj) if err != nil { @@ -1187,13 +1477,26 @@ func (sw *fakeSubResourceClient) Create(ctx context.Context, obj client.Object, _, isEviction = subResource.(*policyv1.Eviction) } if !isEviction { - return apierrors.NewBadRequest(fmt.Sprintf("got invalid type %t, expected Eviction", subResource)) + return apierrors.NewBadRequest(fmt.Sprintf("got invalid type %T, expected Eviction", subResource)) } if _, isPod := obj.(*corev1.Pod); !isPod { return apierrors.NewNotFound(schema.GroupResource{}, "") } return sw.client.Delete(ctx, obj) + case "token": + tokenRequest, isTokenRequest := subResource.(*authenticationv1.TokenRequest) + if !isTokenRequest { + return apierrors.NewBadRequest(fmt.Sprintf("got invalid type %T, expected TokenRequest", subResource)) + } + if _, isServiceAccount := obj.(*corev1.ServiceAccount); !isServiceAccount { + return apierrors.NewNotFound(schema.GroupResource{}, "") + } + + tokenRequest.Status.Token = "fake-token" + tokenRequest.Status.ExpirationTimestamp = metav1.Date(6041, 1, 1, 0, 0, 0, 0, time.UTC) + + return sw.client.Get(ctx, client.ObjectKeyFromObject(obj), obj) default: return fmt.Errorf("fakeSubResourceWriter does not support create for %s", sw.subResource) } @@ -1214,7 +1517,7 @@ func (sw *fakeSubResourceClient) Update(ctx context.Context, obj client.Object, scale, isScale := updateOptions.SubResourceBody.(*autoscalingv1.Scale) if !isScale { - return apierrors.NewBadRequest(fmt.Sprintf("expected Scale, got %t", updateOptions.SubResourceBody)) + return apierrors.NewBadRequest(fmt.Sprintf("expected Scale, got %T", updateOptions.SubResourceBody)) } if err := applyScale(obj, scale); err != nil { return err @@ -1528,3 +1831,81 @@ func applyScale(obj client.Object, scale *autoscalingv1.Scale) error { } return nil } + +// AddIndex adds an index to a fake client. It will panic if used with a client that is not a fake client. +// It will error if there is already an index for given object with the same name as field. +// +// It can be used to test code that adds indexes to the cache at runtime. +func AddIndex(c client.Client, obj runtime.Object, field string, extractValue client.IndexerFunc) error { + fakeClient, isFakeClient := c.(*fakeClient) + if !isFakeClient { + panic("AddIndex can only be used with a fake client") + } + fakeClient.indexesLock.Lock() + defer fakeClient.indexesLock.Unlock() + + if fakeClient.indexes == nil { + fakeClient.indexes = make(map[schema.GroupVersionKind]map[string]client.IndexerFunc, 1) + } + + gvk, err := apiutil.GVKForObject(obj, fakeClient.scheme) + if err != nil { + return fmt.Errorf("failed to get gvk for %T: %w", obj, err) + } + + if fakeClient.indexes[gvk] == nil { + fakeClient.indexes[gvk] = make(map[string]client.IndexerFunc, 1) + } + + if fakeClient.indexes[gvk][field] != nil { + return fmt.Errorf("index %s already exists", field) + } + + fakeClient.indexes[gvk][field] = extractValue + + return nil +} + +func (c *fakeClient) addToSchemeIfUnknownAndUnstructuredOrPartial(obj runtime.Object) error { + c.schemeLock.Lock() + defer c.schemeLock.Unlock() + + _, isUnstructured := obj.(*unstructured.Unstructured) + _, isUnstructuredList := obj.(*unstructured.UnstructuredList) + _, isPartial := obj.(*metav1.PartialObjectMetadata) + _, isPartialList := obj.(*metav1.PartialObjectMetadataList) + if !isUnstructured && !isUnstructuredList && !isPartial && !isPartialList { + return nil + } + + gvk, err := apiutil.GVKForObject(obj, c.scheme) + if err != nil { + return err + } + + if !c.scheme.Recognizes(gvk) { + c.scheme.AddKnownTypeWithName(gvk, obj) + } + + return nil +} + +func ensureTypeMeta(obj runtime.Object, gvk schema.GroupVersionKind) error { + ta, err := meta.TypeAccessor(obj) + if err != nil { + return err + } + _, isUnstructured := obj.(runtime.Unstructured) + _, isPartialObject := obj.(*metav1.PartialObjectMetadata) + _, isPartialObjectList := obj.(*metav1.PartialObjectMetadataList) + if !isUnstructured && !isPartialObject && !isPartialObjectList { + ta.SetKind("") + ta.SetAPIVersion("") + return nil + } + + ta.SetKind(gvk.Kind) + ta.SetAPIVersion(gvk.GroupVersion().String()) + + return nil +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/doc.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/doc.go index d42347a2e2df..47cad3980d99 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/doc.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/doc.go @@ -20,7 +20,7 @@ Package fake provides a fake client for testing. A fake client is backed by its simple object store indexed by GroupVersionResource. You can create a fake client with optional objects. - client := NewClientBuilder().WithScheme(scheme).WithObj(initObjs...).Build() + client := NewClientBuilder().WithScheme(scheme).WithObjects(initObjs...).Build() You can invoke the methods defined in the Client interface. diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/typeconverter.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/typeconverter.go new file mode 100644 index 000000000000..3cb3a0dc77cf --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/typeconverter.go @@ -0,0 +1,60 @@ +/* +Copyright 2025 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 fake + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + kerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/managedfields" + "sigs.k8s.io/structured-merge-diff/v6/typed" +) + +type multiTypeConverter struct { + upstream []managedfields.TypeConverter +} + +func (m multiTypeConverter) ObjectToTyped(r runtime.Object, o ...typed.ValidationOptions) (*typed.TypedValue, error) { + var errs []error + for _, u := range m.upstream { + res, err := u.ObjectToTyped(r, o...) + if err != nil { + errs = append(errs, err) + continue + } + + return res, nil + } + + return nil, fmt.Errorf("failed to convert Object to TypedValue: %w", kerrors.NewAggregate(errs)) +} + +func (m multiTypeConverter) TypedToObject(v *typed.TypedValue) (runtime.Object, error) { + var errs []error + for _, u := range m.upstream { + res, err := u.TypedToObject(v) + if err != nil { + errs = append(errs, err) + continue + } + + return res, nil + } + + return nil, fmt.Errorf("failed to convert TypedValue to Object: %w", kerrors.NewAggregate(errs)) +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fieldowner.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fieldowner.go index 07183cd192ef..93274f950071 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fieldowner.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fieldowner.go @@ -54,6 +54,10 @@ func (f *clientWithFieldManager) Patch(ctx context.Context, obj Object, patch Pa return f.c.Patch(ctx, obj, patch, append([]PatchOption{FieldOwner(f.owner)}, opts...)...) } +func (f *clientWithFieldManager) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error { + return f.c.Apply(ctx, obj, append([]ApplyOption{FieldOwner(f.owner)}, opts...)...) +} + func (f *clientWithFieldManager) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error { return f.c.Delete(ctx, obj, opts...) } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fieldvalidation.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fieldvalidation.go index 659b3d44c916..ce8d0576c7c4 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fieldvalidation.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fieldvalidation.go @@ -53,6 +53,10 @@ func (c *clientWithFieldValidation) Patch(ctx context.Context, obj Object, patch return c.client.Patch(ctx, obj, patch, append([]PatchOption{c.validation}, opts...)...) } +func (c *clientWithFieldValidation) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error { + return c.client.Apply(ctx, obj, opts...) +} + func (c *clientWithFieldValidation) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error { return c.client.Delete(ctx, obj, opts...) } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/interceptor/intercept.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/interceptor/intercept.go index 3d3f3cb0118d..7ff73bd8daa5 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/interceptor/intercept.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/interceptor/intercept.go @@ -19,6 +19,7 @@ type Funcs struct { DeleteAllOf func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.DeleteAllOfOption) error Update func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.UpdateOption) error Patch func(ctx context.Context, client client.WithWatch, obj client.Object, patch client.Patch, opts ...client.PatchOption) error + Apply func(ctx context.Context, client client.WithWatch, obj runtime.ApplyConfiguration, opts ...client.ApplyOption) error Watch func(ctx context.Context, client client.WithWatch, obj client.ObjectList, opts ...client.ListOption) (watch.Interface, error) SubResource func(client client.WithWatch, subResource string) client.SubResourceClient SubResourceGet func(ctx context.Context, client client.Client, subResourceName string, obj client.Object, subResource client.Object, opts ...client.SubResourceGetOption) error @@ -92,6 +93,14 @@ func (c interceptor) Patch(ctx context.Context, obj client.Object, patch client. return c.client.Patch(ctx, obj, patch, opts...) } +func (c interceptor) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.ApplyOption) error { + if c.funcs.Apply != nil { + return c.funcs.Apply(ctx, c.client, obj, opts...) + } + + return c.client.Apply(ctx, obj, opts...) +} + func (c interceptor) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { if c.funcs.DeleteAllOf != nil { return c.funcs.DeleteAllOf(ctx, c.client, obj, opts...) diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/interfaces.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/interfaces.go index 3cd745e4c052..61559ecbe10b 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/interfaces.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/interfaces.go @@ -61,6 +61,9 @@ type Reader interface { // Writer knows how to create, delete, and update Kubernetes objects. type Writer interface { + // Apply applies the given apply configuration to the Kubernetes cluster. + Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error + // Create saves the object obj in the Kubernetes cluster. obj must be a // struct pointer so that obj can be updated with the content returned by the Server. Create(ctx context.Context, obj Object, opts ...CreateOption) error @@ -94,16 +97,16 @@ type SubResourceClientConstructor interface { // - ServiceAccount token creation: // sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}} // token := &authenticationv1.TokenRequest{} - // c.SubResourceClient("token").Create(ctx, sa, token) + // c.SubResource("token").Create(ctx, sa, token) // // - Pod eviction creation: // pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}} - // c.SubResourceClient("eviction").Create(ctx, pod, &policyv1.Eviction{}) + // c.SubResource("eviction").Create(ctx, pod, &policyv1.Eviction{}) // // - Pod binding creation: // pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}} // binding := &corev1.Binding{Target: corev1.ObjectReference{Name: "my-node"}} - // c.SubResourceClient("binding").Create(ctx, pod, binding) + // c.SubResource("binding").Create(ctx, pod, binding) // // - CertificateSigningRequest approval: // csr := &certificatesv1.CertificateSigningRequest{ @@ -115,17 +118,17 @@ type SubResourceClientConstructor interface { // }}, // }, // } - // c.SubResourceClient("approval").Update(ctx, csr) + // c.SubResource("approval").Update(ctx, csr) // // - Scale retrieval: // dep := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}} // scale := &autoscalingv1.Scale{} - // c.SubResourceClient("scale").Get(ctx, dep, scale) + // c.SubResource("scale").Get(ctx, dep, scale) // // - Scale update: // dep := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}} // scale := &autoscalingv1.Scale{Spec: autoscalingv1.ScaleSpec{Replicas: 2}} - // c.SubResourceClient("scale").Update(ctx, dep, client.WithSubResourceBody(scale)) + // c.SubResource("scale").Update(ctx, dep, client.WithSubResourceBody(scale)) SubResource(subResource string) SubResourceClient } @@ -193,7 +196,7 @@ type IndexerFunc func(Object) []string // FieldIndexer knows how to index over a particular "field" such that it // can later be used by a field selector. type FieldIndexer interface { - // IndexFields adds an index with the given field name on the given object type + // IndexField adds an index with the given field name on the given object type // by using the given function to extract the value for that field. If you want // compatibility with the Kubernetes API server, only return one key, and only use // fields that the API server supports. Otherwise, you can return multiple keys, diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/namespaced_client.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/namespaced_client.go index 222dc795793c..d4223eda26c8 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/namespaced_client.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/namespaced_client.go @@ -19,10 +19,13 @@ package client import ( "context" "fmt" + "reflect" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" ) // NewNamespacedClient wraps an existing client enforcing the namespace value. @@ -147,6 +150,52 @@ func (n *namespacedClient) Patch(ctx context.Context, obj Object, patch Patch, o return n.client.Patch(ctx, obj, patch, opts...) } +func (n *namespacedClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error { + var gvk schema.GroupVersionKind + switch o := obj.(type) { + case applyConfiguration: + var err error + gvk, err = gvkFromApplyConfiguration(o) + if err != nil { + return err + } + case *unstructuredApplyConfiguration: + gvk = o.GroupVersionKind() + default: + return fmt.Errorf("object %T is not a valid apply configuration", obj) + } + isNamespaceScoped, err := apiutil.IsGVKNamespaced(gvk, n.RESTMapper()) + if err != nil { + return fmt.Errorf("error finding the scope of the object: %w", err) + } + if isNamespaceScoped { + switch o := obj.(type) { + case applyConfiguration: + if o.GetNamespace() != nil && *o.GetNamespace() != "" && *o.GetNamespace() != n.namespace { + return fmt.Errorf("namespace %s provided for the object %s does not match the namespace %s on the client", + *o.GetNamespace(), ptr.Deref(o.GetName(), ""), n.namespace) + } + v := reflect.ValueOf(o) + withNamespace := v.MethodByName("WithNamespace") + if !withNamespace.IsValid() { + return fmt.Errorf("ApplyConfiguration %T does not have a WithNamespace method", o) + } + if tp := withNamespace.Type(); tp.NumIn() != 1 || tp.In(0).Kind() != reflect.String { + return fmt.Errorf("WithNamespace method of ApplyConfiguration %T must take a single string argument", o) + } + withNamespace.Call([]reflect.Value{reflect.ValueOf(n.namespace)}) + case *unstructuredApplyConfiguration: + if o.GetNamespace() != "" && o.GetNamespace() != n.namespace { + return fmt.Errorf("namespace %s provided for the object %s does not match the namespace %s on the client", + o.GetNamespace(), o.GetName(), n.namespace) + } + o.SetNamespace(n.namespace) + } + } + + return n.client.Apply(ctx, obj, opts...) +} + // Get implements client.Client. func (n *namespacedClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { isNamespaceScoped, err := n.IsObjectNamespaced(obj) @@ -164,7 +213,12 @@ func (n *namespacedClient) Get(ctx context.Context, key ObjectKey, obj Object, o // List implements client.Client. func (n *namespacedClient) List(ctx context.Context, obj ObjectList, opts ...ListOption) error { - if n.namespace != "" { + isNamespaceScoped, err := n.IsObjectNamespaced(obj) + if err != nil { + return fmt.Errorf("error finding the scope of the object: %w", err) + } + + if isNamespaceScoped && n.namespace != "" { opts = append(opts, InNamespace(n.namespace)) } return n.client.List(ctx, obj, opts...) diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/options.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/options.go index db50ed8febe5..33c460738cfe 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/options.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/options.go @@ -21,6 +21,7 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/selection" + "k8s.io/utils/ptr" ) // {{{ "Functional" Option Interfaces @@ -61,6 +62,12 @@ type PatchOption interface { ApplyToPatch(*PatchOptions) } +// ApplyOption is some configuration that modifies options for an apply request. +type ApplyOption interface { + // ApplyToApply applies this configuration to the given apply options. + ApplyToApply(*ApplyOptions) +} + // DeleteAllOfOption is some configuration that modifies options for a delete request. type DeleteAllOfOption interface { // ApplyToDeleteAllOf applies this configuration to the given deletecollection options. @@ -115,7 +122,12 @@ func (dryRunAll) ApplyToPatch(opts *PatchOptions) { opts.DryRun = []string{metav1.DryRunAll} } -// ApplyToPatch applies this configuration to the given delete options. +// ApplyToApply applies this configuration to the given apply options. +func (dryRunAll) ApplyToApply(opts *ApplyOptions) { + opts.DryRun = []string{metav1.DryRunAll} +} + +// ApplyToDelete applies this configuration to the given delete options. func (dryRunAll) ApplyToDelete(opts *DeleteOptions) { opts.DryRun = []string{metav1.DryRunAll} } @@ -154,6 +166,11 @@ func (f FieldOwner) ApplyToUpdate(opts *UpdateOptions) { opts.FieldManager = string(f) } +// ApplyToApply applies this configuration to the given apply options. +func (f FieldOwner) ApplyToApply(opts *ApplyOptions) { + opts.FieldManager = string(f) +} + // ApplyToSubResourcePatch applies this configuration to the given patch options. func (f FieldOwner) ApplyToSubResourcePatch(opts *SubResourcePatchOptions) { opts.FieldManager = string(f) @@ -431,6 +448,12 @@ type GetOptions struct { // Raw represents raw GetOptions, as passed to the API server. Note // that these may not be respected by all implementations of interface. Raw *metav1.GetOptions + + // UnsafeDisableDeepCopy indicates not to deep copy objects during get object. + // Be very careful with this, when enabled you must DeepCopy any object before mutating it, + // otherwise you will mutate the object in the cache. + // +optional + UnsafeDisableDeepCopy *bool } var _ GetOption = &GetOptions{} @@ -440,6 +463,9 @@ func (o *GetOptions) ApplyToGet(lo *GetOptions) { if o.Raw != nil { lo.Raw = o.Raw } + if o.UnsafeDisableDeepCopy != nil { + lo.UnsafeDisableDeepCopy = o.UnsafeDisableDeepCopy + } } // AsGetOptions returns these options as a flattened metav1.GetOptions. @@ -618,6 +644,9 @@ type MatchingLabelsSelector struct { // ApplyToList applies this configuration to the given list options. func (m MatchingLabelsSelector) ApplyToList(opts *ListOptions) { + if m.Selector == nil { + m.Selector = labels.Nothing() + } opts.LabelSelector = m } @@ -651,6 +680,9 @@ type MatchingFieldsSelector struct { // ApplyToList applies this configuration to the given list options. func (m MatchingFieldsSelector) ApplyToList(opts *ListOptions) { + if m.Selector == nil { + m.Selector = fields.Nothing() + } opts.FieldSelector = m } @@ -692,15 +724,14 @@ func (l Limit) ApplyToList(opts *ListOptions) { // otherwise you will mutate the object in the cache. type UnsafeDisableDeepCopyOption bool +// ApplyToGet applies this configuration to the given an Get options. +func (d UnsafeDisableDeepCopyOption) ApplyToGet(opts *GetOptions) { + opts.UnsafeDisableDeepCopy = ptr.To(bool(d)) +} + // ApplyToList applies this configuration to the given an List options. func (d UnsafeDisableDeepCopyOption) ApplyToList(opts *ListOptions) { - definitelyTrue := true - definitelyFalse := false - if d { - opts.UnsafeDisableDeepCopy = &definitelyTrue - } else { - opts.UnsafeDisableDeepCopy = &definitelyFalse - } + opts.UnsafeDisableDeepCopy = ptr.To(bool(d)) } // UnsafeDisableDeepCopy indicates not to deep copy objects during list objects. @@ -863,10 +894,18 @@ func (o *PatchOptions) AsPatchOptions() *metav1.PatchOptions { o.Raw = &metav1.PatchOptions{} } - o.Raw.DryRun = o.DryRun - o.Raw.Force = o.Force - o.Raw.FieldManager = o.FieldManager - o.Raw.FieldValidation = o.FieldValidation + if o.DryRun != nil { + o.Raw.DryRun = o.DryRun + } + if o.Force != nil { + o.Raw.Force = o.Force + } + if o.FieldManager != "" { + o.Raw.FieldManager = o.FieldManager + } + if o.FieldValidation != "" { + o.Raw.FieldValidation = o.FieldValidation + } return o.Raw } @@ -899,13 +938,15 @@ var ForceOwnership = forceOwnership{} type forceOwnership struct{} func (forceOwnership) ApplyToPatch(opts *PatchOptions) { - definitelyTrue := true - opts.Force = &definitelyTrue + opts.Force = ptr.To(true) } func (forceOwnership) ApplyToSubResourcePatch(opts *SubResourcePatchOptions) { - definitelyTrue := true - opts.Force = &definitelyTrue + opts.Force = ptr.To(true) +} + +func (forceOwnership) ApplyToApply(opts *ApplyOptions) { + opts.Force = ptr.To(true) } // }}} @@ -939,3 +980,57 @@ func (o *DeleteAllOfOptions) ApplyToDeleteAllOf(do *DeleteAllOfOptions) { } // }}} + +// ApplyOptions are the options for an apply request. +type ApplyOptions struct { + // When present, indicates that modifications should not be + // persisted. An invalid or unrecognized dryRun directive will + // result in an error response and no further processing of the + // request. Valid values are: + // - All: all dry run stages will be processed + DryRun []string + + // Force is going to "force" Apply requests. It means user will + // re-acquire conflicting fields owned by other people. + Force *bool + + // fieldManager is a name associated with the actor or entity + // that is making these changes. The value must be less than or + // 128 characters long, and only contain printable characters, + // as defined by https://golang.org/pkg/unicode/#IsPrint. This + // field is required. + // + // +required + FieldManager string +} + +// ApplyOptions applies the given opts onto the ApplyOptions +func (o *ApplyOptions) ApplyOptions(opts []ApplyOption) *ApplyOptions { + for _, opt := range opts { + opt.ApplyToApply(o) + } + return o +} + +// ApplyToApply applies the given opts onto the ApplyOptions +func (o *ApplyOptions) ApplyToApply(opts *ApplyOptions) { + if o.DryRun != nil { + opts.DryRun = o.DryRun + } + if o.Force != nil { + opts.Force = o.Force + } + + if o.FieldManager != "" { + opts.FieldManager = o.FieldManager + } +} + +// AsPatchOptions constructs patch options from the given ApplyOptions +func (o *ApplyOptions) AsPatchOptions() *metav1.PatchOptions { + return &metav1.PatchOptions{ + DryRun: o.DryRun, + Force: o.Force, + FieldManager: o.FieldManager, + } +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/patch.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/patch.go index 11d608388562..b99d7663bdff 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/patch.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/patch.go @@ -27,6 +27,11 @@ import ( var ( // Apply uses server-side apply to patch the given object. + // + // This should now only be used to patch sub resources, e.g. with client.Client.Status().Patch(). + // Use client.Client.Apply() instead of client.Client.Patch(..., client.Apply, ...) + // This will be deprecated once the Apply method has been added for sub resources. + // See the following issue for more details: https://github.com/kubernetes-sigs/controller-runtime/issues/3183 Apply Patch = applyPatch{} // Merge uses the raw object as a merge patch, without modifications. diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/typed_client.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/typed_client.go index 92afd9a9c258..3bd762a6382c 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/typed_client.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/typed_client.go @@ -18,8 +18,10 @@ package client import ( "context" + "fmt" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/util/apply" ) var _ Reader = &typedClient{} @@ -41,7 +43,7 @@ func (c *typedClient) Create(ctx context.Context, obj Object, opts ...CreateOpti createOpts.ApplyOptions(opts) return o.Post(). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). Body(obj). VersionedParams(createOpts.AsCreateOptions(), c.paramCodec). @@ -60,9 +62,9 @@ func (c *typedClient) Update(ctx context.Context, obj Object, opts ...UpdateOpti updateOpts.ApplyOptions(opts) return o.Put(). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). - Name(o.GetName()). + Name(o.name). Body(obj). VersionedParams(updateOpts.AsUpdateOptions(), c.paramCodec). Do(ctx). @@ -80,9 +82,9 @@ func (c *typedClient) Delete(ctx context.Context, obj Object, opts ...DeleteOpti deleteOpts.ApplyOptions(opts) return o.Delete(). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). - Name(o.GetName()). + Name(o.name). Body(deleteOpts.AsDeleteOptions()). Do(ctx). Error() @@ -123,15 +125,40 @@ func (c *typedClient) Patch(ctx context.Context, obj Object, patch Patch, opts . patchOpts.ApplyOptions(opts) return o.Patch(patch.Type()). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). - Name(o.GetName()). + Name(o.name). VersionedParams(patchOpts.AsPatchOptions(), c.paramCodec). Body(data). Do(ctx). Into(obj) } +func (c *typedClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error { + o, err := c.resources.getObjMeta(obj) + if err != nil { + return err + } + req, err := apply.NewRequest(o, obj) + if err != nil { + return fmt.Errorf("failed to create apply request: %w", err) + } + applyOpts := &ApplyOptions{} + applyOpts.ApplyOptions(opts) + + return req. + NamespaceIfScoped(o.namespace, o.isNamespaced()). + Resource(o.resource()). + Name(o.name). + VersionedParams(applyOpts.AsPatchOptions(), c.paramCodec). + Do(ctx). + // This is hacky, it is required because `Into` takes a `runtime.Object` and + // that is not implemented by the ApplyConfigurations. The generated clients + // don't have this problem because they deserialize into the api type, not the + // apply configuration: https://github.com/kubernetes/kubernetes/blob/22f5e01a37c0bc6a5f494dec14dd4e3688ee1d55/staging/src/k8s.io/client-go/gentype/type.go#L296-L317 + Into(runtimeObjectFromApplyConfiguration(obj)) +} + // Get implements client.Client. func (c *typedClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { r, err := c.resources.getResource(obj) @@ -179,9 +206,9 @@ func (c *typedClient) GetSubResource(ctx context.Context, obj, subResourceObj Ob getOpts.ApplyOptions(opts) return o.Get(). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). - Name(o.GetName()). + Name(o.name). SubResource(subResource). VersionedParams(getOpts.AsGetOptions(), c.paramCodec). Do(ctx). @@ -202,9 +229,9 @@ func (c *typedClient) CreateSubResource(ctx context.Context, obj Object, subReso createOpts.ApplyOptions(opts) return o.Post(). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). - Name(o.GetName()). + Name(o.name). SubResource(subResource). Body(subResourceObj). VersionedParams(createOpts.AsCreateOptions(), c.paramCodec). @@ -237,9 +264,9 @@ func (c *typedClient) UpdateSubResource(ctx context.Context, obj Object, subReso } return o.Put(). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). - Name(o.GetName()). + Name(o.name). SubResource(subResource). Body(body). VersionedParams(updateOpts.AsUpdateOptions(), c.paramCodec). @@ -268,9 +295,9 @@ func (c *typedClient) PatchSubResource(ctx context.Context, obj Object, subResou } return o.Patch(patch.Type()). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). - Name(o.GetName()). + Name(o.name). SubResource(subResource). Body(data). VersionedParams(patchOpts.AsPatchOptions(), c.paramCodec). diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/unstructured_client.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/unstructured_client.go index 0d96951780ee..e636c3beef25 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/unstructured_client.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/unstructured_client.go @@ -22,6 +22,7 @@ import ( "strings" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/util/apply" ) var _ Reader = &unstructuredClient{} @@ -50,7 +51,7 @@ func (uc *unstructuredClient) Create(ctx context.Context, obj Object, opts ...Cr createOpts.ApplyOptions(opts) result := o.Post(). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). Body(obj). VersionedParams(createOpts.AsCreateOptions(), uc.paramCodec). @@ -79,9 +80,9 @@ func (uc *unstructuredClient) Update(ctx context.Context, obj Object, opts ...Up updateOpts.ApplyOptions(opts) result := o.Put(). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). - Name(o.GetName()). + Name(o.name). Body(obj). VersionedParams(updateOpts.AsUpdateOptions(), uc.paramCodec). Do(ctx). @@ -106,9 +107,9 @@ func (uc *unstructuredClient) Delete(ctx context.Context, obj Object, opts ...De deleteOpts.ApplyOptions(opts) return o.Delete(). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). - Name(o.GetName()). + Name(o.name). Body(deleteOpts.AsDeleteOptions()). Do(ctx). Error() @@ -157,15 +158,41 @@ func (uc *unstructuredClient) Patch(ctx context.Context, obj Object, patch Patch patchOpts.ApplyOptions(opts) return o.Patch(patch.Type()). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). - Name(o.GetName()). + Name(o.name). VersionedParams(patchOpts.AsPatchOptions(), uc.paramCodec). Body(data). Do(ctx). Into(obj) } +func (uc *unstructuredClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error { + unstructuredApplyConfig, ok := obj.(*unstructuredApplyConfiguration) + if !ok { + return fmt.Errorf("bug: unstructured client got an applyconfiguration that was not %T but %T", &unstructuredApplyConfiguration{}, obj) + } + o, err := uc.resources.getObjMeta(unstructuredApplyConfig.Unstructured) + if err != nil { + return err + } + + req, err := apply.NewRequest(o, obj) + if err != nil { + return fmt.Errorf("failed to create apply request: %w", err) + } + applyOpts := &ApplyOptions{} + applyOpts.ApplyOptions(opts) + + return req. + NamespaceIfScoped(o.namespace, o.isNamespaced()). + Resource(o.resource()). + Name(o.name). + VersionedParams(applyOpts.AsPatchOptions(), uc.paramCodec). + Do(ctx). + Into(unstructuredApplyConfig.Unstructured) +} + // Get implements client.Client. func (uc *unstructuredClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { u, ok := obj.(runtime.Unstructured) @@ -244,9 +271,9 @@ func (uc *unstructuredClient) GetSubResource(ctx context.Context, obj, subResour getOpts.ApplyOptions(opts) return o.Get(). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). - Name(o.GetName()). + Name(o.name). SubResource(subResource). VersionedParams(getOpts.AsGetOptions(), uc.paramCodec). Do(ctx). @@ -275,9 +302,9 @@ func (uc *unstructuredClient) CreateSubResource(ctx context.Context, obj, subRes createOpts.ApplyOptions(opts) return o.Post(). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). - Name(o.GetName()). + Name(o.name). SubResource(subResource). Body(subResourceObj). VersionedParams(createOpts.AsCreateOptions(), uc.paramCodec). @@ -310,9 +337,9 @@ func (uc *unstructuredClient) UpdateSubResource(ctx context.Context, obj Object, } return o.Put(). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). - Name(o.GetName()). + Name(o.name). SubResource(subResource). Body(body). VersionedParams(updateOpts.AsUpdateOptions(), uc.paramCodec). @@ -347,9 +374,9 @@ func (uc *unstructuredClient) PatchSubResource(ctx context.Context, obj Object, } result := o.Patch(patch.Type()). - NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). - Name(o.GetName()). + Name(o.name). SubResource(subResource). Body(data). VersionedParams(patchOpts.AsPatchOptions(), uc.paramCodec). diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/cluster/cluster.go b/vendor/sigs.k8s.io/controller-runtime/pkg/cluster/cluster.go index 248893ea31b0..0603f4cde564 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/cluster/cluster.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/cluster/cluster.go @@ -20,7 +20,6 @@ import ( "context" "errors" "net/http" - "time" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/meta" @@ -65,8 +64,8 @@ type Cluster interface { // GetRESTMapper returns a RESTMapper GetRESTMapper() meta.RESTMapper - // GetAPIReader returns a reader that will be configured to use the API server. - // This should be used sparingly and only when the client does not fit your + // GetAPIReader returns a reader that will be configured to use the API server directly. + // This should be used sparingly and only when the cached client does not fit your // use case. GetAPIReader() client.Reader @@ -88,16 +87,6 @@ type Options struct { // If none is set, it defaults to log.Log global logger. Logger logr.Logger - // 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. - // - // Deprecated: Use Cache.SyncPeriod instead. - SyncPeriod *time.Duration - // HTTPClient is the http client that will be used to create the default // Cache and Client. If not set the rest.HTTPClientFor function will be used // to create the http client. @@ -194,9 +183,6 @@ func New(config *rest.Config, opts ...Option) (Cluster, error) { if cacheOpts.HTTPClient == nil { cacheOpts.HTTPClient = options.HTTPClient } - if cacheOpts.SyncPeriod == nil { - cacheOpts.SyncPeriod = options.SyncPeriod - } } cache, err := options.NewCache(config, cacheOpts) if err != nil { diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/config/controller.go b/vendor/sigs.k8s.io/controller-runtime/pkg/config/controller.go index 999ef07e214e..3dafaef93b6a 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/config/controller.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/config/controller.go @@ -16,9 +16,15 @@ limitations under the License. package config -import "time" +import ( + "time" -// Controller contains configuration options for a controller. + "github.com/go-logr/logr" +) + +// Controller contains configuration options for controllers. It only includes options +// that makes sense for a set of controllers and is used for defaulting the options +// of multiple controllers. 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. @@ -53,4 +59,34 @@ type Controller struct { // NeedLeaderElection indicates whether the controller needs to use leader election. // Defaults to true, which means the controller will use leader election. NeedLeaderElection *bool + + // EnableWarmup specifies whether the controller should start its sources when the manager is not + // the leader. This is useful for cases where sources take a long time to start, as it allows + // for the controller to warm up its caches even before it is elected as the leader. This + // improves leadership failover time, as the caches will be prepopulated before the controller + // transitions to be leader. + // + // Setting EnableWarmup to true and NeedLeaderElection to true means the controller will start its + // sources without waiting to become leader. + // Setting EnableWarmup to true and NeedLeaderElection to false is a no-op as controllers without + // leader election do not wait on leader election to start their sources. + // Defaults to false. + // + // Note: This feature is currently in beta and subject to change. + // For more details, see: https://github.com/kubernetes-sigs/controller-runtime/issues/3220. + EnableWarmup *bool + + // UsePriorityQueue configures the controllers queue to use the controller-runtime provided + // priority queue. + // + // Note: This flag is disabled by default until a future version. This feature is currently in beta. + // For more details, see: https://github.com/kubernetes-sigs/controller-runtime/issues/2374. + UsePriorityQueue *bool + + // Logger is the logger controllers should use. + Logger logr.Logger + + // ReconciliationTimeout is used as the timeout passed to the context of each Reconcile call. + // By default, there is no timeout. + ReconciliationTimeout time.Duration } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/controller/controller.go b/vendor/sigs.k8s.io/controller-runtime/pkg/controller/controller.go index f2496236db54..afa15aebec1b 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/controller/controller.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/controller/controller.go @@ -24,7 +24,10 @@ import ( "github.com/go-logr/logr" "k8s.io/client-go/util/workqueue" "k8s.io/klog/v2" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/config" + "sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue" "sigs.k8s.io/controller-runtime/pkg/internal/controller" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -78,13 +81,82 @@ type TypedOptions[request comparable] struct { // Only use a custom NewQueue if you know what you are doing. NewQueue func(controllerName string, rateLimiter workqueue.TypedRateLimiter[request]) workqueue.TypedRateLimitingInterface[request] + // Logger will be used to build a default LogConstructor if unset. + Logger logr.Logger + // LogConstructor is used to construct a logger used for this controller and passed // to each reconciliation via the context field. LogConstructor func(request *request) logr.Logger + + // UsePriorityQueue configures the controllers queue to use the controller-runtime provided + // priority queue. + // + // Note: This flag is disabled by default until a future version. This feature is currently in beta. + // For more details, see: https://github.com/kubernetes-sigs/controller-runtime/issues/2374. + UsePriorityQueue *bool + + // EnableWarmup specifies whether the controller should start its sources when the manager is not + // the leader. This is useful for cases where sources take a long time to start, as it allows + // for the controller to warm up its caches even before it is elected as the leader. This + // improves leadership failover time, as the caches will be prepopulated before the controller + // transitions to be leader. + // + // Setting EnableWarmup to true and NeedLeaderElection to true means the controller will start its + // sources without waiting to become leader. + // Setting EnableWarmup to true and NeedLeaderElection to false is a no-op as controllers without + // leader election do not wait on leader election to start their sources. + // Defaults to false. + // + // Note: This feature is currently in beta and subject to change. + // For more details, see: https://github.com/kubernetes-sigs/controller-runtime/issues/3220. + EnableWarmup *bool + + // ReconciliationTimeout is used as the timeout passed to the context of each Reconcile call. + // By default, there is no timeout. + ReconciliationTimeout time.Duration +} + +// DefaultFromConfig defaults the config from a config.Controller +func (options *TypedOptions[request]) DefaultFromConfig(config config.Controller) { + if options.Logger.GetSink() == nil { + options.Logger = config.Logger + } + + if options.SkipNameValidation == nil { + options.SkipNameValidation = config.SkipNameValidation + } + + if options.MaxConcurrentReconciles <= 0 && config.MaxConcurrentReconciles > 0 { + options.MaxConcurrentReconciles = config.MaxConcurrentReconciles + } + + if options.CacheSyncTimeout == 0 && config.CacheSyncTimeout > 0 { + options.CacheSyncTimeout = config.CacheSyncTimeout + } + + if options.UsePriorityQueue == nil { + options.UsePriorityQueue = config.UsePriorityQueue + } + + if options.RecoverPanic == nil { + options.RecoverPanic = config.RecoverPanic + } + + if options.NeedLeaderElection == nil { + options.NeedLeaderElection = config.NeedLeaderElection + } + + if options.EnableWarmup == nil { + options.EnableWarmup = config.EnableWarmup + } + + if options.ReconciliationTimeout == 0 { + options.ReconciliationTimeout = config.ReconciliationTimeout + } } -// 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. +// Controller implements an 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 = TypedController[reconcile.Request] @@ -117,7 +189,8 @@ func New(name string, mgr manager.Manager, options Options) (Controller, error) // // 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) + options.DefaultFromConfig(mgr.GetControllerOptions()) + c, err := NewTypedUnmanaged(name, options) if err != nil { return nil, err } @@ -130,14 +203,14 @@ func NewTyped[request comparable](name string, mgr manager.Manager, options Type // 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) +func NewUnmanaged(name string, options Options) (Controller, error) { + return NewTypedUnmanaged(name, 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) { +func NewTypedUnmanaged[request comparable](name string, options TypedOptions[request]) (TypedController[request], error) { if options.Reconciler == nil { return nil, fmt.Errorf("must specify Reconciler") } @@ -146,10 +219,6 @@ func NewTypedUnmanaged[request comparable](name string, mgr manager.Manager, opt 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 @@ -157,7 +226,7 @@ func NewTypedUnmanaged[request comparable](name string, mgr manager.Manager, opt } if options.LogConstructor == nil { - log := mgr.GetLogger().WithValues( + log := options.Logger.WithValues( "controller", name, ) options.LogConstructor = func(in *request) logr.Logger { @@ -173,43 +242,37 @@ func NewTypedUnmanaged[request comparable](name string, mgr manager.Manager, opt } if options.MaxConcurrentReconciles <= 0 { - if mgr.GetControllerOptions().MaxConcurrentReconciles > 0 { - options.MaxConcurrentReconciles = mgr.GetControllerOptions().MaxConcurrentReconciles - } else { - options.MaxConcurrentReconciles = 1 - } + options.MaxConcurrentReconciles = 1 } if options.CacheSyncTimeout == 0 { - if mgr.GetControllerOptions().CacheSyncTimeout != 0 { - options.CacheSyncTimeout = mgr.GetControllerOptions().CacheSyncTimeout - } else { - options.CacheSyncTimeout = 2 * time.Minute - } + options.CacheSyncTimeout = 2 * time.Minute } if options.RateLimiter == nil { - options.RateLimiter = workqueue.DefaultTypedControllerRateLimiter[request]() + if ptr.Deref(options.UsePriorityQueue, false) { + options.RateLimiter = workqueue.NewTypedItemExponentialFailureRateLimiter[request](5*time.Millisecond, 1000*time.Second) + } else { + options.RateLimiter = workqueue.DefaultTypedControllerRateLimiter[request]() + } } if options.NewQueue == nil { options.NewQueue = func(controllerName string, rateLimiter workqueue.TypedRateLimiter[request]) workqueue.TypedRateLimitingInterface[request] { + if ptr.Deref(options.UsePriorityQueue, false) { + return priorityqueue.New(controllerName, func(o *priorityqueue.Opts[request]) { + o.Log = options.Logger.WithValues("controller", controllerName) + o.RateLimiter = rateLimiter + }) + } return workqueue.NewTypedRateLimitingQueueWithConfig(rateLimiter, workqueue.TypedRateLimitingQueueConfig[request]{ Name: controllerName, }) } } - if options.RecoverPanic == nil { - options.RecoverPanic = mgr.GetControllerOptions().RecoverPanic - } - - if options.NeedLeaderElection == nil { - options.NeedLeaderElection = mgr.GetControllerOptions().NeedLeaderElection - } - // Create controller with dependencies set - return &controller.Controller[request]{ + return controller.New[request](controller.Options[request]{ Do: options.Reconciler, RateLimiter: options.RateLimiter, NewQueue: options.NewQueue, @@ -219,7 +282,9 @@ func NewTypedUnmanaged[request comparable](name string, mgr manager.Manager, opt LogConstructor: options.LogConstructor, RecoverPanic: options.RecoverPanic, LeaderElected: options.NeedLeaderElection, - }, nil + EnableWarmup: options.EnableWarmup, + ReconciliationTimeout: options.ReconciliationTimeout, + }), nil } // ReconcileIDFromContext gets the reconcileID from the current context. diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/controller/controllerutil/controllerutil.go b/vendor/sigs.k8s.io/controller-runtime/pkg/controller/controllerutil/controllerutil.go index 176ce0db0f14..0088f88e5d57 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/controller/controllerutil/controllerutil.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/controller/controllerutil/controllerutil.go @@ -181,6 +181,21 @@ func HasControllerReference(object metav1.Object) bool { return false } +// HasOwnerReference returns true if the owners list contains an owner reference +// that matches the object's group, kind, and name. +func HasOwnerReference(ownerRefs []metav1.OwnerReference, obj client.Object, scheme *runtime.Scheme) (bool, error) { + gvk, err := apiutil.GVKForObject(obj, scheme) + if err != nil { + return false, err + } + idx := indexOwnerRef(ownerRefs, metav1.OwnerReference{ + APIVersion: gvk.GroupVersion().String(), + Name: obj.GetName(), + Kind: gvk.Kind, + }) + return idx != -1, nil +} + // RemoveControllerReference removes an owner reference where the controller // equals true func RemoveControllerReference(owner, object metav1.Object, scheme *runtime.Scheme) error { @@ -263,7 +278,7 @@ func referSameObject(a, b metav1.OwnerReference) bool { return aGV.Group == bGV.Group && a.Kind == b.Kind && a.Name == b.Name } -// OperationResult is the action result of a CreateOrUpdate call. +// OperationResult is the action result of a CreateOrUpdate or CreateOrPatch call. type OperationResult string const ( // They should complete the sentence "Deployment default/foo has been ..." @@ -279,13 +294,26 @@ const ( // They should complete the sentence "Deployment default/foo has been .. OperationResultUpdatedStatusOnly OperationResult = "updatedStatusOnly" ) -// CreateOrUpdate creates or updates the given object in the Kubernetes -// cluster. The object's desired state must be reconciled with the existing -// state inside the passed in callback MutateFn. +// CreateOrUpdate attempts to fetch the given object from the Kubernetes cluster. +// If the object didn't exist, MutateFn will be called, and it will be created. +// If the object did exist, MutateFn will be called, and if it changed the +// object, it will be updated. +// Otherwise, it will be left unchanged. +// The executed operation (and an error) will be returned. // -// The MutateFn is called regardless of creating or updating an object. +// WARNING: If the MutateFn resets a value on obj that has a default value, +// CreateOrUpdate will *always* perform an update. This is because when the +// object is fetched from the API server, the value will have taken on the +// default value, and the check for equality will fail. For example, Deployments +// must have a Replicas value set. If the MutateFn sets a Deployment's Replicas +// to nil, then it will never match with the object returned from the API +// server, which defaults the value to 1. // -// It returns the executed operation and an error. +// WARNING: CreateOrUpdate assumes that no values have been set on obj aside +// from the Name/Namespace. Values other than Name and Namespace that existed on +// obj may be overwritten by the corresponding values in the object returned +// from the Kubernetes API server. When this happens, the Update will not work +// as expected. // // Note: changes made by MutateFn to any sub-resource (status...), will be // discarded. @@ -295,9 +323,12 @@ func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object, f M if !apierrors.IsNotFound(err) { return OperationResultNone, err } - if err := mutate(f, key, obj); err != nil { - return OperationResultNone, err + if f != nil { + if err := mutate(f, key, obj); err != nil { + return OperationResultNone, err + } } + if err := c.Create(ctx, obj); err != nil { return OperationResultNone, err } @@ -305,8 +336,10 @@ func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object, f M } existing := obj.DeepCopyObject() - if err := mutate(f, key, obj); err != nil { - return OperationResultNone, err + if f != nil { + if err := mutate(f, key, obj); err != nil { + return OperationResultNone, err + } } if equality.Semantic.DeepEqual(existing, obj) { @@ -319,13 +352,26 @@ func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object, f M return OperationResultUpdated, nil } -// CreateOrPatch creates or patches the given object in the Kubernetes -// cluster. The object's desired state must be reconciled with the before -// state inside the passed in callback MutateFn. +// CreateOrPatch attempts to fetch the given object from the Kubernetes cluster. +// If the object didn't exist, MutateFn will be called, and it will be created. +// If the object did exist, MutateFn will be called, and if it changed the +// object, it will be patched. +// Otherwise, it will be left unchanged. +// The executed operation (and an error) will be returned. // -// The MutateFn is called regardless of creating or updating an object. +// WARNING: If the MutateFn resets a value on obj that has a default value, +// CreateOrPatch will *always* perform a patch. This is because when the +// object is fetched from the API server, the value will have taken on the +// default value, and the check for equality will fail. +// For example, Deployments must have a Replicas value set. If the MutateFn sets +// a Deployment's Replicas to nil, then it will never match with the object +// returned from the API server, which defaults the value to 1. // -// It returns the executed operation and an error. +// WARNING: CreateOrPatch assumes that no values have been set on obj aside +// from the Name/Namespace. Values other than Name and Namespace that existed on +// obj may be overwritten by the corresponding values in the object returned +// from the Kubernetes API server. When this happens, the Patch will not work +// as expected. // // Note: changes to any sub-resource other than status will be ignored. // Changes to the status sub-resource will only be applied if the object diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/controller/name.go b/vendor/sigs.k8s.io/controller-runtime/pkg/controller/name.go index 0e71a01c6673..00ca65512813 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/controller/name.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/controller/name.go @@ -34,7 +34,7 @@ func checkName(name string) error { } 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) + return fmt.Errorf("controller with name %s already exists. Controller names must be unique to avoid multiple controllers reporting the same metric. This validation can be disabled via the SkipNameValidation option", name) } usedNames.Insert(name) diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue/metrics.go b/vendor/sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue/metrics.go new file mode 100644 index 000000000000..967a252dfbed --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue/metrics.go @@ -0,0 +1,172 @@ +package priorityqueue + +import ( + "sync" + "time" + + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" + "sigs.k8s.io/controller-runtime/pkg/internal/metrics" +) + +// This file is mostly a copy of unexported code from +// https://github.com/kubernetes/kubernetes/blob/1d8828ce707ed9dd7a6a9756385419cce1d202ac/staging/src/k8s.io/client-go/util/workqueue/metrics.go +// +// The only two differences are the addition of mapLock in defaultQueueMetrics and converging retryMetrics into queueMetrics. + +type queueMetrics[T comparable] interface { + add(item T, priority int) + get(item T, priority int) + updateDepthWithPriorityMetric(oldPriority, newPriority int) + done(item T) + updateUnfinishedWork() + retry() +} + +func newQueueMetrics[T comparable](mp workqueue.MetricsProvider, name string, clock clock.Clock) queueMetrics[T] { + if len(name) == 0 { + return noMetrics[T]{} + } + + dqm := &defaultQueueMetrics[T]{ + clock: clock, + adds: mp.NewAddsMetric(name), + latency: mp.NewLatencyMetric(name), + workDuration: mp.NewWorkDurationMetric(name), + unfinishedWorkSeconds: mp.NewUnfinishedWorkSecondsMetric(name), + longestRunningProcessor: mp.NewLongestRunningProcessorSecondsMetric(name), + addTimes: map[T]time.Time{}, + processingStartTimes: map[T]time.Time{}, + retries: mp.NewRetriesMetric(name), + } + + if mpp, ok := mp.(metrics.MetricsProviderWithPriority); ok { + dqm.depthWithPriority = mpp.NewDepthMetricWithPriority(name) + } else { + dqm.depth = mp.NewDepthMetric(name) + } + return dqm +} + +// defaultQueueMetrics expects the caller to lock before setting any metrics. +type defaultQueueMetrics[T comparable] struct { + clock clock.Clock + + // current depth of a workqueue + depth workqueue.GaugeMetric + depthWithPriority metrics.DepthMetricWithPriority + // total number of adds handled by a workqueue + adds workqueue.CounterMetric + // how long an item stays in a workqueue + latency workqueue.HistogramMetric + // how long processing an item from a workqueue takes + workDuration workqueue.HistogramMetric + + mapLock sync.RWMutex + addTimes map[T]time.Time + processingStartTimes map[T]time.Time + + // how long have current threads been working? + unfinishedWorkSeconds workqueue.SettableGaugeMetric + longestRunningProcessor workqueue.SettableGaugeMetric + + retries workqueue.CounterMetric +} + +// add is called for ready items only +func (m *defaultQueueMetrics[T]) add(item T, priority int) { + if m == nil { + return + } + + m.adds.Inc() + if m.depthWithPriority != nil { + m.depthWithPriority.Inc(priority) + } else { + m.depth.Inc() + } + + m.mapLock.Lock() + defer m.mapLock.Unlock() + + if _, exists := m.addTimes[item]; !exists { + m.addTimes[item] = m.clock.Now() + } +} + +func (m *defaultQueueMetrics[T]) get(item T, priority int) { + if m == nil { + return + } + + if m.depthWithPriority != nil { + m.depthWithPriority.Dec(priority) + } else { + m.depth.Dec() + } + + m.mapLock.Lock() + defer m.mapLock.Unlock() + + m.processingStartTimes[item] = m.clock.Now() + if startTime, exists := m.addTimes[item]; exists { + m.latency.Observe(m.sinceInSeconds(startTime)) + delete(m.addTimes, item) + } +} + +func (m *defaultQueueMetrics[T]) updateDepthWithPriorityMetric(oldPriority, newPriority int) { + if m.depthWithPriority != nil { + m.depthWithPriority.Dec(oldPriority) + m.depthWithPriority.Inc(newPriority) + } +} + +func (m *defaultQueueMetrics[T]) done(item T) { + if m == nil { + return + } + + m.mapLock.Lock() + defer m.mapLock.Unlock() + if startTime, exists := m.processingStartTimes[item]; exists { + m.workDuration.Observe(m.sinceInSeconds(startTime)) + delete(m.processingStartTimes, item) + } +} + +func (m *defaultQueueMetrics[T]) updateUnfinishedWork() { + m.mapLock.RLock() + defer m.mapLock.RUnlock() + // Note that a summary metric would be better for this, but prometheus + // doesn't seem to have non-hacky ways to reset the summary metrics. + var total float64 + var oldest float64 + for _, t := range m.processingStartTimes { + age := m.sinceInSeconds(t) + total += age + if age > oldest { + oldest = age + } + } + m.unfinishedWorkSeconds.Set(total) + m.longestRunningProcessor.Set(oldest) +} + +// Gets the time since the specified start in seconds. +func (m *defaultQueueMetrics[T]) sinceInSeconds(start time.Time) float64 { + return m.clock.Since(start).Seconds() +} + +func (m *defaultQueueMetrics[T]) retry() { + m.retries.Inc() +} + +type noMetrics[T any] struct{} + +func (noMetrics[T]) add(item T, priority int) {} +func (noMetrics[T]) get(item T, priority int) {} +func (noMetrics[T]) updateDepthWithPriorityMetric(oldPriority, newPriority int) {} +func (noMetrics[T]) done(item T) {} +func (noMetrics[T]) updateUnfinishedWork() {} +func (noMetrics[T]) retry() {} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue/priorityqueue.go b/vendor/sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue/priorityqueue.go new file mode 100644 index 000000000000..71363f0d1792 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue/priorityqueue.go @@ -0,0 +1,462 @@ +package priorityqueue + +import ( + "math" + "sync" + "sync/atomic" + "time" + + "github.com/go-logr/logr" + "github.com/google/btree" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/util/workqueue" + "k8s.io/utils/clock" + "k8s.io/utils/ptr" + + "sigs.k8s.io/controller-runtime/pkg/internal/metrics" +) + +// AddOpts describes the options for adding items to the queue. +type AddOpts struct { + After time.Duration + RateLimited bool + // Priority is the priority of the item. Higher values + // indicate higher priority. + // Defaults to zero if unset. + Priority *int +} + +// PriorityQueue is a priority queue for a controller. It +// internally de-duplicates all items that are added to +// it. It will use the max of the passed priorities and the +// min of possible durations. +type PriorityQueue[T comparable] interface { + workqueue.TypedRateLimitingInterface[T] + AddWithOpts(o AddOpts, Items ...T) + GetWithPriority() (item T, priority int, shutdown bool) +} + +// Opts contains the options for a PriorityQueue. +type Opts[T comparable] struct { + // Ratelimiter is being used when AddRateLimited is called. Defaults to a per-item exponential backoff + // limiter with an initial delay of five milliseconds and a max delay of 1000 seconds. + RateLimiter workqueue.TypedRateLimiter[T] + MetricProvider workqueue.MetricsProvider + Log logr.Logger +} + +// Opt allows to configure a PriorityQueue. +type Opt[T comparable] func(*Opts[T]) + +// New constructs a new PriorityQueue. +func New[T comparable](name string, o ...Opt[T]) PriorityQueue[T] { + opts := &Opts[T]{} + for _, f := range o { + f(opts) + } + + if opts.RateLimiter == nil { + opts.RateLimiter = workqueue.NewTypedItemExponentialFailureRateLimiter[T](5*time.Millisecond, 1000*time.Second) + } + + if opts.MetricProvider == nil { + opts.MetricProvider = metrics.WorkqueueMetricsProvider{} + } + + pq := &priorityqueue[T]{ + log: opts.Log, + items: map[T]*item[T]{}, + queue: btree.NewG(32, less[T]), + becameReady: sets.Set[T]{}, + metrics: newQueueMetrics[T](opts.MetricProvider, name, clock.RealClock{}), + // itemOrWaiterAdded indicates that an item or + // waiter was added. It must be buffered, because + // if we currently process items we can't tell + // if that included the new item/waiter. + itemOrWaiterAdded: make(chan struct{}, 1), + rateLimiter: opts.RateLimiter, + locked: sets.Set[T]{}, + done: make(chan struct{}), + get: make(chan item[T]), + now: time.Now, + tick: time.Tick, + } + + go pq.spin() + go pq.logState() + if _, ok := pq.metrics.(noMetrics[T]); !ok { + go pq.updateUnfinishedWorkLoop() + } + + return pq +} + +type priorityqueue[T comparable] struct { + log logr.Logger + // lock has to be acquired for any access any of items, queue, addedCounter + // or becameReady + lock sync.Mutex + items map[T]*item[T] + queue bTree[*item[T]] + + // addedCounter is a counter of elements added, we need it + // because unixNano is not guaranteed to be unique. + addedCounter uint64 + + // becameReady holds items that are in the queue, were added + // with non-zero after and became ready. We need it to call the + // metrics add exactly once for them. + becameReady sets.Set[T] + metrics queueMetrics[T] + + itemOrWaiterAdded chan struct{} + + rateLimiter workqueue.TypedRateLimiter[T] + + // locked contains the keys we handed out through Get() and that haven't + // yet been returned through Done(). + locked sets.Set[T] + lockedLock sync.RWMutex + + shutdown atomic.Bool + done chan struct{} + + get chan item[T] + + // waiters is the number of routines blocked in Get, we use it to determine + // if we can push items. Every manipulation has to be protected with the lock. + waiters int64 + + // Configurable for testing + now func() time.Time + tick func(time.Duration) <-chan time.Time +} + +func (w *priorityqueue[T]) AddWithOpts(o AddOpts, items ...T) { + if w.shutdown.Load() { + return + } + + w.lock.Lock() + defer w.lock.Unlock() + + for _, key := range items { + after := o.After + if o.RateLimited { + rlAfter := w.rateLimiter.When(key) + if after == 0 || rlAfter < after { + after = rlAfter + } + } + + var readyAt *time.Time + if after > 0 { + readyAt = ptr.To(w.now().Add(after)) + w.metrics.retry() + } + if _, ok := w.items[key]; !ok { + item := &item[T]{ + Key: key, + AddedCounter: w.addedCounter, + Priority: ptr.Deref(o.Priority, 0), + ReadyAt: readyAt, + } + w.items[key] = item + w.queue.ReplaceOrInsert(item) + if item.ReadyAt == nil { + w.metrics.add(key, item.Priority) + } + w.addedCounter++ + continue + } + + // The b-tree de-duplicates based on ordering and any change here + // will affect the order - Just delete and re-add. + item, _ := w.queue.Delete(w.items[key]) + if newPriority := ptr.Deref(o.Priority, 0); newPriority > item.Priority { + // Update depth metric only if the item in the queue was already added to the depth metric. + if item.ReadyAt == nil || w.becameReady.Has(key) { + w.metrics.updateDepthWithPriorityMetric(item.Priority, newPriority) + } + item.Priority = newPriority + } + + if item.ReadyAt != nil && (readyAt == nil || readyAt.Before(*item.ReadyAt)) { + if readyAt == nil && !w.becameReady.Has(key) { + w.metrics.add(key, item.Priority) + } + item.ReadyAt = readyAt + } + + w.queue.ReplaceOrInsert(item) + } + + if len(items) > 0 { + w.notifyItemOrWaiterAdded() + } +} + +func (w *priorityqueue[T]) notifyItemOrWaiterAdded() { + select { + case w.itemOrWaiterAdded <- struct{}{}: + default: + } +} + +func (w *priorityqueue[T]) spin() { + blockForever := make(chan time.Time) + var nextReady <-chan time.Time + nextReady = blockForever + var nextItemReadyAt time.Time + + for { + select { + case <-w.done: + return + case <-w.itemOrWaiterAdded: + case <-nextReady: + nextReady = blockForever + nextItemReadyAt = time.Time{} + } + + func() { + w.lock.Lock() + defer w.lock.Unlock() + + w.lockedLock.Lock() + defer w.lockedLock.Unlock() + + // manipulating the tree from within Ascend might lead to panics, so + // track what we want to delete and do it after we are done ascending. + var toDelete []*item[T] + + var key T + + // Items in the queue tree are sorted first by priority and second by readiness, so + // items with a lower priority might be ready further down in the queue. + // We iterate through the priorities high to low until we find a ready item + pivot := item[T]{ + Key: key, + AddedCounter: 0, + Priority: math.MaxInt, + ReadyAt: nil, + } + + for { + pivotChange := false + + w.queue.AscendGreaterOrEqual(&pivot, func(item *item[T]) bool { + // Item is locked, we can not hand it out + if w.locked.Has(item.Key) { + return true + } + + if item.ReadyAt != nil { + if readyAt := item.ReadyAt.Sub(w.now()); readyAt > 0 { + if nextItemReadyAt.After(*item.ReadyAt) || nextItemReadyAt.IsZero() { + nextReady = w.tick(readyAt) + nextItemReadyAt = *item.ReadyAt + } + + // Adjusting the pivot item moves the ascend to the next lower priority + pivot.Priority = item.Priority - 1 + pivotChange = true + return false + } + if !w.becameReady.Has(item.Key) { + w.metrics.add(item.Key, item.Priority) + w.becameReady.Insert(item.Key) + } + } + + if w.waiters == 0 { + // Have to keep iterating here to ensure we update metrics + // for further items that became ready and set nextReady. + return true + } + + w.metrics.get(item.Key, item.Priority) + w.locked.Insert(item.Key) + w.waiters-- + delete(w.items, item.Key) + toDelete = append(toDelete, item) + w.becameReady.Delete(item.Key) + w.get <- *item + + return true + }) + + if !pivotChange { + break + } + } + + for _, item := range toDelete { + w.queue.Delete(item) + } + }() + } +} + +func (w *priorityqueue[T]) Add(item T) { + w.AddWithOpts(AddOpts{}, item) +} + +func (w *priorityqueue[T]) AddAfter(item T, after time.Duration) { + w.AddWithOpts(AddOpts{After: after}, item) +} + +func (w *priorityqueue[T]) AddRateLimited(item T) { + w.AddWithOpts(AddOpts{RateLimited: true}, item) +} + +func (w *priorityqueue[T]) GetWithPriority() (_ T, priority int, shutdown bool) { + if w.shutdown.Load() { + var zero T + return zero, 0, true + } + + w.lock.Lock() + w.waiters++ + w.lock.Unlock() + + w.notifyItemOrWaiterAdded() + + select { + case <-w.done: + // Return if the queue was shutdown while we were already waiting for an item here. + // For example controller workers are continuously calling GetWithPriority and + // GetWithPriority is blocking the workers if there are no items in the queue. + // If the controller and accordingly the queue is then shut down, without this code + // branch the controller workers remain blocked here and are unable to shut down. + var zero T + return zero, 0, true + case item := <-w.get: + return item.Key, item.Priority, w.shutdown.Load() + } +} + +func (w *priorityqueue[T]) Get() (item T, shutdown bool) { + key, _, shutdown := w.GetWithPriority() + return key, shutdown +} + +func (w *priorityqueue[T]) Forget(item T) { + w.rateLimiter.Forget(item) +} + +func (w *priorityqueue[T]) NumRequeues(item T) int { + return w.rateLimiter.NumRequeues(item) +} + +func (w *priorityqueue[T]) ShuttingDown() bool { + return w.shutdown.Load() +} + +func (w *priorityqueue[T]) Done(item T) { + w.lockedLock.Lock() + defer w.lockedLock.Unlock() + w.locked.Delete(item) + w.metrics.done(item) + w.notifyItemOrWaiterAdded() +} + +func (w *priorityqueue[T]) ShutDown() { + w.shutdown.Store(true) + close(w.done) +} + +// ShutDownWithDrain just calls ShutDown, as the draining +// functionality is not used by controller-runtime. +func (w *priorityqueue[T]) ShutDownWithDrain() { + w.ShutDown() +} + +// Len returns the number of items that are ready to be +// picked up. It does not include items that are not yet +// ready. +func (w *priorityqueue[T]) Len() int { + w.lock.Lock() + defer w.lock.Unlock() + + var result int + w.queue.Ascend(func(item *item[T]) bool { + if item.ReadyAt == nil || item.ReadyAt.Compare(w.now()) <= 0 { + result++ + return true + } + return false + }) + + return result +} + +func (w *priorityqueue[T]) logState() { + t := time.Tick(10 * time.Second) + for { + select { + case <-w.done: + return + case <-t: + } + + // Log level may change at runtime, so keep the + // loop going even if a given level is currently + // not enabled. + if !w.log.V(5).Enabled() { + continue + } + w.lock.Lock() + items := make([]*item[T], 0, len(w.items)) + w.queue.Ascend(func(item *item[T]) bool { + items = append(items, item) + return true + }) + w.lock.Unlock() + + w.log.V(5).Info("workqueue_items", "items", items) + } +} + +func less[T comparable](a, b *item[T]) bool { + if a.Priority != b.Priority { + return a.Priority > b.Priority + } + if a.ReadyAt == nil && b.ReadyAt != nil { + return true + } + if b.ReadyAt == nil && a.ReadyAt != nil { + return false + } + if a.ReadyAt != nil && b.ReadyAt != nil && !a.ReadyAt.Equal(*b.ReadyAt) { + return a.ReadyAt.Before(*b.ReadyAt) + } + + return a.AddedCounter < b.AddedCounter +} + +type item[T comparable] struct { + Key T `json:"key"` + AddedCounter uint64 `json:"addedCounter"` + Priority int `json:"priority"` + ReadyAt *time.Time `json:"readyAt,omitempty"` +} + +func (w *priorityqueue[T]) updateUnfinishedWorkLoop() { + t := time.Tick(500 * time.Millisecond) // borrowed from workqueue: https://github.com/kubernetes/kubernetes/blob/67a807bf142c7a2a5ecfdb2a5d24b4cdea4cc79c/staging/src/k8s.io/client-go/util/workqueue/queue.go#L182 + for { + select { + case <-w.done: + return + case <-t: + } + w.metrics.updateUnfinishedWork() + } +} + +type bTree[T any] interface { + ReplaceOrInsert(item T) (_ T, _ bool) + Delete(item T) (T, bool) + Ascend(iterator btree.ItemIteratorG[T]) + AscendGreaterOrEqual(pivot T, iterator btree.ItemIteratorG[T]) +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/binaries.go b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/binaries.go new file mode 100644 index 000000000000..5110d3265830 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/binaries.go @@ -0,0 +1,387 @@ +/* +Copyright 2025 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 envtest + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "crypto/sha512" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "runtime" + "strings" + + "k8s.io/apimachinery/pkg/util/version" + "sigs.k8s.io/yaml" +) + +// DefaultBinaryAssetsIndexURL is the default index used in HTTPClient. +var DefaultBinaryAssetsIndexURL = "https://raw.githubusercontent.com/kubernetes-sigs/controller-tools/HEAD/envtest-releases.yaml" + +// SetupEnvtestDefaultBinaryAssetsDirectory returns the default location that setup-envtest uses to store envtest binaries. +// Setting BinaryAssetsDirectory to this directory allows sharing envtest binaries with setup-envtest. +// +// The directory is dependent on operating system: +// +// - Windows: %LocalAppData%\kubebuilder-envtest +// - OSX: ~/Library/Application Support/io.kubebuilder.envtest +// - Others: ${XDG_DATA_HOME:-~/.local/share}/kubebuilder-envtest +// +// Otherwise, it errors out. Note that these paths must not be relied upon +// manually. +func SetupEnvtestDefaultBinaryAssetsDirectory() (string, error) { + var baseDir string + + // find the base data directory + switch runtime.GOOS { + case "windows": + baseDir = os.Getenv("LocalAppData") + if baseDir == "" { + return "", errors.New("%LocalAppData% is not defined") + } + case "darwin": + homeDir := os.Getenv("HOME") + if homeDir == "" { + return "", errors.New("$HOME is not defined") + } + baseDir = filepath.Join(homeDir, "Library/Application Support") + default: + baseDir = os.Getenv("XDG_DATA_HOME") + if baseDir == "" { + homeDir := os.Getenv("HOME") + if homeDir == "" { + return "", errors.New("neither $XDG_DATA_HOME nor $HOME are defined") + } + baseDir = filepath.Join(homeDir, ".local/share") + } + } + + // append our program-specific dir to it (OSX has a slightly different + // convention so try to follow that). + switch runtime.GOOS { + case "darwin", "ios": + return filepath.Join(baseDir, "io.kubebuilder.envtest", "k8s"), nil + default: + return filepath.Join(baseDir, "kubebuilder-envtest", "k8s"), nil + } +} + +// index represents an index of envtest binary archives. Example: +// +// releases: +// v1.28.0: +// envtest-v1.28.0-darwin-amd64.tar.gz: +// hash: +// selfLink: +type index struct { + // Releases maps Kubernetes versions to Releases (envtest archives). + Releases map[string]release `json:"releases"` +} + +// release maps an archive name to an archive. +type release map[string]archive + +// archive contains the self link to an archive and its hash. +type archive struct { + Hash string `json:"hash"` + SelfLink string `json:"selfLink"` +} + +// parseKubernetesVersion returns: +// 1. the SemVer form of s when it refers to a specific Kubernetes release, or +// 2. the major and minor portions of s when it refers to a release series, or +// 3. an error +func parseKubernetesVersion(s string) (exact string, major, minor uint, err error) { + if v, err := version.ParseSemantic(s); err == nil { + return v.String(), 0, 0, nil + } + + // See two parseable components and nothing else. + if v, err := version.ParseGeneric(s); err == nil && len(v.Components()) == 2 { + if v.String() == strings.TrimPrefix(s, "v") { + return "", v.Major(), v.Minor(), nil + } + } + + return "", 0, 0, fmt.Errorf("could not parse %q as version", s) +} + +func downloadBinaryAssets(ctx context.Context, binaryAssetsDirectory, binaryAssetsVersion, binaryAssetsIndexURL string) (string, string, string, error) { + if binaryAssetsIndexURL == "" { + binaryAssetsIndexURL = DefaultBinaryAssetsIndexURL + } + + downloadRootDir := binaryAssetsDirectory + if downloadRootDir == "" { + var err error + if downloadRootDir, err = os.MkdirTemp("", "envtest-binaries-"); err != nil { + return "", "", "", fmt.Errorf("failed to create tmp directory for envtest binaries: %w", err) + } + } + + var binaryAssetsIndex *index + switch exact, major, minor, err := parseKubernetesVersion(binaryAssetsVersion); { + case binaryAssetsVersion != "" && err != nil: + return "", "", "", err + + case binaryAssetsVersion != "" && exact != "": + // Look for these specific binaries locally before downloading them from the release index. + // Use the canonical form of the version from here on. + binaryAssetsVersion = "v" + exact + + case binaryAssetsVersion == "" || major != 0 || minor != 0: + // Select a stable version from the release index before continuing. + binaryAssetsIndex, err = getIndex(ctx, binaryAssetsIndexURL) + if err != nil { + return "", "", "", err + } + + binaryAssetsVersion, err = latestStableVersionFromIndex(binaryAssetsIndex, major, minor) + if err != nil { + return "", "", "", err + } + } + + // Storing the envtest binaries in a directory structure that is compatible with setup-envtest. + // This makes it possible to share the envtest binaries with setup-envtest if the BinaryAssetsDirectory is set to SetupEnvtestDefaultBinaryAssetsDirectory(). + downloadDir := path.Join(downloadRootDir, fmt.Sprintf("%s-%s-%s", strings.TrimPrefix(binaryAssetsVersion, "v"), runtime.GOOS, runtime.GOARCH)) + if !fileExists(downloadDir) { + if err := os.MkdirAll(downloadDir, 0700); err != nil { + return "", "", "", fmt.Errorf("failed to create directory %q for envtest binaries: %w", downloadDir, err) + } + } + + apiServerPath := path.Join(downloadDir, "kube-apiserver") + etcdPath := path.Join(downloadDir, "etcd") + kubectlPath := path.Join(downloadDir, "kubectl") + + if fileExists(apiServerPath) && fileExists(etcdPath) && fileExists(kubectlPath) { + // Nothing to do if the binaries already exist. + return apiServerPath, etcdPath, kubectlPath, nil + } + + // Get Index if we didn't have to get it above to get the latest stable version. + if binaryAssetsIndex == nil { + var err error + binaryAssetsIndex, err = getIndex(ctx, binaryAssetsIndexURL) + if err != nil { + return "", "", "", err + } + } + + buf := &bytes.Buffer{} + if err := downloadBinaryAssetsArchive(ctx, binaryAssetsIndex, binaryAssetsVersion, buf); err != nil { + return "", "", "", err + } + + gzStream, err := gzip.NewReader(buf) + if err != nil { + return "", "", "", fmt.Errorf("failed to create gzip reader to extract envtest binaries: %w", err) + } + tarReader := tar.NewReader(gzStream) + + var header *tar.Header + for header, err = tarReader.Next(); err == nil; header, err = tarReader.Next() { + if header.Typeflag != tar.TypeReg { + // Skip non-regular file entry in archive. + continue + } + + // Just dump all files directly into the download directory, ignoring the prefixed directory paths. + // We also ignore bits for the most part (except for X). + fileName := filepath.Base(header.Name) + perms := 0555 & header.Mode // make sure we're at most r+x + + // Setting O_EXCL to get an error if the file already exists. + f, err := os.OpenFile(path.Join(downloadDir, fileName), os.O_RDWR|os.O_CREATE|os.O_EXCL|os.O_TRUNC, os.FileMode(perms)) + if err != nil { + if os.IsExist(err) { + // Nothing to do if the file already exists. We assume another process created the file concurrently. + continue + } + return "", "", "", fmt.Errorf("failed to create file %s in directory %s: %w", fileName, downloadDir, err) + } + if err := func() error { + defer f.Close() + if _, err := io.Copy(f, tarReader); err != nil { + return fmt.Errorf("failed to write file %s in directory %s: %w", fileName, downloadDir, err) + } + return nil + }(); err != nil { + return "", "", "", fmt.Errorf("failed to close file %s in directory %s: %w", fileName, downloadDir, err) + } + } + + return apiServerPath, etcdPath, kubectlPath, nil +} + +func fileExists(path string) bool { + if _, err := os.Stat(path); err == nil { + return true + } + return false +} + +func downloadBinaryAssetsArchive(ctx context.Context, index *index, version string, out io.Writer) error { + archives, ok := index.Releases[version] + if !ok { + return fmt.Errorf("failed to find envtest binaries for version %s", version) + } + + archiveName := fmt.Sprintf("envtest-%s-%s-%s.tar.gz", version, runtime.GOOS, runtime.GOARCH) + archive, ok := archives[archiveName] + if !ok { + return fmt.Errorf("failed to find envtest binaries for version %s with archiveName %s", version, archiveName) + } + + archiveURL, err := url.Parse(archive.SelfLink) + if err != nil { + return fmt.Errorf("failed to parse envtest binaries archive URL %q: %w", archiveURL, err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", archiveURL.String(), nil) + if err != nil { + return fmt.Errorf("failed to create request to download %s: %w", archiveURL.String(), err) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("failed to download %s: %w", archiveURL.String(), err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("failed to download %s, got status %q", archiveURL.String(), resp.Status) + } + + return readBody(resp, out, archiveName, archive.Hash) +} + +// latestStableVersionFromIndex returns the version with highest [precedence] in index that is not a prerelease. +// When either major or minor are not zero, the returned version will have those major and minor versions. +// Note that the version cannot be limited to 0.0.x this way. +// +// It is an error when there is no appropriate version in index. +// +// [precedence]: https://semver.org/spec/v2.0.0.html#spec-item-11 +func latestStableVersionFromIndex(index *index, major, minor uint) (string, error) { + if len(index.Releases) == 0 { + return "", fmt.Errorf("failed to find latest stable version from index: index is empty") + } + + var found *version.Version + for releaseVersion := range index.Releases { + v, err := version.ParseSemantic(releaseVersion) + if err != nil { + return "", fmt.Errorf("failed to parse version %q: %w", releaseVersion, err) + } + + // Filter out pre-releases. + if len(v.PreRelease()) > 0 { + continue + } + + // Filter on release series, if any. + if (major != 0 || minor != 0) && (v.Major() != major || v.Minor() != minor) { + continue + } + + if found == nil || v.GreaterThan(found) { + found = v + } + } + + if found == nil { + search := "any" + if major != 0 || minor != 0 { + search = fmt.Sprint(major, ".", minor) + } + + return "", fmt.Errorf("failed to find latest stable version from index: index does not have %s stable versions", search) + } + + return "v" + found.String(), nil +} + +func getIndex(ctx context.Context, indexURL string) (*index, error) { + loc, err := url.Parse(indexURL) + if err != nil { + return nil, fmt.Errorf("unable to parse index URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", loc.String(), nil) + if err != nil { + return nil, fmt.Errorf("unable to construct request to get index: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("unable to perform request to get index: %w", err) + } + + defer resp.Body.Close() + if resp.StatusCode != 200 { + return nil, fmt.Errorf("unable to get index -- got status %q", resp.Status) + } + + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("unable to get index -- unable to read body %w", err) + } + + var index index + if err := yaml.Unmarshal(responseBody, &index); err != nil { + return nil, fmt.Errorf("unable to unmarshal index: %w", err) + } + return &index, nil +} + +func readBody(resp *http.Response, out io.Writer, archiveName string, expectedHash string) error { + // Stream in chunks to do the checksum + buf := make([]byte, 32*1024) // 32KiB, same as io.Copy + hasher := sha512.New() + + for cont := true; cont; { + amt, err := resp.Body.Read(buf) + if err != nil && !errors.Is(err, io.EOF) { + return fmt.Errorf("unable read next chunk of %s: %w", archiveName, err) + } + if amt > 0 { + // checksum never returns errors according to docs + hasher.Write(buf[:amt]) + if _, err := out.Write(buf[:amt]); err != nil { + return fmt.Errorf("unable write next chunk of %s: %w", archiveName, err) + } + } + cont = amt > 0 && !errors.Is(err, io.EOF) + } + + actualHash := hex.EncodeToString(hasher.Sum(nil)) + if actualHash != expectedHash { + return fmt.Errorf("checksum mismatch for %s: %s (computed) != %s (expected)", archiveName, actualHash, expectedHash) + } + + return nil +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/crd.go b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/crd.go index 5fdd657cd7f8..8ed2224cfe8d 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/crd.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/crd.go @@ -94,7 +94,7 @@ func InstallCRDs(config *rest.Config, options CRDInstallOptions) ([]*apiextensio defaultCRDOptions(&options) // Read the CRD yamls into options.CRDs - if err := readCRDFiles(&options); err != nil { + if err := ReadCRDFiles(&options); err != nil { return nil, fmt.Errorf("unable to read CRD files: %w", err) } @@ -115,8 +115,8 @@ func InstallCRDs(config *rest.Config, options CRDInstallOptions) ([]*apiextensio return options.CRDs, nil } -// readCRDFiles reads the directories of CRDs in options.Paths and adds the CRD structs to options.CRDs. -func readCRDFiles(options *CRDInstallOptions) error { +// ReadCRDFiles reads the directories of CRDs in options.Paths and adds the CRD structs to options.CRDs. +func ReadCRDFiles(options *CRDInstallOptions) error { if len(options.Paths) > 0 { crdList, err := renderCRDs(options) if err != nil { @@ -217,7 +217,7 @@ func (p *poller) poll(ctx context.Context) (done bool, err error) { // UninstallCRDs uninstalls a collection of CRDs by reading the crd yaml files from a directory. func UninstallCRDs(config *rest.Config, options CRDInstallOptions) error { // Read the CRD yamls into options.CRDs - if err := readCRDFiles(&options); err != nil { + if err := ReadCRDFiles(&options); err != nil { return err } @@ -229,7 +229,6 @@ func UninstallCRDs(config *rest.Config, options CRDInstallOptions) error { // Uninstall each CRD for _, crd := range options.CRDs { - crd := crd log.V(1).Info("uninstalling CRD", "crd", crd.GetName()) if err := cs.Delete(context.TODO(), crd); err != nil { // If CRD is not found, we can consider success @@ -251,7 +250,6 @@ func CreateCRDs(config *rest.Config, crds []*apiextensionsv1.CustomResourceDefin // Create each CRD for _, crd := range crds { - crd := crd log.V(1).Info("installing CRD", "crd", crd.GetName()) existingCrd := crd.DeepCopy() err := cs.Get(context.TODO(), client.ObjectKey{Name: crd.GetName()}, existingCrd) diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/server.go b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/server.go index 854365764564..c9f19da97772 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/server.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/server.go @@ -109,7 +109,11 @@ var ( // Environment creates a Kubernetes test environment that will start / stop the Kubernetes control plane and // install extension APIs. type Environment struct { - // ControlPlane is the ControlPlane including the apiserver and etcd + // ControlPlane is the ControlPlane including the apiserver and etcd. + // Binary paths (APIServer.Path, Etcd.Path, KubectlPath) can be pre-configured in ControlPlane. + // If DownloadBinaryAssets is true, the downloaded paths will always be used. + // If DownloadBinaryAssets is false and paths are not pre-configured (default is empty), they will be + // automatically resolved using BinaryAssetsDirectory. ControlPlane controlplane.ControlPlane // Scheme is used to determine if conversion webhooks should be enabled @@ -126,6 +130,10 @@ type Environment struct { // loading. Config *rest.Config + // KubeConfig provides []byte of a kubeconfig file to talk to the apiserver + // It's automatically populated if not set based on the `Config` + KubeConfig []byte + // CRDInstallOptions are the options for installing CRDs. CRDInstallOptions CRDInstallOptions @@ -147,8 +155,22 @@ type Environment struct { // values are merged. CRDDirectoryPaths []string + // DownloadBinaryAssets indicates that the envtest binaries should be downloaded. + // If BinaryAssetsDirectory is also set, it is used to store the downloaded binaries, + // otherwise a tmp directory is created. + DownloadBinaryAssets bool + + // DownloadBinaryAssetsVersion is the version of envtest binaries to download. + // Defaults to the latest stable version (i.e. excluding alpha / beta / RC versions). + DownloadBinaryAssetsVersion string + + // DownloadBinaryAssetsIndexURL is the index used to discover envtest binaries to download. + // Defaults to https://raw.githubusercontent.com/kubernetes-sigs/controller-tools/HEAD/envtest-releases.yaml. + DownloadBinaryAssetsIndexURL string + // BinaryAssetsDirectory is the path where the binaries required for the envtest are // located in the local environment. This field can be overridden by setting KUBEBUILDER_ASSETS. + // Set this field to SetupEnvtestDefaultBinaryAssetsDirectory() to share binaries with setup-envtest. BinaryAssetsDirectory string // UseExistingCluster indicates that this environments should use an @@ -193,6 +215,40 @@ func (te *Environment) Stop() error { return te.ControlPlane.Stop() } +// configureBinaryPaths configures the binary paths for the API server, etcd, and kubectl. +// If DownloadBinaryAssets is true, it downloads and uses those paths. +// If DownloadBinaryAssets is false, it only sets paths that are not already configured (empty). +func (te *Environment) configureBinaryPaths() error { + apiServer := te.ControlPlane.GetAPIServer() + + if te.ControlPlane.Etcd == nil { + te.ControlPlane.Etcd = &controlplane.Etcd{} + } + + if te.DownloadBinaryAssets { + apiServerPath, etcdPath, kubectlPath, err := downloadBinaryAssets(context.TODO(), + te.BinaryAssetsDirectory, te.DownloadBinaryAssetsVersion, te.DownloadBinaryAssetsIndexURL) + if err != nil { + return err + } + + apiServer.Path = apiServerPath + te.ControlPlane.Etcd.Path = etcdPath + te.ControlPlane.KubectlPath = kubectlPath + } else { + if apiServer.Path == "" { + apiServer.Path = process.BinPathFinder("kube-apiserver", te.BinaryAssetsDirectory) + } + if te.ControlPlane.Etcd.Path == "" { + te.ControlPlane.Etcd.Path = process.BinPathFinder("etcd", te.BinaryAssetsDirectory) + } + if te.ControlPlane.KubectlPath == "" { + te.ControlPlane.KubectlPath = process.BinPathFinder("kubectl", te.BinaryAssetsDirectory) + } + } + return nil +} + // Start starts a local Kubernetes server and updates te.ApiserverPort with the port it is listening on. func (te *Environment) Start() (*rest.Config, error) { if te.useExistingCluster() { @@ -211,10 +267,6 @@ func (te *Environment) Start() (*rest.Config, error) { } else { apiServer := te.ControlPlane.GetAPIServer() - if te.ControlPlane.Etcd == nil { - te.ControlPlane.Etcd = &controlplane.Etcd{} - } - if os.Getenv(envAttachOutput) == "true" { te.AttachControlPlaneOutput = true } @@ -225,6 +277,9 @@ func (te *Environment) Start() (*rest.Config, error) { if apiServer.Err == nil { apiServer.Err = os.Stderr } + if te.ControlPlane.Etcd == nil { + te.ControlPlane.Etcd = &controlplane.Etcd{} + } if te.ControlPlane.Etcd.Out == nil { te.ControlPlane.Etcd.Out = os.Stdout } @@ -233,9 +288,9 @@ func (te *Environment) Start() (*rest.Config, error) { } } - apiServer.Path = process.BinPathFinder("kube-apiserver", te.BinaryAssetsDirectory) - te.ControlPlane.Etcd.Path = process.BinPathFinder("etcd", te.BinaryAssetsDirectory) - te.ControlPlane.KubectlPath = process.BinPathFinder("kubectl", te.BinaryAssetsDirectory) + if err := te.configureBinaryPaths(); err != nil { + return nil, fmt.Errorf("failed to configure binary paths: %w", err) + } if err := te.defaultTimeouts(); err != nil { return nil, fmt.Errorf("failed to default controlplane timeouts: %w", err) @@ -265,6 +320,14 @@ func (te *Environment) Start() (*rest.Config, error) { te.Config = adminUser.Config() } + if len(te.KubeConfig) == 0 { + var err error + te.KubeConfig, err = controlplane.KubeConfigFromREST(te.Config) + if err != nil { + return nil, fmt.Errorf("unable to set KubeConfig field: %w", err) + } + } + // Set the default scheme if nil. if te.Scheme == nil { te.Scheme = scheme.Scheme diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/webhook.go b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/webhook.go index e4e54e472c78..a6961bf7c6ce 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/webhook.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/envtest/webhook.go @@ -294,10 +294,10 @@ func (o *WebhookInstallOptions) setupCA() error { return fmt.Errorf("unable to marshal webhook serving certs: %w", err) } - if err := os.WriteFile(filepath.Join(localServingCertsDir, "tls.crt"), certData, 0640); err != nil { //nolint:gosec + if err := os.WriteFile(filepath.Join(localServingCertsDir, "tls.crt"), certData, 0640); err != nil { return fmt.Errorf("unable to write webhook serving cert to disk: %w", err) } - if err := os.WriteFile(filepath.Join(localServingCertsDir, "tls.key"), keyData, 0640); err != nil { //nolint:gosec + if err := os.WriteFile(filepath.Join(localServingCertsDir, "tls.key"), keyData, 0640); err != nil { return fmt.Errorf("unable to write webhook serving key to disk: %w", err) } @@ -313,14 +313,12 @@ func createWebhooks(config *rest.Config, mutHooks []*admissionv1.MutatingWebhook // Create each webhook for _, hook := range mutHooks { - hook := hook log.V(1).Info("installing mutating webhook", "webhook", hook.GetName()) if err := ensureCreated(cs, hook); err != nil { return err } } for _, hook := range valHooks { - hook := hook log.V(1).Info("installing validating webhook", "webhook", hook.GetName()) if err := ensureCreated(cs, hook); err != nil { return err @@ -421,8 +419,8 @@ func readWebhooks(path string) ([]*admissionv1.MutatingWebhookConfiguration, []* const ( admissionregv1 = "admissionregistration.k8s.io/v1" ) - switch { - case generic.Kind == "MutatingWebhookConfiguration": + switch generic.Kind { + case "MutatingWebhookConfiguration": if generic.APIVersion != admissionregv1 { return nil, nil, fmt.Errorf("only v1 is supported right now for MutatingWebhookConfiguration (name: %s)", generic.Name) } @@ -431,7 +429,7 @@ func readWebhooks(path string) ([]*admissionv1.MutatingWebhookConfiguration, []* return nil, nil, err } mutHooks = append(mutHooks, hook) - case generic.Kind == "ValidatingWebhookConfiguration": + case "ValidatingWebhookConfiguration": if generic.APIVersion != admissionregv1 { return nil, nil, fmt.Errorf("only v1 is supported right now for ValidatingWebhookConfiguration (name: %s)", generic.Name) } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/event/event.go b/vendor/sigs.k8s.io/controller-runtime/pkg/event/event.go index 81229fc2d301..82b1793f53c9 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/event/event.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/event/event.go @@ -40,6 +40,9 @@ type GenericEvent = TypedGenericEvent[client.Object] type TypedCreateEvent[object any] struct { // Object is the object from the event Object object + + // IsInInitialList is true if the Create event was triggered by the initial list. + IsInInitialList bool } // TypedUpdateEvent is an event where a Kubernetes object was updated. TypedUpdateEvent should be generated diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/handler/enqueue.go b/vendor/sigs.k8s.io/controller-runtime/pkg/handler/enqueue.go index 1a1d1ab2f443..64cbe8a4d13e 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/handler/enqueue.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/handler/enqueue.go @@ -52,25 +52,32 @@ func (e *TypedEnqueueRequestForObject[T]) Create(ctx context.Context, evt event. enqueueLog.Error(nil, "CreateEvent received with no metadata", "event", evt) return } - q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + + item := reconcile.Request{NamespacedName: types.NamespacedName{ Name: evt.Object.GetName(), Namespace: evt.Object.GetNamespace(), - }}) + }} + + addToQueueCreate(q, evt, item) } // Update implements EventHandler. func (e *TypedEnqueueRequestForObject[T]) Update(ctx context.Context, evt event.TypedUpdateEvent[T], q workqueue.TypedRateLimitingInterface[reconcile.Request]) { switch { case !isNil(evt.ObjectNew): - q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + item := reconcile.Request{NamespacedName: types.NamespacedName{ Name: evt.ObjectNew.GetName(), Namespace: evt.ObjectNew.GetNamespace(), - }}) + }} + + addToQueueUpdate(q, evt, item) case !isNil(evt.ObjectOld): - q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + item := reconcile.Request{NamespacedName: types.NamespacedName{ Name: evt.ObjectOld.GetName(), Namespace: evt.ObjectOld.GetNamespace(), - }}) + }} + + addToQueueUpdate(q, evt, item) default: enqueueLog.Error(nil, "UpdateEvent received with no metadata", "event", evt) } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/handler/enqueue_mapped.go b/vendor/sigs.k8s.io/controller-runtime/pkg/handler/enqueue_mapped.go index 491bc40c4205..62d67281512c 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/handler/enqueue_mapped.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/handler/enqueue_mapped.go @@ -20,7 +20,9 @@ import ( "context" "k8s.io/client-go/util/workqueue" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -63,7 +65,8 @@ func EnqueueRequestsFromMapFunc(fn MapFunc) EventHandler { // 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, + toRequests: fn, + objectImplementsClientObject: implementsClientObject[object](), } } @@ -71,7 +74,8 @@ var _ EventHandler = &enqueueRequestsFromMapFunc[client.Object, reconcile.Reques type enqueueRequestsFromMapFunc[object any, request comparable] struct { // Mapper transforms the argument into a slice of keys to be reconciled - toRequests TypedMapFunc[object, request] + toRequests TypedMapFunc[object, request] + objectImplementsClientObject bool } // Create implements EventHandler. @@ -81,7 +85,14 @@ func (e *enqueueRequestsFromMapFunc[object, request]) Create( q workqueue.TypedRateLimitingInterface[request], ) { reqs := map[request]empty{} - e.mapAndEnqueue(ctx, q, evt.Object, reqs) + + var lowPriority bool + if isPriorityQueue(q) && !isNil(evt.Object) { + if evt.IsInInitialList { + lowPriority = true + } + } + e.mapAndEnqueue(ctx, q, evt.Object, reqs, lowPriority) } // Update implements EventHandler. @@ -90,9 +101,13 @@ func (e *enqueueRequestsFromMapFunc[object, request]) Update( evt event.TypedUpdateEvent[object], q workqueue.TypedRateLimitingInterface[request], ) { + var lowPriority bool + if e.objectImplementsClientObject && isPriorityQueue(q) && !isNil(evt.ObjectOld) && !isNil(evt.ObjectNew) { + lowPriority = any(evt.ObjectOld).(client.Object).GetResourceVersion() == any(evt.ObjectNew).(client.Object).GetResourceVersion() + } reqs := map[request]empty{} - e.mapAndEnqueue(ctx, q, evt.ObjectOld, reqs) - e.mapAndEnqueue(ctx, q, evt.ObjectNew, reqs) + e.mapAndEnqueue(ctx, q, evt.ObjectOld, reqs, lowPriority) + e.mapAndEnqueue(ctx, q, evt.ObjectNew, reqs, lowPriority) } // Delete implements EventHandler. @@ -102,7 +117,7 @@ func (e *enqueueRequestsFromMapFunc[object, request]) Delete( q workqueue.TypedRateLimitingInterface[request], ) { reqs := map[request]empty{} - e.mapAndEnqueue(ctx, q, evt.Object, reqs) + e.mapAndEnqueue(ctx, q, evt.Object, reqs, false) } // Generic implements EventHandler. @@ -112,14 +127,26 @@ func (e *enqueueRequestsFromMapFunc[object, request]) Generic( q workqueue.TypedRateLimitingInterface[request], ) { reqs := map[request]empty{} - e.mapAndEnqueue(ctx, q, evt.Object, reqs) + e.mapAndEnqueue(ctx, q, evt.Object, reqs, false) } -func (e *enqueueRequestsFromMapFunc[object, request]) mapAndEnqueue(ctx context.Context, q workqueue.TypedRateLimitingInterface[request], o object, reqs map[request]empty) { +func (e *enqueueRequestsFromMapFunc[object, request]) mapAndEnqueue( + ctx context.Context, + q workqueue.TypedRateLimitingInterface[request], + o object, + reqs map[request]empty, + lowPriority bool, +) { for _, req := range e.toRequests(ctx, o) { _, ok := reqs[req] if !ok { - q.Add(req) + if lowPriority { + q.(priorityqueue.PriorityQueue[request]).AddWithOpts(priorityqueue.AddOpts{ + Priority: ptr.To(LowPriority), + }, req) + } else { + q.Add(req) + } reqs[req] = empty{} } } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/handler/enqueue_owner.go b/vendor/sigs.k8s.io/controller-runtime/pkg/handler/enqueue_owner.go index 1680043b46f2..e8fc8eb46e2c 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/handler/enqueue_owner.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/handler/enqueue_owner.go @@ -72,7 +72,7 @@ func TypedEnqueueRequestForOwner[object client.Object](scheme *runtime.Scheme, m for _, opt := range opts { opt(e) } - return e + return WithLowPriorityWhenUnchanged(e) } // OnlyControllerOwner if provided will only look at the first OwnerReference with Controller: true. diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/handler/eventhandler.go b/vendor/sigs.k8s.io/controller-runtime/pkg/handler/eventhandler.go index ea4bcee31e6d..88510d29eded 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/handler/eventhandler.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/handler/eventhandler.go @@ -18,9 +18,13 @@ package handler import ( "context" + "reflect" + "time" "k8s.io/client-go/util/workqueue" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -106,10 +110,34 @@ type TypedFuncs[object any, request comparable] struct { GenericFunc func(context.Context, event.TypedGenericEvent[object], workqueue.TypedRateLimitingInterface[request]) } +var typeForClientObject = reflect.TypeFor[client.Object]() + +func implementsClientObject[object any]() bool { + return reflect.TypeFor[object]().Implements(typeForClientObject) +} + +func isPriorityQueue[request comparable](q workqueue.TypedRateLimitingInterface[request]) bool { + _, ok := q.(priorityqueue.PriorityQueue[request]) + return ok +} + // Create implements EventHandler. 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) + if !implementsClientObject[object]() || !isPriorityQueue(q) || isNil(e.Object) { + h.CreateFunc(ctx, e, q) + return + } + + wq := workqueueWithDefaultPriority[request]{ + // We already know that we have a priority queue, that event.Object implements + // client.Object and that its not nil + PriorityQueue: q.(priorityqueue.PriorityQueue[request]), + } + if e.IsInInitialList { + wq.priority = ptr.To(LowPriority) + } + h.CreateFunc(ctx, e, wq) } } @@ -123,7 +151,20 @@ func (h TypedFuncs[object, request]) Delete(ctx context.Context, e event.TypedDe // Update implements EventHandler. 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) + if !implementsClientObject[object]() || !isPriorityQueue(q) || isNil(e.ObjectOld) || isNil(e.ObjectNew) { + h.UpdateFunc(ctx, e, q) + return + } + + wq := workqueueWithDefaultPriority[request]{ + // We already know that we have a priority queue, that event.ObjectOld and ObjectNew implement + // client.Object and that they are not nil + PriorityQueue: q.(priorityqueue.PriorityQueue[request]), + } + if any(e.ObjectOld).(client.Object).GetResourceVersion() == any(e.ObjectNew).(client.Object).GetResourceVersion() { + wq.priority = ptr.To(LowPriority) + } + h.UpdateFunc(ctx, e, wq) } } @@ -133,3 +174,74 @@ func (h TypedFuncs[object, request]) Generic(ctx context.Context, e event.TypedG h.GenericFunc(ctx, e, q) } } + +// LowPriority is the priority set by WithLowPriorityWhenUnchanged +const LowPriority = -100 + +// WithLowPriorityWhenUnchanged reduces the priority of events stemming from the initial listwatch or from a resync if +// and only if a priorityqueue.PriorityQueue is used. If not, it does nothing. +func WithLowPriorityWhenUnchanged[object client.Object, request comparable](u TypedEventHandler[object, request]) TypedEventHandler[object, request] { + // TypedFuncs already implements this so just wrap + return TypedFuncs[object, request]{ + CreateFunc: u.Create, + UpdateFunc: u.Update, + DeleteFunc: u.Delete, + GenericFunc: u.Generic, + } +} + +type workqueueWithDefaultPriority[request comparable] struct { + priorityqueue.PriorityQueue[request] + priority *int +} + +func (w workqueueWithDefaultPriority[request]) Add(item request) { + w.PriorityQueue.AddWithOpts(priorityqueue.AddOpts{Priority: w.priority}, item) +} + +func (w workqueueWithDefaultPriority[request]) AddAfter(item request, after time.Duration) { + w.PriorityQueue.AddWithOpts(priorityqueue.AddOpts{Priority: w.priority, After: after}, item) +} + +func (w workqueueWithDefaultPriority[request]) AddRateLimited(item request) { + w.PriorityQueue.AddWithOpts(priorityqueue.AddOpts{Priority: w.priority, RateLimited: true}, item) +} + +func (w workqueueWithDefaultPriority[request]) AddWithOpts(o priorityqueue.AddOpts, items ...request) { + if o.Priority == nil { + o.Priority = w.priority + } + w.PriorityQueue.AddWithOpts(o, items...) +} + +// addToQueueCreate adds the reconcile.Request to the priorityqueue in the handler +// for Create requests if and only if the workqueue being used is of type priorityqueue.PriorityQueue[reconcile.Request] +func addToQueueCreate[T client.Object, request comparable](q workqueue.TypedRateLimitingInterface[request], evt event.TypedCreateEvent[T], item request) { + priorityQueue, isPriorityQueue := q.(priorityqueue.PriorityQueue[request]) + if !isPriorityQueue { + q.Add(item) + return + } + + var priority *int + if evt.IsInInitialList { + priority = ptr.To(LowPriority) + } + priorityQueue.AddWithOpts(priorityqueue.AddOpts{Priority: priority}, item) +} + +// addToQueueUpdate adds the reconcile.Request to the priorityqueue in the handler +// for Update requests if and only if the workqueue being used is of type priorityqueue.PriorityQueue[reconcile.Request] +func addToQueueUpdate[T client.Object, request comparable](q workqueue.TypedRateLimitingInterface[request], evt event.TypedUpdateEvent[T], item request) { + priorityQueue, isPriorityQueue := q.(priorityqueue.PriorityQueue[request]) + if !isPriorityQueue { + q.Add(item) + return + } + + var priority *int + if evt.ObjectOld.GetResourceVersion() == evt.ObjectNew.GetResourceVersion() { + priority = ptr.To(LowPriority) + } + priorityQueue.AddWithOpts(priorityqueue.AddOpts{Priority: priority}, item) +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/controller.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/controller.go index 1f7752ba6e6b..ea7968186233 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/controller.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/controller.go @@ -21,20 +21,73 @@ import ( "errors" "fmt" "sync" + "sync/atomic" "time" "github.com/go-logr/logr" + "golang.org/x/sync/errgroup" "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/client-go/util/workqueue" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue" ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics" logf "sigs.k8s.io/controller-runtime/pkg/log" "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[request comparable] 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.TypedReconciler[request] + + // 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. + NewQueue func(controllerName string, rateLimiter workqueue.TypedRateLimiter[request]) workqueue.TypedRateLimitingInterface[request] + + // MaxConcurrentReconciles is the maximum number of concurrent Reconciles which can be run. Defaults to 1. + MaxConcurrentReconciles int + + // CacheSyncTimeout refers to the time limit set on waiting for cache to sync + // Defaults to 2 minutes if not set. + CacheSyncTimeout time.Duration + + // Name is used to uniquely identify a Controller in tracing, logging and monitoring. Name is required. + Name string + + // 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 *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 + + // EnableWarmup specifies whether the controller should start its sources + // when the manager is not the leader. + // Defaults to false, which means that the controller will wait for leader election to start + // before starting sources. + EnableWarmup *bool + + // ReconciliationTimeout is used as the timeout passed to the context of each Reconcile call. + // By default, there is no timeout. + ReconciliationTimeout time.Duration +} + // Controller implements controller.Controller. type Controller[request comparable] struct { // Name is used to uniquely identify a Controller in tracing, logging and monitoring. Name is required. @@ -58,7 +111,7 @@ type Controller[request comparable] struct { // Queue is an listeningQueue that listens for events from Informers and adds object keys to // the Queue for processing - Queue workqueue.TypedRateLimitingInterface[request] + Queue priorityqueue.PriorityQueue[request] // mu is used to synchronize Controller setup mu sync.Mutex @@ -80,6 +133,14 @@ type Controller[request comparable] struct { // startWatches maintains a list of sources, handlers, and predicates to start when the controller is started. startWatches []source.TypedSource[request] + // startedEventSourcesAndQueue is used to track if the event sources have been started. + // It ensures that we append sources to c.startWatches only until we call Start() / Warmup() + // It is true if startEventSourcesAndQueueLocked has been called at least once. + startedEventSourcesAndQueue bool + + // didStartEventSourcesOnce is used to ensure that the event sources are only started once. + didStartEventSourcesOnce sync.Once + // 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 @@ -92,6 +153,38 @@ type Controller[request comparable] struct { // LeaderElected indicates whether the controller is leader elected or always running. LeaderElected *bool + + // EnableWarmup specifies whether the controller should start its sources when the manager is not + // the leader. This is useful for cases where sources take a long time to start, as it allows + // for the controller to warm up its caches even before it is elected as the leader. This + // improves leadership failover time, as the caches will be prepopulated before the controller + // transitions to be leader. + // + // Setting EnableWarmup to true and NeedLeaderElection to true means the controller will start its + // sources without waiting to become leader. + // Setting EnableWarmup to true and NeedLeaderElection to false is a no-op as controllers without + // leader election do not wait on leader election to start their sources. + // Defaults to false. + EnableWarmup *bool + + ReconciliationTimeout time.Duration +} + +// New returns a new Controller configured with the given options. +func New[request comparable](options Options[request]) *Controller[request] { + return &Controller[request]{ + Do: options.Do, + RateLimiter: options.RateLimiter, + NewQueue: options.NewQueue, + MaxConcurrentReconciles: options.MaxConcurrentReconciles, + CacheSyncTimeout: options.CacheSyncTimeout, + Name: options.Name, + LogConstructor: options.LogConstructor, + RecoverPanic: options.RecoverPanic, + LeaderElected: options.LeaderElected, + EnableWarmup: options.EnableWarmup, + ReconciliationTimeout: options.ReconciliationTimeout, + } } // Reconcile implements reconcile.Reconciler. @@ -113,6 +206,13 @@ func (c *Controller[request]) Reconcile(ctx context.Context, req request) (_ rec panic(r) } }() + + if c.ReconciliationTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, c.ReconciliationTimeout) + defer cancel() + } + return c.Do.Reconcile(ctx, req) } @@ -121,10 +221,9 @@ func (c *Controller[request]) Watch(src source.TypedSource[request]) error { c.mu.Lock() defer c.mu.Unlock() - // Controller hasn't started yet, store the watches locally and return. - // - // These watches are going to be held on the controller struct until the manager or user calls Start(...). - if !c.Started { + // Sources weren't started yet, store the watches locally and return. + // These sources are going to be held until either Warmup() or Start(...) is called. + if !c.startedEventSourcesAndQueue { c.startWatches = append(c.startWatches, src) return nil } @@ -141,6 +240,21 @@ func (c *Controller[request]) NeedLeaderElection() bool { return *c.LeaderElected } +// Warmup implements the manager.WarmupRunnable interface. +func (c *Controller[request]) Warmup(ctx context.Context) error { + if c.EnableWarmup == nil || !*c.EnableWarmup { + return nil + } + + c.mu.Lock() + defer c.mu.Unlock() + + // Set the ctx so later calls to watch use this internal context + c.ctx = ctx + + return c.startEventSourcesAndQueueLocked(ctx) +} + // Start implements controller.Controller. func (c *Controller[request]) Start(ctx context.Context) error { // use an IIFE to get proper lock handling @@ -155,64 +269,22 @@ func (c *Controller[request]) Start(ctx context.Context) error { // Set the internal context. c.ctx = ctx - c.Queue = c.NewQueue(c.Name, c.RateLimiter) - go func() { - <-ctx.Done() - c.Queue.ShutDown() - }() - wg := &sync.WaitGroup{} err := func() error { defer c.mu.Unlock() // TODO(pwittrock): Reconsider HandleCrash - defer utilruntime.HandleCrash() + defer utilruntime.HandleCrashWithLogger(c.LogConstructor(nil)) // NB(directxman12): launch the sources *before* trying to wait for the - // caches to sync so that they have a chance to register their intendeded + // caches to sync so that they have a chance to register their intended // caches. - for _, watch := range c.startWatches { - c.LogConstructor(nil).Info("Starting EventSource", "source", fmt.Sprintf("%s", watch)) - - if err := watch.Start(ctx, c.Queue); err != nil { - return err - } + if err := c.startEventSourcesAndQueueLocked(ctx); err != nil { + return err } - // Start the SharedIndexInformer factories to begin populating the SharedIndexInformer caches c.LogConstructor(nil).Info("Starting Controller") - for _, watch := range c.startWatches { - syncingSource, ok := watch.(source.TypedSyncingSource[request]) - if !ok { - continue - } - - if err := func() error { - // use a context with timeout for launching sources and syncing caches. - sourceStartCtx, cancel := context.WithTimeout(ctx, c.CacheSyncTimeout) - defer cancel() - - // WaitForSync waits for a definitive timeout, and returns if there - // is an error or a timeout - if err := syncingSource.WaitForSync(sourceStartCtx); err != nil { - err := fmt.Errorf("failed to wait for %s caches to sync: %w", c.Name, err) - c.LogConstructor(nil).Error(err, "Could not wait for Cache to sync") - return err - } - - return nil - }(); err != nil { - return err - } - } - - // All the watches have been started, we can reset the local slice. - // - // We should never hold watches more than necessary, each watch source can hold a backing cache, - // which won't be garbage collected if we hold a reference to it. - c.startWatches = nil - // Launch workers to process resources c.LogConstructor(nil).Info("Starting workers", "worker count", c.MaxConcurrentReconciles) wg.Add(c.MaxConcurrentReconciles) @@ -240,10 +312,96 @@ func (c *Controller[request]) Start(ctx context.Context) error { return nil } +// startEventSourcesAndQueueLocked launches all the sources registered with this controller and waits +// for them to sync. It returns an error if any of the sources fail to start or sync. +func (c *Controller[request]) startEventSourcesAndQueueLocked(ctx context.Context) error { + var retErr error + + c.didStartEventSourcesOnce.Do(func() { + queue := c.NewQueue(c.Name, c.RateLimiter) + if priorityQueue, isPriorityQueue := queue.(priorityqueue.PriorityQueue[request]); isPriorityQueue { + c.Queue = priorityQueue + } else { + c.Queue = &priorityQueueWrapper[request]{TypedRateLimitingInterface: queue} + } + go func() { + <-ctx.Done() + c.Queue.ShutDown() + }() + + errGroup := &errgroup.Group{} + for _, watch := range c.startWatches { + log := c.LogConstructor(nil) + _, ok := watch.(interface { + String() string + }) + if !ok { + log = log.WithValues("source", fmt.Sprintf("%T", watch)) + } else { + log = log.WithValues("source", fmt.Sprintf("%s", watch)) + } + didStartSyncingSource := &atomic.Bool{} + errGroup.Go(func() error { + // Use a timeout for starting and syncing the source to avoid silently + // blocking startup indefinitely if it doesn't come up. + sourceStartCtx, cancel := context.WithTimeout(ctx, c.CacheSyncTimeout) + defer cancel() + + sourceStartErrChan := make(chan error, 1) // Buffer chan to not leak goroutine if we time out + go func() { + defer close(sourceStartErrChan) + log.Info("Starting EventSource") + + if err := watch.Start(ctx, c.Queue); err != nil { + sourceStartErrChan <- err + return + } + syncingSource, ok := watch.(source.TypedSyncingSource[request]) + if !ok { + return + } + didStartSyncingSource.Store(true) + if err := syncingSource.WaitForSync(sourceStartCtx); err != nil { + err := fmt.Errorf("failed to wait for %s caches to sync %v: %w", c.Name, syncingSource, err) + log.Error(err, "Could not wait for Cache to sync") + sourceStartErrChan <- err + } + }() + + select { + case err := <-sourceStartErrChan: + return err + case <-sourceStartCtx.Done(): + if didStartSyncingSource.Load() { // We are racing with WaitForSync, wait for it to let it tell us what happened + return <-sourceStartErrChan + } + if ctx.Err() != nil { // Don't return an error if the root context got cancelled + return nil + } + return fmt.Errorf("timed out waiting for source %s to Start. Please ensure that its Start() method is non-blocking", watch) + } + }) + } + retErr = errGroup.Wait() + + // All the watches have been started, we can reset the local slice. + // + // We should never hold watches more than necessary, each watch source can hold a backing cache, + // which won't be garbage collected if we hold a reference to it. + c.startWatches = nil + + // Mark event sources as started after resetting the startWatches slice so that watches from + // a new Watch() call are immediately started. + c.startedEventSourcesAndQueue = true + }) + + return retErr +} + // processNextWorkItem will read a single work item off the workqueue and // attempt to process it, by calling the reconcileHandler. func (c *Controller[request]) processNextWorkItem(ctx context.Context) bool { - obj, shutdown := c.Queue.Get() + obj, priority, shutdown := c.Queue.GetWithPriority() if shutdown { // Stop working return false @@ -260,7 +418,7 @@ func (c *Controller[request]) processNextWorkItem(ctx context.Context) bool { ctrlmetrics.ActiveWorkers.WithLabelValues(c.Name).Add(1) defer ctrlmetrics.ActiveWorkers.WithLabelValues(c.Name).Add(-1) - c.reconcileHandler(ctx, obj) + c.reconcileHandler(ctx, obj, priority) return true } @@ -283,7 +441,7 @@ func (c *Controller[request]) initMetrics() { ctrlmetrics.ActiveWorkers.WithLabelValues(c.Name).Set(0) } -func (c *Controller[request]) reconcileHandler(ctx context.Context, req request) { +func (c *Controller[request]) reconcileHandler(ctx context.Context, req request, priority int) { // Update metrics after processing each item reconcileStartTS := time.Now() defer func() { @@ -306,12 +464,12 @@ func (c *Controller[request]) reconcileHandler(ctx context.Context, req request) if errors.Is(err, reconcile.TerminalError(nil)) { ctrlmetrics.TerminalReconcileErrors.WithLabelValues(c.Name).Inc() } else { - c.Queue.AddRateLimited(req) + c.Queue.AddWithOpts(priorityqueue.AddOpts{RateLimited: true, Priority: ptr.To(priority)}, req) } ctrlmetrics.ReconcileErrors.WithLabelValues(c.Name).Inc() ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, labelError).Inc() if !result.IsZero() { - log.Info("Warning: Reconciler returned both a non-zero result and a non-nil error. The result will always be ignored if the error is non-nil and the non-nil error causes reqeueuing with exponential backoff. For more details, see: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile#Reconciler") + log.Info("Warning: Reconciler returned both a non-zero result and a non-nil error. The result will always be ignored if the error is non-nil and the non-nil error causes requeuing with exponential backoff. For more details, see: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile#Reconciler") } log.Error(err, "Reconciler error") case result.RequeueAfter > 0: @@ -321,11 +479,11 @@ func (c *Controller[request]) reconcileHandler(ctx context.Context, req request) // We need to drive to stable reconcile loops before queuing due // to result.RequestAfter c.Queue.Forget(req) - c.Queue.AddAfter(req, result.RequeueAfter) + c.Queue.AddWithOpts(priorityqueue.AddOpts{After: result.RequeueAfter, Priority: ptr.To(priority)}, req) ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, labelRequeueAfter).Inc() - case result.Requeue: + case result.Requeue: //nolint: staticcheck // We have to handle it until it is removed log.V(5).Info("Reconcile done, requeueing") - c.Queue.AddRateLimited(req) + c.Queue.AddWithOpts(priorityqueue.AddOpts{RateLimited: true, Priority: ptr.To(priority)}, req) ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, labelRequeue).Inc() default: log.V(5).Info("Reconcile successful") @@ -363,3 +521,25 @@ type reconcileIDKey struct{} func addReconcileID(ctx context.Context, reconcileID types.UID) context.Context { return context.WithValue(ctx, reconcileIDKey{}, reconcileID) } + +type priorityQueueWrapper[request comparable] struct { + workqueue.TypedRateLimitingInterface[request] +} + +func (p *priorityQueueWrapper[request]) AddWithOpts(opts priorityqueue.AddOpts, items ...request) { + for _, item := range items { + switch { + case opts.RateLimited: + p.TypedRateLimitingInterface.AddRateLimited(item) + case opts.After > 0: + p.TypedRateLimitingInterface.AddAfter(item, opts.After) + default: + p.TypedRateLimitingInterface.Add(item) + } + } +} + +func (p *priorityQueueWrapper[request]) GetWithPriority() (request, int, bool) { + item, shutdown := p.TypedRateLimitingInterface.Get() + return item, 0, shutdown +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics/metrics.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics/metrics.go index fbf15669d5dc..450e9ae25b2d 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics/metrics.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics/metrics.go @@ -17,6 +17,8 @@ limitations under the License. package metrics import ( + "time" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "sigs.k8s.io/controller-runtime/pkg/metrics" @@ -60,6 +62,9 @@ var ( Help: "Length of time per reconciliation per controller", Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 40, 50, 60}, + NativeHistogramBucketFactor: 1.1, + NativeHistogramMaxBucketNumber: 100, + NativeHistogramMinResetDuration: 1 * time.Hour, }, []string{"controller"}) // WorkerCount is a prometheus metric which holds the number of @@ -88,7 +93,7 @@ func init() { ActiveWorkers, // expose process metrics like CPU, Memory, file descriptor usage etc. collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), - // expose Go runtime metrics like GC stats, memory stats etc. - collectors.NewGoCollector(), + // expose all Go runtime metrics like GC stats, memory stats etc. + collectors.NewGoCollector(collectors.WithGoCollectorRuntimeMetrics(collectors.MetricsAll)), ) } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/metrics/workqueue.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/metrics/workqueue.go new file mode 100644 index 000000000000..402319817bd3 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/metrics/workqueue.go @@ -0,0 +1,170 @@ +/* +Copyright 2018 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 ( + "strconv" + "time" + + "github.com/prometheus/client_golang/prometheus" + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/metrics" +) + +// This file is copied and adapted from k8s.io/component-base/metrics/prometheus/workqueue +// which registers metrics to the k8s legacy Registry. We require very +// similar functionality, but must register metrics to a different Registry. + +// Metrics subsystem and all keys used by the workqueue. +const ( + WorkQueueSubsystem = metrics.WorkQueueSubsystem + DepthKey = metrics.DepthKey + AddsKey = metrics.AddsKey + QueueLatencyKey = metrics.QueueLatencyKey + WorkDurationKey = metrics.WorkDurationKey + UnfinishedWorkKey = metrics.UnfinishedWorkKey + LongestRunningProcessorKey = metrics.LongestRunningProcessorKey + RetriesKey = metrics.RetriesKey +) + +var ( + depth = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Subsystem: WorkQueueSubsystem, + Name: DepthKey, + Help: "Current depth of workqueue by workqueue and priority", + }, []string{"name", "controller", "priority"}) + + adds = prometheus.NewCounterVec(prometheus.CounterOpts{ + Subsystem: WorkQueueSubsystem, + Name: AddsKey, + Help: "Total number of adds handled by workqueue", + }, []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), + NativeHistogramBucketFactor: 1.1, + NativeHistogramMaxBucketNumber: 100, + NativeHistogramMinResetDuration: 1 * time.Hour, + }, []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), + NativeHistogramBucketFactor: 1.1, + NativeHistogramMaxBucketNumber: 100, + NativeHistogramMinResetDuration: 1 * time.Hour, + }, []string{"name", "controller"}) + + unfinished = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Subsystem: WorkQueueSubsystem, + Name: UnfinishedWorkKey, + Help: "How many seconds of work has been done that " + + "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", "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", "controller"}) + + retries = prometheus.NewCounterVec(prometheus.CounterOpts{ + Subsystem: WorkQueueSubsystem, + Name: RetriesKey, + Help: "Total number of retries handled by workqueue", + }, []string{"name", "controller"}) +) + +func init() { + metrics.Registry.MustRegister(depth) + metrics.Registry.MustRegister(adds) + metrics.Registry.MustRegister(latency) + metrics.Registry.MustRegister(workDuration) + metrics.Registry.MustRegister(unfinished) + metrics.Registry.MustRegister(longestRunningProcessor) + metrics.Registry.MustRegister(retries) + + workqueue.SetProvider(WorkqueueMetricsProvider{}) +} + +type WorkqueueMetricsProvider struct{} + +func (WorkqueueMetricsProvider) NewDepthMetric(name string) workqueue.GaugeMetric { + return depth.WithLabelValues(name, name, "") // no priority +} + +func (WorkqueueMetricsProvider) NewAddsMetric(name string) workqueue.CounterMetric { + return adds.WithLabelValues(name, name) +} + +func (WorkqueueMetricsProvider) NewLatencyMetric(name string) workqueue.HistogramMetric { + return latency.WithLabelValues(name, name) +} + +func (WorkqueueMetricsProvider) NewWorkDurationMetric(name string) workqueue.HistogramMetric { + return workDuration.WithLabelValues(name, name) +} + +func (WorkqueueMetricsProvider) NewUnfinishedWorkSecondsMetric(name string) workqueue.SettableGaugeMetric { + return unfinished.WithLabelValues(name, name) +} + +func (WorkqueueMetricsProvider) NewLongestRunningProcessorSecondsMetric(name string) workqueue.SettableGaugeMetric { + return longestRunningProcessor.WithLabelValues(name, name) +} + +func (WorkqueueMetricsProvider) NewRetriesMetric(name string) workqueue.CounterMetric { + return retries.WithLabelValues(name, name) +} + +type MetricsProviderWithPriority interface { + workqueue.MetricsProvider + + NewDepthMetricWithPriority(name string) DepthMetricWithPriority +} + +// DepthMetricWithPriority represents a depth metric with priority. +type DepthMetricWithPriority interface { + Inc(priority int) + Dec(priority int) +} + +var _ MetricsProviderWithPriority = WorkqueueMetricsProvider{} + +func (WorkqueueMetricsProvider) NewDepthMetricWithPriority(name string) DepthMetricWithPriority { + return &depthWithPriorityMetric{lvs: []string{name, name}} +} + +type depthWithPriorityMetric struct { + lvs []string +} + +func (g *depthWithPriorityMetric) Inc(priority int) { + depth.WithLabelValues(append(g.lvs, strconv.Itoa(priority))...).Inc() +} + +func (g *depthWithPriorityMetric) Dec(priority int) { + depth.WithLabelValues(append(g.lvs, strconv.Itoa(priority))...).Dec() +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/source/event_handler.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/source/event_handler.go index 38432a1a79cc..7cc8c5155589 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/source/event_handler.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/source/event_handler.go @@ -32,6 +32,8 @@ import ( var log = logf.RuntimeLog.WithName("source").WithName("EventHandler") +var _ cache.ResourceEventHandler = &EventHandler[client.Object, any]{} + // NewEventHandler creates a new EventHandler. func NewEventHandler[object client.Object, request comparable]( ctx context.Context, @@ -57,19 +59,11 @@ type EventHandler[object client.Object, request comparable] struct { predicates []predicate.TypedPredicate[object] } -// HandlerFuncs converts EventHandler to a ResourceEventHandlerFuncs -// TODO: switch to ResourceEventHandlerDetailedFuncs with client-go 1.27 -func (e *EventHandler[object, request]) HandlerFuncs() cache.ResourceEventHandlerFuncs { - return cache.ResourceEventHandlerFuncs{ - AddFunc: e.OnAdd, - UpdateFunc: e.OnUpdate, - DeleteFunc: e.OnDelete, - } -} - // OnAdd creates CreateEvent and calls Create on EventHandler. -func (e *EventHandler[object, request]) OnAdd(obj interface{}) { - c := event.TypedCreateEvent[object]{} +func (e *EventHandler[object, request]) OnAdd(obj interface{}, isInInitialList bool) { + c := event.TypedCreateEvent[object]{ + IsInInitialList: isInInitialList, + } // Pull Object out of the object if o, ok := obj.(object); ok { diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/source/kind.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/source/kind.go index 4999edc43215..285424452319 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/source/kind.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/source/kind.go @@ -10,7 +10,9 @@ import ( "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/wait" + toolscache "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" + logf "sigs.k8s.io/controller-runtime/pkg/internal/log" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" @@ -18,6 +20,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" ) +var logKind = logf.RuntimeLog.WithName("source").WithName("Kind") + // Kind is used to provide a source of events originating inside the cluster from Watches (e.g. Pod Create). type Kind[object client.Object, request comparable] struct { // Type is the type of object to watch. e.g. &v1.Pod{} @@ -52,7 +56,7 @@ func (ks *Kind[object, request]) Start(ctx context.Context, queue workqueue.Type // 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.startedErr = make(chan error) + ks.startedErr = make(chan error, 1) // Buffer chan to not leak goroutines if WaitForSync isn't called go func() { var ( i cache.Informer @@ -68,12 +72,12 @@ func (ks *Kind[object, request]) Start(ctx context.Context, queue workqueue.Type kindMatchErr := &meta.NoKindMatchError{} switch { case errors.As(lastErr, &kindMatchErr): - log.Error(lastErr, "if kind is a CRD, it should be installed before calling Start", + logKind.Error(lastErr, "if kind is a CRD, it should be installed before calling Start", "kind", kindMatchErr.GroupKind) case runtime.IsNotRegisteredError(lastErr): - log.Error(lastErr, "kind must be registered to the Scheme") + logKind.Error(lastErr, "kind must be registered to the Scheme") default: - log.Error(lastErr, "failed to get informer from cache") + logKind.Error(lastErr, "failed to get informer from cache") } return false, nil // Retry. } @@ -87,7 +91,9 @@ func (ks *Kind[object, request]) Start(ctx context.Context, queue workqueue.Type return } - _, err := i.AddEventHandler(NewEventHandler(ctx, queue, ks.Handler, ks.Predicates).HandlerFuncs()) + _, err := i.AddEventHandlerWithOptions(NewEventHandler(ctx, queue, ks.Handler, ks.Predicates), toolscache.HandlerOptions{ + Logger: &logKind, + }) if err != nil { ks.startedErr <- err return diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/apiserver.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/apiserver.go index c9a1a232ea94..aadb69e84f18 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/apiserver.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/apiserver.go @@ -374,7 +374,12 @@ func (s *APIServer) populateAPIServerCerts() error { return err } - servingCerts, err := ca.NewServingCert() + servingAddresses := []string{"localhost"} + if s.SecureServing.ListenAddr.Address != "" { + servingAddresses = append(servingAddresses, s.SecureServing.ListenAddr.Address) + } + + servingCerts, err := ca.NewServingCert(servingAddresses...) if err != nil { return err } @@ -384,10 +389,10 @@ func (s *APIServer) populateAPIServerCerts() error { return err } - if err := os.WriteFile(filepath.Join(s.CertDir, "apiserver.crt"), certData, 0640); err != nil { //nolint:gosec + if err := os.WriteFile(filepath.Join(s.CertDir, "apiserver.crt"), certData, 0o640); err != nil { return err } - if err := os.WriteFile(filepath.Join(s.CertDir, "apiserver.key"), keyData, 0640); err != nil { //nolint:gosec + if err := os.WriteFile(filepath.Join(s.CertDir, "apiserver.key"), keyData, 0o640); err != nil { return err } @@ -404,10 +409,10 @@ func (s *APIServer) populateAPIServerCerts() error { return err } - if err := os.WriteFile(filepath.Join(s.CertDir, saCertFile), saCert, 0640); err != nil { //nolint:gosec + if err := os.WriteFile(filepath.Join(s.CertDir, saCertFile), saCert, 0o640); err != nil { return err } - return os.WriteFile(filepath.Join(s.CertDir, saKeyFile), saKey, 0640) //nolint:gosec + return os.WriteFile(filepath.Join(s.CertDir, saKeyFile), saKey, 0o640) } // Stop stops this process gracefully, waits for its termination, and cleans up @@ -421,6 +426,9 @@ func (s *APIServer) Stop() error { return err } } + if s.Authn == nil { + return nil + } return s.Authn.Stop() } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/auth.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/auth.go index 16c86a712c11..b44035ebf2cf 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/auth.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/auth.go @@ -128,7 +128,7 @@ func (c *CertAuthn) Start() error { return fmt.Errorf("start called before configure") } caCrt := c.ca.CA.CertBytes() - if err := os.WriteFile(c.caCrtPath(), caCrt, 0640); err != nil { //nolint:gosec + if err := os.WriteFile(c.caCrtPath(), caCrt, 0640); err != nil { return fmt.Errorf("unable to save the client certificate CA to %s: %w", c.caCrtPath(), err) } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/etcd.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/etcd.go index c30d21329527..98ffe3ac5e22 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/etcd.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane/etcd.go @@ -159,6 +159,10 @@ func (e *Etcd) setProcessState() error { // Stop stops this process gracefully, waits for its termination, and cleans up // the DataDir if necessary. func (e *Etcd) Stop() error { + if e.processState == nil { + return nil + } + if e.processState.DirNeedsCleaning { e.DataDir = "" // reset the directory if it was randomly allocated, so that we can safely restart } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/process/process.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/process/process.go index 03f252524a37..0d541921e2a1 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/process/process.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/testing/process/process.go @@ -215,7 +215,7 @@ func pollURLUntilOK(url url.URL, interval time.Duration, ready chan bool, stopCh // there's probably certs *somewhere*, // but it's fine to just skip validating // them for health checks during testing - InsecureSkipVerify: true, //nolint:gosec + InsecureSkipVerify: true, }, }, } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/leaderelection/leader_election.go b/vendor/sigs.k8s.io/controller-runtime/pkg/leaderelection/leader_election.go index 5cc253917a3b..6c013e7992a8 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/leaderelection/leader_election.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/leaderelection/leader_election.go @@ -56,6 +56,10 @@ type Options struct { // Without that, a single slow response from the API server can result // in losing leadership. RenewDeadline time.Duration + + // LeaderLabels are an optional set of labels that will be set on the lease object + // when this replica becomes leader + LeaderLabels map[string]string } // NewResourceLock creates a new resource lock for use in a leader election loop. @@ -63,7 +67,6 @@ func NewResourceLock(config *rest.Config, recorderProvider recorder.Provider, op if !options.LeaderElection { return nil, nil } - // Default resource lock to "leases". The previous default (from v0.7.0 to v0.11.x) was configmapsleases, which was // used to migrate from configmaps to leases. Since the default was "configmapsleases" for over a year, spanning // five minor releases, any actively maintained operators are very likely to have a released version that uses @@ -93,22 +96,21 @@ func NewResourceLock(config *rest.Config, recorderProvider recorder.Provider, op } id = id + "_" + string(uuid.NewUUID()) - // Construct clients for leader election - rest.AddUserAgent(config, "leader-election") + // Construct config for leader election + config = rest.AddUserAgent(config, "leader-election") + // Timeout set for a client used to contact to Kubernetes should be lower than + // RenewDeadline to keep a single hung request from forcing a leader loss. + // Setting it to max(time.Second, RenewDeadline/2) as a reasonable heuristic. if options.RenewDeadline != 0 { - return resourcelock.NewFromKubeconfig(options.LeaderElectionResourceLock, - options.LeaderElectionNamespace, - options.LeaderElectionID, - resourcelock.ResourceLockConfig{ - Identity: id, - EventRecorder: recorderProvider.GetEventRecorderFor(id), - }, - config, - options.RenewDeadline, - ) + timeout := options.RenewDeadline / 2 + if timeout < time.Second { + timeout = time.Second + } + config.Timeout = timeout } + // Construct clients for leader election corev1Client, err := corev1client.NewForConfig(config) if err != nil { return nil, err @@ -118,7 +120,8 @@ func NewResourceLock(config *rest.Config, recorderProvider recorder.Provider, op if err != nil { return nil, err } - return resourcelock.New(options.LeaderElectionResourceLock, + + return resourcelock.NewWithLabels(options.LeaderElectionResourceLock, options.LeaderElectionNamespace, options.LeaderElectionID, corev1Client, @@ -127,6 +130,7 @@ func NewResourceLock(config *rest.Config, recorderProvider recorder.Provider, op Identity: id, EventRecorder: recorderProvider.GetEventRecorderFor(id), }, + options.LeaderLabels, ) } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/log/warning_handler.go b/vendor/sigs.k8s.io/controller-runtime/pkg/log/warning_handler.go index e9522632d3d9..413b56d2e492 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/log/warning_handler.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/log/warning_handler.go @@ -17,13 +17,12 @@ limitations under the License. package log import ( + "context" "sync" - - "github.com/go-logr/logr" ) // KubeAPIWarningLoggerOptions controls the behavior -// of a rest.WarningHandler constructed using NewKubeAPIWarningLogger(). +// of a rest.WarningHandlerWithContext constructed using NewKubeAPIWarningLogger(). type KubeAPIWarningLoggerOptions struct { // Deduplicate indicates a given warning message should only be written once. // Setting this to true in a long-running process handling many warnings can @@ -33,10 +32,8 @@ type KubeAPIWarningLoggerOptions struct { // KubeAPIWarningLogger is a wrapper around // a provided logr.Logger that implements the -// rest.WarningHandler interface. +// rest.WarningHandlerWithContext interface. type KubeAPIWarningLogger struct { - // logger is used to log responses with the warning header - logger logr.Logger // opts contain options controlling warning output opts KubeAPIWarningLoggerOptions // writtenLock gurads written @@ -46,9 +43,11 @@ type KubeAPIWarningLogger struct { written map[string]struct{} } -// HandleWarningHeader handles logging for responses from API server that are -// warnings with code being 299 and uses a logr.Logger for its logging purposes. -func (l *KubeAPIWarningLogger) HandleWarningHeader(code int, agent string, message string) { +// HandleWarningHeaderWithContext handles logging for responses from API server that are +// warnings with code being 299 and uses a logr.Logger from context for its logging purposes. +func (l *KubeAPIWarningLogger) HandleWarningHeaderWithContext(ctx context.Context, code int, _ string, message string) { + log := FromContext(ctx) + if code != 299 || len(message) == 0 { return } @@ -62,13 +61,13 @@ func (l *KubeAPIWarningLogger) HandleWarningHeader(code int, agent string, messa } l.written[message] = struct{}{} } - l.logger.Info(message) + log.Info(message) } -// NewKubeAPIWarningLogger returns an implementation of rest.WarningHandler that logs warnings -// with code = 299 to the provided logr.Logger. -func NewKubeAPIWarningLogger(l logr.Logger, opts KubeAPIWarningLoggerOptions) *KubeAPIWarningLogger { - h := &KubeAPIWarningLogger{logger: l, opts: opts} +// NewKubeAPIWarningLogger returns an implementation of rest.WarningHandlerWithContext that logs warnings +// with code = 299 to the logger passed into HandleWarningHeaderWithContext via the context. +func NewKubeAPIWarningLogger(opts KubeAPIWarningLoggerOptions) *KubeAPIWarningLogger { + h := &KubeAPIWarningLogger{opts: opts} if opts.Deduplicate { h.written = map[string]struct{}{} } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/flags.go b/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/flags.go index fb492b14da8b..2c88ad42abe3 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/flags.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/flags.go @@ -32,6 +32,7 @@ var levelStrings = map[string]zapcore.Level{ "debug": zap.DebugLevel, "info": zap.InfoLevel, "error": zap.ErrorLevel, + "panic": zap.PanicLevel, } var stackLevelStrings = map[string]zapcore.Level{ diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/kube_helpers.go b/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/kube_helpers.go index 3b4ebfdaa0b7..c47fe6646f7b 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/kube_helpers.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/kube_helpers.go @@ -78,6 +78,7 @@ func (w kubeObjectWrapper) MarshalLogObject(enc zapcore.ObjectEncoder) error { func (k *KubeAwareEncoder) Clone() zapcore.Encoder { return &KubeAwareEncoder{ Encoder: k.Encoder.Clone(), + Verbose: k.Verbose, } } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/zap.go b/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/zap.go index 3a114667bdbd..607b6680d588 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/zap.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/log/zap/zap.go @@ -247,7 +247,7 @@ func NewRaw(opts ...Opts) *zap.Logger { // Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn) // Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error) // - zap-encoder: Zap log encoding (one of 'json' or 'console') -// - zap-log-level: Zap Level to configure the verbosity of logging. Can be one of 'debug', 'info', 'error', +// - zap-log-level: Zap Level to configure the verbosity of logging. Can be one of 'debug', 'info', 'error', 'panic' // or any integer value > 0 which corresponds to custom debug levels of increasing verbosity"). // - zap-stacktrace-level: Zap Level at and above which stacktraces are captured (one of 'info', 'error' or 'panic') // - zap-time-encoding: Zap time encoding (one of 'epoch', 'millis', 'nano', 'iso8601', 'rfc3339' or 'rfc3339nano'), @@ -271,7 +271,7 @@ func (o *Options) BindFlags(fs *flag.FlagSet) { o.Level = fromFlag } fs.Var(&levelVal, "zap-log-level", - "Zap Level to configure the verbosity of logging. Can be one of 'debug', 'info', 'error', "+ + "Zap Level to configure the verbosity of logging. Can be one of 'debug', 'info', 'error', 'panic'"+ "or any integer value > 0 which corresponds to custom debug levels of increasing verbosity") // Set the StrackTrace Level diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/manager/internal.go b/vendor/sigs.k8s.io/controller-runtime/pkg/manager/internal.go index e5204a7506b9..a9f91cbdd56b 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/manager/internal.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/manager/internal.go @@ -439,6 +439,11 @@ func (cm *controllerManager) Start(ctx context.Context) (err error) { return fmt.Errorf("failed to start other runnables: %w", err) } + // Start WarmupRunnables and wait for warmup to complete. + if err := cm.runnables.Warmup.Start(cm.internalCtx); err != nil { + return fmt.Errorf("failed to start warmup runnables: %w", err) + } + // Start the leader election and all required runnables. { ctx, cancel := context.WithCancel(context.Background()) @@ -534,6 +539,18 @@ func (cm *controllerManager) engageStopProcedure(stopComplete <-chan struct{}) e }() go func() { + go func() { + // Stop the warmup runnables in a separate goroutine to avoid blocking. + // It is important to stop the warmup runnables in parallel with the other runnables + // since we cannot assume ordering of whether or not one of the warmup runnables or one + // of the other runnables is holding a lock. + // Cancelling the wrong runnable (one that is not holding the lock) will cause the + // shutdown sequence to block indefinitely as it will wait for the runnable that is + // holding the lock to finish. + cm.logger.Info("Stopping and waiting for warmup runnables") + cm.runnables.Warmup.StopAndWait(cm.shutdownCtx) + }() + // First stop the non-leader election runnables. cm.logger.Info("Stopping and waiting for non leader election runnables") cm.runnables.Others.StopAndWait(cm.shutdownCtx) diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/manager/manager.go b/vendor/sigs.k8s.io/controller-runtime/pkg/manager/manager.go index 92906fe6ca77..e0e94245e77c 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/manager/manager.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/manager/manager.go @@ -201,10 +201,15 @@ type Options struct { // LeaseDuration time first. LeaderElectionReleaseOnCancel bool + // LeaderElectionLabels allows a controller to supplement all leader election api calls with a set of custom labels based on + // the replica attempting to acquire leader status. + LeaderElectionLabels map[string]string + // LeaderElectionResourceLockInterface allows to provide a custom resourcelock.Interface that was created outside // of the controller-runtime. If this value is set the options LeaderElectionID, LeaderElectionNamespace, - // LeaderElectionResourceLock, LeaseDuration, RenewDeadline and RetryPeriod will be ignored. This can be useful if you - // want to use a locking mechanism that is currently not supported, like a MultiLock across two Kubernetes clusters. + // LeaderElectionResourceLock, LeaseDuration, RenewDeadline, RetryPeriod and LeaderElectionLeases will be ignored. + // This can be useful if you want to use a locking mechanism that is currently not supported, like a MultiLock across + // two Kubernetes clusters. LeaderElectionResourceLockInterface resourcelock.Interface // LeaseDuration is the duration that non-leader candidates will @@ -314,6 +319,15 @@ type LeaderElectionRunnable interface { NeedLeaderElection() bool } +// warmupRunnable knows if a Runnable requires warmup. A warmup runnable is a runnable +// that should be run when the manager is started but before it becomes leader. +// Note: Implementing this interface is only useful when LeaderElection can be enabled, as the +// behavior when leaderelection is not enabled is to run LeaderElectionRunnables immediately. +type warmupRunnable interface { + // Warmup will be called when the manager is started but before it becomes leader. + Warmup(context.Context) error +} + // New returns a new Manager for creating Controllers. // Note that if ContentType in the given config is not set, "application/vnd.kubernetes.protobuf" // will be used for all built-in resources of Kubernetes, and "application/json" is for other types @@ -390,6 +404,7 @@ func New(config *rest.Config, options Options) (Manager, error) { LeaderElectionID: options.LeaderElectionID, LeaderElectionNamespace: options.LeaderElectionNamespace, RenewDeadline: *options.RenewDeadline, + LeaderLabels: options.LeaderElectionLabels, }) if err != nil { return nil, err @@ -417,7 +432,7 @@ func New(config *rest.Config, options Options) (Manager, error) { } errChan := make(chan error, 1) - runnables := newRunnables(options.BaseContext, errChan) + runnables := newRunnables(options.BaseContext, errChan).withLogger(options.Logger) return &controllerManager{ stopProcedureEngaged: ptr.To(int64(0)), cluster: cluster, @@ -544,6 +559,10 @@ func setOptionsDefaults(options Options) Options { options.Logger = log.Log } + if options.Controller.Logger.GetSink() == nil { + options.Controller.Logger = options.Logger + } + if options.BaseContext == nil { options.BaseContext = defaultBaseContext } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/manager/runnable_group.go b/vendor/sigs.k8s.io/controller-runtime/pkg/manager/runnable_group.go index db5cda7c88f6..53e29fc56fad 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/manager/runnable_group.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/manager/runnable_group.go @@ -5,6 +5,7 @@ import ( "errors" "sync" + "github.com/go-logr/logr" "sigs.k8s.io/controller-runtime/pkg/webhook" ) @@ -32,6 +33,7 @@ type runnables struct { Webhooks *runnableGroup Caches *runnableGroup LeaderElection *runnableGroup + Warmup *runnableGroup Others *runnableGroup } @@ -42,10 +44,21 @@ func newRunnables(baseContext BaseContextFunc, errChan chan error) *runnables { Webhooks: newRunnableGroup(baseContext, errChan), Caches: newRunnableGroup(baseContext, errChan), LeaderElection: newRunnableGroup(baseContext, errChan), + Warmup: newRunnableGroup(baseContext, errChan), Others: newRunnableGroup(baseContext, errChan), } } +// withLogger returns the runnables with the logger set for all runnable groups. +func (r *runnables) withLogger(logger logr.Logger) *runnables { + r.HTTPServers.withLogger(logger) + r.Webhooks.withLogger(logger) + r.Caches.withLogger(logger) + r.LeaderElection.withLogger(logger) + r.Others.withLogger(logger) + return r +} + // Add adds a runnable to closest group of runnable that they belong to. // // Add should be able to be called before and after Start, but not after StopAndWait. @@ -65,8 +78,20 @@ func (r *runnables) Add(fn Runnable) error { }) case webhook.Server: return r.Webhooks.Add(fn, nil) - case LeaderElectionRunnable: - if !runnable.NeedLeaderElection() { + case warmupRunnable, LeaderElectionRunnable: + if warmupRunnable, ok := fn.(warmupRunnable); ok { + if err := r.Warmup.Add(RunnableFunc(warmupRunnable.Warmup), nil); err != nil { + return err + } + } + + leaderElectionRunnable, ok := fn.(LeaderElectionRunnable) + if !ok { + // If the runnable is not a LeaderElectionRunnable, add it to the leader election group for backwards compatibility + return r.LeaderElection.Add(fn, nil) + } + + if !leaderElectionRunnable.NeedLeaderElection() { return r.Others.Add(fn, nil) } return r.LeaderElection.Add(fn, nil) @@ -105,6 +130,9 @@ type runnableGroup struct { // wg is an internal sync.WaitGroup that allows us to properly stop // and wait for all the runnables to finish before returning. wg *sync.WaitGroup + + // logger is used for logging when errors are dropped during shutdown + logger logr.Logger } func newRunnableGroup(baseContext BaseContextFunc, errChan chan error) *runnableGroup { @@ -113,12 +141,18 @@ func newRunnableGroup(baseContext BaseContextFunc, errChan chan error) *runnable errChan: errChan, ch: make(chan *readyRunnable), wg: new(sync.WaitGroup), + logger: logr.Discard(), // Default to no-op logger } r.ctx, r.cancel = context.WithCancel(baseContext()) return r } +// withLogger sets the logger for this runnable group. +func (r *runnableGroup) withLogger(logger logr.Logger) { + r.logger = logger +} + // Started returns true if the group has started. func (r *runnableGroup) Started() bool { r.start.Lock() @@ -224,7 +258,27 @@ func (r *runnableGroup) reconcile() { // Start the runnable. if err := rn.Start(r.ctx); err != nil { - r.errChan <- err + // Check if we're during the shutdown process. + r.stop.RLock() + isStopped := r.stopped + r.stop.RUnlock() + + if isStopped { + // During shutdown, try to send error first (error drain goroutine might still be running) + // but drop if it would block to prevent goroutine leaks + select { + case r.errChan <- err: + // Error sent successfully (error drain goroutine is still running) + default: + // Error drain goroutine has exited, drop error to prevent goroutine leak + if !errors.Is(err, context.Canceled) { // don't log context.Canceled errors as they are expected during shutdown + r.logger.Info("error dropped during shutdown to prevent goroutine leak", "error", err) + } + } + } else { + // During normal operation, always try to send errors (may block briefly) + r.errChan <- err + } } }(runnable) } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/manager/server.go b/vendor/sigs.k8s.io/controller-runtime/pkg/manager/server.go index 76f6165b5323..1983165da873 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/manager/server.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/manager/server.go @@ -70,7 +70,7 @@ func (s *Server) Start(ctx context.Context) error { shutdownCtx := context.Background() if s.ShutdownTimeout != nil { var shutdownCancel context.CancelFunc - shutdownCtx, shutdownCancel = context.WithTimeout(context.Background(), *s.ShutdownTimeout) + shutdownCtx, shutdownCancel = context.WithTimeout(shutdownCtx, *s.ShutdownTimeout) defer shutdownCancel() } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/server/server.go b/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/server/server.go index 5eb0c62a7239..939c333f7a53 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/server/server.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/server/server.go @@ -275,7 +275,7 @@ func (s *defaultServer) createListener(ctx context.Context, log logr.Logger) (ne return s.options.ListenConfig.Listen(ctx, "tcp", s.options.BindAddress) } - cfg := &tls.Config{ //nolint:gosec + cfg := &tls.Config{ NextProtos: []string{"h2"}, } // fallback TLS config ready, will now mutate if passer wants full control over it diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/workqueue.go b/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/workqueue.go index 590653e70ffa..cd7ccc773eaa 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/workqueue.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/workqueue.go @@ -16,15 +16,6 @@ limitations under the License. package metrics -import ( - "github.com/prometheus/client_golang/prometheus" - "k8s.io/client-go/util/workqueue" -) - -// This file is copied and adapted from k8s.io/component-base/metrics/prometheus/workqueue -// which registers metrics to the k8s legacy Registry. We require very -// similar functionality, but must register metrics to a different Registry. - // Metrics subsystem and all keys used by the workqueue. const ( WorkQueueSubsystem = "workqueue" @@ -36,95 +27,3 @@ const ( LongestRunningProcessorKey = "longest_running_processor_seconds" RetriesKey = "retries_total" ) - -var ( - depth = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Subsystem: WorkQueueSubsystem, - Name: DepthKey, - Help: "Current depth of workqueue", - }, []string{"name", "controller"}) - - adds = prometheus.NewCounterVec(prometheus.CounterOpts{ - Subsystem: WorkQueueSubsystem, - Name: AddsKey, - Help: "Total number of adds handled by workqueue", - }, []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", "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", "controller"}) - - unfinished = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Subsystem: WorkQueueSubsystem, - Name: UnfinishedWorkKey, - Help: "How many seconds of work has been done that " + - "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", "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", "controller"}) - - retries = prometheus.NewCounterVec(prometheus.CounterOpts{ - Subsystem: WorkQueueSubsystem, - Name: RetriesKey, - Help: "Total number of retries handled by workqueue", - }, []string{"name", "controller"}) -) - -func init() { - Registry.MustRegister(depth) - Registry.MustRegister(adds) - Registry.MustRegister(latency) - Registry.MustRegister(workDuration) - Registry.MustRegister(unfinished) - Registry.MustRegister(longestRunningProcessor) - Registry.MustRegister(retries) - - workqueue.SetProvider(workqueueMetricsProvider{}) -} - -type workqueueMetricsProvider struct{} - -func (workqueueMetricsProvider) NewDepthMetric(name string) workqueue.GaugeMetric { - return depth.WithLabelValues(name, name) -} - -func (workqueueMetricsProvider) NewAddsMetric(name string) workqueue.CounterMetric { - return adds.WithLabelValues(name, name) -} - -func (workqueueMetricsProvider) NewLatencyMetric(name string) workqueue.HistogramMetric { - return latency.WithLabelValues(name, name) -} - -func (workqueueMetricsProvider) NewWorkDurationMetric(name string) workqueue.HistogramMetric { - return workDuration.WithLabelValues(name, name) -} - -func (workqueueMetricsProvider) NewUnfinishedWorkSecondsMetric(name string) workqueue.SettableGaugeMetric { - return unfinished.WithLabelValues(name, name) -} - -func (workqueueMetricsProvider) NewLongestRunningProcessorSecondsMetric(name string) workqueue.SettableGaugeMetric { - return longestRunningProcessor.WithLabelValues(name, name) -} - -func (workqueueMetricsProvider) NewRetriesMetric(name string) workqueue.CounterMetric { - return retries.WithLabelValues(name, name) -} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/predicate/predicate.go b/vendor/sigs.k8s.io/controller-runtime/pkg/predicate/predicate.go index 90918db57aed..9f24cb178cb3 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/predicate/predicate.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/predicate/predicate.go @@ -47,13 +47,15 @@ type TypedPredicate[object any] interface { Generic(event.TypedGenericEvent[object]) bool } -var _ Predicate = Funcs{} -var _ Predicate = ResourceVersionChangedPredicate{} -var _ Predicate = GenerationChangedPredicate{} -var _ Predicate = AnnotationChangedPredicate{} -var _ Predicate = or[client.Object]{} -var _ Predicate = and[client.Object]{} -var _ Predicate = not[client.Object]{} +var ( + _ Predicate = Funcs{} + _ Predicate = ResourceVersionChangedPredicate{} + _ Predicate = GenerationChangedPredicate{} + _ Predicate = AnnotationChangedPredicate{} + _ Predicate = or[client.Object]{} + _ Predicate = and[client.Object]{} + _ Predicate = not[client.Object]{} +) // Funcs is a function that implements Predicate. type Funcs = TypedFuncs[client.Object] @@ -173,7 +175,8 @@ func (TypedResourceVersionChangedPredicate[T]) Update(e event.TypedUpdateEvent[T // 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. +// For CustomResource objects the Generation is incremented when spec is changed, or status changed and status not modeled as subresource. +// subresource status update will not increase Generation. // // Caveats: // @@ -191,7 +194,8 @@ type GenerationChangedPredicate = TypedGenerationChangedPredicate[client.Object] // 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. +// For CustomResource objects the Generation is incremented when spec is changed, or status changed and status not modeled as subresource. +// subresource status update will not increase Generation. // // Caveats: // @@ -257,11 +261,10 @@ func (TypedAnnotationChangedPredicate[object]) Update(e event.TypedUpdateEvent[o // This predicate will skip update events that have no change in the object's label. // It is intended to be used in conjunction with the GenerationChangedPredicate, as in the following example: // -// Controller.Watch( -// -// &source.Kind{Type: v1.MyCustomKind}, -// &handler.EnqueueRequestForObject{}, -// predicate.Or(predicate.GenerationChangedPredicate{}, predicate.LabelChangedPredicate{})) +// Controller.Watch( +// &source.Kind{Type: v1.MyCustomKind}, +// &handler.EnqueueRequestForObject{}, +// predicate.Or(predicate.GenerationChangedPredicate{}, predicate.LabelChangedPredicate{})) // // 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. diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/reconcile/reconcile.go b/vendor/sigs.k8s.io/controller-runtime/pkg/reconcile/reconcile.go index ee63f681cccf..c98b1864ef3e 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/reconcile/reconcile.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/reconcile/reconcile.go @@ -28,7 +28,17 @@ import ( // Result contains the result of a Reconciler invocation. type Result struct { - // Requeue tells the Controller to requeue the reconcile key. Defaults to false. + // Requeue tells the Controller to perform a ratelimited requeue + // using the workqueues ratelimiter. Defaults to false. + // + // This setting is deprecated as it causes confusion and there is + // no good reason to use it. When waiting for an external event to + // happen, either the duration until it is supposed to happen or an + // appropriate poll interval should be used, rather than an + // interval emitted by a ratelimiter whose purpose it is to control + // retry on error. + // + // Deprecated: Use `RequeueAfter` instead. Requeue bool // RequeueAfter if greater than 0, tells the Controller to requeue the reconcile key after the Duration. diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/source/source.go b/vendor/sigs.k8s.io/controller-runtime/pkg/source/source.go index 267a6470b86f..c2c2dc4e0739 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/source/source.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/source/source.go @@ -22,11 +22,13 @@ import ( "fmt" "sync" + toolscache "k8s.io/client-go/tools/cache" "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" + logf "sigs.k8s.io/controller-runtime/pkg/internal/log" internal "sigs.k8s.io/controller-runtime/pkg/internal/source" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -34,6 +36,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" ) +var logInformer = logf.RuntimeLog.WithName("source").WithName("Informer") + // 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. // @@ -282,7 +286,9 @@ func (is *Informer) Start(ctx context.Context, queue workqueue.TypedRateLimiting return errors.New("must specify Informer.Handler") } - _, err := is.Informer.AddEventHandler(internal.NewEventHandler(ctx, queue, is.Handler, is.Predicates).HandlerFuncs()) + _, err := is.Informer.AddEventHandlerWithOptions(internal.NewEventHandler(ctx, queue, is.Handler, is.Predicates), toolscache.HandlerOptions{ + Logger: &logInformer, + }) if err != nil { return err } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/defaulter.go b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/defaulter.go deleted file mode 100644 index efbbf6028277..000000000000 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/defaulter.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright 2018 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 admission - -import ( - "context" - "encoding/json" - "net/http" - - admissionv1 "k8s.io/api/admission/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" -) - -// Defaulter defines functions for setting defaults on resources. -// Deprecated: Ue CustomDefaulter instead. -type Defaulter interface { - runtime.Object - Default() -} - -// DefaultingWebhookFor creates a new Webhook for Defaulting the provided type. -// Deprecated: Use WithCustomDefaulter instead. -func DefaultingWebhookFor(scheme *runtime.Scheme, defaulter Defaulter) *Webhook { - return &Webhook{ - Handler: &mutatingHandler{defaulter: defaulter, decoder: NewDecoder(scheme)}, - } -} - -type mutatingHandler struct { - defaulter Defaulter - decoder Decoder -} - -// Handle handles admission requests. -func (h *mutatingHandler) Handle(ctx context.Context, req Request) Response { - if h.decoder == nil { - panic("decoder should never be nil") - } - if h.defaulter == nil { - panic("defaulter should never be nil") - } - - // always skip when a DELETE operation received in mutation handler - // describe in https://github.com/kubernetes-sigs/controller-runtime/issues/1762 - if req.Operation == admissionv1.Delete { - return Response{AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: true, - Result: &metav1.Status{ - Code: http.StatusOK, - }, - }} - } - - // Get the object in the request - obj := h.defaulter.DeepCopyObject().(Defaulter) - if err := h.decoder.Decode(req, obj); err != nil { - return Errored(http.StatusBadRequest, err) - } - - // Default the object - obj.Default() - marshalled, err := json.Marshal(obj) - if err != nil { - return Errored(http.StatusInternalServerError, err) - } - - // Create the patch - return PatchResponseFromRaw(req.Object.Raw, marshalled) -} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/defaulter_custom.go b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/defaulter_custom.go index d15dec7a0518..a703cbd2c54c 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/defaulter_custom.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/defaulter_custom.go @@ -21,11 +21,14 @@ import ( "encoding/json" "errors" "net/http" + "slices" + "gomodules.xyz/jsonpatch/v2" admissionv1 "k8s.io/api/admission/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" ) // CustomDefaulter defines functions for setting defaults on resources. @@ -33,17 +36,41 @@ type CustomDefaulter interface { Default(ctx context.Context, obj runtime.Object) error } +type defaulterOptions struct { + removeUnknownOrOmitableFields bool +} + +// DefaulterOption defines the type of a CustomDefaulter's option +type DefaulterOption func(*defaulterOptions) + +// DefaulterRemoveUnknownOrOmitableFields makes the defaulter prune fields that are in the json object retrieved by the +// webhook but not in the local go type json representation. This happens for example when the CRD in the apiserver has +// fields that our go type doesn't know about, because it's outdated, or the field has a zero value and is `omitempty`. +func DefaulterRemoveUnknownOrOmitableFields(o *defaulterOptions) { + o.removeUnknownOrOmitableFields = true +} + // WithCustomDefaulter creates a new Webhook for a CustomDefaulter interface. -func WithCustomDefaulter(scheme *runtime.Scheme, obj runtime.Object, defaulter CustomDefaulter) *Webhook { +func WithCustomDefaulter(scheme *runtime.Scheme, obj runtime.Object, defaulter CustomDefaulter, opts ...DefaulterOption) *Webhook { + options := &defaulterOptions{} + for _, o := range opts { + o(options) + } return &Webhook{ - Handler: &defaulterForType{object: obj, defaulter: defaulter, decoder: NewDecoder(scheme)}, + Handler: &defaulterForType{ + object: obj, + defaulter: defaulter, + decoder: NewDecoder(scheme), + removeUnknownOrOmitableFields: options.removeUnknownOrOmitableFields, + }, } } type defaulterForType struct { - defaulter CustomDefaulter - object runtime.Object - decoder Decoder + defaulter CustomDefaulter + object runtime.Object + decoder Decoder + removeUnknownOrOmitableFields bool } // Handle handles admission requests. @@ -76,6 +103,12 @@ func (h *defaulterForType) Handle(ctx context.Context, req Request) Response { return Errored(http.StatusBadRequest, err) } + // Keep a copy of the object if needed + var originalObj runtime.Object + if !h.removeUnknownOrOmitableFields { + originalObj = obj.DeepCopyObject() + } + // Default the object if err := h.defaulter.Default(ctx, obj); err != nil { var apiStatus apierrors.APIStatus @@ -90,5 +123,43 @@ func (h *defaulterForType) Handle(ctx context.Context, req Request) Response { if err != nil { return Errored(http.StatusInternalServerError, err) } - return PatchResponseFromRaw(req.Object.Raw, marshalled) + + handlerResponse := PatchResponseFromRaw(req.Object.Raw, marshalled) + if !h.removeUnknownOrOmitableFields { + handlerResponse = h.dropSchemeRemovals(handlerResponse, originalObj, req.Object.Raw) + } + return handlerResponse +} + +func (h *defaulterForType) dropSchemeRemovals(r Response, original runtime.Object, raw []byte) Response { + const opRemove = "remove" + if !r.Allowed || r.PatchType == nil { + return r + } + + // If we don't have removals in the patch. + if !slices.ContainsFunc(r.Patches, func(o jsonpatch.JsonPatchOperation) bool { return o.Operation == opRemove }) { + return r + } + + // Get the raw to original patch + marshalledOriginal, err := json.Marshal(original) + if err != nil { + return Errored(http.StatusInternalServerError, err) + } + + patchOriginal, err := jsonpatch.CreatePatch(raw, marshalledOriginal) + if err != nil { + return Errored(http.StatusInternalServerError, err) + } + removedByScheme := sets.New(slices.DeleteFunc(patchOriginal, func(p jsonpatch.JsonPatchOperation) bool { return p.Operation != opRemove })...) + + r.Patches = slices.DeleteFunc(r.Patches, func(p jsonpatch.JsonPatchOperation) bool { + return p.Operation == opRemove && removedByScheme.Has(p) + }) + + if len(r.Patches) == 0 { + r.PatchType = nil + } + return r } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/multi.go b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/multi.go index 2f7820d04b47..ef9c45624817 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/multi.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/multi.go @@ -31,6 +31,7 @@ type multiMutating []Handler func (hs multiMutating) Handle(ctx context.Context, req Request) Response { patches := []jsonpatch.JsonPatchOperation{} + warnings := []string{} for _, handler := range hs { resp := handler.Handle(ctx, req) if !resp.Allowed { @@ -42,6 +43,7 @@ func (hs multiMutating) Handle(ctx context.Context, req Request) Response { resp.PatchType, admissionv1.PatchTypeJSONPatch)) } patches = append(patches, resp.Patches...) + warnings = append(warnings, resp.Warnings...) } var err error marshaledPatch, err := json.Marshal(patches) @@ -55,6 +57,7 @@ func (hs multiMutating) Handle(ctx context.Context, req Request) Response { Code: http.StatusOK, }, Patch: marshaledPatch, + Warnings: warnings, PatchType: func() *admissionv1.PatchType { pt := admissionv1.PatchTypeJSONPatch; return &pt }(), }, } @@ -71,11 +74,13 @@ func MultiMutatingHandler(handlers ...Handler) Handler { type multiValidating []Handler func (hs multiValidating) Handle(ctx context.Context, req Request) Response { + warnings := []string{} for _, handler := range hs { resp := handler.Handle(ctx, req) if !resp.Allowed { return resp } + warnings = append(warnings, resp.Warnings...) } return Response{ AdmissionResponse: admissionv1.AdmissionResponse{ @@ -83,6 +88,7 @@ func (hs multiValidating) Handle(ctx context.Context, req Request) Response { Result: &metav1.Status{ Code: http.StatusOK, }, + Warnings: warnings, }, } } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/validator.go b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/validator.go deleted file mode 100644 index b28a56eef89f..000000000000 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/validator.go +++ /dev/null @@ -1,127 +0,0 @@ -/* -Copyright 2018 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 admission - -import ( - "context" - "errors" - "fmt" - "net/http" - - v1 "k8s.io/api/admission/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" -) - -// Warnings represents warning messages. -type Warnings []string - -// Validator defines functions for validating an operation. -// The custom resource kind which implements this interface can validate itself. -// To validate the custom resource with another specific struct, use CustomValidator instead. -// Deprecated: Use CustomValidator instead. -type Validator interface { - runtime.Object - - // ValidateCreate validates the object on creation. - // The optional warnings will be added to the response as warning messages. - // Return an error if the object is invalid. - ValidateCreate() (warnings Warnings, err error) - - // ValidateUpdate validates the object on update. The oldObj is the object before the update. - // The optional warnings will be added to the response as warning messages. - // Return an error if the object is invalid. - ValidateUpdate(old runtime.Object) (warnings Warnings, err error) - - // ValidateDelete validates the object on deletion. - // The optional warnings will be added to the response as warning messages. - // Return an error if the object is invalid. - ValidateDelete() (warnings Warnings, err error) -} - -// ValidatingWebhookFor creates a new Webhook for validating the provided type. -// Deprecated: Use WithCustomValidator instead. -func ValidatingWebhookFor(scheme *runtime.Scheme, validator Validator) *Webhook { - return &Webhook{ - Handler: &validatingHandler{validator: validator, decoder: NewDecoder(scheme)}, - } -} - -type validatingHandler struct { - validator Validator - decoder Decoder -} - -// Handle handles admission requests. -func (h *validatingHandler) Handle(ctx context.Context, req Request) Response { - if h.decoder == nil { - panic("decoder should never be nil") - } - if h.validator == nil { - panic("validator should never be nil") - } - // Get the object in the request - obj := h.validator.DeepCopyObject().(Validator) - - var err error - var warnings []string - - switch req.Operation { - case v1.Connect: - // No validation for connect requests. - // TODO(vincepri): Should we validate CONNECT requests? In what cases? - case v1.Create: - if err = h.decoder.Decode(req, obj); err != nil { - return Errored(http.StatusBadRequest, err) - } - - warnings, err = obj.ValidateCreate() - case v1.Update: - oldObj := obj.DeepCopyObject() - - err = h.decoder.DecodeRaw(req.Object, obj) - if err != nil { - return Errored(http.StatusBadRequest, err) - } - err = h.decoder.DecodeRaw(req.OldObject, oldObj) - if err != nil { - return Errored(http.StatusBadRequest, err) - } - - warnings, err = obj.ValidateUpdate(oldObj) - case v1.Delete: - // In reference to PR: https://github.com/kubernetes/kubernetes/pull/76346 - // OldObject contains the object being deleted - err = h.decoder.DecodeRaw(req.OldObject, obj) - if err != nil { - return Errored(http.StatusBadRequest, err) - } - - warnings, err = obj.ValidateDelete() - default: - return Errored(http.StatusBadRequest, fmt.Errorf("unknown operation %q", req.Operation)) - } - - if err != nil { - var apiStatus apierrors.APIStatus - if errors.As(err, &apiStatus) { - return validationResponseFromStatus(false, apiStatus.Status()).WithWarnings(warnings...) - } - return Denied(err.Error()).WithWarnings(warnings...) - } - return Allowed("").WithWarnings(warnings...) -} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/validator_custom.go b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/validator_custom.go index b8f194401ecd..ef1be52a8fb9 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/validator_custom.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/validator_custom.go @@ -27,6 +27,9 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +// Warnings represents warning messages. +type Warnings []string + // CustomValidator defines functions for validating an operation. // The object to be validated is passed into methods as a parameter. type CustomValidator interface { diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/alias.go b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/alias.go index e8439e2ea234..2882e7bab367 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/alias.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/alias.go @@ -23,14 +23,6 @@ import ( // define some aliases for common bits of the webhook functionality -// Defaulter defines functions for setting defaults on resources. -// Deprecated: Use CustomDefaulter instead. -type Defaulter = admission.Defaulter - -// Validator defines functions for validating an operation. -// Deprecated: Use CustomValidator instead. -type Validator = admission.Validator - // CustomDefaulter defines functions for setting defaults on resources. type CustomDefaulter = admission.CustomDefaulter diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/conversion/conversion.go b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/conversion/conversion.go index 249a364b3819..a26fa348bb70 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/conversion/conversion.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/conversion/conversion.go @@ -22,7 +22,9 @@ See pkg/conversion for interface definitions required to ensure an API Type is c package conversion import ( + "context" "encoding/json" + "errors" "fmt" "net/http" @@ -31,8 +33,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" "sigs.k8s.io/controller-runtime/pkg/conversion" logf "sigs.k8s.io/controller-runtime/pkg/log" + conversionmetrics "sigs.k8s.io/controller-runtime/pkg/webhook/conversion/metrics" ) var ( @@ -53,6 +57,8 @@ type webhook struct { var _ http.Handler = &webhook{} func (wh *webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + convertReview := &apix.ConversionReview{} err := json.NewDecoder(r.Body).Decode(convertReview) if err != nil { @@ -69,7 +75,7 @@ func (wh *webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) { // TODO(droot): may be move the conversion logic to a separate module to // decouple it from the http layer ? - resp, err := wh.handleConvertRequest(convertReview.Request) + resp, err := wh.handleConvertRequest(ctx, convertReview.Request) if err != nil { log.Error(err, "failed to convert", "request", convertReview.Request.UID) convertReview.Response = errored(err) @@ -87,7 +93,18 @@ func (wh *webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // handles a version conversion request. -func (wh *webhook) handleConvertRequest(req *apix.ConversionRequest) (*apix.ConversionResponse, error) { +func (wh *webhook) handleConvertRequest(ctx context.Context, req *apix.ConversionRequest) (_ *apix.ConversionResponse, retErr error) { + defer func() { + if r := recover(); r != nil { + conversionmetrics.WebhookPanics.WithLabelValues().Inc() + + for _, fn := range utilruntime.PanicHandlers { + fn(ctx, r) + } + retErr = errors.New("internal error occurred during conversion") + return + } + }() if req == nil { return nil, fmt.Errorf("conversion request is nil") } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/conversion/metrics/metrics.go b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/conversion/metrics/metrics.go new file mode 100644 index 000000000000..c825f17f0b9e --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/conversion/metrics/metrics.go @@ -0,0 +1,39 @@ +/* +Copyright 2025 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 conversion webhooks. + WebhookPanics = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "controller_runtime_conversion_webhook_panics_total", + Help: "Total number of conversion webhook panics", + }, []string{}) +) + +func init() { + metrics.Registry.MustRegister( + WebhookPanics, + ) + // Init metric. + WebhookPanics.WithLabelValues().Add(0) +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics/metrics.go b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics/metrics.go index 557004908b83..f1e6ce68f5e0 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics/metrics.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics/metrics.go @@ -18,6 +18,7 @@ package metrics import ( "net/http" + "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -30,8 +31,11 @@ var ( // of processing admission requests. RequestLatency = prometheus.NewHistogramVec( prometheus.HistogramOpts{ - Name: "controller_runtime_webhook_latency_seconds", - Help: "Histogram of the latency of processing admission requests", + Name: "controller_runtime_webhook_latency_seconds", + Help: "Histogram of the latency of processing admission requests", + NativeHistogramBucketFactor: 1.1, + NativeHistogramMaxBucketNumber: 100, + NativeHistogramMinResetDuration: 1 * time.Hour, }, []string{"webhook"}, ) diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/server.go b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/server.go index f8820e8b7c81..4d8ae9ec7adb 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/server.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/server.go @@ -190,7 +190,7 @@ func (s *DefaultServer) Start(ctx context.Context) error { log.Info("Starting webhook server") - cfg := &tls.Config{ //nolint:gosec + cfg := &tls.Config{ NextProtos: []string{"h2"}, } // fallback TLS config ready, will now mutate if passer wants full control over it @@ -272,7 +272,7 @@ func (s *DefaultServer) Start(ctx context.Context) error { // server has been started. func (s *DefaultServer) StartedChecker() healthz.Checker { config := &tls.Config{ - InsecureSkipVerify: true, //nolint:gosec // config is used to connect to our own webhook port. + InsecureSkipVerify: true, } return func(req *http.Request) error { s.mu.Lock() From 6dbadfad59d227c95e85cf93715b2b9ad611ef00 Mon Sep 17 00:00:00 2001 From: Bryan Cox Date: Fri, 5 Jun 2026 07:37:53 -0400 Subject: [PATCH 3/4] fix(hypershift-operator): adapt to controller-runtime v0.22 API changes - Remove mgr argument from controller.NewUnmanaged() call in proxy controller (v0.22 dropped the manager.Manager parameter) - Add APIVersion and FieldsType to ManagedFieldsEntry in apply_test (v0.22 fake client validates these fields) - Remove TypeMeta from expected objects in annotation enforcer test (v0.22 fake client clears TypeMeta on Get, matching real API server) Signed-off-by: Bryan Cox Commit-Message-Assisted-by: Claude (via Claude Code) --- .../createorupdate_annotation_enforcer_test.go | 12 ------------ hypershift-operator/controllers/proxy/reconciler.go | 2 +- support/upsert/apply_test.go | 8 +++++--- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/hypershift-operator/controllers/hostedcluster/createorupdate_annotation_enforcer_test.go b/hypershift-operator/controllers/hostedcluster/createorupdate_annotation_enforcer_test.go index a6b1fb1a88f6..8b0731d83657 100644 --- a/hypershift-operator/controllers/hostedcluster/createorupdate_annotation_enforcer_test.go +++ b/hypershift-operator/controllers/hostedcluster/createorupdate_annotation_enforcer_test.go @@ -43,10 +43,6 @@ func TestCreateOrUpdateWithAnnotationFactory(t *testing.T) { return func() error { return nil } }, expected: &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - Kind: "ConfigMap", - APIVersion: "v1", - }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", Namespace: "bar", @@ -77,10 +73,6 @@ func TestCreateOrUpdateWithAnnotationFactory(t *testing.T) { } }, expected: &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - Kind: "ConfigMap", - APIVersion: "v1", - }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", Namespace: "bar", @@ -106,10 +98,6 @@ func TestCreateOrUpdateWithAnnotationFactory(t *testing.T) { return func() error { return nil } }, expected: &corev1.Namespace{ - TypeMeta: metav1.TypeMeta{ - Kind: "Namespace", - APIVersion: "v1", - }, ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, diff --git a/hypershift-operator/controllers/proxy/reconciler.go b/hypershift-operator/controllers/proxy/reconciler.go index 888a9035d932..db72ec5502da 100644 --- a/hypershift-operator/controllers/proxy/reconciler.go +++ b/hypershift-operator/controllers/proxy/reconciler.go @@ -25,7 +25,7 @@ func Setup(mgr manager.Manager, deploymentNamespace string, deploymentName strin // We do not want this controller to require leader election, as that slows things down drastically when there is a proxy // and if we have multiple instances running, they should all attempt to do the same change. This means we can not use // the builder and have to wrap the controller. - c, err := controller.NewUnmanaged("proxy", mgr, controller.Options{ + c, err := controller.NewUnmanaged("proxy", controller.Options{ Reconciler: &reconciler{ client: mgr.GetClient(), deploymentNamespace: deploymentNamespace, diff --git a/support/upsert/apply_test.go b/support/upsert/apply_test.go index e3a5b54940a1..1cbf4024f701 100644 --- a/support/upsert/apply_test.go +++ b/support/upsert/apply_test.go @@ -45,9 +45,11 @@ func TestApplyManifest(t *testing.T) { existingDeployment.DeletionTimestamp = &metav1.Time{Time: time.Now()} existingDeployment.ManagedFields = []metav1.ManagedFieldsEntry{ { - Manager: "hypershift-controlplane-manager", - Operation: metav1.ManagedFieldsOperationUpdate, - Time: &metav1.Time{}, + Manager: "hypershift-controlplane-manager", + Operation: metav1.ManagedFieldsOperationUpdate, + APIVersion: "apps/v1", + FieldsType: "FieldsV1", + Time: &metav1.Time{}, }, } From 0132ec7571dc4c52c2d6ad3e6b5533b8ff3cc65d Mon Sep 17 00:00:00 2001 From: Bryan Cox Date: Fri, 5 Jun 2026 07:37:58 -0400 Subject: [PATCH 4/4] fix(control-plane-operator): adapt to controller-runtime v0.22 changes - Normalize nil Items slices to empty slices in infra tests (v0.22 fake client returns nil for empty list results) - Replace deprecated Requeue with RequeueAfter in Azure PLS controller conflict handling (Requeue field removed in controller-runtime v0.22) Signed-off-by: Bryan Cox Commit-Message-Assisted-by: Claude (via Claude Code) --- .../azureprivatelinkservice/controller.go | 6 +-- .../controller_test.go | 7 +-- .../azureprivatelinkservice/observer_test.go | 10 +++-- .../gcpprivateserviceconnect/observer_test.go | 13 +++--- .../hostedcontrolplane/infra/infra_test.go | 45 ++++++++++++++++--- 5 files changed, 60 insertions(+), 21 deletions(-) diff --git a/control-plane-operator/controllers/azureprivatelinkservice/controller.go b/control-plane-operator/controllers/azureprivatelinkservice/controller.go index 1c75c9a2529b..46c37fb4c73c 100644 --- a/control-plane-operator/controllers/azureprivatelinkservice/controller.go +++ b/control-plane-operator/controllers/azureprivatelinkservice/controller.go @@ -269,7 +269,7 @@ func (r *AzurePrivateLinkServiceReconciler) Reconcile(ctx context.Context, req c controllerutil.AddFinalizer(azPLS, azurePrivateLinkServiceFinalizer) if err := r.Update(ctx, azPLS); err != nil { if apierrors.IsConflict(err) { - return ctrl.Result{Requeue: true}, nil + return ctrl.Result{RequeueAfter: time.Second}, nil } return ctrl.Result{}, err } @@ -368,7 +368,7 @@ func (r *AzurePrivateLinkServiceReconciler) ensureHCPFinalizer(ctx context.Conte controllerutil.AddFinalizer(hcp, hcpAzurePLSFinalizerName) if err := r.Patch(ctx, hcp, client.MergeFromWithOptions(originalHCP, client.MergeFromWithOptimisticLock{})); err != nil { if apierrors.IsConflict(err) { - return ctrl.Result{Requeue: true}, nil + return ctrl.Result{RequeueAfter: time.Second}, nil } return ctrl.Result{}, fmt.Errorf("failed to add HCP finalizer: %w", err) } @@ -400,7 +400,7 @@ func (r *AzurePrivateLinkServiceReconciler) reconcileHCPDeletion(ctx context.Con controllerutil.RemoveFinalizer(hcp, hcpAzurePLSFinalizerName) if err := r.Patch(ctx, hcp, client.MergeFromWithOptions(originalHCP, client.MergeFromWithOptimisticLock{})); err != nil { if apierrors.IsConflict(err) { - return ctrl.Result{Requeue: true}, nil + return ctrl.Result{RequeueAfter: time.Second}, nil } return ctrl.Result{}, fmt.Errorf("failed to remove HCP finalizer: %w", err) } diff --git a/control-plane-operator/controllers/azureprivatelinkservice/controller_test.go b/control-plane-operator/controllers/azureprivatelinkservice/controller_test.go index 3e63b043a3e8..92572d5c7d6e 100644 --- a/control-plane-operator/controllers/azureprivatelinkservice/controller_test.go +++ b/control-plane-operator/controllers/azureprivatelinkservice/controller_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "testing" + "time" . "github.com/onsi/gomega" @@ -3426,7 +3427,7 @@ func TestReconcile_WhenFinalizerAddConflicts_ItShouldRequeue(t *testing.T) { }) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(result.Requeue).To(BeTrue(), "should requeue on conflict") + g.Expect(result.RequeueAfter).To(Equal(time.Second), "should requeue on conflict") } func TestEnsureHCPFinalizer_WhenPatchConflicts_ItShouldRequeue(t *testing.T) { @@ -3457,7 +3458,7 @@ func TestEnsureHCPFinalizer_WhenPatchConflicts_ItShouldRequeue(t *testing.T) { result, err := r.ensureHCPFinalizer(t.Context(), hcp, testr.New(t)) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(result.Requeue).To(BeTrue(), "should requeue on conflict") + g.Expect(result.RequeueAfter).To(Equal(time.Second), "should requeue on conflict") } func TestReconcileHCPDeletion_WhenPatchConflicts_ItShouldRequeue(t *testing.T) { @@ -3499,7 +3500,7 @@ func TestReconcileHCPDeletion_WhenPatchConflicts_ItShouldRequeue(t *testing.T) { result, err := r.reconcileHCPDeletion(t.Context(), azPLS, hcp, testr.New(t)) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(result.Requeue).To(BeTrue(), "should requeue on conflict") + g.Expect(result.RequeueAfter).To(Equal(time.Second), "should requeue on conflict") } func TestUpdatePrivateEndpointStatus_WhenStatusPatchFails_ItShouldReturnError(t *testing.T) { diff --git a/control-plane-operator/controllers/azureprivatelinkservice/observer_test.go b/control-plane-operator/controllers/azureprivatelinkservice/observer_test.go index 422a5b5bd9ab..8ca40590be5b 100644 --- a/control-plane-operator/controllers/azureprivatelinkservice/observer_test.go +++ b/control-plane-operator/controllers/azureprivatelinkservice/observer_test.go @@ -138,6 +138,7 @@ func TestReconcile(t *testing.T) { hcp *hyperv1.HostedControlPlane existingPLS *hyperv1.AzurePrivateLinkService expectError bool + expectRequeueAfter time.Duration expectPLSCreated bool expectedLoadBalancerIP string }{ @@ -173,9 +174,10 @@ func TestReconcile(t *testing.T) { svc.Status.LoadBalancer.Ingress = []corev1.LoadBalancerIngress{} return svc }(), - hcp: defaultHCP(), - expectError: false, - expectPLSCreated: false, + hcp: defaultHCP(), + expectError: false, + expectRequeueAfter: 30 * time.Second, + expectPLSCreated: false, }, { name: "When HCP is being deleted, it should not create CR", @@ -318,7 +320,7 @@ func TestReconcile(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) } - g.Expect(result.Requeue).To(BeFalse()) + g.Expect(result.RequeueAfter).To(Equal(tt.expectRequeueAfter)) // Check if AzurePrivateLinkService CR was created/updated if tt.expectPLSCreated { diff --git a/control-plane-operator/controllers/gcpprivateserviceconnect/observer_test.go b/control-plane-operator/controllers/gcpprivateserviceconnect/observer_test.go index d63a0fef0b90..9bcc2282e6e7 100644 --- a/control-plane-operator/controllers/gcpprivateserviceconnect/observer_test.go +++ b/control-plane-operator/controllers/gcpprivateserviceconnect/observer_test.go @@ -3,6 +3,7 @@ package gcpprivateserviceconnect import ( "context" "testing" + "time" . "github.com/onsi/gomega" @@ -103,7 +104,7 @@ func TestReconcileIntegration(t *testing.T) { requestName string service *corev1.Service hcp *hyperv1.HostedControlPlane - expectRequeue bool + expectRequeueAfter time.Duration expectError bool expectGCPPSCCreated bool }{ @@ -148,7 +149,7 @@ func TestReconcileIntegration(t *testing.T) { }, }, }, - expectRequeue: false, + expectRequeueAfter: 0, expectError: false, expectGCPPSCCreated: true, }, @@ -172,7 +173,7 @@ func TestReconcileIntegration(t *testing.T) { }, }, }, - expectRequeue: false, + expectRequeueAfter: 0, expectError: false, expectGCPPSCCreated: false, }, @@ -194,7 +195,7 @@ func TestReconcileIntegration(t *testing.T) { }, }, }, - expectRequeue: false, + expectRequeueAfter: 0, expectError: false, expectGCPPSCCreated: false, }, @@ -218,7 +219,7 @@ func TestReconcileIntegration(t *testing.T) { }, }, }, - expectRequeue: false, + expectRequeueAfter: 0, expectError: false, expectGCPPSCCreated: false, }, @@ -273,7 +274,7 @@ func TestReconcileIntegration(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) } - g.Expect(result.Requeue).To(Equal(tt.expectRequeue)) + g.Expect(result.RequeueAfter).To(Equal(tt.expectRequeueAfter)) // Check if GCPPrivateServiceConnect CR was created if tt.expectGCPPSCCreated { diff --git a/control-plane-operator/controllers/hostedcontrolplane/infra/infra_test.go b/control-plane-operator/controllers/hostedcontrolplane/infra/infra_test.go index 1636e722d9ba..a27066892926 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/infra/infra_test.go +++ b/control-plane-operator/controllers/hostedcontrolplane/infra/infra_test.go @@ -1136,8 +1136,15 @@ func TestReconcileOAuthService(t *testing.T) { if err := fakeClient.List(ctx, &actualServices); err != nil { t.Fatalf("failed to list services: %v", err) } + if actualServices.Items == nil { + actualServices.Items = []corev1.Service{} + } - if diff := testutil.MarshalYamlAndDiff(&actualServices, &corev1.ServiceList{Items: tc.expectedServices}, t); diff != "" { + expectedServices := tc.expectedServices + if expectedServices == nil { + expectedServices = []corev1.Service{} + } + if diff := testutil.MarshalYamlAndDiff(&actualServices, &corev1.ServiceList{Items: expectedServices}, t); diff != "" { t.Errorf("actual services differ from expected: %s", diff) } @@ -1145,7 +1152,14 @@ func TestReconcileOAuthService(t *testing.T) { if err := fakeClient.List(ctx, &actualRoutes); err != nil { t.Fatalf("failed to list routes: %v", err) } - if diff := testutil.MarshalYamlAndDiff(&actualRoutes, &routev1.RouteList{Items: tc.expectedRoutes}, t); diff != "" { + if actualRoutes.Items == nil { + actualRoutes.Items = []routev1.Route{} + } + expectedRoutes := tc.expectedRoutes + if expectedRoutes == nil { + expectedRoutes = []routev1.Route{} + } + if diff := testutil.MarshalYamlAndDiff(&actualRoutes, &routev1.RouteList{Items: expectedRoutes}, t); diff != "" { t.Errorf("actual routes differ from expected: %s", diff) } }) @@ -1451,8 +1465,15 @@ func TestReconcileAPIServerService(t *testing.T) { if err := fakeClient.List(ctx, &actualServices); err != nil { t.Fatalf("failed to list services: %v", err) } + if actualServices.Items == nil { + actualServices.Items = []corev1.Service{} + } - if diff := testutil.MarshalYamlAndDiff(&actualServices, &corev1.ServiceList{Items: tc.expectedServices}, t); diff != "" { + expectedServices := tc.expectedServices + if expectedServices == nil { + expectedServices = []corev1.Service{} + } + if diff := testutil.MarshalYamlAndDiff(&actualServices, &corev1.ServiceList{Items: expectedServices}, t); diff != "" { t.Errorf("actual services differ from expected: %s", diff) } @@ -1460,7 +1481,14 @@ func TestReconcileAPIServerService(t *testing.T) { if err := fakeClient.List(ctx, &actualRoutes); err != nil { t.Fatalf("failed to list routes: %v", err) } - if diff := testutil.MarshalYamlAndDiff(&actualRoutes, &routev1.RouteList{Items: tc.expectedRoutes}, t); diff != "" { + if actualRoutes.Items == nil { + actualRoutes.Items = []routev1.Route{} + } + expectedRoutes := tc.expectedRoutes + if expectedRoutes == nil { + expectedRoutes = []routev1.Route{} + } + if diff := testutil.MarshalYamlAndDiff(&actualRoutes, &routev1.RouteList{Items: expectedRoutes}, t); diff != "" { t.Errorf("actual routes differ from expected: %s", diff) } }) @@ -1671,7 +1699,14 @@ func TestReconcileHCPRouterServices(t *testing.T) { if err := c.List(ctx, &services); err != nil { t.Fatalf("failed to list services: %v", err) } - if diff := testutil.MarshalYamlAndDiff(&services, &corev1.ServiceList{Items: tc.expectedServices}, t); diff != "" { + expectedServices := tc.expectedServices + if expectedServices == nil { + expectedServices = []corev1.Service{} + } + if services.Items == nil { + services.Items = []corev1.Service{} + } + if diff := testutil.MarshalYamlAndDiff(&services, &corev1.ServiceList{Items: expectedServices}, t); diff != "" { t.Errorf("actual services differ from expected: %s", diff) } })