diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 185510e77b..cb3f972671 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -34,6 +34,6 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # tag=v8.0.0 with: - version: v2.1.6 + version: v2.3.0 args: --output.text.print-linter-name=true --output.text.colors=true --timeout 10m working-directory: ${{matrix.working-directory}} diff --git a/.github/workflows/ossf-scorecard.yaml b/.github/workflows/ossf-scorecard.yaml index 88828d9013..c15807e929 100644 --- a/.github/workflows/ossf-scorecard.yaml +++ b/.github/workflows/ossf-scorecard.yaml @@ -31,7 +31,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # tag=v2.4.1 + uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # tag=v2.4.2 with: results_file: results.sarif results_format: sarif diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9236e6237e..46c63ea53b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -29,7 +29,7 @@ jobs: run: | make release - name: Release - uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # tag=v2.2.2 + uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # tag=v2.3.2 with: draft: false files: tools/setup-envtest/out/* diff --git a/.golangci.yml b/.golangci.yml index 7390d2024b..1741432a01 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -17,6 +17,7 @@ linters: - errchkjson - errorlint - exhaustive + - forbidigo - ginkgolinter - goconst - gocritic @@ -39,6 +40,12 @@ linters: - unused - whitespace 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 @@ -94,6 +101,9 @@ linters: - zz_generated.*\.go$ - .*conversion.*\.go$ rules: + - linters: + - forbidigo + path-except: _test\.go - linters: - gosec text: 'G108: Profiling endpoint is automatically exposed on /debug/pprof' diff --git a/OWNERS_ALIASES b/OWNERS_ALIASES index 5f5b2b66d5..47bf6eedf3 100644 --- a/OWNERS_ALIASES +++ b/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: diff --git a/README.md b/README.md index 20f7fd817b..54bacad42e 100644 --- a/README.md +++ b/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: diff --git a/VERSIONING.md b/VERSIONING.md index 2c0f2f9b2d..7ad6b142cc 100644 --- a/VERSIONING.md +++ b/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/examples/builtins/main.go b/examples/builtins/main.go index 5a6e313f7b..3a47814d8c 100644 --- a/examples/builtins/main.go +++ b/examples/builtins/main.go @@ -22,23 +22,19 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" - "sigs.k8s.io/controller-runtime/pkg/builder" + + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client/config" - "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/manager/signals" "sigs.k8s.io/controller-runtime/pkg/source" ) -func init() { - log.SetLogger(zap.New()) -} - func main() { - entryLog := log.Log.WithName("entrypoint") + ctrl.SetLogger(zap.New()) + entryLog := ctrl.Log.WithName("entrypoint") // Setup a Manager entryLog.Info("setting up manager") @@ -50,28 +46,21 @@ func main() { // Setup a new controller to reconcile ReplicaSets entryLog.Info("Setting up controller") - c, err := controller.New("foo-controller", mgr, controller.Options{ - Reconciler: &reconcileReplicaSet{client: mgr.GetClient()}, - }) - if err != nil { - entryLog.Error(err, "unable to set up individual controller") - os.Exit(1) - } - // Watch ReplicaSets and enqueue ReplicaSet object key - if err := c.Watch(source.Kind(mgr.GetCache(), &appsv1.ReplicaSet{}, &handler.TypedEnqueueRequestForObject[*appsv1.ReplicaSet]{})); err != nil { - entryLog.Error(err, "unable to watch ReplicaSets") - os.Exit(1) - } - - // Watch Pods and enqueue owning ReplicaSet key - if err := c.Watch(source.Kind(mgr.GetCache(), &corev1.Pod{}, - handler.TypedEnqueueRequestForOwner[*corev1.Pod](mgr.GetScheme(), mgr.GetRESTMapper(), &appsv1.ReplicaSet{}, handler.OnlyControllerOwner()))); err != nil { - entryLog.Error(err, "unable to watch Pods") + err = ctrl. + NewControllerManagedBy(mgr). + Named("foo-controller"). + WatchesRawSource(source.Kind(mgr.GetCache(), &appsv1.ReplicaSet{}, + &handler.TypedEnqueueRequestForObject[*appsv1.ReplicaSet]{})). + WatchesRawSource(source.Kind(mgr.GetCache(), &corev1.Pod{}, + handler.TypedEnqueueRequestForOwner[*corev1.Pod](mgr.GetScheme(), mgr.GetRESTMapper(), &appsv1.ReplicaSet{}, handler.OnlyControllerOwner()))). + Complete(&reconcileReplicaSet{client: mgr.GetClient()}) + if err != nil { + entryLog.Error(err, "could not create controller") os.Exit(1) } - if err := builder.WebhookManagedBy(mgr). + if err := ctrl.NewWebhookManagedBy(mgr). For(&corev1.Pod{}). WithDefaulter(&podAnnotator{}). WithValidator(&podValidator{}). diff --git a/examples/scratch-env/go.mod b/examples/scratch-env/go.mod index 1e46bc41b8..25d8edae92 100644 --- a/examples/scratch-env/go.mod +++ b/examples/scratch-env/go.mod @@ -13,10 +13,10 @@ require ( github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.8.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -24,22 +24,25 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sync v0.12.0 // indirect @@ -52,17 +55,17 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.33.0 // indirect - k8s.io/apiextensions-apiserver v0.33.0 // indirect - k8s.io/apimachinery v0.33.0 // indirect - k8s.io/client-go v0.33.0 // indirect + k8s.io/api v0.34.0-beta.0 // indirect + k8s.io/apiextensions-apiserver v0.34.0-beta.0 // indirect + k8s.io/apimachinery v0.34.0-beta.0 // indirect + k8s.io/client-go v0.34.0-beta.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.2.0 // indirect + sigs.k8s.io/yaml v1.5.0 // indirect ) replace sigs.k8s.io/controller-runtime => ../.. diff --git a/examples/scratch-env/go.sum b/examples/scratch-env/go.sum index e56aae7f0b..c6f1a8fdd9 100644 --- a/examples/scratch-env/go.sum +++ b/examples/scratch-env/go.sum @@ -9,16 +9,16 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= +github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= @@ -37,9 +37,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -71,8 +70,9 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= @@ -116,6 +116,10 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -171,26 +175,25 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= -k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= -k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= -k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= -k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= -k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= -k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= +k8s.io/api v0.34.0-beta.0 h1:Yok13a6+0GTJYv56+lSNlgRjF46TLUVv+gaijkLdkB4= +k8s.io/api v0.34.0-beta.0/go.mod h1:OOkMaR1spg7vtjCCQSdxgiRD0Bbp2pcTCBxJtEEb4Pk= +k8s.io/apiextensions-apiserver v0.34.0-beta.0 h1:zzR2yWEBkfYcjfwM42CwrBwtwLWLBJ/VwYpyP2ldNMI= +k8s.io/apiextensions-apiserver v0.34.0-beta.0/go.mod h1:lCs/ZFdQjQWkNdEa0CS7qvLAMVDCj3fVXwSmKZ7Fn8E= +k8s.io/apimachinery v0.34.0-beta.0 h1:C6teSJBCx6ArW7122MM9hQqeGW2w/QQ0lB4x4Z4Iftc= +k8s.io/apimachinery v0.34.0-beta.0/go.mod h1:TP8uyOuDEOnzGpLOdffo8hPnIjVDljZCxCM/fruV+5M= +k8s.io/client-go v0.34.0-beta.0 h1:snyuzY74xHIp7qxMYul8QRn1WxPNhs281srSl2CX9nY= +k8s.io/client-go v0.34.0-beta.0/go.mod h1:KNxQTmxlWq7adFeedQLu+qVkIBTZqhghesFk41YWyBQ= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/structured-merge-diff/v6 v6.2.0 h1:msyqjP8Nyd5sF3QSmJouFSzcBIdwq4ct8d1/7VSBHIQ= +sigs.k8s.io/structured-merge-diff/v6 v6.2.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= +sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= diff --git a/go.mod b/go.mod index bda2c722a0..9c1bfba876 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.0 require ( github.com/blang/semver/v4 v4.0.0 github.com/evanphx/json-patch/v5 v5.9.11 - github.com/fsnotify/fsnotify v1.7.0 + github.com/fsnotify/fsnotify v1.9.0 github.com/go-logr/logr v1.4.2 github.com/go-logr/zapr v1.3.0 github.com/google/btree v1.1.3 @@ -22,62 +22,66 @@ require ( golang.org/x/sys v0.31.0 gomodules.xyz/jsonpatch/v2 v2.4.0 gopkg.in/evanphx/json-patch.v4 v4.12.0 // Using v4 to match upstream - k8s.io/api v0.33.0 - k8s.io/apiextensions-apiserver v0.33.0 - k8s.io/apimachinery v0.33.0 - k8s.io/apiserver v0.33.0 - k8s.io/client-go v0.33.0 + k8s.io/api v0.34.0-beta.0 + k8s.io/apiextensions-apiserver v0.34.0-beta.0 + k8s.io/apimachinery v0.34.0-beta.0 + k8s.io/apiserver v0.34.0-beta.0 + k8s.io/client-go v0.34.0-beta.0 k8s.io/klog/v2 v2.130.1 - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 - sigs.k8s.io/yaml v1.4.0 + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 + sigs.k8s.io/structured-merge-diff/v6 v6.2.0 + sigs.k8s.io/yaml v1.5.0 ) require ( - cel.dev/expr v0.19.1 // indirect + cel.dev/expr v0.23.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.8.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/cel-go v0.23.2 // indirect - github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/cel-go v0.25.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/sdk v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect - go.opentelemetry.io/proto/otlp v1.4.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect @@ -85,16 +89,15 @@ require ( golang.org/x/text v0.23.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.26.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect - google.golang.org/grpc v1.68.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/grpc v1.72.1 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/component-base v0.33.0 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/component-base v0.34.0-beta.0 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect ) diff --git a/go.sum b/go.sum index ed52e99b20..b210c7d9c5 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= -cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg= +cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -10,23 +10,23 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= +github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -50,11 +50,10 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4= -github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY= +github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -64,8 +63,8 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -90,8 +89,9 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= @@ -113,10 +113,10 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -138,26 +138,32 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= -go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= -go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -205,12 +211,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= -google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= -google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -223,32 +229,31 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= -k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= -k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= -k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= -k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= -k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.0 h1:QqcM6c+qEEjkOODHppFXRiw/cE2zP85704YrQ9YaBbc= -k8s.io/apiserver v0.33.0/go.mod h1:EixYOit0YTxt8zrO2kBU7ixAtxFce9gKGq367nFmqI8= -k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= -k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= -k8s.io/component-base v0.33.0 h1:Ot4PyJI+0JAD9covDhwLp9UNkUja209OzsJ4FzScBNk= -k8s.io/component-base v0.33.0/go.mod h1:aXYZLbw3kihdkOPMDhWbjGCO6sg+luw554KP51t8qCU= +k8s.io/api v0.34.0-beta.0 h1:Yok13a6+0GTJYv56+lSNlgRjF46TLUVv+gaijkLdkB4= +k8s.io/api v0.34.0-beta.0/go.mod h1:OOkMaR1spg7vtjCCQSdxgiRD0Bbp2pcTCBxJtEEb4Pk= +k8s.io/apiextensions-apiserver v0.34.0-beta.0 h1:zzR2yWEBkfYcjfwM42CwrBwtwLWLBJ/VwYpyP2ldNMI= +k8s.io/apiextensions-apiserver v0.34.0-beta.0/go.mod h1:lCs/ZFdQjQWkNdEa0CS7qvLAMVDCj3fVXwSmKZ7Fn8E= +k8s.io/apimachinery v0.34.0-beta.0 h1:C6teSJBCx6ArW7122MM9hQqeGW2w/QQ0lB4x4Z4Iftc= +k8s.io/apimachinery v0.34.0-beta.0/go.mod h1:TP8uyOuDEOnzGpLOdffo8hPnIjVDljZCxCM/fruV+5M= +k8s.io/apiserver v0.34.0-beta.0 h1:cLhc6un0qtCb/nwGpkj8020yvs3oUxCu4vLLKEGkd4c= +k8s.io/apiserver v0.34.0-beta.0/go.mod h1:RO2L/h9tBdqlrQGz/ghIGOWzF+aEwCkb13ckUdmLolo= +k8s.io/client-go v0.34.0-beta.0 h1:snyuzY74xHIp7qxMYul8QRn1WxPNhs281srSl2CX9nY= +k8s.io/client-go v0.34.0-beta.0/go.mod h1:KNxQTmxlWq7adFeedQLu+qVkIBTZqhghesFk41YWyBQ= +k8s.io/component-base v0.34.0-beta.0 h1:w2zC7n8lXbOqsC8UQ97JqTneDsy+ThFcbxQnfQ98/TM= +k8s.io/component-base v0.34.0-beta.0/go.mod h1:KRfhQL9KZm3sywusY38Z2An7ulFDCaL+1juNF4NLhUw= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/structured-merge-diff/v6 v6.2.0 h1:msyqjP8Nyd5sF3QSmJouFSzcBIdwq4ct8d1/7VSBHIQ= +sigs.k8s.io/structured-merge-diff/v6 v6.2.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= +sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= diff --git a/pkg/builder/controller_test.go b/pkg/builder/controller_test.go index cbeb1c43bc..b1c9c3de3b 100644 --- a/pkg/builder/controller_test.go +++ b/pkg/builder/controller_test.go @@ -402,7 +402,7 @@ var _ = Describe("application", func() { }) Describe("Start with ControllerManagedBy", func() { - It("should Reconcile Owns objects", func() { + It("should Reconcile Owns objects", func(ctx SpecContext) { m, err := manager.New(cfg, manager.Options{}) Expect(err).NotTo(HaveOccurred()) @@ -411,12 +411,10 @@ var _ = Describe("application", func() { Named("deployment-0"). Owns(&appsv1.ReplicaSet{}) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() doReconcileTest(ctx, "3", m, false, bldr) }) - It("should Reconcile Owns objects for every owner", func() { + It("should Reconcile Owns objects for every owner", func(ctx SpecContext) { m, err := manager.New(cfg, manager.Options{}) Expect(err).NotTo(HaveOccurred()) @@ -425,12 +423,10 @@ var _ = Describe("application", func() { Named("deployment-1"). Owns(&appsv1.ReplicaSet{}, MatchEveryOwner) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() doReconcileTest(ctx, "12", m, false, bldr) }) - It("should Reconcile Watches objects", func() { + It("should Reconcile Watches objects", func(ctx SpecContext) { m, err := manager.New(cfg, manager.Options{}) Expect(err).NotTo(HaveOccurred()) @@ -441,12 +437,10 @@ var _ = Describe("application", func() { handler.EnqueueRequestForOwner(m.GetScheme(), m.GetRESTMapper(), &appsv1.Deployment{}, handler.OnlyControllerOwner()), ) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() doReconcileTest(ctx, "4", m, true, bldr) }) - It("should Reconcile without For", func() { + It("should Reconcile without For", func(ctx SpecContext) { m, err := manager.New(cfg, manager.Options{}) Expect(err).NotTo(HaveOccurred()) @@ -460,14 +454,12 @@ var _ = Describe("application", func() { handler.EnqueueRequestForOwner(m.GetScheme(), m.GetRESTMapper(), &appsv1.Deployment{}, handler.OnlyControllerOwner()), ) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() doReconcileTest(ctx, "9", m, true, bldr) }) }) Describe("Set custom predicates", func() { - It("should execute registered predicates only for assigned kind", func() { + It("should execute registered predicates only for assigned kind", func(ctx SpecContext) { m, err := manager.New(cfg, manager.Options{}) Expect(err).NotTo(HaveOccurred()) @@ -517,8 +509,6 @@ var _ = Describe("application", func() { Owns(&appsv1.ReplicaSet{}, WithPredicates(replicaSetPrct)). WithEventFilter(allPrct) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() doReconcileTest(ctx, "5", m, true, bldr) Expect(deployPrctExecuted).To(BeTrue(), "Deploy predicated should be called at least once") @@ -537,17 +527,14 @@ var _ = Describe("application", func() { Expect(err).NotTo(HaveOccurred()) }) - It("should support multiple controllers watching the same metadata kind", func() { + It("should support multiple controllers watching the same metadata kind", func(ctx SpecContext) { bldr1 := ControllerManagedBy(mgr).For(&appsv1.Deployment{}, OnlyMetadata).Named("deployment-4") bldr2 := ControllerManagedBy(mgr).For(&appsv1.Deployment{}, OnlyMetadata).Named("deployment-5") - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - doReconcileTest(ctx, "6", mgr, true, bldr1, bldr2) }) - It("should support watching For, Owns, and Watch as metadata", func() { + It("should support watching For, Owns, and Watch as metadata", func(ctx SpecContext) { statefulSetMaps := make(chan *metav1.PartialObjectMetadata) bldr := ControllerManagedBy(mgr). @@ -571,8 +558,6 @@ var _ = Describe("application", func() { }), OnlyMetadata) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() doReconcileTest(ctx, "8", mgr, true, bldr) By("Creating a new stateful set") @@ -601,7 +586,7 @@ var _ = Describe("application", func() { }, }, } - err := mgr.GetClient().Create(context.TODO(), set) + err := mgr.GetClient().Create(ctx, set) Expect(err).NotTo(HaveOccurred()) By("Checking that the mapping function has been called") diff --git a/pkg/builder/webhook_test.go b/pkg/builder/webhook_test.go index 099612ab6f..eb70af2e0a 100644 --- a/pkg/builder/webhook_test.go +++ b/pkg/builder/webhook_test.go @@ -79,7 +79,7 @@ func runTests(admissionReviewVersion string) { close(stop) }) - It("should scaffold a custom defaulting webhook", func() { + It("should scaffold a custom defaulting webhook", func(specCtx SpecContext) { By("creating a controller manager") m, err := manager.New(cfg, manager.Options{}) ExpectWithOffset(1, err).NotTo(HaveOccurred()) @@ -124,7 +124,7 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) cancel() err = svr.Start(ctx) if err != nil && !os.IsNotExist(err) { @@ -155,7 +155,7 @@ func runTests(admissionReviewVersion string) { ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) }) - It("should scaffold a custom defaulting webhook with a custom path", func() { + It("should scaffold a custom defaulting webhook with a custom path", func(specCtx SpecContext) { By("creating a controller manager") m, err := manager.New(cfg, manager.Options{}) ExpectWithOffset(1, err).NotTo(HaveOccurred()) @@ -202,7 +202,7 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) cancel() err = svr.Start(ctx) if err != nil && !os.IsNotExist(err) { @@ -234,7 +234,7 @@ func runTests(admissionReviewVersion string) { ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) }) - It("should scaffold a custom defaulting webhook which recovers from panics", func() { + It("should scaffold a custom defaulting webhook which recovers from panics", func(specCtx SpecContext) { By("creating a controller manager") m, err := manager.New(cfg, manager.Options{}) ExpectWithOffset(1, err).NotTo(HaveOccurred()) @@ -278,7 +278,7 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) cancel() err = svr.Start(ctx) if err != nil && !os.IsNotExist(err) { @@ -298,7 +298,7 @@ func runTests(admissionReviewVersion string) { ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`)) }) - It("should scaffold a custom validating webhook", func() { + It("should scaffold a custom validating webhook", func(specCtx SpecContext) { By("creating a controller manager") m, err := manager.New(cfg, manager.Options{}) ExpectWithOffset(1, err).NotTo(HaveOccurred()) @@ -345,7 +345,7 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) cancel() err = svr.Start(ctx) if err != nil && !os.IsNotExist(err) { @@ -375,7 +375,7 @@ func runTests(admissionReviewVersion string) { EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) }) - It("should scaffold a custom validating webhook with a custom path", func() { + It("should scaffold a custom validating webhook with a custom path", func(specCtx SpecContext) { By("creating a controller manager") m, err := manager.New(cfg, manager.Options{}) ExpectWithOffset(1, err).NotTo(HaveOccurred()) @@ -424,7 +424,7 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) cancel() err = svr.Start(ctx) if err != nil && !os.IsNotExist(err) { @@ -455,7 +455,7 @@ func runTests(admissionReviewVersion string) { ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) }) - It("should scaffold a custom validating webhook which recovers from panics", func() { + It("should scaffold a custom validating webhook which recovers from panics", func(specCtx SpecContext) { By("creating a controller manager") m, err := manager.New(cfg, manager.Options{}) ExpectWithOffset(1, err).NotTo(HaveOccurred()) @@ -497,7 +497,7 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) cancel() err = svr.Start(ctx) if err != nil && !os.IsNotExist(err) { @@ -519,9 +519,9 @@ func runTests(admissionReviewVersion string) { ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`)) }) - It("should scaffold a custom validating webhook to validate deletes", func() { + It("should scaffold a custom validating webhook to validate deletes", func(specCtx SpecContext) { By("creating a controller manager") - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) m, err := manager.New(cfg, manager.Options{}) ExpectWithOffset(1, err).NotTo(HaveOccurred()) @@ -630,7 +630,7 @@ func runTests(admissionReviewVersion string) { Expect(err).To(HaveOccurred()) }) - It("should scaffold a custom defaulting and validating webhook", func() { + It("should scaffold a custom defaulting and validating webhook", func(specCtx SpecContext) { By("creating a controller manager") m, err := manager.New(cfg, manager.Options{}) ExpectWithOffset(1, err).NotTo(HaveOccurred()) @@ -678,7 +678,7 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) cancel() err = svr.Start(ctx) if err != nil && !os.IsNotExist(err) { @@ -713,7 +713,7 @@ func runTests(admissionReviewVersion string) { EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) }) - It("should scaffold a custom defaulting and validating webhook with a custom path for each of them", func() { + It("should scaffold a custom defaulting and validating webhook with a custom path for each of them", func(specCtx SpecContext) { By("creating a controller manager") m, err := manager.New(cfg, manager.Options{}) ExpectWithOffset(1, err).NotTo(HaveOccurred()) @@ -765,7 +765,7 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) cancel() err = svr.Start(ctx) if err != nil && !os.IsNotExist(err) { diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go index 955c3cc020..2364eec3e1 100644 --- a/pkg/cache/cache_test.go +++ b/pkg/cache/cache_test.go @@ -55,7 +55,7 @@ const testNamespaceThree = "test-namespace-3" // TODO(community): Pull these helper functions into testenv. // Restart policy is included to allow indexing on that field. -func createPodWithLabels(name, namespace string, restartPolicy corev1.RestartPolicy, labels map[string]string) client.Object { +func createPodWithLabels(ctx context.Context, name, namespace string, restartPolicy corev1.RestartPolicy, labels map[string]string) client.Object { three := int64(3) if labels == nil { labels = map[string]string{} @@ -75,12 +75,12 @@ func createPodWithLabels(name, namespace string, restartPolicy corev1.RestartPol } cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) - err = cl.Create(context.Background(), pod) + err = cl.Create(ctx, pod) Expect(err).NotTo(HaveOccurred()) return pod } -func createSvc(name, namespace string, cl client.Client) client.Object { +func createSvc(ctx context.Context, name, namespace string, cl client.Client) client.Object { svc := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -90,31 +90,31 @@ func createSvc(name, namespace string, cl client.Client) client.Object { Ports: []corev1.ServicePort{{Port: 1}}, }, } - err := cl.Create(context.Background(), svc) + err := cl.Create(ctx, svc) Expect(err).NotTo(HaveOccurred()) return svc } -func createSA(name, namespace string, cl client.Client) client.Object { +func createSA(ctx context.Context, name, namespace string, cl client.Client) client.Object { sa := &corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, } - err := cl.Create(context.Background(), sa) + err := cl.Create(ctx, sa) Expect(err).NotTo(HaveOccurred()) return sa } -func createPod(name, namespace string, restartPolicy corev1.RestartPolicy) client.Object { - return createPodWithLabels(name, namespace, restartPolicy, nil) +func createPod(ctx context.Context, name, namespace string, restartPolicy corev1.RestartPolicy) client.Object { + return createPodWithLabels(ctx, name, namespace, restartPolicy, nil) } -func deletePod(pod client.Object) { +func deletePod(ctx context.Context, pod client.Object) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) - err = cl.Delete(context.Background(), pod) + err = cl.Delete(ctx, pod) Expect(err).NotTo(HaveOccurred()) } @@ -156,7 +156,6 @@ var _ = Describe("Informer Cache without global DeepCopy", func() { var _ = Describe("Cache with transformers", func() { var ( informerCache cache.Cache - informerCacheCtx context.Context informerCacheCancel context.CancelFunc knownPod1 client.Object knownPod2 client.Object @@ -177,28 +176,31 @@ var _ = Describe("Cache with transformers", func() { return "" } - BeforeEach(func() { - informerCacheCtx, informerCacheCancel = context.WithCancel(context.Background()) + BeforeEach(func(ctx SpecContext) { + var informerCacheCtx context.Context + // Has to be derived from context.Background as it has to stay valid past the + // BeforeEach. + informerCacheCtx, informerCacheCancel = context.WithCancel(context.Background()) //nolint:forbidigo Expect(cfg).NotTo(BeNil()) By("creating three pods") cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) - err = ensureNode(testNodeOne, cl) + err = ensureNode(ctx, testNodeOne, cl) Expect(err).NotTo(HaveOccurred()) - err = ensureNamespace(testNamespaceOne, cl) + err = ensureNamespace(ctx, testNamespaceOne, cl) Expect(err).NotTo(HaveOccurred()) - err = ensureNamespace(testNamespaceTwo, cl) + err = ensureNamespace(ctx, testNamespaceTwo, cl) Expect(err).NotTo(HaveOccurred()) - err = ensureNamespace(testNamespaceThree, cl) + err = ensureNamespace(ctx, testNamespaceThree, cl) Expect(err).NotTo(HaveOccurred()) // Includes restart policy since these objects are indexed on this field. - knownPod1 = createPod("test-pod-1", testNamespaceOne, corev1.RestartPolicyNever) - knownPod2 = createPod("test-pod-2", testNamespaceTwo, corev1.RestartPolicyAlways) - knownPod3 = createPodWithLabels("test-pod-3", testNamespaceTwo, corev1.RestartPolicyOnFailure, map[string]string{"common-label": "common"}) - knownPod4 = createPodWithLabels("test-pod-4", testNamespaceThree, corev1.RestartPolicyNever, map[string]string{"common-label": "common"}) - knownPod5 = createPod("test-pod-5", testNamespaceOne, corev1.RestartPolicyNever) - knownPod6 = createPod("test-pod-6", testNamespaceTwo, corev1.RestartPolicyAlways) + knownPod1 = createPod(ctx, "test-pod-1", testNamespaceOne, corev1.RestartPolicyNever) + knownPod2 = createPod(ctx, "test-pod-2", testNamespaceTwo, corev1.RestartPolicyAlways) + knownPod3 = createPodWithLabels(ctx, "test-pod-3", testNamespaceTwo, corev1.RestartPolicyOnFailure, map[string]string{"common-label": "common"}) + knownPod4 = createPodWithLabels(ctx, "test-pod-4", testNamespaceThree, corev1.RestartPolicyNever, map[string]string{"common-label": "common"}) + knownPod5 = createPod(ctx, "test-pod-5", testNamespaceOne, corev1.RestartPolicyNever) + knownPod6 = createPod(ctx, "test-pod-6", testNamespaceTwo, corev1.RestartPolicyAlways) podGVK := schema.GroupVersionKind{ Kind: "Pod", @@ -265,26 +267,26 @@ var _ = Describe("Cache with transformers", func() { defer GinkgoRecover() Expect(informerCache.Start(ctx)).To(Succeed()) }(informerCacheCtx) - Expect(informerCache.WaitForCacheSync(informerCacheCtx)).To(BeTrue()) + Expect(informerCache.WaitForCacheSync(ctx)).To(BeTrue()) }) - AfterEach(func() { + AfterEach(func(ctx SpecContext) { By("cleaning up created pods") - deletePod(knownPod1) - deletePod(knownPod2) - deletePod(knownPod3) - deletePod(knownPod4) - deletePod(knownPod5) - deletePod(knownPod6) + deletePod(ctx, knownPod1) + deletePod(ctx, knownPod2) + deletePod(ctx, knownPod3) + deletePod(ctx, knownPod4) + deletePod(ctx, knownPod5) + deletePod(ctx, knownPod6) informerCacheCancel() }) Context("with structured objects", func() { - It("should apply transformers to explicitly specified GVKS", func() { + It("should apply transformers to explicitly specified GVKS", func(ctx SpecContext) { By("listing pods") out := corev1.PodList{} - Expect(informerCache.List(context.Background(), &out)).To(Succeed()) + Expect(informerCache.List(ctx, &out)).To(Succeed()) By("verifying that the returned pods were transformed") for i := 0; i < len(out.Items); i++ { @@ -292,11 +294,11 @@ var _ = Describe("Cache with transformers", func() { } }) - It("should apply default transformer to objects when none is specified", func() { + It("should apply default transformer to objects when none is specified", func(ctx SpecContext) { By("getting the Kubernetes service") svc := &corev1.Service{} svcKey := client.ObjectKey{Namespace: "default", Name: "kubernetes"} - Expect(informerCache.Get(context.Background(), svcKey, svc)).To(Succeed()) + Expect(informerCache.Get(ctx, svcKey, svc)).To(Succeed()) By("verifying that the returned service was transformed") Expect(getTransformValue(svc)).To(BeIdenticalTo("default")) @@ -304,7 +306,7 @@ var _ = Describe("Cache with transformers", func() { }) Context("with unstructured objects", func() { - It("should apply transformers to explicitly specified GVKS", func() { + It("should apply transformers to explicitly specified GVKS", func(ctx SpecContext) { By("listing pods") out := unstructured.UnstructuredList{} out.SetGroupVersionKind(schema.GroupVersionKind{ @@ -312,7 +314,7 @@ var _ = Describe("Cache with transformers", func() { Version: "v1", Kind: "PodList", }) - Expect(informerCache.List(context.Background(), &out)).To(Succeed()) + Expect(informerCache.List(ctx, &out)).To(Succeed()) By("verifying that the returned pods were transformed") for i := 0; i < len(out.Items); i++ { @@ -320,7 +322,7 @@ var _ = Describe("Cache with transformers", func() { } }) - It("should apply default transformer to objects when none is specified", func() { + It("should apply default transformer to objects when none is specified", func(ctx SpecContext) { By("getting the Kubernetes service") svc := &unstructured.Unstructured{} svc.SetGroupVersionKind(schema.GroupVersionKind{ @@ -329,7 +331,7 @@ var _ = Describe("Cache with transformers", func() { Kind: "Service", }) svcKey := client.ObjectKey{Namespace: "default", Name: "kubernetes"} - Expect(informerCache.Get(context.Background(), svcKey, svc)).To(Succeed()) + Expect(informerCache.Get(ctx, svcKey, svc)).To(Succeed()) By("verifying that the returned service was transformed") Expect(getTransformValue(svc)).To(BeIdenticalTo("default")) @@ -337,7 +339,7 @@ var _ = Describe("Cache with transformers", func() { }) Context("with metadata-only objects", func() { - It("should apply transformers to explicitly specified GVKS", func() { + It("should apply transformers to explicitly specified GVKS", func(ctx SpecContext) { By("listing pods") out := metav1.PartialObjectMetadataList{} out.SetGroupVersionKind(schema.GroupVersionKind{ @@ -345,14 +347,14 @@ var _ = Describe("Cache with transformers", func() { Version: "v1", Kind: "PodList", }) - Expect(informerCache.List(context.Background(), &out)).To(Succeed()) + Expect(informerCache.List(ctx, &out)).To(Succeed()) By("verifying that the returned pods were transformed") for i := 0; i < len(out.Items); i++ { Expect(getTransformValue(&out.Items[i])).To(BeIdenticalTo("explicit")) } }) - It("should apply default transformer to objects when none is specified", func() { + It("should apply default transformer to objects when none is specified", func(ctx SpecContext) { By("getting the Kubernetes service") svc := &metav1.PartialObjectMetadata{} svc.SetGroupVersionKind(schema.GroupVersionKind{ @@ -361,7 +363,7 @@ var _ = Describe("Cache with transformers", func() { Kind: "Service", }) svcKey := client.ObjectKey{Namespace: "default", Name: "kubernetes"} - Expect(informerCache.Get(context.Background(), svcKey, svc)).To(Succeed()) + Expect(informerCache.Get(ctx, svcKey, svc)).To(Succeed()) By("verifying that the returned service was transformed") Expect(getTransformValue(svc)).To(BeIdenticalTo("default")) @@ -373,22 +375,24 @@ var _ = Describe("Cache with selectors", func() { defer GinkgoRecover() var ( informerCache cache.Cache - informerCacheCtx context.Context informerCacheCancel context.CancelFunc ) - BeforeEach(func() { - informerCacheCtx, informerCacheCancel = context.WithCancel(context.Background()) + BeforeEach(func(ctx SpecContext) { + var informerCacheCtx context.Context + // Has to be derived from context.Background as it has to stay valid past the + // BeforeEach. + informerCacheCtx, informerCacheCancel = context.WithCancel(context.Background()) //nolint:forbidigo Expect(cfg).NotTo(BeNil()) cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) - err = ensureNamespace(testNamespaceOne, cl) + err = ensureNamespace(ctx, testNamespaceOne, cl) Expect(err).NotTo(HaveOccurred()) - err = ensureNamespace(testNamespaceTwo, cl) + err = ensureNamespace(ctx, testNamespaceTwo, cl) Expect(err).NotTo(HaveOccurred()) for idx, namespace := range []string{testNamespaceOne, testNamespaceTwo} { - _ = createSA("test-sa-"+strconv.Itoa(idx), namespace, cl) - _ = createSvc("test-svc-"+strconv.Itoa(idx), namespace, cl) + _ = createSA(ctx, "test-sa-"+strconv.Itoa(idx), namespace, cl) + _ = createSvc(ctx, "test-svc-"+strconv.Itoa(idx), namespace, cl) } opts := cache.Options{ @@ -412,8 +416,7 @@ var _ = Describe("Cache with selectors", func() { Expect(informerCache.WaitForCacheSync(informerCacheCtx)).To(BeTrue()) }) - AfterEach(func() { - ctx := context.Background() + AfterEach(func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) for idx, namespace := range []string{testNamespaceOne, testNamespaceTwo} { @@ -425,17 +428,17 @@ var _ = Describe("Cache with selectors", func() { informerCacheCancel() }) - It("Should list serviceaccounts and find exactly one in namespace "+testNamespaceOne, func() { + It("Should list serviceaccounts and find exactly one in namespace "+testNamespaceOne, func(ctx SpecContext) { var sas corev1.ServiceAccountList - err := informerCache.List(informerCacheCtx, &sas) + err := informerCache.List(ctx, &sas) Expect(err).NotTo(HaveOccurred()) Expect(sas.Items).To(HaveLen(1)) Expect(sas.Items[0].Namespace).To(Equal(testNamespaceOne)) }) - It("Should list services and find exactly one in namespace "+testNamespaceTwo, func() { + It("Should list services and find exactly one in namespace "+testNamespaceTwo, func(ctx SpecContext) { var svcs corev1.ServiceList - err := informerCache.List(informerCacheCtx, &svcs) + err := informerCache.List(ctx, &svcs) Expect(err).NotTo(HaveOccurred()) Expect(svcs.Items).To(HaveLen(1)) Expect(svcs.Items[0].Namespace).To(Equal(testNamespaceTwo)) @@ -446,13 +449,15 @@ func CacheTestReaderFailOnMissingInformer(createCacheFunc func(config *rest.Conf Describe("Cache test with ReaderFailOnMissingInformer = true", func() { var ( informerCache cache.Cache - informerCacheCtx context.Context informerCacheCancel context.CancelFunc errNotCached *cache.ErrResourceNotCached ) - BeforeEach(func() { - informerCacheCtx, informerCacheCancel = context.WithCancel(context.Background()) + BeforeEach(func(ctx SpecContext) { + var informerCacheCtx context.Context + // Has to be derived from context.Background as it has to stay valid past the + // BeforeEach. + informerCacheCtx, informerCacheCancel = context.WithCancel(context.Background()) //nolint:forbidigo Expect(cfg).NotTo(BeNil()) By("creating the informer cache") var err error @@ -464,7 +469,7 @@ func CacheTestReaderFailOnMissingInformer(createCacheFunc func(config *rest.Conf defer GinkgoRecover() Expect(informerCache.Start(ctx)).To(Succeed()) }(informerCacheCtx) - Expect(informerCache.WaitForCacheSync(informerCacheCtx)).To(BeTrue()) + Expect(informerCache.WaitForCacheSync(ctx)).To(BeTrue()) }) AfterEach(func() { @@ -473,27 +478,27 @@ func CacheTestReaderFailOnMissingInformer(createCacheFunc func(config *rest.Conf Describe("as a Reader", func() { Context("with structured objects", func() { - It("should not be able to list objects that haven't been watched previously", func() { + It("should not be able to list objects that haven't been watched previously", func(ctx SpecContext) { By("listing all services in the cluster") listObj := &corev1.ServiceList{} - Expect(errors.As(informerCache.List(context.Background(), listObj), &errNotCached)).To(BeTrue()) + Expect(errors.As(informerCache.List(ctx, listObj), &errNotCached)).To(BeTrue()) }) - It("should not be able to get objects that haven't been watched previously", func() { + It("should not be able to get objects that haven't been watched previously", func(ctx SpecContext) { By("getting the Kubernetes service") svc := &corev1.Service{} svcKey := client.ObjectKey{Namespace: "default", Name: "kubernetes"} - Expect(errors.As(informerCache.Get(context.Background(), svcKey, svc), &errNotCached)).To(BeTrue()) + Expect(errors.As(informerCache.Get(ctx, svcKey, svc), &errNotCached)).To(BeTrue()) }) - It("should be able to list objects that are configured to be watched", func() { + It("should be able to list objects that are configured to be watched", func(ctx SpecContext) { By("indicating that we need to watch services") - _, err := informerCache.GetInformer(context.Background(), &corev1.Service{}) + _, err := informerCache.GetInformer(ctx, &corev1.Service{}) Expect(err).ToNot(HaveOccurred()) By("listing all services in the cluster") svcList := &corev1.ServiceList{} - Expect(informerCache.List(context.Background(), svcList)).To(Succeed()) + Expect(informerCache.List(ctx, svcList)).To(Succeed()) By("verifying that the returned service looks reasonable") Expect(svcList.Items).To(HaveLen(1)) @@ -501,15 +506,15 @@ func CacheTestReaderFailOnMissingInformer(createCacheFunc func(config *rest.Conf Expect(svcList.Items[0].Namespace).To(Equal("default")) }) - It("should be able to get objects that are configured to be watched", func() { + It("should be able to get objects that are configured to be watched", func(ctx SpecContext) { By("indicating that we need to watch services") - _, err := informerCache.GetInformer(context.Background(), &corev1.Service{}) + _, err := informerCache.GetInformer(ctx, &corev1.Service{}) Expect(err).ToNot(HaveOccurred()) By("getting the Kubernetes service") svc := &corev1.Service{} svcKey := client.ObjectKey{Namespace: "default", Name: "kubernetes"} - Expect(informerCache.Get(context.Background(), svcKey, svc)).To(Succeed()) + Expect(informerCache.Get(ctx, svcKey, svc)).To(Succeed()) By("verifying that the returned service looks reasonable") Expect(svc.Name).To(Equal("kubernetes")) @@ -524,23 +529,25 @@ func NonBlockingGetTest(createCacheFunc func(config *rest.Config, opts cache.Opt Describe("non-blocking get test", func() { var ( informerCache cache.Cache - informerCacheCtx context.Context informerCacheCancel context.CancelFunc ) - BeforeEach(func() { - informerCacheCtx, informerCacheCancel = context.WithCancel(context.Background()) + BeforeEach(func(ctx SpecContext) { + var informerCacheCtx context.Context + // Has to be derived from context.Background as it has to stay valid past the + // BeforeEach. + informerCacheCtx, informerCacheCancel = context.WithCancel(context.Background()) //nolint:forbidigo Expect(cfg).NotTo(BeNil()) By("creating expected namespaces") cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) - err = ensureNode(testNodeOne, cl) + err = ensureNode(ctx, testNodeOne, cl) Expect(err).NotTo(HaveOccurred()) - err = ensureNamespace(testNamespaceOne, cl) + err = ensureNamespace(ctx, testNamespaceOne, cl) Expect(err).NotTo(HaveOccurred()) - err = ensureNamespace(testNamespaceTwo, cl) + err = ensureNamespace(ctx, testNamespaceTwo, cl) Expect(err).NotTo(HaveOccurred()) - err = ensureNamespace(testNamespaceThree, cl) + err = ensureNamespace(ctx, testNamespaceThree, cl) Expect(err).NotTo(HaveOccurred()) By("creating the informer cache") @@ -555,7 +562,7 @@ func NonBlockingGetTest(createCacheFunc func(config *rest.Config, opts cache.Opt defer GinkgoRecover() Expect(informerCache.Start(ctx)).To(Succeed()) }(informerCacheCtx) - Expect(informerCache.WaitForCacheSync(informerCacheCtx)).To(BeTrue()) + Expect(informerCache.WaitForCacheSync(ctx)).To(BeTrue()) }) AfterEach(func() { @@ -564,7 +571,7 @@ func NonBlockingGetTest(createCacheFunc func(config *rest.Config, opts cache.Opt }) Describe("as an Informer", func() { - It("should be able to get informer for the object without blocking", func() { + It("should be able to get informer for the object without blocking", func(specCtx SpecContext) { By("getting a shared index informer for a pod") pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ @@ -581,7 +588,7 @@ func NonBlockingGetTest(createCacheFunc func(config *rest.Config, opts cache.Opt }, } - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + ctx, cancel := context.WithTimeout(specCtx, 5*time.Second) defer cancel() sii, err := informerCache.GetInformer(ctx, pod, cache.BlockUntilSynced(false)) Expect(err).NotTo(HaveOccurred()) @@ -596,7 +603,6 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Describe("Cache test", func() { var ( informerCache cache.Cache - informerCacheCtx context.Context informerCacheCancel context.CancelFunc knownPod1 client.Object knownPod2 client.Object @@ -606,30 +612,33 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca knownPod6 client.Object ) - BeforeEach(func() { - informerCacheCtx, informerCacheCancel = context.WithCancel(context.Background()) + BeforeEach(func(ctx SpecContext) { + var informerCacheCtx context.Context + // Has to be derived from context.Background as it has to stay valid past the + // BeforeEach. + informerCacheCtx, informerCacheCancel = context.WithCancel(context.Background()) //nolint:forbidigo Expect(cfg).NotTo(BeNil()) By("creating three pods") cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) - err = ensureNode(testNodeOne, cl) + err = ensureNode(ctx, testNodeOne, cl) Expect(err).NotTo(HaveOccurred()) - err = ensureNode(testNodeTwo, cl) + err = ensureNode(ctx, testNodeTwo, cl) Expect(err).NotTo(HaveOccurred()) - err = ensureNamespace(testNamespaceOne, cl) + err = ensureNamespace(ctx, testNamespaceOne, cl) Expect(err).NotTo(HaveOccurred()) - err = ensureNamespace(testNamespaceTwo, cl) + err = ensureNamespace(ctx, testNamespaceTwo, cl) Expect(err).NotTo(HaveOccurred()) - err = ensureNamespace(testNamespaceThree, cl) + err = ensureNamespace(ctx, testNamespaceThree, cl) Expect(err).NotTo(HaveOccurred()) // Includes restart policy since these objects are indexed on this field. - knownPod1 = createPod("test-pod-1", testNamespaceOne, corev1.RestartPolicyNever) - knownPod2 = createPod("test-pod-2", testNamespaceTwo, corev1.RestartPolicyAlways) - knownPod3 = createPodWithLabels("test-pod-3", testNamespaceTwo, corev1.RestartPolicyOnFailure, map[string]string{"common-label": "common"}) - knownPod4 = createPodWithLabels("test-pod-4", testNamespaceThree, corev1.RestartPolicyNever, map[string]string{"common-label": "common"}) - knownPod5 = createPod("test-pod-5", testNamespaceOne, corev1.RestartPolicyNever) - knownPod6 = createPod("test-pod-6", testNamespaceTwo, corev1.RestartPolicyAlways) + knownPod1 = createPod(ctx, "test-pod-1", testNamespaceOne, corev1.RestartPolicyNever) + knownPod2 = createPod(ctx, "test-pod-2", testNamespaceTwo, corev1.RestartPolicyAlways) + knownPod3 = createPodWithLabels(ctx, "test-pod-3", testNamespaceTwo, corev1.RestartPolicyOnFailure, map[string]string{"common-label": "common"}) + knownPod4 = createPodWithLabels(ctx, "test-pod-4", testNamespaceThree, corev1.RestartPolicyNever, map[string]string{"common-label": "common"}) + knownPod5 = createPod(ctx, "test-pod-5", testNamespaceOne, corev1.RestartPolicyNever) + knownPod6 = createPod(ctx, "test-pod-6", testNamespaceTwo, corev1.RestartPolicyAlways) podGVK := schema.GroupVersionKind{ Kind: "Pod", @@ -652,27 +661,27 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca defer GinkgoRecover() Expect(informerCache.Start(ctx)).To(Succeed()) }(informerCacheCtx) - Expect(informerCache.WaitForCacheSync(informerCacheCtx)).To(BeTrue()) + Expect(informerCache.WaitForCacheSync(ctx)).To(BeTrue()) }) - AfterEach(func() { + AfterEach(func(ctx SpecContext) { By("cleaning up created pods") - deletePod(knownPod1) - deletePod(knownPod2) - deletePod(knownPod3) - deletePod(knownPod4) - deletePod(knownPod5) - deletePod(knownPod6) + deletePod(ctx, knownPod1) + deletePod(ctx, knownPod2) + deletePod(ctx, knownPod3) + deletePod(ctx, knownPod4) + deletePod(ctx, knownPod5) + deletePod(ctx, knownPod6) informerCacheCancel() }) Describe("as a Reader", func() { Context("with structured objects", func() { - It("should be able to list objects that haven't been watched previously", func() { + It("should be able to list objects that haven't been watched previously", func(ctx SpecContext) { By("listing all services in the cluster") listObj := &corev1.ServiceList{} - Expect(informerCache.List(context.Background(), listObj)).To(Succeed()) + Expect(informerCache.List(ctx, listObj)).To(Succeed()) By("verifying that the returned list contains the Kubernetes service") // NB: kubernetes default service is automatically created in testenv. @@ -688,22 +697,22 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(hasKubeService).To(BeTrue()) }) - It("should be able to get objects that haven't been watched previously", func() { + It("should be able to get objects that haven't been watched previously", func(ctx SpecContext) { By("getting the Kubernetes service") svc := &corev1.Service{} svcKey := client.ObjectKey{Namespace: "default", Name: "kubernetes"} - Expect(informerCache.Get(context.Background(), svcKey, svc)).To(Succeed()) + Expect(informerCache.Get(ctx, svcKey, svc)).To(Succeed()) By("verifying that the returned service looks reasonable") Expect(svc.Name).To(Equal("kubernetes")) Expect(svc.Namespace).To(Equal("default")) }) - It("should support filtering by labels in a single namespace", func() { + It("should support filtering by labels in a single namespace", func(ctx SpecContext) { By("listing pods with a particular label") // NB: each pod has a "test-label": out := corev1.PodList{} - Expect(informerCache.List(context.Background(), &out, + Expect(informerCache.List(ctx, &out, client.InNamespace(testNamespaceTwo), client.MatchingLabels(map[string]string{"test-label": "test-pod-2"}))).To(Succeed()) @@ -714,16 +723,16 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(actual.Labels["test-label"]).To(Equal("test-pod-2")) }) - It("should support filtering by labels from multiple namespaces", func() { + It("should support filtering by labels from multiple namespaces", func(ctx SpecContext) { By("creating another pod with the same label but different namespace") - anotherPod := createPod("test-pod-2", testNamespaceOne, corev1.RestartPolicyAlways) - defer deletePod(anotherPod) + anotherPod := createPod(ctx, "test-pod-2", testNamespaceOne, corev1.RestartPolicyAlways) + defer deletePod(ctx, anotherPod) By("listing pods with a particular label") // NB: each pod has a "test-label": out := corev1.PodList{} labels := map[string]string{"test-label": "test-pod-2"} - Expect(informerCache.List(context.Background(), &out, client.MatchingLabels(labels))).To(Succeed()) + Expect(informerCache.List(ctx, &out, client.MatchingLabels(labels))).To(Succeed()) By("verifying multiple pods with the same label in different namespaces are returned") Expect(out.Items).NotTo(BeEmpty()) @@ -734,10 +743,10 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca }) if !isPodDisableDeepCopy(opts) { - It("should be able to list objects with GVK populated", func() { + It("should be able to list objects with GVK populated", func(ctx SpecContext) { By("listing pods") out := &corev1.PodList{} - Expect(informerCache.List(context.Background(), out)).To(Succeed()) + Expect(informerCache.List(ctx, out)).To(Succeed()) By("verifying that the returned pods have GVK populated") Expect(out.Items).NotTo(BeEmpty()) @@ -748,10 +757,10 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca }) } - It("should be able to list objects by namespace", func() { + It("should be able to list objects by namespace", func(ctx SpecContext) { By("listing pods in test-namespace-1") listObj := &corev1.PodList{} - Expect(informerCache.List(context.Background(), listObj, + Expect(informerCache.List(ctx, listObj, client.InNamespace(testNamespaceOne))).To(Succeed()) By("verifying that the returned pods are in test-namespace-1") @@ -763,11 +772,11 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca }) if !isPodDisableDeepCopy(opts) { - It("should deep copy the object unless told otherwise", func() { + It("should deep copy the object unless told otherwise", func(ctx SpecContext) { By("retrieving a specific pod from the cache") out := &corev1.Pod{} podKey := client.ObjectKey{Name: "test-pod-2", Namespace: testNamespaceTwo} - Expect(informerCache.Get(context.Background(), podKey, out)).To(Succeed()) + Expect(informerCache.Get(ctx, podKey, out)).To(Succeed()) By("verifying the retrieved pod is equal to a known pod") Expect(out).To(Equal(knownPod2)) @@ -779,13 +788,13 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(out).NotTo(Equal(knownPod2)) }) } else { - It("should not deep copy the object if UnsafeDisableDeepCopy is enabled", func() { + It("should not deep copy the object if UnsafeDisableDeepCopy is enabled", func(ctx SpecContext) { By("getting a specific pod from the cache twice") podKey := client.ObjectKey{Name: "test-pod-2", Namespace: testNamespaceTwo} out1 := &corev1.Pod{} - Expect(informerCache.Get(context.Background(), podKey, out1)).To(Succeed()) + Expect(informerCache.Get(ctx, podKey, out1)).To(Succeed()) out2 := &corev1.Pod{} - Expect(informerCache.Get(context.Background(), podKey, out2)).To(Succeed()) + Expect(informerCache.Get(ctx, podKey, out2)).To(Succeed()) By("verifying the pointer fields in pod have the same addresses") Expect(out1).To(Equal(out2)) @@ -793,9 +802,9 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("listing pods from the cache twice") outList1 := &corev1.PodList{} - Expect(informerCache.List(context.Background(), outList1, client.InNamespace(testNamespaceOne))).To(Succeed()) + Expect(informerCache.List(ctx, outList1, client.InNamespace(testNamespaceOne))).To(Succeed()) outList2 := &corev1.PodList{} - Expect(informerCache.List(context.Background(), outList2, client.InNamespace(testNamespaceOne))).To(Succeed()) + Expect(informerCache.List(ctx, outList2, client.InNamespace(testNamespaceOne))).To(Succeed()) By("verifying the pointer fields in pod have the same addresses") Expect(outList1.Items).To(HaveLen(len(outList2.Items))) @@ -810,81 +819,81 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca }) } - It("should return an error if the object is not found", func() { + It("should return an error if the object is not found", func(ctx SpecContext) { By("getting a service that does not exists") svc := &corev1.Service{} svcKey := client.ObjectKey{Namespace: testNamespaceOne, Name: "unknown"} By("verifying that an error is returned") - err := informerCache.Get(context.Background(), svcKey, svc) + err := informerCache.Get(ctx, svcKey, svc) Expect(err).To(HaveOccurred()) Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) - It("should return an error if getting object in unwatched namespace", func() { + It("should return an error if getting object in unwatched namespace", func(ctx SpecContext) { By("getting a service that does not exists") svc := &corev1.Service{} svcKey := client.ObjectKey{Namespace: "unknown", Name: "unknown"} By("verifying that an error is returned") - err := informerCache.Get(context.Background(), svcKey, svc) + err := informerCache.Get(ctx, svcKey, svc) Expect(err).To(HaveOccurred()) }) - It("should return an error when context is cancelled", func() { + It("should return an error when context is cancelled", func(specCtx SpecContext) { By("cancelling the context") - informerCacheCancel() + ctx := cancelledCtx(specCtx) By("listing pods in test-namespace-1 with a cancelled context") listObj := &corev1.PodList{} - err := informerCache.List(informerCacheCtx, listObj, client.InNamespace(testNamespaceOne)) + err := informerCache.List(ctx, listObj, client.InNamespace(testNamespaceOne)) By("verifying that an error is returned") Expect(err).To(HaveOccurred()) Expect(apierrors.IsTimeout(err)).To(BeTrue()) }) - It("should set the Limit option and limit number of objects to Limit when List is called", func() { + It("should set the Limit option and limit number of objects to Limit when List is called", func(ctx SpecContext) { opts := &client.ListOptions{Limit: int64(3)} By("verifying that only Limit (3) number of objects are retrieved from the cache") listObj := &corev1.PodList{} - Expect(informerCache.List(context.Background(), listObj, opts)).To(Succeed()) + Expect(informerCache.List(ctx, listObj, opts)).To(Succeed()) Expect(listObj.Items).Should(HaveLen(3)) }) - It("should return a limited result set matching the correct label", func() { + It("should return a limited result set matching the correct label", func(ctx SpecContext) { listObj := &corev1.PodList{} labelOpt := client.MatchingLabels(map[string]string{"common-label": "common"}) limitOpt := client.Limit(1) By("verifying that only Limit (1) number of objects are retrieved from the cache") - Expect(informerCache.List(context.Background(), listObj, labelOpt, limitOpt)).To(Succeed()) + Expect(informerCache.List(ctx, listObj, labelOpt, limitOpt)).To(Succeed()) Expect(listObj.Items).Should(HaveLen(1)) }) - It("should return an error if pagination is used", func() { + It("should return an error if pagination is used", func(ctx SpecContext) { listObj := &corev1.PodList{} By("verifying that the first list works and returns a sentinel continue") - err := informerCache.List(context.Background(), listObj) + err := informerCache.List(ctx, listObj) Expect(err).ToNot(HaveOccurred()) Expect(listObj.Continue).To(Equal("continue-not-supported")) By("verifying that an error is returned") - err = informerCache.List(context.Background(), listObj, client.Continue(listObj.Continue)) + err = informerCache.List(ctx, listObj, client.Continue(listObj.Continue)) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("continue list option is not supported by the cache")) }) - It("should return an error if the continue list options is set", func() { + It("should return an error if the continue list options is set", func(ctx SpecContext) { listObj := &corev1.PodList{} continueOpt := client.Continue("token") By("verifying that an error is returned") - err := informerCache.List(context.Background(), listObj, continueOpt) + err := informerCache.List(ctx, listObj, continueOpt) Expect(err).To(HaveOccurred()) }) }) Context("with unstructured objects", func() { - It("should be able to list objects that haven't been watched previously", func() { + It("should be able to list objects that haven't been watched previously", func(ctx SpecContext) { By("listing all services in the cluster") listObj := &unstructured.UnstructuredList{} listObj.SetGroupVersionKind(schema.GroupVersionKind{ @@ -892,7 +901,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "ServiceList", }) - err := informerCache.List(context.Background(), listObj) + err := informerCache.List(ctx, listObj) Expect(err).To(Succeed()) By("verifying that the returned list contains the Kubernetes service") @@ -908,7 +917,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca } Expect(hasKubeService).To(BeTrue()) }) - It("should be able to get objects that haven't been watched previously", func() { + It("should be able to get objects that haven't been watched previously", func(ctx SpecContext) { By("getting the Kubernetes service") svc := &unstructured.Unstructured{} svc.SetGroupVersionKind(schema.GroupVersionKind{ @@ -917,14 +926,14 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Kind: "Service", }) svcKey := client.ObjectKey{Namespace: "default", Name: "kubernetes"} - Expect(informerCache.Get(context.Background(), svcKey, svc)).To(Succeed()) + Expect(informerCache.Get(ctx, svcKey, svc)).To(Succeed()) By("verifying that the returned service looks reasonable") Expect(svc.GetName()).To(Equal("kubernetes")) Expect(svc.GetNamespace()).To(Equal("default")) }) - It("should support filtering by labels in a single namespace", func() { + It("should support filtering by labels in a single namespace", func(ctx SpecContext) { By("listing pods with a particular label") // NB: each pod has a "test-label": out := unstructured.UnstructuredList{} @@ -933,7 +942,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "PodList", }) - err := informerCache.List(context.Background(), &out, + err := informerCache.List(ctx, &out, client.InNamespace(testNamespaceTwo), client.MatchingLabels(map[string]string{"test-label": "test-pod-2"})) Expect(err).To(Succeed()) @@ -945,10 +954,10 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(actual.GetLabels()["test-label"]).To(Equal("test-pod-2")) }) - It("should support filtering by labels from multiple namespaces", func() { + It("should support filtering by labels from multiple namespaces", func(ctx SpecContext) { By("creating another pod with the same label but different namespace") - anotherPod := createPod("test-pod-2", testNamespaceOne, corev1.RestartPolicyAlways) - defer deletePod(anotherPod) + anotherPod := createPod(ctx, "test-pod-2", testNamespaceOne, corev1.RestartPolicyAlways) + defer deletePod(ctx, anotherPod) By("listing pods with a particular label") // NB: each pod has a "test-label": @@ -959,7 +968,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Kind: "PodList", }) labels := map[string]string{"test-label": "test-pod-2"} - err := informerCache.List(context.Background(), &out, client.MatchingLabels(labels)) + err := informerCache.List(ctx, &out, client.MatchingLabels(labels)) Expect(err).To(Succeed()) By("verifying multiple pods with the same label in different namespaces are returned") @@ -970,7 +979,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca } }) - It("should be able to list objects by namespace", func() { + It("should be able to list objects by namespace", func(ctx SpecContext) { By("listing pods in test-namespace-1") listObj := &unstructured.UnstructuredList{} listObj.SetGroupVersionKind(schema.GroupVersionKind{ @@ -978,7 +987,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "PodList", }) - err := informerCache.List(context.Background(), listObj, client.InNamespace(testNamespaceOne)) + err := informerCache.List(ctx, listObj, client.InNamespace(testNamespaceOne)) Expect(err).To(Succeed()) By("verifying that the returned pods are in test-namespace-1") @@ -1016,7 +1025,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca } for _, tc := range cacheRestrictSubTests { - It("should be able to restrict cache to a namespace "+tc.nameSuffix, func() { + It("should be able to restrict cache to a namespace "+tc.nameSuffix, func(ctx SpecContext) { By("creating a namespaced cache") namespacedCache, err := cache.New(cfg, tc.cacheOpts) Expect(err).NotTo(HaveOccurred()) @@ -1024,9 +1033,9 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("running the cache and waiting for it to sync") go func() { defer GinkgoRecover() - Expect(namespacedCache.Start(informerCacheCtx)).To(Succeed()) + Expect(namespacedCache.Start(ctx)).To(Succeed()) }() - Expect(namespacedCache.WaitForCacheSync(informerCacheCtx)).To(BeTrue()) + Expect(namespacedCache.WaitForCacheSync(ctx)).To(BeTrue()) By("listing pods in all namespaces") out := &unstructured.UnstructuredList{} @@ -1036,7 +1045,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Kind: "PodList", }) for range 2 { - Expect(namespacedCache.List(context.Background(), out)).To(Succeed()) + Expect(namespacedCache.List(ctx, out)).To(Succeed()) By("verifying the returned pod is from the watched namespace") Expect(out.Items).NotTo(BeEmpty()) @@ -1052,7 +1061,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "NodeList", }) - Expect(namespacedCache.List(context.Background(), nodeList)).To(Succeed()) + Expect(namespacedCache.List(ctx, nodeList)).To(Succeed()) By("verifying the node list is not empty") Expect(nodeList.Items).NotTo(BeEmpty()) @@ -1067,16 +1076,16 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("verifying that getting the node works with an empty namespace") key1 := client.ObjectKey{Namespace: "", Name: testNodeOne} - Expect(namespacedCache.Get(context.Background(), key1, node)).To(Succeed()) + Expect(namespacedCache.Get(ctx, key1, node)).To(Succeed()) By("verifying that the namespace is ignored when getting a cluster-scoped resource") key2 := client.ObjectKey{Namespace: "random", Name: testNodeOne} - Expect(namespacedCache.Get(context.Background(), key2, node)).To(Succeed()) + Expect(namespacedCache.Get(ctx, key2, node)).To(Succeed()) }) } if !isPodDisableDeepCopy(opts) { - It("should deep copy the object unless told otherwise", func() { + It("should deep copy the object unless told otherwise", func(ctx SpecContext) { By("retrieving a specific pod from the cache") out := &unstructured.Unstructured{} out.SetGroupVersionKind(schema.GroupVersionKind{ @@ -1088,7 +1097,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(kscheme.Scheme.Convert(knownPod2, uKnownPod2, nil)).To(Succeed()) podKey := client.ObjectKey{Name: "test-pod-2", Namespace: testNamespaceTwo} - Expect(informerCache.Get(context.Background(), podKey, out)).To(Succeed()) + Expect(informerCache.Get(ctx, podKey, out)).To(Succeed()) By("verifying the retrieved pod is equal to a known pod") Expect(out).To(Equal(uKnownPod2)) @@ -1101,15 +1110,15 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(out).NotTo(Equal(knownPod2)) }) } else { - It("should not deep copy the object if UnsafeDisableDeepCopy is enabled", func() { + It("should not deep copy the object if UnsafeDisableDeepCopy is enabled", func(ctx SpecContext) { By("getting a specific pod from the cache twice") podKey := client.ObjectKey{Name: "test-pod-2", Namespace: testNamespaceTwo} out1 := &unstructured.Unstructured{} out1.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}) - Expect(informerCache.Get(context.Background(), podKey, out1)).To(Succeed()) + Expect(informerCache.Get(ctx, podKey, out1)).To(Succeed()) out2 := &unstructured.Unstructured{} out2.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}) - Expect(informerCache.Get(context.Background(), podKey, out2)).To(Succeed()) + Expect(informerCache.Get(ctx, podKey, out2)).To(Succeed()) By("verifying the pointer fields in pod have the same addresses") Expect(out1).To(Equal(out2)) @@ -1118,10 +1127,10 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("listing pods from the cache twice") outList1 := &unstructured.UnstructuredList{} outList1.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "PodList"}) - Expect(informerCache.List(context.Background(), outList1, client.InNamespace(testNamespaceOne))).To(Succeed()) + Expect(informerCache.List(ctx, outList1, client.InNamespace(testNamespaceOne))).To(Succeed()) outList2 := &unstructured.UnstructuredList{} outList2.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "PodList"}) - Expect(informerCache.List(context.Background(), outList2, client.InNamespace(testNamespaceOne))).To(Succeed()) + Expect(informerCache.List(ctx, outList2, client.InNamespace(testNamespaceOne))).To(Succeed()) By("verifying the pointer fields in pod have the same addresses") Expect(outList1.Items).To(HaveLen(len(outList2.Items))) @@ -1136,7 +1145,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca }) } - It("should return an error if the object is not found", func() { + It("should return an error if the object is not found", func(ctx SpecContext) { By("getting a service that does not exists") svc := &unstructured.Unstructured{} svc.SetGroupVersionKind(schema.GroupVersionKind{ @@ -1147,20 +1156,20 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca svcKey := client.ObjectKey{Namespace: testNamespaceOne, Name: "unknown"} By("verifying that an error is returned") - err := informerCache.Get(context.Background(), svcKey, svc) + err := informerCache.Get(ctx, svcKey, svc) Expect(err).To(HaveOccurred()) Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) - It("should return an error if getting object in unwatched namespace", func() { + It("should return an error if getting object in unwatched namespace", func(ctx SpecContext) { By("getting a service that does not exists") svc := &corev1.Service{} svcKey := client.ObjectKey{Namespace: "unknown", Name: "unknown"} By("verifying that an error is returned") - err := informerCache.Get(context.Background(), svcKey, svc) + err := informerCache.Get(ctx, svcKey, svc) Expect(err).To(HaveOccurred()) }) - It("test multinamespaced cache for cluster scoped resources", func() { + It("test multinamespaced cache for cluster scoped resources", func(ctx SpecContext) { By("creating a multinamespaced cache to watch specific namespaces") m, err := cache.New(cfg, cache.Options{ DefaultNamespaces: map[string]cache.Config{ @@ -1173,16 +1182,16 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("running the cache and waiting it for sync") go func() { defer GinkgoRecover() - Expect(m.Start(informerCacheCtx)).To(Succeed()) + Expect(m.Start(ctx)).To(Succeed()) }() - Expect(m.WaitForCacheSync(informerCacheCtx)).To(BeTrue()) + Expect(m.WaitForCacheSync(ctx)).To(BeTrue()) By("should be able to fetch cluster scoped resource") node := &corev1.Node{} By("verifying that getting the node works with an empty namespace") key1 := client.ObjectKey{Namespace: "", Name: testNodeOne} - Expect(m.Get(context.Background(), key1, node)).To(Succeed()) + Expect(m.Get(ctx, key1, node)).To(Succeed()) By("verifying if the cluster scoped resources are not duplicated") nodeList := &unstructured.UnstructuredList{} @@ -1191,14 +1200,14 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "NodeList", }) - Expect(m.List(context.Background(), nodeList)).To(Succeed()) + Expect(m.List(ctx, nodeList)).To(Succeed()) By("verifying the node list is not empty") Expect(nodeList.Items).NotTo(BeEmpty()) Expect(len(nodeList.Items)).To(BeEquivalentTo(2)) }) - It("should return an error if pagination is used", func() { + It("should return an error if pagination is used", func(ctx SpecContext) { nodeList := &unstructured.UnstructuredList{} nodeList.SetGroupVersionKind(schema.GroupVersionKind{ Group: "", @@ -1206,26 +1215,26 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Kind: "NodeList", }) By("verifying that the first list works and returns a sentinel continue") - err := informerCache.List(context.Background(), nodeList) + err := informerCache.List(ctx, nodeList) Expect(err).ToNot(HaveOccurred()) Expect(nodeList.GetContinue()).To(Equal("continue-not-supported")) By("verifying that an error is returned") - err = informerCache.List(context.Background(), nodeList, client.Continue(nodeList.GetContinue())) + err = informerCache.List(ctx, nodeList, client.Continue(nodeList.GetContinue())) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("continue list option is not supported by the cache")) }) - It("should return an error if the continue list options is set", func() { + It("should return an error if the continue list options is set", func(ctx SpecContext) { podList := &unstructured.Unstructured{} continueOpt := client.Continue("token") By("verifying that an error is returned") - err := informerCache.List(context.Background(), podList, continueOpt) + err := informerCache.List(ctx, podList, continueOpt) Expect(err).To(HaveOccurred()) }) }) Context("with metadata-only objects", func() { - It("should be able to list objects that haven't been watched previously", func() { + It("should be able to list objects that haven't been watched previously", func(ctx SpecContext) { By("listing all services in the cluster") listObj := &metav1.PartialObjectMetadataList{} listObj.SetGroupVersionKind(schema.GroupVersionKind{ @@ -1233,7 +1242,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "ServiceList", }) - err := informerCache.List(context.Background(), listObj) + err := informerCache.List(ctx, listObj) Expect(err).To(Succeed()) By("verifying that the returned list contains the Kubernetes service") @@ -1249,7 +1258,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca } Expect(hasKubeService).To(BeTrue()) }) - It("should be able to get objects that haven't been watched previously", func() { + It("should be able to get objects that haven't been watched previously", func(ctx SpecContext) { By("getting the Kubernetes service") svc := &metav1.PartialObjectMetadata{} svc.SetGroupVersionKind(schema.GroupVersionKind{ @@ -1258,14 +1267,14 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Kind: "Service", }) svcKey := client.ObjectKey{Namespace: "default", Name: "kubernetes"} - Expect(informerCache.Get(context.Background(), svcKey, svc)).To(Succeed()) + Expect(informerCache.Get(ctx, svcKey, svc)).To(Succeed()) By("verifying that the returned service looks reasonable") Expect(svc.GetName()).To(Equal("kubernetes")) Expect(svc.GetNamespace()).To(Equal("default")) }) - It("should support filtering by labels in a single namespace", func() { + It("should support filtering by labels in a single namespace", func(ctx SpecContext) { By("listing pods with a particular label") // NB: each pod has a "test-label": out := metav1.PartialObjectMetadataList{} @@ -1274,7 +1283,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "PodList", }) - err := informerCache.List(context.Background(), &out, + err := informerCache.List(ctx, &out, client.InNamespace(testNamespaceTwo), client.MatchingLabels(map[string]string{"test-label": "test-pod-2"})) Expect(err).To(Succeed()) @@ -1286,10 +1295,10 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(actual.GetLabels()["test-label"]).To(Equal("test-pod-2")) }) - It("should support filtering by labels from multiple namespaces", func() { + It("should support filtering by labels from multiple namespaces", func(ctx SpecContext) { By("creating another pod with the same label but different namespace") - anotherPod := createPod("test-pod-2", testNamespaceOne, corev1.RestartPolicyAlways) - defer deletePod(anotherPod) + anotherPod := createPod(ctx, "test-pod-2", testNamespaceOne, corev1.RestartPolicyAlways) + defer deletePod(ctx, anotherPod) By("listing pods with a particular label") // NB: each pod has a "test-label": @@ -1300,7 +1309,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Kind: "PodList", }) labels := map[string]string{"test-label": "test-pod-2"} - err := informerCache.List(context.Background(), &out, client.MatchingLabels(labels)) + err := informerCache.List(ctx, &out, client.MatchingLabels(labels)) Expect(err).To(Succeed()) By("verifying multiple pods with the same label in different namespaces are returned") @@ -1311,7 +1320,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca } }) - It("should be able to list objects by namespace", func() { + It("should be able to list objects by namespace", func(ctx SpecContext) { By("listing pods in test-namespace-1") listObj := &metav1.PartialObjectMetadataList{} listObj.SetGroupVersionKind(schema.GroupVersionKind{ @@ -1319,7 +1328,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "PodList", }) - err := informerCache.List(context.Background(), listObj, client.InNamespace(testNamespaceOne)) + err := informerCache.List(ctx, listObj, client.InNamespace(testNamespaceOne)) Expect(err).To(Succeed()) By("verifying that the returned pods are in test-namespace-1") @@ -1330,7 +1339,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca } }) - It("should be able to restrict cache to a namespace", func() { + It("should be able to restrict cache to a namespace", func(ctx SpecContext) { By("creating a namespaced cache") namespacedCache, err := cache.New(cfg, cache.Options{DefaultNamespaces: map[string]cache.Config{testNamespaceOne: {}}}) Expect(err).NotTo(HaveOccurred()) @@ -1338,9 +1347,9 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("running the cache and waiting for it to sync") go func() { defer GinkgoRecover() - Expect(namespacedCache.Start(informerCacheCtx)).To(Succeed()) + Expect(namespacedCache.Start(ctx)).To(Succeed()) }() - Expect(namespacedCache.WaitForCacheSync(informerCacheCtx)).To(BeTrue()) + Expect(namespacedCache.WaitForCacheSync(ctx)).To(BeTrue()) By("listing pods in all namespaces") out := &metav1.PartialObjectMetadataList{} @@ -1349,7 +1358,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "PodList", }) - Expect(namespacedCache.List(context.Background(), out)).To(Succeed()) + Expect(namespacedCache.List(ctx, out)).To(Succeed()) By("verifying the returned pod is from the watched namespace") Expect(out.Items).NotTo(BeEmpty()) @@ -1364,7 +1373,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "NodeList", }) - Expect(namespacedCache.List(context.Background(), nodeList)).To(Succeed()) + Expect(namespacedCache.List(ctx, nodeList)).To(Succeed()) By("verifying the node list is not empty") Expect(nodeList.Items).NotTo(BeEmpty()) @@ -1379,14 +1388,14 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("verifying that getting the node works with an empty namespace") key1 := client.ObjectKey{Namespace: "", Name: testNodeOne} - Expect(namespacedCache.Get(context.Background(), key1, node)).To(Succeed()) + Expect(namespacedCache.Get(ctx, key1, node)).To(Succeed()) By("verifying that the namespace is ignored when getting a cluster-scoped resource") key2 := client.ObjectKey{Namespace: "random", Name: testNodeOne} - Expect(namespacedCache.Get(context.Background(), key2, node)).To(Succeed()) + Expect(namespacedCache.Get(ctx, key2, node)).To(Succeed()) }) - It("should be able to restrict cache to a namespace for namespaced object and to given selectors for non namespaced object", func() { + It("should be able to restrict cache to a namespace for namespaced object and to given selectors for non namespaced object", func(ctx SpecContext) { By("creating a namespaced cache") namespacedCache, err := cache.New(cfg, cache.Options{ DefaultNamespaces: map[string]cache.Config{testNamespaceOne: {}}, @@ -1401,9 +1410,9 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("running the cache and waiting for it to sync") go func() { defer GinkgoRecover() - Expect(namespacedCache.Start(informerCacheCtx)).To(Succeed()) + Expect(namespacedCache.Start(ctx)).To(Succeed()) }() - Expect(namespacedCache.WaitForCacheSync(informerCacheCtx)).To(BeTrue()) + Expect(namespacedCache.WaitForCacheSync(ctx)).To(BeTrue()) By("listing pods in all namespaces") out := &metav1.PartialObjectMetadataList{} @@ -1412,7 +1421,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "PodList", }) - Expect(namespacedCache.List(context.Background(), out)).To(Succeed()) + Expect(namespacedCache.List(ctx, out)).To(Succeed()) By("verifying the returned pod is from the watched namespace") Expect(out.Items).NotTo(BeEmpty()) @@ -1427,7 +1436,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "NodeList", }) - Expect(namespacedCache.List(context.Background(), nodeList)).To(Succeed()) + Expect(namespacedCache.List(ctx, nodeList)).To(Succeed()) By("verifying the node list is not empty") Expect(nodeList.Items).NotTo(BeEmpty()) @@ -1442,21 +1451,21 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("verifying that getting the node works with an empty namespace") key1 := client.ObjectKey{Namespace: "", Name: testNodeTwo} - Expect(namespacedCache.Get(context.Background(), key1, node)).To(Succeed()) + Expect(namespacedCache.Get(ctx, key1, node)).To(Succeed()) By("verifying that the namespace is ignored when getting a cluster-scoped resource") key2 := client.ObjectKey{Namespace: "random", Name: testNodeTwo} - Expect(namespacedCache.Get(context.Background(), key2, node)).To(Succeed()) + Expect(namespacedCache.Get(ctx, key2, node)).To(Succeed()) By("verifying that an error is returned for node with not matching label") key3 := client.ObjectKey{Namespace: "", Name: testNodeOne} - err = namespacedCache.Get(context.Background(), key3, node) + err = namespacedCache.Get(ctx, key3, node) Expect(err).To(HaveOccurred()) Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) if !isPodDisableDeepCopy(opts) { - It("should deep copy the object unless told otherwise", func() { + It("should deep copy the object unless told otherwise", func(ctx SpecContext) { By("retrieving a specific pod from the cache") out := &metav1.PartialObjectMetadata{} out.SetGroupVersionKind(schema.GroupVersionKind{ @@ -1473,7 +1482,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca }) podKey := client.ObjectKey{Name: "test-pod-2", Namespace: testNamespaceTwo} - Expect(informerCache.Get(context.Background(), podKey, out)).To(Succeed()) + Expect(informerCache.Get(ctx, podKey, out)).To(Succeed()) By("verifying the retrieved pod is equal to a known pod") Expect(out).To(Equal(uKnownPod2)) @@ -1485,15 +1494,15 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(out).NotTo(Equal(knownPod2)) }) } else { - It("should not deep copy the object if UnsafeDisableDeepCopy is enabled", func() { + It("should not deep copy the object if UnsafeDisableDeepCopy is enabled", func(ctx SpecContext) { By("getting a specific pod from the cache twice") podKey := client.ObjectKey{Name: "test-pod-2", Namespace: testNamespaceTwo} out1 := &metav1.PartialObjectMetadata{} out1.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}) - Expect(informerCache.Get(context.Background(), podKey, out1)).To(Succeed()) + Expect(informerCache.Get(ctx, podKey, out1)).To(Succeed()) out2 := &metav1.PartialObjectMetadata{} out2.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}) - Expect(informerCache.Get(context.Background(), podKey, out2)).To(Succeed()) + Expect(informerCache.Get(ctx, podKey, out2)).To(Succeed()) By("verifying the pods have the same pointer addresses") By("verifying the pointer fields in pod have the same addresses") @@ -1503,10 +1512,10 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("listing pods from the cache twice") outList1 := &metav1.PartialObjectMetadataList{} outList1.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "PodList"}) - Expect(informerCache.List(context.Background(), outList1, client.InNamespace(testNamespaceOne))).To(Succeed()) + Expect(informerCache.List(ctx, outList1, client.InNamespace(testNamespaceOne))).To(Succeed()) outList2 := &metav1.PartialObjectMetadataList{} outList2.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "PodList"}) - Expect(informerCache.List(context.Background(), outList2, client.InNamespace(testNamespaceOne))).To(Succeed()) + Expect(informerCache.List(ctx, outList2, client.InNamespace(testNamespaceOne))).To(Succeed()) By("verifying the pointer fields in pod have the same addresses") Expect(outList1.Items).To(HaveLen(len(outList2.Items))) @@ -1521,7 +1530,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca }) } - It("should return an error if the object is not found", func() { + It("should return an error if the object is not found", func(ctx SpecContext) { By("getting a service that does not exists") svc := &metav1.PartialObjectMetadata{} svc.SetGroupVersionKind(schema.GroupVersionKind{ @@ -1532,21 +1541,21 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca svcKey := client.ObjectKey{Namespace: testNamespaceOne, Name: "unknown"} By("verifying that an error is returned") - err := informerCache.Get(context.Background(), svcKey, svc) + err := informerCache.Get(ctx, svcKey, svc) Expect(err).To(HaveOccurred()) Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) - It("should return an error if getting object in unwatched namespace", func() { + It("should return an error if getting object in unwatched namespace", func(ctx SpecContext) { By("getting a service that does not exists") svc := &corev1.Service{} svcKey := client.ObjectKey{Namespace: "unknown", Name: "unknown"} By("verifying that an error is returned") - err := informerCache.Get(context.Background(), svcKey, svc) + err := informerCache.Get(ctx, svcKey, svc) Expect(err).To(HaveOccurred()) }) - It("should return an error if pagination is used", func() { + It("should return an error if pagination is used", func(ctx SpecContext) { nodeList := &metav1.PartialObjectMetadataList{} nodeList.SetGroupVersionKind(schema.GroupVersionKind{ Group: "", @@ -1554,12 +1563,12 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Kind: "NodeList", }) By("verifying that the first list works and returns a sentinel continue") - err := informerCache.List(context.Background(), nodeList) + err := informerCache.List(ctx, nodeList) Expect(err).ToNot(HaveOccurred()) Expect(nodeList.GetContinue()).To(Equal("continue-not-supported")) By("verifying that an error is returned") - err = informerCache.List(context.Background(), nodeList, client.Continue(nodeList.GetContinue())) + err = informerCache.List(ctx, nodeList, client.Continue(nodeList.GetContinue())) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("continue list option is not supported by the cache")) }) @@ -1568,7 +1577,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca options cache.Options expectedPods []string } - DescribeTable(" and cache with selectors", func(tc selectorsTestCase) { + DescribeTable(" and cache with selectors", func(ctx SpecContext, tc selectorsTestCase) { By("creating the cache") informer, err := cache.New(cfg, tc.options) Expect(err).NotTo(HaveOccurred()) @@ -1576,13 +1585,13 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("running the cache and waiting for it to sync") go func() { defer GinkgoRecover() - Expect(informer.Start(informerCacheCtx)).To(Succeed()) + Expect(informer.Start(ctx)).To(Succeed()) }() - Expect(informer.WaitForCacheSync(informerCacheCtx)).To(BeTrue()) + Expect(informer.WaitForCacheSync(ctx)).To(BeTrue()) By("Checking with structured") obtainedStructuredPodList := corev1.PodList{} - Expect(informer.List(context.Background(), &obtainedStructuredPodList)).To(Succeed()) + Expect(informer.List(ctx, &obtainedStructuredPodList)).To(Succeed()) Expect(obtainedStructuredPodList.Items).Should(WithTransform(func(pods []corev1.Pod) []string { obtainedPodNames := []string{} for _, pod := range pods { @@ -1591,7 +1600,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca return obtainedPodNames }, ConsistOf(tc.expectedPods))) for _, pod := range obtainedStructuredPodList.Items { - Expect(informer.Get(context.Background(), client.ObjectKeyFromObject(&pod), &pod)).To(Succeed()) + Expect(informer.Get(ctx, client.ObjectKeyFromObject(&pod), &pod)).To(Succeed()) } By("Checking with unstructured") @@ -1601,7 +1610,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "PodList", }) - err = informer.List(context.Background(), &obtainedUnstructuredPodList) + err = informer.List(ctx, &obtainedUnstructuredPodList) Expect(err).To(Succeed()) Expect(obtainedUnstructuredPodList.Items).Should(WithTransform(func(pods []unstructured.Unstructured) []string { obtainedPodNames := []string{} @@ -1611,7 +1620,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca return obtainedPodNames }, ConsistOf(tc.expectedPods))) for _, pod := range obtainedUnstructuredPodList.Items { - Expect(informer.Get(context.Background(), client.ObjectKeyFromObject(&pod), &pod)).To(Succeed()) + Expect(informer.Get(ctx, client.ObjectKeyFromObject(&pod), &pod)).To(Succeed()) } By("Checking with metadata") @@ -1621,7 +1630,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "PodList", }) - err = informer.List(context.Background(), &obtainedMetadataPodList) + err = informer.List(ctx, &obtainedMetadataPodList) Expect(err).To(Succeed()) Expect(obtainedMetadataPodList.Items).Should(WithTransform(func(pods []metav1.PartialObjectMetadata) []string { obtainedPodNames := []string{} @@ -1631,7 +1640,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca return obtainedPodNames }, ConsistOf(tc.expectedPods))) for _, pod := range obtainedMetadataPodList.Items { - Expect(informer.Get(context.Background(), client.ObjectKeyFromObject(&pod), &pod)).To(Succeed()) + Expect(informer.Get(ctx, client.ObjectKeyFromObject(&pod), &pod)).To(Succeed()) } }, Entry("when selectors are empty it has to inform about all the pods", selectorsTestCase{ @@ -1916,14 +1925,14 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca ) }) Describe("as an Informer", func() { - It("should error when starting the cache a second time", func() { - err := informerCache.Start(context.Background()) + It("should error when starting the cache a second time", func(ctx SpecContext) { + err := informerCache.Start(ctx) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("informer already started")) }) Context("with structured objects", func() { - It("should be able to get informer for the object", func() { + It("should be able to get informer for the object", func(ctx SpecContext) { By("getting a shared index informer for a pod") pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ @@ -1939,7 +1948,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca }, }, } - sii, err := informerCache.GetInformer(context.TODO(), pod) + sii, err := informerCache.GetInformer(ctx, pod) Expect(err).NotTo(HaveOccurred()) Expect(sii).NotTo(BeNil()) Expect(sii.HasSynced()).To(BeTrue()) @@ -1954,13 +1963,13 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("adding an object") cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) - Expect(cl.Create(context.Background(), pod)).To(Succeed()) - defer deletePod(pod) + Expect(cl.Create(ctx, pod)).To(Succeed()) + defer deletePod(ctx, pod) By("verifying the object is received on the channel") Eventually(out).Should(Receive(Equal(pod))) }) - It("should be able to stop and restart informers", func() { + It("should be able to stop and restart informers", func(ctx SpecContext) { By("getting a shared index informer for a pod") pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ @@ -1976,18 +1985,18 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca }, }, } - sii, err := informerCache.GetInformer(context.TODO(), pod) + sii, err := informerCache.GetInformer(ctx, pod) Expect(err).NotTo(HaveOccurred()) Expect(sii).NotTo(BeNil()) Expect(sii.HasSynced()).To(BeTrue()) By("removing the existing informer") - Expect(informerCache.RemoveInformer(context.TODO(), pod)).To(Succeed()) + Expect(informerCache.RemoveInformer(ctx, pod)).To(Succeed()) Eventually(sii.IsStopped).WithTimeout(5 * time.Second).Should(BeTrue()) By("recreating the informer") - sii2, err := informerCache.GetInformer(context.TODO(), pod) + sii2, err := informerCache.GetInformer(ctx, pod) Expect(err).NotTo(HaveOccurred()) Expect(sii2).NotTo(BeNil()) Expect(sii2.HasSynced()).To(BeTrue()) @@ -1996,10 +2005,10 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(sii.IsStopped()).To(BeTrue()) Expect(sii2.IsStopped()).To(BeFalse()) }) - It("should be able to get an informer by group/version/kind", func() { + It("should be able to get an informer by group/version/kind", func(ctx SpecContext) { By("getting an shared index informer for gvk = core/v1/pod") gvk := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"} - sii, err := informerCache.GetInformerForKind(context.TODO(), gvk) + sii, err := informerCache.GetInformerForKind(ctx, gvk) Expect(err).NotTo(HaveOccurred()) Expect(sii).NotTo(BeNil()) Expect(sii.HasSynced()).To(BeTrue()) @@ -2028,13 +2037,13 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca }, }, } - Expect(cl.Create(context.Background(), pod)).To(Succeed()) - defer deletePod(pod) + Expect(cl.Create(ctx, pod)).To(Succeed()) + defer deletePod(ctx, pod) By("verifying the object is received on the channel") Eventually(out).Should(Receive(Equal(pod))) }) - It("should be able to index an object field then retrieve objects by that field", func() { + It("should be able to index an object field then retrieve objects by that field", func(ctx SpecContext) { By("creating the cache") informer, err := cache.New(cfg, cache.Options{}) Expect(err).NotTo(HaveOccurred()) @@ -2044,18 +2053,18 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca indexFunc := func(obj client.Object) []string { return []string{string(obj.(*corev1.Pod).Spec.RestartPolicy)} } - Expect(informer.IndexField(context.TODO(), pod, "spec.restartPolicy", indexFunc)).To(Succeed()) + Expect(informer.IndexField(ctx, pod, "spec.restartPolicy", indexFunc)).To(Succeed()) By("running the cache and waiting for it to sync") go func() { defer GinkgoRecover() - Expect(informer.Start(informerCacheCtx)).To(Succeed()) + Expect(informer.Start(ctx)).To(Succeed()) }() - Expect(informer.WaitForCacheSync(informerCacheCtx)).To(BeTrue()) + Expect(informer.WaitForCacheSync(ctx)).To(BeTrue()) By("listing Pods with restartPolicyOnFailure") listObj := &corev1.PodList{} - Expect(informer.List(context.Background(), listObj, + Expect(informer.List(ctx, listObj, client.MatchingFields{"spec.restartPolicy": "OnFailure"})).To(Succeed()) By("verifying that the returned pods have correct restart policy") Expect(listObj.Items).NotTo(BeEmpty()) @@ -2064,9 +2073,10 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(actual.Name).To(Equal("test-pod-3")) }) - It("should allow for get informer to be cancelled", func() { + It("should allow for get informer to be cancelled", func(specCtx SpecContext) { By("creating a context and cancelling it") - informerCacheCancel() + ctx, cancel := context.WithCancel(specCtx) + cancel() By("getting a shared index informer for a pod with a cancelled context") pod := &corev1.Pod{ @@ -2083,25 +2093,26 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca }, }, } - sii, err := informerCache.GetInformer(informerCacheCtx, pod) + sii, err := informerCache.GetInformer(ctx, pod) Expect(err).To(HaveOccurred()) Expect(sii).To(BeNil()) Expect(apierrors.IsTimeout(err)).To(BeTrue()) }) - It("should allow getting an informer by group/version/kind to be cancelled", func() { + It("should allow getting an informer by group/version/kind to be cancelled", func(specCtx SpecContext) { By("creating a context and cancelling it") - informerCacheCancel() + ctx, cancel := context.WithCancel(specCtx) + cancel() By("getting an shared index informer for gvk = core/v1/pod with a cancelled context") gvk := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"} - sii, err := informerCache.GetInformerForKind(informerCacheCtx, gvk) + sii, err := informerCache.GetInformerForKind(ctx, gvk) Expect(err).To(HaveOccurred()) Expect(sii).To(BeNil()) Expect(apierrors.IsTimeout(err)).To(BeTrue()) }) - It("should be able not to change indexer values after indexing cluster-scope objects", func() { + It("should be able not to change indexer values after indexing cluster-scope objects", func(ctx SpecContext) { By("creating the cache") informer, err := cache.New(cfg, cache.Options{}) Expect(err).NotTo(HaveOccurred()) @@ -2113,18 +2124,18 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca indexFunc := func(obj client.Object) []string { return indexerValues } - Expect(informer.IndexField(context.TODO(), ns, fieldName, indexFunc)).To(Succeed()) + Expect(informer.IndexField(ctx, ns, fieldName, indexFunc)).To(Succeed()) By("running the cache and waiting for it to sync") go func() { defer GinkgoRecover() - Expect(informer.Start(informerCacheCtx)).To(Succeed()) + Expect(informer.Start(ctx)).To(Succeed()) }() - Expect(informer.WaitForCacheSync(informerCacheCtx)).To(BeTrue()) + Expect(informer.WaitForCacheSync(ctx)).To(BeTrue()) By("listing Namespaces with fixed indexer") listObj := &corev1.NamespaceList{} - Expect(informer.List(context.Background(), listObj, + Expect(informer.List(ctx, listObj, client.MatchingFields{fieldName: "a"})).To(Succeed()) Expect(listObj.Items).NotTo(BeZero()) @@ -2135,7 +2146,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(indexerValues[2]).To(Equal("c")) }) - It("should be able to matching fields with multiple indexes", func() { + It("should be able to matching fields with multiple indexes", func(ctx SpecContext) { By("creating the cache") informer, err := cache.New(cfg, cache.Options{}) Expect(err).NotTo(HaveOccurred()) @@ -2146,42 +2157,42 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca indexFunc1 := func(obj client.Object) []string { return []string{obj.(*corev1.Pod).Labels["common-label"]} } - Expect(informer.IndexField(context.TODO(), pod, fieldName1, indexFunc1)).To(Succeed()) + Expect(informer.IndexField(ctx, pod, fieldName1, indexFunc1)).To(Succeed()) By("indexing pods with restart policy before starting") fieldName2 := "indexByPolicy" indexFunc2 := func(obj client.Object) []string { return []string{string(obj.(*corev1.Pod).Spec.RestartPolicy)} } - Expect(informer.IndexField(context.TODO(), pod, fieldName2, indexFunc2)).To(Succeed()) + Expect(informer.IndexField(ctx, pod, fieldName2, indexFunc2)).To(Succeed()) By("running the cache and waiting for it to sync") go func() { defer GinkgoRecover() - Expect(informer.Start(informerCacheCtx)).To(Succeed()) + Expect(informer.Start(ctx)).To(Succeed()) }() - Expect(informer.WaitForCacheSync(informerCacheCtx)).To(BeTrue()) + Expect(informer.WaitForCacheSync(ctx)).To(BeTrue()) By("listing pods with label index") listObj := &corev1.PodList{} - Expect(informer.List(context.Background(), listObj, + Expect(informer.List(ctx, listObj, client.MatchingFields{fieldName1: "common"})).To(Succeed()) Expect(listObj.Items).To(HaveLen(2)) By("listing pods with restart policy index") listObj = &corev1.PodList{} - Expect(informer.List(context.Background(), listObj, + Expect(informer.List(ctx, listObj, client.MatchingFields{fieldName2: string(corev1.RestartPolicyNever)})).To(Succeed()) Expect(listObj.Items).To(HaveLen(3)) By("listing pods with both fixed indexers 1 and 2") listObj = &corev1.PodList{} - Expect(informer.List(context.Background(), listObj, + Expect(informer.List(ctx, listObj, client.MatchingFields{fieldName1: "common", fieldName2: string(corev1.RestartPolicyNever)})).To(Succeed()) Expect(listObj.Items).To(HaveLen(1)) }) }) Context("with unstructured objects", func() { - It("should be able to get informer for the object", func() { + It("should be able to get informer for the object", func(ctx SpecContext) { By("getting a shared index informer for a pod") pod := &unstructured.Unstructured{ @@ -2203,7 +2214,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "Pod", }) - sii, err := informerCache.GetInformer(context.TODO(), pod) + sii, err := informerCache.GetInformer(ctx, pod) Expect(err).NotTo(HaveOccurred()) Expect(sii).NotTo(BeNil()) Expect(sii.HasSynced()).To(BeTrue()) @@ -2218,14 +2229,14 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("adding an object") cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) - Expect(cl.Create(context.Background(), pod)).To(Succeed()) - defer deletePod(pod) + Expect(cl.Create(ctx, pod)).To(Succeed()) + defer deletePod(ctx, pod) By("verifying the object is received on the channel") Eventually(out).Should(Receive(Equal(pod))) }) - It("should be able to stop and restart informers", func() { + It("should be able to stop and restart informers", func(ctx SpecContext) { By("getting a shared index informer for a pod") pod := &unstructured.Unstructured{ Object: map[string]interface{}{ @@ -2246,18 +2257,17 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "Pod", }) - sii, err := informerCache.GetInformer(context.TODO(), pod) + sii, err := informerCache.GetInformer(ctx, pod) Expect(err).NotTo(HaveOccurred()) Expect(sii).NotTo(BeNil()) Expect(sii.HasSynced()).To(BeTrue()) By("removing the existing informer") - Expect(informerCache.RemoveInformer(context.TODO(), pod)).To(Succeed()) + Expect(informerCache.RemoveInformer(ctx, pod)).To(Succeed()) Eventually(sii.IsStopped).WithTimeout(5 * time.Second).Should(BeTrue()) By("recreating the informer") - - sii2, err := informerCache.GetInformer(context.TODO(), pod) + sii2, err := informerCache.GetInformer(ctx, pod) Expect(err).NotTo(HaveOccurred()) Expect(sii2).NotTo(BeNil()) Expect(sii2.HasSynced()).To(BeTrue()) @@ -2267,7 +2277,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(sii2.IsStopped()).To(BeFalse()) }) - It("should be able to index an object field then retrieve objects by that field", func() { + It("should be able to index an object field then retrieve objects by that field", func(ctx SpecContext) { By("creating the cache") informer, err := cache.New(cfg, cache.Options{}) Expect(err).NotTo(HaveOccurred()) @@ -2290,14 +2300,14 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca } return []string{fmt.Sprintf("%v", m["restartPolicy"])} } - Expect(informer.IndexField(context.TODO(), pod, "spec.restartPolicy", indexFunc)).To(Succeed()) + Expect(informer.IndexField(ctx, pod, "spec.restartPolicy", indexFunc)).To(Succeed()) By("running the cache and waiting for it to sync") go func() { defer GinkgoRecover() - Expect(informer.Start(informerCacheCtx)).To(Succeed()) + Expect(informer.Start(ctx)).To(Succeed()) }() - Expect(informer.WaitForCacheSync(informerCacheCtx)).To(BeTrue()) + Expect(informer.WaitForCacheSync(ctx)).To(BeTrue()) By("listing Pods with restartPolicyOnFailure") listObj := &unstructured.UnstructuredList{} @@ -2306,7 +2316,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "PodList", }) - err = informer.List(context.Background(), listObj, + err = informer.List(ctx, listObj, client.MatchingFields{"spec.restartPolicy": "OnFailure"}) Expect(err).To(Succeed()) @@ -2317,9 +2327,9 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(actual.GetName()).To(Equal("test-pod-3")) }) - It("should allow for get informer to be cancelled", func() { + It("should allow for get informer to be cancelled", func(specCtx SpecContext) { By("cancelling the context") - informerCacheCancel() + ctx := cancelledCtx(specCtx) By("getting a shared index informer for a pod with a cancelled context") pod := &unstructured.Unstructured{} @@ -2330,14 +2340,14 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Version: "v1", Kind: "Pod", }) - sii, err := informerCache.GetInformer(informerCacheCtx, pod) + sii, err := informerCache.GetInformer(ctx, pod) Expect(err).To(HaveOccurred()) Expect(sii).To(BeNil()) Expect(apierrors.IsTimeout(err)).To(BeTrue()) }) }) Context("with metadata-only objects", func() { - It("should be able to get informer for the object", func() { + It("should be able to get informer for the object", func(ctx SpecContext) { By("getting a shared index informer for a pod") pod := &corev1.Pod{ @@ -2363,7 +2373,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Kind: "Pod", }) - sii, err := informerCache.GetInformer(context.TODO(), podMeta) + sii, err := informerCache.GetInformer(ctx, podMeta) Expect(err).NotTo(HaveOccurred()) Expect(sii).NotTo(BeNil()) Expect(sii.HasSynced()).To(BeTrue()) @@ -2378,8 +2388,8 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("adding an object") cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) - Expect(cl.Create(context.Background(), pod)).To(Succeed()) - defer deletePod(pod) + Expect(cl.Create(ctx, pod)).To(Succeed()) + defer deletePod(ctx, pod) // re-copy the result in so that we can match on it properly pod.ObjectMeta.DeepCopyInto(&podMeta.ObjectMeta) @@ -2387,7 +2397,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Eventually(out).Should(Receive(Equal(podMeta))) }) - It("should be able to index an object field then retrieve objects by that field", func() { + It("should be able to index an object field then retrieve objects by that field", func(ctx SpecContext) { By("creating the cache") informer, err := cache.New(cfg, cache.Options{}) Expect(err).NotTo(HaveOccurred()) @@ -2403,14 +2413,14 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca metadata := obj.(*metav1.PartialObjectMetadata) return []string{metadata.Labels["test-label"]} } - Expect(informer.IndexField(context.TODO(), pod, "metadata.labels.test-label", indexFunc)).To(Succeed()) + Expect(informer.IndexField(ctx, pod, "metadata.labels.test-label", indexFunc)).To(Succeed()) By("running the cache and waiting for it to sync") go func() { defer GinkgoRecover() - Expect(informer.Start(informerCacheCtx)).To(Succeed()) + Expect(informer.Start(ctx)).To(Succeed()) }() - Expect(informer.WaitForCacheSync(informerCacheCtx)).To(BeTrue()) + Expect(informer.WaitForCacheSync(ctx)).To(BeTrue()) By("listing Pods with restartPolicyOnFailure") listObj := &metav1.PartialObjectMetadataList{} @@ -2420,7 +2430,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Kind: "PodList", } listObj.SetGroupVersionKind(gvk) - err = informer.List(context.Background(), listObj, + err = informer.List(ctx, listObj, client.MatchingFields{"metadata.labels.test-label": "test-pod-3"}) Expect(err).To(Succeed()) @@ -2441,10 +2451,9 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca })) }) - It("should allow for get informer to be cancelled", func() { + It("should allow for get informer to be cancelled", func(specContext SpecContext) { By("creating a context and cancelling it") - ctx, cancel := context.WithCancel(context.Background()) - cancel() + ctx := cancelledCtx(specContext) By("getting a shared index informer for a pod with a cancelled context") pod := &metav1.PartialObjectMetadata{} @@ -2462,27 +2471,43 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca }) }) }) - Describe("use UnsafeDisableDeepCopy list options", func() { - It("should be able to change object in informer cache", func() { - By("listing pods") - out := corev1.PodList{} - Expect(informerCache.List(context.Background(), &out, client.UnsafeDisableDeepCopy)).To(Succeed()) - for _, item := range out.Items { - if strings.Compare(item.Name, "test-pod-3") == 0 { // test-pod-3 has labels - item.Labels["UnsafeDisableDeepCopy"] = "true" - break + Context("using UnsafeDisableDeepCopy", func() { + Describe("with ListOptions", func() { + It("should be able to change object in informer cache", func(ctx SpecContext) { + By("listing pods") + out := corev1.PodList{} + Expect(informerCache.List(ctx, &out, client.UnsafeDisableDeepCopy)).To(Succeed()) + for _, item := range out.Items { + if strings.Compare(item.Name, "test-pod-3") == 0 { // test-pod-3 has labels + item.Labels["UnsafeDisableDeepCopy"] = "true" + break + } } - } - By("verifying that the returned pods were changed") - out2 := corev1.PodList{} - Expect(informerCache.List(context.Background(), &out, client.UnsafeDisableDeepCopy)).To(Succeed()) - for _, item := range out2.Items { - if strings.Compare(item.Name, "test-pod-3") == 0 { - Expect(item.Labels["UnsafeDisableDeepCopy"]).To(Equal("true")) - break + By("verifying that the returned pods were changed") + out2 := corev1.PodList{} + Expect(informerCache.List(ctx, &out, client.UnsafeDisableDeepCopy)).To(Succeed()) + for _, item := range out2.Items { + if strings.Compare(item.Name, "test-pod-3") == 0 { + Expect(item.Labels["UnsafeDisableDeepCopy"]).To(Equal("true")) + break + } } - } + }) + }) + Describe("with GetOptions", func() { + It("should be able to change object in informer cache", func(ctx SpecContext) { + out := corev1.Pod{} + podKey := client.ObjectKey{Name: "test-pod-2", Namespace: testNamespaceTwo} + Expect(informerCache.Get(ctx, podKey, &out, client.UnsafeDisableDeepCopy)).To(Succeed()) + + out.Labels["UnsafeDisableDeepCopy"] = "true" + + By("verifying that the returned pod was changed") + out2 := corev1.Pod{} + Expect(informerCache.Get(ctx, podKey, &out2, client.UnsafeDisableDeepCopy)).To(Succeed()) + Expect(out2.Labels["UnsafeDisableDeepCopy"]).To(Equal("true")) + }) }) }) }) @@ -2508,7 +2533,7 @@ var _ = Describe("TransformStripManagedFields", func() { }) // ensureNamespace installs namespace of a given name if not exists. -func ensureNamespace(namespace string, client client.Client) error { +func ensureNamespace(ctx context.Context, namespace string, client client.Client) error { ns := corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: namespace, @@ -2518,14 +2543,14 @@ func ensureNamespace(namespace string, client client.Client) error { APIVersion: "v1", }, } - err := client.Create(context.TODO(), &ns) + err := client.Create(ctx, &ns) if apierrors.IsAlreadyExists(err) { return nil } return err } -func ensureNode(name string, client client.Client) error { +func ensureNode(ctx context.Context, name string, client client.Client) error { node := corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -2536,7 +2561,7 @@ func ensureNode(name string, client client.Client) error { APIVersion: "v1", }, } - err := client.Create(context.TODO(), &node) + err := client.Create(ctx, &node) if apierrors.IsAlreadyExists(err) { return nil } @@ -2558,3 +2583,9 @@ func isPodDisableDeepCopy(opts cache.Options) bool { } return false } + +func cancelledCtx(ctx context.Context) context.Context { + cancelCtx, cancel := context.WithCancel(ctx) + cancel() + return cancelCtx +} diff --git a/pkg/cache/internal/cache_reader.go b/pkg/cache/internal/cache_reader.go index 33ce8a830a..eb6b544855 100644 --- a/pkg/cache/internal/cache_reader.go +++ b/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) } diff --git a/pkg/cache/internal/informers.go b/pkg/cache/internal/informers.go index 4bf832b2d9..f216be0d9e 100644 --- a/pkg/cache/internal/informers.go +++ b/pkg/cache/internal/informers.go @@ -518,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 } diff --git a/pkg/certwatcher/certwatcher_test.go b/pkg/certwatcher/certwatcher_test.go index b8018dbdc0..5579c3fdad 100644 --- a/pkg/certwatcher/certwatcher_test.go +++ b/pkg/certwatcher/certwatcher_test.go @@ -49,13 +49,14 @@ var _ = Describe("CertWatcher", func() { var _ = Describe("certwatcher Start", func() { var ( - ctx context.Context - ctxCancel context.CancelFunc - watcher *certwatcher.CertWatcher + ctxCancel context.CancelFunc + watcher *certwatcher.CertWatcher + startWatcher func(interval time.Duration) (done <-chan struct{}) ) BeforeEach(func() { - ctx, ctxCancel = context.WithCancel(context.Background()) + var ctx context.Context + ctx, ctxCancel = context.WithCancel(context.Background()) //nolint:forbidigo // the watcher outlives the BeforeEach err := writeCerts(certPath, keyPath, "127.0.0.1") Expect(err).ToNot(HaveOccurred()) @@ -74,22 +75,22 @@ var _ = Describe("CertWatcher", func() { watcher, err = certwatcher.New(certPath, keyPath) Expect(err).ToNot(HaveOccurred()) - }) - startWatcher := func(interval time.Duration) (done <-chan struct{}) { - doneCh := make(chan struct{}) - go func() { - defer GinkgoRecover() - defer close(doneCh) - Expect(watcher.WithWatchInterval(interval).Start(ctx)).To(Succeed()) - }() - // wait till we read first cert - Eventually(func() error { - err := watcher.ReadCertificate() - return err - }).Should(Succeed()) - return doneCh - } + startWatcher = func(interval time.Duration) (done <-chan struct{}) { + doneCh := make(chan struct{}) + go func() { + defer GinkgoRecover() + defer close(doneCh) + Expect(watcher.WithWatchInterval(interval).Start(ctx)).To(Succeed()) + }() + // wait till we read first cert + Eventually(func() error { + err := watcher.ReadCertificate() + return err + }).Should(Succeed()) + return doneCh + } + }) It("should read the initial cert/key", func() { // This test verifies the initial read succeeded. So interval doesn't matter. diff --git a/pkg/client/apiutil/apimachinery.go b/pkg/client/apiutil/apimachinery.go index 1d4ce264c9..b132cb2d4d 100644 --- a/pkg/client/apiutil/apimachinery.go +++ b/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/pkg/client/apiutil/apimachinery_test.go b/pkg/client/apiutil/apimachinery_test.go index c35ac086fb..122c5cc542 100644 --- a/pkg/client/apiutil/apimachinery_test.go +++ b/pkg/client/apiutil/apimachinery_test.go @@ -104,7 +104,6 @@ func TestApiMachinery(t *testing.T) { for _, runtimeGvk := range runtimeGvks { t.Run("IsGVKNamespaced should report scope for "+runtimeGvk.name, func(t *testing.T) { g := gmg.NewWithT(t) - ctx := context.Background() httpClient, err := rest.HTTPClientFor(restCfg) g.Expect(err).NotTo(gmg.HaveOccurred()) @@ -128,7 +127,7 @@ func TestApiMachinery(t *testing.T) { g.Expect(scope).To(gmg.BeTrue()) // Register a new CRD at runtime. - crd := newCRD(ctx, g, c, runtimeGvk.gvk.Group, runtimeGvk.gvk.Kind, runtimeGvk.plural) + crd := newCRD(t.Context(), g, c, runtimeGvk.gvk.Group, runtimeGvk.gvk.Kind, runtimeGvk.plural) version := crd.Spec.Versions[0] version.Name = runtimeGvk.gvk.Version version.Storage = true @@ -136,9 +135,9 @@ func TestApiMachinery(t *testing.T) { crd.Spec.Versions = []apiextensionsv1.CustomResourceDefinitionVersion{version} crd.Spec.Scope = apiextensionsv1.NamespaceScoped - g.Expect(c.Create(ctx, crd)).To(gmg.Succeed()) + g.Expect(c.Create(t.Context(), crd)).To(gmg.Succeed()) t.Cleanup(func() { - g.Expect(c.Delete(ctx, crd)).To(gmg.Succeed()) + g.Expect(c.Delete(context.Background(), crd)).To(gmg.Succeed()) //nolint:forbidigo //t.Context is cancelled in t.Cleanup }) // Wait until the CRD is registered. diff --git a/pkg/client/apiutil/restmapper_test.go b/pkg/client/apiutil/restmapper_test.go index 1b080aeec8..51807f12de 100644 --- a/pkg/client/apiutil/restmapper_test.go +++ b/pkg/client/apiutil/restmapper_test.go @@ -599,7 +599,7 @@ func TestLazyRestMapperProvider(t *testing.T) { g.Expect(err).NotTo(gmg.HaveOccurred()) // Register another CRD in runtime - "riders.crew.example.com". - createNewCRD(context.TODO(), g, c, "crew.example.com", "Rider", "riders") + createNewCRD(t.Context(), g, c, "crew.example.com", "Rider", "riders") // Wait a bit until the CRD is registered. g.Eventually(func() error { @@ -621,7 +621,6 @@ func TestLazyRestMapperProvider(t *testing.T) { t.Run("LazyRESTMapper should invalidate the group cache if a version is not found", func(t *testing.T) { g := gmg.NewWithT(t) - ctx := context.Background() httpClient, err := rest.HTTPClientFor(restCfg) g.Expect(err).NotTo(gmg.HaveOccurred()) @@ -646,7 +645,7 @@ func TestLazyRestMapperProvider(t *testing.T) { crdName := plural + "." + group // Create a CRD with two versions: v1alpha1 and v1 where both are served and // v1 is the storage version so we can easily remove v1alpha1 later. - crd := newCRD(ctx, g, c, group, kind, plural) + crd := newCRD(t.Context(), g, c, group, kind, plural) v1alpha1 := crd.Spec.Versions[0] v1alpha1.Name = "v1alpha1" v1alpha1.Storage = false @@ -656,9 +655,9 @@ func TestLazyRestMapperProvider(t *testing.T) { v1.Storage = true v1.Served = true crd.Spec.Versions = []apiextensionsv1.CustomResourceDefinitionVersion{v1alpha1, v1} - g.Expect(c.Create(ctx, crd)).To(gmg.Succeed()) + g.Expect(c.Create(t.Context(), crd)).To(gmg.Succeed()) t.Cleanup(func() { - g.Expect(c.Delete(ctx, crd)).To(gmg.Succeed()) + g.Expect(c.Delete(context.Background(), crd)).To(gmg.Succeed()) //nolint:forbidigo //t.Context is cancelled in t.Cleanup }) // Wait until the CRD is registered. @@ -698,7 +697,7 @@ func TestLazyRestMapperProvider(t *testing.T) { g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) // We update the CRD to only have v1 version. - g.Expect(c.Get(ctx, types.NamespacedName{Name: crdName}, crd)).To(gmg.Succeed()) + g.Expect(c.Get(t.Context(), types.NamespacedName{Name: crdName}, crd)).To(gmg.Succeed()) for _, version := range crd.Spec.Versions { if version.Name == "v1" { v1 = version @@ -706,7 +705,7 @@ func TestLazyRestMapperProvider(t *testing.T) { } } crd.Spec.Versions = []apiextensionsv1.CustomResourceDefinitionVersion{v1} - g.Expect(c.Update(ctx, crd)).To(gmg.Succeed()) + g.Expect(c.Update(t.Context(), crd)).To(gmg.Succeed()) // We wait until v1alpha1 is not available anymore. g.Eventually(func(g gmg.Gomega) { diff --git a/pkg/client/applyconfigurations.go b/pkg/client/applyconfigurations.go new file mode 100644 index 0000000000..97192050f9 --- /dev/null +++ b/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/pkg/client/client.go b/pkg/client/client.go index 50b0ebf338..092deb43d4 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -329,6 +329,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/pkg/client/client_rest_resources.go b/pkg/client/client_rest_resources.go index 2d07879520..acff7a46a4 100644 --- a/pkg/client/client_rest_resources.go +++ b/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" ) @@ -56,13 +57,17 @@ type clientRestResources struct { // 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,15 +78,41 @@ 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 @@ -97,10 +128,15 @@ func (c *clientRestResources) getResource(obj runtime.Object) (*resourceMeta, er 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 } @@ -109,16 +145,29 @@ func (c *clientRestResources) getResource(obj runtime.Object) (*resourceMeta, er } // 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 +195,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/pkg/client/client_test.go b/pkg/client/client_test.go index ff014e7321..c775f28718 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -43,6 +43,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + corev1applyconfigurations "k8s.io/client-go/applyconfigurations/core/v1" kscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/utils/ptr" @@ -150,7 +151,6 @@ var _ = Describe("Client", func() { var replicaCount int32 = 2 var ns = "default" var errNotCached *cache.ErrResourceNotCached - ctx := context.TODO() BeforeEach(func() { atomic.AddUint64(&count, 1) @@ -209,7 +209,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) var delOptions *metav1.DeleteOptions - AfterEach(func() { + AfterEach(func(ctx SpecContext) { // Cleanup var zero int64 = 0 policy := metav1.DeletePropagationForeground @@ -231,7 +231,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) Describe("WarningHandler", func() { - It("should log warnings with config.WarningHandler, if one is defined", func() { + It("should log warnings with config.WarningHandler, if one is defined", func(ctx SpecContext) { cache := &fakeReader{} testCfg := rest.CopyConfig(cfg) @@ -334,7 +334,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(cl.RESTMapper()).ToNot(BeNil()) }) - It("should use the provided reader cache if provided, on get and list", func() { + It("should use the provided reader cache if provided, on get and list", func(ctx SpecContext) { cache := &fakeReader{} cl, err := client.New(cfg, client.Options{Cache: &client.CacheOptions{Reader: cache}}) Expect(err).NotTo(HaveOccurred()) @@ -344,7 +344,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(cache.Called).To(Equal(2)) }) - It("should propagate ErrResourceNotCached errors", func() { + It("should propagate ErrResourceNotCached errors", func(ctx SpecContext) { c := &fakeUncachedReader{} cl, err := client.New(cfg, client.Options{Cache: &client.CacheOptions{Reader: c}}) Expect(err).NotTo(HaveOccurred()) @@ -354,7 +354,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(c.Called).To(Equal(2)) }) - It("should not use the provided reader cache if provided, on get and list for uncached GVKs", func() { + It("should not use the provided reader cache if provided, on get and list for uncached GVKs", func(ctx SpecContext) { cache := &fakeReader{} cl, err := client.New(cfg, client.Options{Cache: &client.CacheOptions{Reader: cache, DisableFor: []client.Object{&corev1.Namespace{}}}}) Expect(err).NotTo(HaveOccurred()) @@ -367,13 +367,13 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Describe("Create", func() { Context("with structured objects", func() { - It("should create a new object from a go struct", func() { + It("should create a new object from a go struct", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) By("creating the object") - err = cl.Create(context.TODO(), dep) + err = cl.Create(ctx, dep) Expect(err).NotTo(HaveOccurred()) actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) @@ -384,13 +384,13 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(dep).To(Equal(actual)) }) - It("should create a new object non-namespace object from a go struct", func() { + It("should create a new object non-namespace object from a go struct", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) By("creating the object") - err = cl.Create(context.TODO(), node) + err = cl.Create(ctx, node) Expect(err).NotTo(HaveOccurred()) actual, err := clientset.CoreV1().Nodes().Get(ctx, node.Name, metav1.GetOptions{}) @@ -401,7 +401,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(node).To(Equal(actual)) }) - It("should fail if the object already exists", func() { + It("should fail if the object already exists", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -409,7 +409,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC old := dep.DeepCopy() By("creating the object") - err = cl.Create(context.TODO(), dep) + err = cl.Create(ctx, dep) Expect(err).NotTo(HaveOccurred()) actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) @@ -417,24 +417,24 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actual).NotTo(BeNil()) By("creating the object a second time") - err = cl.Create(context.TODO(), old) + err = cl.Create(ctx, old) Expect(err).To(HaveOccurred()) Expect(apierrors.IsAlreadyExists(err)).To(BeTrue()) }) - It("should fail if the object does not pass server-side validation", func() { + It("should fail if the object does not pass server-side validation", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) By("creating the pod, since required field Containers is empty") - err = cl.Create(context.TODO(), pod) + err = cl.Create(ctx, pod) Expect(err).To(HaveOccurred()) // TODO(seans): Add test to validate the returned error. Problems currently with // different returned error locally versus travis. }) - It("should fail if the object cannot be mapped to a GVK", func() { + It("should fail if the object cannot be mapped to a GVK", func(ctx SpecContext) { By("creating client with empty Scheme") emptyScheme := runtime.NewScheme() cl, err := client.New(cfg, client.Options{Scheme: emptyScheme}) @@ -442,7 +442,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(cl).NotTo(BeNil()) By("creating the object fails") - err = cl.Create(context.TODO(), dep) + err = cl.Create(ctx, dep) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("no kind is registered for the type")) }) @@ -453,13 +453,13 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) Context("with the DryRun option", func() { - It("should not create a new object, global option", func() { + It("should not create a new object, global option", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{DryRun: ptr.To(true)}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) By("creating the object (with DryRun)") - err = cl.Create(context.TODO(), dep) + err = cl.Create(ctx, dep) Expect(err).NotTo(HaveOccurred()) actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) @@ -468,13 +468,13 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actual).To(Equal(&appsv1.Deployment{})) }) - It("should not create a new object, inline option", func() { + It("should not create a new object, inline option", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) By("creating the object (with DryRun)") - err = cl.Create(context.TODO(), dep, client.DryRunAll) + err = cl.Create(ctx, dep, client.DryRunAll) Expect(err).NotTo(HaveOccurred()) actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) @@ -486,7 +486,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) Context("with unstructured objects", func() { - It("should create a new object from a go struct", func() { + It("should create a new object from a go struct", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -501,7 +501,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) By("creating the object") - err = cl.Create(context.TODO(), u) + err = cl.Create(ctx, u) Expect(err).NotTo(HaveOccurred()) actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) @@ -509,7 +509,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actual).NotTo(BeNil()) }) - It("should create a new non-namespace object ", func() { + It("should create a new non-namespace object ", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -524,7 +524,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) By("creating the object") - err = cl.Create(context.TODO(), node) + err = cl.Create(ctx, node) Expect(err).NotTo(HaveOccurred()) actual, err := clientset.CoreV1().Nodes().Get(ctx, node.Name, metav1.GetOptions{}) @@ -538,7 +538,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(u).To(Equal(au)) }) - It("should fail if the object already exists", func() { + It("should fail if the object already exists", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -546,7 +546,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC old := dep.DeepCopy() By("creating the object") - err = cl.Create(context.TODO(), dep) + err = cl.Create(ctx, dep) Expect(err).NotTo(HaveOccurred()) actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) Expect(err).NotTo(HaveOccurred()) @@ -562,12 +562,12 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) By("creating the object a second time") - err = cl.Create(context.TODO(), u) + err = cl.Create(ctx, u) Expect(err).To(HaveOccurred()) Expect(apierrors.IsAlreadyExists(err)).To(BeTrue()) }) - It("should fail if the object does not pass server-side validation", func() { + It("should fail if the object does not pass server-side validation", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -580,7 +580,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Version: "v1", Kind: "Pod", }) - err = cl.Create(context.TODO(), u) + err = cl.Create(ctx, u) Expect(err).To(HaveOccurred()) // TODO(seans): Add test to validate the returned error. Problems currently with // different returned error locally versus travis. @@ -589,17 +589,17 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) Context("with metadata objects", func() { - It("should fail with an error", func() { + It("should fail with an error", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) obj := metaOnlyFromObj(dep, scheme) - Expect(cl.Create(context.TODO(), obj)).NotTo(Succeed()) + Expect(cl.Create(ctx, obj)).NotTo(Succeed()) }) }) Context("with the DryRun option", func() { - It("should not create a new object from a go struct", func() { + It("should not create a new object from a go struct", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -614,7 +614,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) By("creating the object") - err = cl.Create(context.TODO(), u, client.DryRunAll) + err = cl.Create(ctx, u, client.DryRunAll) Expect(err).NotTo(HaveOccurred()) actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) @@ -627,7 +627,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Describe("Update", func() { Context("with structured objects", func() { - It("should update an existing object from a go struct", func() { + It("should update an existing object from a go struct", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -638,7 +638,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("updating the Deployment") dep.Annotations = map[string]string{"foo": "bar"} - err = cl.Update(context.TODO(), dep) + err = cl.Update(ctx, dep) Expect(err).NotTo(HaveOccurred()) By("validating updated Deployment has new annotation") @@ -648,7 +648,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actual.Annotations["foo"]).To(Equal("bar")) }) - It("should update and preserve type information", func() { + It("should update and preserve type information", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -659,14 +659,14 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("updating the Deployment") dep.SetGroupVersionKind(depGvk) - err = cl.Update(context.TODO(), dep) + err = cl.Update(ctx, dep) Expect(err).NotTo(HaveOccurred()) By("validating updated Deployment has type information") Expect(dep.GroupVersionKind()).To(Equal(depGvk)) }) - It("should update an existing object non-namespace object from a go struct", func() { + It("should update an existing object non-namespace object from a go struct", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -676,7 +676,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("updating the object") node.Annotations = map[string]string{"foo": "bar"} - err = cl.Update(context.TODO(), node) + err = cl.Update(ctx, node) Expect(err).NotTo(HaveOccurred()) By("validate updated Node had new annotation") @@ -686,13 +686,13 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actual.Annotations["foo"]).To(Equal("bar")) }) - It("should fail if the object does not exist", func() { + It("should fail if the object does not exist", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) By("updating non-existent object") - err = cl.Update(context.TODO(), dep) + err = cl.Update(ctx, dep) Expect(err).To(HaveOccurred()) }) @@ -704,7 +704,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) - It("should fail if the object cannot be mapped to a GVK", func() { + It("should fail if the object cannot be mapped to a GVK", func(ctx SpecContext) { By("creating client with empty Scheme") emptyScheme := runtime.NewScheme() cl, err := client.New(cfg, client.Options{Scheme: emptyScheme}) @@ -717,7 +717,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("updating the Deployment") dep.Annotations = map[string]string{"foo": "bar"} - err = cl.Update(context.TODO(), dep) + err = cl.Update(ctx, dep) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("no kind is registered for the type")) }) @@ -727,7 +727,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) }) Context("with unstructured objects", func() { - It("should update an existing object from a go struct", func() { + It("should update an existing object from a go struct", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -745,7 +745,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Version: "v1", }) u.SetAnnotations(map[string]string{"foo": "bar"}) - err = cl.Update(context.TODO(), u) + err = cl.Update(ctx, u) Expect(err).NotTo(HaveOccurred()) By("validating updated Deployment has new annotation") @@ -755,7 +755,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actual.Annotations["foo"]).To(Equal("bar")) }) - It("should update and preserve type information", func() { + It("should update and preserve type information", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -769,14 +769,14 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(scheme.Convert(dep, u, nil)).To(Succeed()) u.SetGroupVersionKind(depGvk) u.SetAnnotations(map[string]string{"foo": "bar"}) - err = cl.Update(context.TODO(), u) + err = cl.Update(ctx, u) Expect(err).NotTo(HaveOccurred()) By("validating updated Deployment has type information") Expect(u.GroupVersionKind()).To(Equal(depGvk)) }) - It("should update an existing object non-namespace object from a go struct", func() { + It("should update an existing object non-namespace object from a go struct", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -793,7 +793,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Version: "v1", }) u.SetAnnotations(map[string]string{"foo": "bar"}) - err = cl.Update(context.TODO(), u) + err = cl.Update(ctx, u) Expect(err).NotTo(HaveOccurred()) By("validate updated Node had new annotation") @@ -802,7 +802,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actual).NotTo(BeNil()) Expect(actual.Annotations["foo"]).To(Equal("bar")) }) - It("should fail if the object does not exist", func() { + It("should fail if the object does not exist", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -811,25 +811,25 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC u := &unstructured.Unstructured{} Expect(scheme.Convert(dep, u, nil)).To(Succeed()) u.SetGroupVersionKind(depGvk) - err = cl.Update(context.TODO(), dep) + err = cl.Update(ctx, dep) Expect(err).To(HaveOccurred()) }) }) Context("with metadata objects", func() { - It("should fail with an error", func() { + It("should fail with an error", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) obj := metaOnlyFromObj(dep, scheme) - Expect(cl.Update(context.TODO(), obj)).NotTo(Succeed()) + Expect(cl.Update(ctx, obj)).NotTo(Succeed()) }) }) }) Describe("Patch", func() { Context("Metadata Client", func() { - It("should merge patch with options", func() { + It("should merge patch with options", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -845,7 +845,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC metadata.Labels["foo"] = "bar" testOption := &mockPatchOption{} - Expect(cl.Patch(context.TODO(), metadata, client.Merge, testOption)).To(Succeed()) + Expect(cl.Patch(ctx, metadata, client.Merge, testOption)).To(Succeed()) By("validating that patched metadata has new labels") actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) @@ -859,9 +859,104 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) }) + Describe("Apply", func() { + Context("Unstructured Client", func() { + It("should create and update a configMap using SSA", func(ctx SpecContext) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + data := map[string]any{ + "some-key": "some-value", + } + obj := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]any{ + "name": "test-configmap", + "namespace": "default", + }, + "data": data, + }} + + err = cl.Apply(ctx, client.ApplyConfigurationFromUnstructured(obj), &client.ApplyOptions{FieldManager: "test-manager"}) + Expect(err).NotTo(HaveOccurred()) + + cm, err := clientset.CoreV1().ConfigMaps(obj.GetNamespace()).Get(ctx, obj.GetName(), metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + + actualData := map[string]any{} + for k, v := range cm.Data { + actualData[k] = v + } + + Expect(actualData).To(BeComparableTo(data)) + Expect(actualData).To(BeComparableTo(obj.Object["data"])) + + data = map[string]any{ + "a-new-key": "a-new-value", + } + obj.Object["data"] = data + unstructured.RemoveNestedField(obj.Object, "metadata", "managedFields") + + err = cl.Apply(ctx, client.ApplyConfigurationFromUnstructured(obj), &client.ApplyOptions{FieldManager: "test-manager"}) + Expect(err).NotTo(HaveOccurred()) + + cm, err = clientset.CoreV1().ConfigMaps(obj.GetNamespace()).Get(ctx, obj.GetName(), metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + + actualData = map[string]any{} + for k, v := range cm.Data { + actualData[k] = v + } + + Expect(actualData).To(BeComparableTo(data)) + Expect(actualData).To(BeComparableTo(obj.Object["data"])) + }) + }) + + Context("Structured Client", func() { + It("should create and update a configMap using SSA", func(ctx SpecContext) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + data := map[string]string{ + "some-key": "some-value", + } + obj := corev1applyconfigurations. + ConfigMap("test-configmap", "default"). + WithData(data) + + err = cl.Apply(ctx, obj, &client.ApplyOptions{FieldManager: "test-manager"}) + Expect(err).NotTo(HaveOccurred()) + + cm, err := clientset.CoreV1().ConfigMaps(ptr.Deref(obj.GetNamespace(), "")).Get(ctx, ptr.Deref(obj.GetName(), ""), metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + + Expect(cm.Data).To(BeComparableTo(data)) + Expect(cm.Data).To(BeComparableTo(obj.Data)) + + data = map[string]string{ + "a-new-key": "a-new-value", + } + obj.Data = data + + err = cl.Apply(ctx, obj, &client.ApplyOptions{FieldManager: "test-manager"}) + Expect(err).NotTo(HaveOccurred()) + + cm, err = clientset.CoreV1().ConfigMaps(ptr.Deref(obj.GetNamespace(), "")).Get(ctx, ptr.Deref(obj.GetName(), ""), metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + + Expect(cm.Data).To(BeComparableTo(data)) + Expect(cm.Data).To(BeComparableTo(obj.Data)) + }) + }) + }) + Describe("SubResourceClient", func() { Context("with structured objects", func() { - It("should be able to read the Scale subresource", func() { + It("should be able to read the Scale subresource", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -876,7 +971,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(err).NotTo(HaveOccurred()) Expect(scale.Spec.Replicas).To(Equal(*dep.Spec.Replicas)) }) - It("should be able to create ServiceAccount tokens", func() { + It("should be able to create ServiceAccount tokens", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -892,7 +987,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(token.Status.Token).NotTo(Equal("")) }) - It("should be able to create Pod evictions", func() { + It("should be able to create Pod evictions", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -916,7 +1011,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) - It("should be able to create Pod bindings", func() { + It("should be able to create Pod bindings", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -941,7 +1036,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(pod.Spec.NodeName).To(Equal(node.Name)) }) - It("should be able to approve CSRs", func() { + It("should be able to approve CSRs", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -965,7 +1060,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(csr.Status.Conditions[0].Status).To(Equal(corev1.ConditionTrue)) }) - It("should be able to approve CSRs using Patch", func() { + It("should be able to approve CSRs using Patch", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -990,7 +1085,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(csr.Status.Conditions[0].Status).To(Equal(corev1.ConditionTrue)) }) - It("should be able to update the scale subresource", func() { + It("should be able to update the scale subresource", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1011,7 +1106,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(*dep.Spec.Replicas).To(Equal(replicaCount)) }) - It("should be able to patch the scale subresource", func() { + It("should be able to patch the scale subresource", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1035,7 +1130,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) Context("with unstructured objects", func() { - It("should be able to read the Scale subresource", func() { + It("should be able to read the Scale subresource", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{Scheme: runtime.NewScheme()}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1060,7 +1155,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(found).To(BeTrue()) Expect(int32(val)).To(Equal(*dep.Spec.Replicas)) }) - It("should be able to create ServiceAccount tokens", func() { + It("should be able to create ServiceAccount tokens", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{Scheme: runtime.NewScheme()}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1088,7 +1183,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(val).NotTo(Equal("")) }) - It("should be able to create Pod evictions", func() { + It("should be able to create Pod evictions", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{Scheme: runtime.NewScheme()}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1121,7 +1216,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) - It("should be able to create Pod bindings", func() { + It("should be able to create Pod bindings", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{Scheme: runtime.NewScheme()}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1156,7 +1251,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(pod.Spec.NodeName).To(Equal(node.Name)) }) - It("should be able to approve CSRs", func() { + It("should be able to approve CSRs", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{Scheme: runtime.NewScheme()}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1187,7 +1282,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(csr.Status.Conditions[0].Status).To(Equal(corev1.ConditionTrue)) }) - It("should be able to approve CSRs using Patch", func() { + It("should be able to approve CSRs using Patch", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{Scheme: runtime.NewScheme()}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1219,7 +1314,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(csr.Status.Conditions[0].Status).To(Equal(corev1.ConditionTrue)) }) - It("should be able to update the scale subresource", func() { + It("should be able to update the scale subresource", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{Scheme: runtime.NewScheme()}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1249,7 +1344,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(*dep.Spec.Replicas).To(Equal(replicaCount)) }) - It("should be able to patch the scale subresource", func() { + It("should be able to patch the scale subresource", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{Scheme: runtime.NewScheme()}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1285,7 +1380,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Describe("StatusClient", func() { Context("with structured objects", func() { - It("should update status of an existing object", func() { + It("should update status of an existing object", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1296,7 +1391,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("updating the status of Deployment") dep.Status.Replicas = 1 - err = cl.Status().Update(context.TODO(), dep) + err = cl.Status().Update(ctx, dep) Expect(err).NotTo(HaveOccurred()) By("validating updated Deployment has new status") @@ -1306,7 +1401,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actual.Status.Replicas).To(BeEquivalentTo(1)) }) - It("should update status and preserve type information", func() { + It("should update status and preserve type information", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1318,14 +1413,14 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("updating the status of Deployment") dep.SetGroupVersionKind(depGvk) dep.Status.Replicas = 1 - err = cl.Status().Update(context.TODO(), dep) + err = cl.Status().Update(ctx, dep) Expect(err).NotTo(HaveOccurred()) By("validating updated Deployment has type information") Expect(dep.GroupVersionKind()).To(Equal(depGvk)) }) - It("should patch status and preserve type information", func() { + It("should patch status and preserve type information", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1338,14 +1433,14 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC dep.SetGroupVersionKind(depGvk) depPatch := client.MergeFrom(dep.DeepCopy()) dep.Status.Replicas = 1 - err = cl.Status().Patch(context.TODO(), dep, depPatch) + err = cl.Status().Patch(ctx, dep, depPatch) Expect(err).NotTo(HaveOccurred()) By("validating updated Deployment has type information") Expect(dep.GroupVersionKind()).To(Equal(depGvk)) }) - It("should not update spec of an existing object", func() { + It("should not update spec of an existing object", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1358,7 +1453,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC var rc int32 = 1 dep.Status.Replicas = 1 dep.Spec.Replicas = &rc - err = cl.Status().Update(context.TODO(), dep) + err = cl.Status().Update(ctx, dep) Expect(err).NotTo(HaveOccurred()) By("validating updated Deployment has new status and unchanged spec") @@ -1369,7 +1464,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(*actual.Spec.Replicas).To(BeEquivalentTo(replicaCount)) }) - It("should update an existing object non-namespace object", func() { + It("should update an existing object non-namespace object", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1379,7 +1474,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("updating status of the object") node.Status.Phase = corev1.NodeRunning - err = cl.Status().Update(context.TODO(), node) + err = cl.Status().Update(ctx, node) Expect(err).NotTo(HaveOccurred()) By("validate updated Node had new annotation") @@ -1389,17 +1484,17 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actual.Status.Phase).To(Equal(corev1.NodeRunning)) }) - It("should fail if the object does not exist", func() { + It("should fail if the object does not exist", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) By("updating status of a non-existent object") - err = cl.Status().Update(context.TODO(), dep) + err = cl.Status().Update(ctx, dep) Expect(err).To(HaveOccurred()) }) - It("should fail if the object cannot be mapped to a GVK", func() { + It("should fail if the object cannot be mapped to a GVK", func(ctx SpecContext) { By("creating client with empty Scheme") emptyScheme := runtime.NewScheme() cl, err := client.New(cfg, client.Options{Scheme: emptyScheme}) @@ -1412,7 +1507,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("updating status of the Deployment") dep.Status.Replicas = 1 - err = cl.Status().Update(context.TODO(), dep) + err = cl.Status().Update(ctx, dep) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("no kind is registered for the type")) }) @@ -1427,7 +1522,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) Context("with unstructured objects", func() { - It("should update status of an existing object", func() { + It("should update status of an existing object", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1440,7 +1535,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC u := &unstructured.Unstructured{} dep.Status.Replicas = 1 Expect(scheme.Convert(dep, u, nil)).To(Succeed()) - err = cl.Status().Update(context.TODO(), u) + err = cl.Status().Update(ctx, u) Expect(err).NotTo(HaveOccurred()) By("validating updated Deployment has new status") @@ -1450,7 +1545,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actual.Status.Replicas).To(BeEquivalentTo(1)) }) - It("should update status and preserve type information", func() { + It("should update status and preserve type information", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1463,14 +1558,14 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC u := &unstructured.Unstructured{} dep.Status.Replicas = 1 Expect(scheme.Convert(dep, u, nil)).To(Succeed()) - err = cl.Status().Update(context.TODO(), u) + err = cl.Status().Update(ctx, u) Expect(err).NotTo(HaveOccurred()) By("validating updated Deployment has type information") Expect(u.GroupVersionKind()).To(Equal(depGvk)) }) - It("should patch status and preserve type information", func() { + It("should patch status and preserve type information", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1484,7 +1579,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC depPatch := client.MergeFrom(dep.DeepCopy()) dep.Status.Replicas = 1 Expect(scheme.Convert(dep, u, nil)).To(Succeed()) - err = cl.Status().Patch(context.TODO(), u, depPatch) + err = cl.Status().Patch(ctx, u, depPatch) Expect(err).NotTo(HaveOccurred()) By("validating updated Deployment has type information") @@ -1497,7 +1592,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actual.Status.Replicas).To(BeEquivalentTo(1)) }) - It("should not update spec of an existing object", func() { + It("should not update spec of an existing object", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1512,7 +1607,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC dep.Status.Replicas = 1 dep.Spec.Replicas = &rc Expect(scheme.Convert(dep, u, nil)).To(Succeed()) - err = cl.Status().Update(context.TODO(), u) + err = cl.Status().Update(ctx, u) Expect(err).NotTo(HaveOccurred()) By("validating updated Deployment has new status and unchanged spec") @@ -1523,7 +1618,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(*actual.Spec.Replicas).To(BeEquivalentTo(replicaCount)) }) - It("should update an existing object non-namespace object", func() { + It("should update an existing object non-namespace object", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1535,7 +1630,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC u := &unstructured.Unstructured{} node.Status.Phase = corev1.NodeRunning Expect(scheme.Convert(node, u, nil)).To(Succeed()) - err = cl.Status().Update(context.TODO(), u) + err = cl.Status().Update(ctx, u) Expect(err).NotTo(HaveOccurred()) By("validate updated Node had new annotation") @@ -1545,7 +1640,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actual.Status.Phase).To(Equal(corev1.NodeRunning)) }) - It("should fail if the object does not exist", func() { + It("should fail if the object does not exist", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1553,7 +1648,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("updating status of a non-existent object") u := &unstructured.Unstructured{} Expect(scheme.Convert(dep, u, nil)).To(Succeed()) - err = cl.Status().Update(context.TODO(), u) + err = cl.Status().Update(ctx, u) Expect(err).To(HaveOccurred()) }) @@ -1568,15 +1663,15 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) Context("with metadata objects", func() { - It("should fail to update with an error", func() { + It("should fail to update with an error", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) obj := metaOnlyFromObj(dep, scheme) - Expect(cl.Status().Update(context.TODO(), obj)).NotTo(Succeed()) + Expect(cl.Status().Update(ctx, obj)).NotTo(Succeed()) }) - It("should patch status and preserve type information", func() { + It("should patch status and preserve type information", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1589,7 +1684,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC objPatch := client.MergeFrom(metaOnlyFromObj(dep, scheme)) dep.Annotations = map[string]string{"some-new-annotation": "some-new-value"} obj := metaOnlyFromObj(dep, scheme) - err = cl.Status().Patch(context.TODO(), obj, objPatch) + err = cl.Status().Patch(ctx, obj, objPatch) Expect(err).NotTo(HaveOccurred()) By("validating updated Deployment has type information") @@ -1606,7 +1701,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Describe("Delete", func() { Context("with structured objects", func() { - It("should delete an existing object from a go struct", func() { + It("should delete an existing object from a go struct", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1617,7 +1712,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("deleting the Deployment") depName := dep.Name - err = cl.Delete(context.TODO(), dep) + err = cl.Delete(ctx, dep) Expect(err).NotTo(HaveOccurred()) By("validating the Deployment no longer exists") @@ -1625,7 +1720,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(err).To(HaveOccurred()) }) - It("should delete an existing object non-namespace object from a go struct", func() { + It("should delete an existing object non-namespace object from a go struct", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1636,7 +1731,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("deleting the Node") nodeName := node.Name - err = cl.Delete(context.TODO(), node) + err = cl.Delete(ctx, node) Expect(err).NotTo(HaveOccurred()) By("validating the Node no longer exists") @@ -1644,13 +1739,13 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(err).To(HaveOccurred()) }) - It("should fail if the object does not exist", func() { + It("should fail if the object does not exist", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) By("Deleting node before it is ever created") - err = cl.Delete(context.TODO(), node) + err = cl.Delete(ctx, node) Expect(err).To(HaveOccurred()) }) @@ -1658,7 +1753,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) - It("should fail if the object cannot be mapped to a GVK", func() { + It("should fail if the object cannot be mapped to a GVK", func(ctx SpecContext) { By("creating client with empty Scheme") emptyScheme := runtime.NewScheme() cl, err := client.New(cfg, client.Options{Scheme: emptyScheme}) @@ -1670,7 +1765,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(err).NotTo(HaveOccurred()) By("deleting the Deployment fails") - err = cl.Delete(context.TODO(), dep) + err = cl.Delete(ctx, dep) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("no kind is registered for the type")) }) @@ -1679,7 +1774,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) - It("should delete a collection of objects", func() { + It("should delete a collection of objects", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1698,7 +1793,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC dep2Name := dep2.Name By("deleting Deployments") - err = cl.DeleteAllOf(context.TODO(), dep, client.InNamespace(ns), client.MatchingLabels(dep.ObjectMeta.Labels)) + err = cl.DeleteAllOf(ctx, dep, client.InNamespace(ns), client.MatchingLabels(dep.ObjectMeta.Labels)) Expect(err).NotTo(HaveOccurred()) By("validating the Deployment no longer exists") @@ -1709,7 +1804,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) }) Context("with unstructured objects", func() { - It("should delete an existing object from a go struct", func() { + It("should delete an existing object from a go struct", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1727,7 +1822,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Kind: "Deployment", Version: "v1", }) - err = cl.Delete(context.TODO(), u) + err = cl.Delete(ctx, u) Expect(err).NotTo(HaveOccurred()) By("validating the Deployment no longer exists") @@ -1735,7 +1830,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(err).To(HaveOccurred()) }) - It("should delete an existing object non-namespace object from a go struct", func() { + It("should delete an existing object non-namespace object from a go struct", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1753,7 +1848,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Kind: "Node", Version: "v1", }) - err = cl.Delete(context.TODO(), u) + err = cl.Delete(ctx, u) Expect(err).NotTo(HaveOccurred()) By("validating the Node no longer exists") @@ -1761,7 +1856,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(err).To(HaveOccurred()) }) - It("should fail if the object does not exist", func() { + It("should fail if the object does not exist", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1774,11 +1869,11 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Kind: "Node", Version: "v1", }) - err = cl.Delete(context.TODO(), node) + err = cl.Delete(ctx, node) Expect(err).To(HaveOccurred()) }) - It("should delete a collection of object", func() { + It("should delete a collection of object", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1804,7 +1899,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Kind: "Deployment", Version: "v1", }) - err = cl.DeleteAllOf(context.TODO(), u, client.InNamespace(ns), client.MatchingLabels(dep.ObjectMeta.Labels)) + err = cl.DeleteAllOf(ctx, u, client.InNamespace(ns), client.MatchingLabels(dep.ObjectMeta.Labels)) Expect(err).NotTo(HaveOccurred()) By("validating the Deployment no longer exists") @@ -1815,7 +1910,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) }) Context("with metadata objects", func() { - It("should delete an existing object from a go struct", func() { + It("should delete an existing object from a go struct", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1826,7 +1921,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("deleting the Deployment") metaObj := metaOnlyFromObj(dep, scheme) - err = cl.Delete(context.TODO(), metaObj) + err = cl.Delete(ctx, metaObj) Expect(err).NotTo(HaveOccurred()) By("validating the Deployment no longer exists") @@ -1834,7 +1929,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(err).To(HaveOccurred()) }) - It("should delete an existing object non-namespace object from a go struct", func() { + It("should delete an existing object non-namespace object from a go struct", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1845,7 +1940,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("deleting the Node") metaObj := metaOnlyFromObj(node, scheme) - err = cl.Delete(context.TODO(), metaObj) + err = cl.Delete(ctx, metaObj) Expect(err).NotTo(HaveOccurred()) By("validating the Node no longer exists") @@ -1853,18 +1948,18 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(err).To(HaveOccurred()) }) - It("should fail if the object does not exist", func() { + It("should fail if the object does not exist", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) By("Deleting node before it is ever created") metaObj := metaOnlyFromObj(node, scheme) - err = cl.Delete(context.TODO(), metaObj) + err = cl.Delete(ctx, metaObj) Expect(err).To(HaveOccurred()) }) - It("should delete a collection of object", func() { + It("should delete a collection of object", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1884,7 +1979,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("deleting Deployments") metaObj := metaOnlyFromObj(dep, scheme) - err = cl.DeleteAllOf(context.TODO(), metaObj, client.InNamespace(ns), client.MatchingLabels(dep.ObjectMeta.Labels)) + err = cl.DeleteAllOf(ctx, metaObj, client.InNamespace(ns), client.MatchingLabels(dep.ObjectMeta.Labels)) Expect(err).NotTo(HaveOccurred()) By("validating the Deployment no longer exists") @@ -1898,7 +1993,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Describe("Get", func() { Context("with structured objects", func() { - It("should fetch an existing object for a go struct", func() { + It("should fetch an existing object for a go struct", func(ctx SpecContext) { By("first creating the Deployment") dep, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) @@ -1910,7 +2005,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("fetching the created Deployment") var actual appsv1.Deployment key := client.ObjectKey{Namespace: ns, Name: dep.Name} - err = cl.Get(context.TODO(), key, &actual) + err = cl.Get(ctx, key, &actual) Expect(err).NotTo(HaveOccurred()) Expect(actual).NotTo(BeNil()) @@ -1918,7 +2013,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(dep).To(Equal(&actual)) }) - It("should fetch an existing non-namespace object for a go struct", func() { + It("should fetch an existing non-namespace object for a go struct", func(ctx SpecContext) { By("first creating the object") node, err := clientset.CoreV1().Nodes().Create(ctx, node, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) @@ -1930,14 +2025,14 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("retrieving node through client") var actual corev1.Node key := client.ObjectKey{Namespace: ns, Name: node.Name} - err = cl.Get(context.TODO(), key, &actual) + err = cl.Get(ctx, key, &actual) Expect(err).NotTo(HaveOccurred()) Expect(actual).NotTo(BeNil()) Expect(node).To(Equal(&actual)) }) - It("should fail if the object does not exist", func() { + It("should fail if the object does not exist", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -1945,7 +2040,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("fetching object that has not been created yet") key := client.ObjectKey{Namespace: ns, Name: dep.Name} var actual appsv1.Deployment - err = cl.Get(context.TODO(), key, &actual) + err = cl.Get(ctx, key, &actual) Expect(err).To(HaveOccurred()) }) @@ -1953,7 +2048,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) - It("should fail if the object cannot be mapped to a GVK", func() { + It("should fail if the object cannot be mapped to a GVK", func(ctx SpecContext) { By("first creating the Deployment") dep, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) @@ -1967,7 +2062,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("fetching the created Deployment fails") var actual appsv1.Deployment key := client.ObjectKey{Namespace: ns, Name: dep.Name} - err = cl.Get(context.TODO(), key, &actual) + err = cl.Get(ctx, key, &actual) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("no kind is registered for the type")) }) @@ -1979,7 +2074,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC // Test this with an integrated type and a CRD to make sure it covers both proto // and json deserialization. for idx, object := range []client.Object{&corev1.ConfigMap{}, &pkg.ChaosPod{}} { - It(fmt.Sprintf("should not retain any data in the obj variable that is not on the server for %T", object), func() { + It(fmt.Sprintf("should not retain any data in the obj variable that is not on the server for %T", object), func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -2004,7 +2099,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) Context("with unstructured objects", func() { - It("should fetch an existing object", func() { + It("should fetch an existing object", func(ctx SpecContext) { By("first creating the Deployment") dep, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) @@ -2025,15 +2120,16 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Version: "v1", }) key := client.ObjectKey{Namespace: ns, Name: dep.Name} - err = cl.Get(context.TODO(), key, &actual) + err = cl.Get(ctx, key, &actual) Expect(err).NotTo(HaveOccurred()) Expect(actual).NotTo(BeNil()) By("validating the fetched Deployment equals the created one") - Expect(u).To(Equal(&actual)) + unstructured.RemoveNestedField(actual.Object, "spec", "template", "metadata", "creationTimestamp") + Expect(u).To(BeComparableTo(&actual)) }) - It("should fetch an existing non-namespace object", func() { + It("should fetch an existing non-namespace object", func(ctx SpecContext) { By("first creating the Node") node, err := clientset.CoreV1().Nodes().Create(ctx, node, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) @@ -2054,7 +2150,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Version: "v1", }) key := client.ObjectKey{Namespace: ns, Name: node.Name} - err = cl.Get(context.TODO(), key, &actual) + err = cl.Get(ctx, key, &actual) Expect(err).NotTo(HaveOccurred()) Expect(actual).NotTo(BeNil()) @@ -2062,7 +2158,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(u).To(Equal(&actual)) }) - It("should fail if the object does not exist", func() { + It("should fail if the object does not exist", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -2070,11 +2166,11 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("fetching object that has not been created yet") key := client.ObjectKey{Namespace: ns, Name: dep.Name} u := &unstructured.Unstructured{} - err = cl.Get(context.TODO(), key, u) + err = cl.Get(ctx, key, u) Expect(err).To(HaveOccurred()) }) - It("should not retain any data in the obj variable that is not on the server", func() { + It("should not retain any data in the obj variable that is not on the server", func(ctx SpecContext) { object := &unstructured.Unstructured{} cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) @@ -2099,7 +2195,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) }) Context("with metadata objects", func() { - It("should fetch an existing object for a go struct", func() { + It("should fetch an existing object for a go struct", func(ctx SpecContext) { By("first creating the Deployment") dep, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) @@ -2117,7 +2213,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC } actual.SetGroupVersionKind(gvk) key := client.ObjectKey{Namespace: ns, Name: dep.Name} - err = cl.Get(context.TODO(), key, &actual) + err = cl.Get(ctx, key, &actual) Expect(err).NotTo(HaveOccurred()) Expect(actual).NotTo(BeNil()) @@ -2128,7 +2224,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(metaOnlyFromObj(dep, scheme)).To(Equal(&actual)) }) - It("should fetch an existing non-namespace object for a go struct", func() { + It("should fetch an existing non-namespace object for a go struct", func(ctx SpecContext) { By("first creating the object") node, err := clientset.CoreV1().Nodes().Create(ctx, node, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) @@ -2144,14 +2240,14 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Kind: "Node", }) key := client.ObjectKey{Namespace: ns, Name: node.Name} - err = cl.Get(context.TODO(), key, &actual) + err = cl.Get(ctx, key, &actual) Expect(err).NotTo(HaveOccurred()) Expect(actual).NotTo(BeNil()) Expect(metaOnlyFromObj(node, scheme)).To(Equal(&actual)) }) - It("should fail if the object does not exist", func() { + It("should fail if the object does not exist", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -2164,7 +2260,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Version: "v1", Kind: "Deployment", }) - err = cl.Get(context.TODO(), key, &actual) + err = cl.Get(ctx, key, &actual) Expect(err).To(HaveOccurred()) }) @@ -2176,7 +2272,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) - It("should not retain any data in the obj variable that is not on the server", func() { + It("should not retain any data in the obj variable that is not on the server", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -2201,7 +2297,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Describe("List", func() { Context("with structured objects", func() { - It("should fetch collection of objects", func() { + It("should fetch collection of objects", func(ctx SpecContext) { By("creating an initial object") dep, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) @@ -2211,7 +2307,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("listing all objects of that type in the cluster") deps := &appsv1.DeploymentList{} - Expect(cl.List(context.Background(), deps)).NotTo(HaveOccurred()) + Expect(cl.List(ctx, deps)).NotTo(HaveOccurred()) Expect(deps.Items).NotTo(BeEmpty()) hasDep := false @@ -2224,7 +2320,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(hasDep).To(BeTrue()) }) - It("should fetch unstructured collection of objects", func() { + It("should fetch unstructured collection of objects", func(ctx SpecContext) { By("create an initial object") _, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) @@ -2239,7 +2335,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Kind: "DeploymentList", Version: "v1", }) - err = cl.List(context.Background(), deps) + err = cl.List(ctx, deps) Expect(err).NotTo(HaveOccurred()) Expect(deps.Items).NotTo(BeEmpty()) @@ -2258,7 +2354,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(hasDep).To(BeTrue()) }) - It("should fetch unstructured collection of objects, even if scheme is empty", func() { + It("should fetch unstructured collection of objects, even if scheme is empty", func(ctx SpecContext) { By("create an initial object") _, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) @@ -2273,7 +2369,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Kind: "DeploymentList", Version: "v1", }) - err = cl.List(context.Background(), deps) + err = cl.List(ctx, deps) Expect(err).NotTo(HaveOccurred()) Expect(deps.Items).NotTo(BeEmpty()) @@ -2287,20 +2383,20 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(hasDep).To(BeTrue()) }) - It("should return an empty list if there are no matching objects", func() { + It("should return an empty list if there are no matching objects", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) By("listing all Deployments in the cluster") deps := &appsv1.DeploymentList{} - Expect(cl.List(context.Background(), deps)).NotTo(HaveOccurred()) + Expect(cl.List(ctx, deps)).NotTo(HaveOccurred()) By("validating no Deployments are returned") Expect(deps.Items).To(BeEmpty()) }) // TODO(seans): get label selector test working - It("should filter results by label selector", func() { + It("should filter results by label selector", func(ctx SpecContext) { By("creating a Deployment with the app=frontend label") depFrontend := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ @@ -2347,7 +2443,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("listing all Deployments with label app=backend") deps := &appsv1.DeploymentList{} labels := map[string]string{"app": "backend"} - err = cl.List(context.Background(), deps, client.MatchingLabels(labels)) + err = cl.List(ctx, deps, client.MatchingLabels(labels)) Expect(err).NotTo(HaveOccurred()) By("only the Deployment with the backend label is returned") @@ -2360,7 +2456,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC deleteDeployment(ctx, depBackend, ns) }) - It("should filter results by namespace selector", func() { + It("should filter results by namespace selector", func(ctx SpecContext) { By("creating a Deployment in test-namespace-1") tns1 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-1"}} _, err := clientset.CoreV1().Namespaces().Create(ctx, tns1, metav1.CreateOptions{}) @@ -2404,7 +2500,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("listing all Deployments in test-namespace-1") deps := &appsv1.DeploymentList{} - err = cl.List(context.Background(), deps, client.InNamespace("test-namespace-1")) + err = cl.List(ctx, deps, client.InNamespace("test-namespace-1")) Expect(err).NotTo(HaveOccurred()) By("only the Deployment in test-namespace-1 is returned") @@ -2419,7 +2515,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC deleteNamespace(ctx, tns2) }) - It("should filter results by field selector", func() { + It("should filter results by field selector", func(ctx SpecContext) { By("creating a Deployment with name deployment-frontend") depFrontend := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "deployment-frontend", Namespace: ns}, @@ -2457,7 +2553,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("listing all Deployments with field metadata.name=deployment-backend") deps := &appsv1.DeploymentList{} - err = cl.List(context.Background(), deps, + err = cl.List(ctx, deps, client.MatchingFields{"metadata.name": "deployment-backend"}) Expect(err).NotTo(HaveOccurred()) @@ -2471,7 +2567,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC deleteDeployment(ctx, depBackend, ns) }) - It("should filter results by namespace selector and label selector", func() { + It("should filter results by namespace selector and label selector", func(ctx SpecContext) { By("creating a Deployment in test-namespace-3 with the app=frontend label") tns3 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-3"}} _, err := clientset.CoreV1().Namespaces().Create(ctx, tns3, metav1.CreateOptions{}) @@ -2544,7 +2640,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("listing all Deployments in test-namespace-3 with label app=frontend") deps := &appsv1.DeploymentList{} labels := map[string]string{"app": "frontend"} - err = cl.List(context.Background(), deps, + err = cl.List(ctx, deps, client.InNamespace("test-namespace-3"), client.MatchingLabels(labels), ) @@ -2564,7 +2660,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC deleteNamespace(ctx, tns4) }) - It("should filter results using limit and continue options", func() { + It("should filter results using limit and continue options", func(ctx SpecContext) { makeDeployment := func(suffix string) *appsv1.Deployment { return &appsv1.Deployment{ @@ -2609,7 +2705,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("listing 1 deployment when limit=1 is used") deps := &appsv1.DeploymentList{} - err = cl.List(context.Background(), deps, + err = cl.List(ctx, deps, client.Limit(1), ) Expect(err).NotTo(HaveOccurred()) @@ -2622,7 +2718,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("listing the next deployment when previous continuation token is used and limit=1") deps = &appsv1.DeploymentList{} - err = cl.List(context.Background(), deps, + err = cl.List(ctx, deps, client.Limit(1), client.Continue(continueToken), ) @@ -2636,7 +2732,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("listing the 2 remaining deployments when previous continuation token is used without a limit") deps = &appsv1.DeploymentList{} - err = cl.List(context.Background(), deps, + err = cl.List(ctx, deps, client.Continue(continueToken), ) Expect(err).NotTo(HaveOccurred()) @@ -2661,7 +2757,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) Context("with unstructured objects", func() { - It("should fetch collection of objects", func() { + It("should fetch collection of objects", func(ctx SpecContext) { By("create an initial object") _, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) @@ -2676,7 +2772,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Kind: "DeploymentList", Version: "v1", }) - err = cl.List(context.Background(), deps) + err = cl.List(ctx, deps) Expect(err).NotTo(HaveOccurred()) Expect(deps.Items).NotTo(BeEmpty()) @@ -2690,7 +2786,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(hasDep).To(BeTrue()) }) - It("should return an empty list if there are no matching objects", func() { + It("should return an empty list if there are no matching objects", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) @@ -2701,13 +2797,13 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Kind: "DeploymentList", Version: "v1", }) - Expect(cl.List(context.Background(), deps)).NotTo(HaveOccurred()) + Expect(cl.List(ctx, deps)).NotTo(HaveOccurred()) By("validating no Deployments are returned") Expect(deps.Items).To(BeEmpty()) }) - It("should filter results by namespace selector", func() { + It("should filter results by namespace selector", func(ctx SpecContext) { By("creating a Deployment in test-namespace-5") tns1 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-5"}} _, err := clientset.CoreV1().Namespaces().Create(ctx, tns1, metav1.CreateOptions{}) @@ -2756,7 +2852,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Kind: "DeploymentList", Version: "v1", }) - err = cl.List(context.Background(), deps, client.InNamespace("test-namespace-5")) + err = cl.List(ctx, deps, client.InNamespace("test-namespace-5")) Expect(err).NotTo(HaveOccurred()) By("only the Deployment in test-namespace-5 is returned") @@ -2771,7 +2867,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC deleteNamespace(ctx, tns2) }) - It("should filter results by field selector", func() { + It("should filter results by field selector", func(ctx SpecContext) { By("creating a Deployment with name deployment-frontend") depFrontend := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "deployment-frontend", Namespace: ns}, @@ -2814,7 +2910,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Kind: "DeploymentList", Version: "v1", }) - err = cl.List(context.Background(), deps, + err = cl.List(ctx, deps, client.MatchingFields{"metadata.name": "deployment-backend"}) Expect(err).NotTo(HaveOccurred()) @@ -2828,7 +2924,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC deleteDeployment(ctx, depBackend, ns) }) - It("should filter results by namespace selector and label selector", func() { + It("should filter results by namespace selector and label selector", func(ctx SpecContext) { By("creating a Deployment in test-namespace-7 with the app=frontend label") tns3 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-7"}} _, err := clientset.CoreV1().Namespaces().Create(ctx, tns3, metav1.CreateOptions{}) @@ -2906,7 +3002,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Version: "v1", }) labels := map[string]string{"app": "frontend"} - err = cl.List(context.Background(), deps, + err = cl.List(ctx, deps, client.InNamespace("test-namespace-7"), client.MatchingLabels(labels)) Expect(err).NotTo(HaveOccurred()) @@ -2933,7 +3029,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) }) Context("with metadata objects", func() { - It("should fetch collection of objects", func() { + It("should fetch collection of objects", func(ctx SpecContext) { By("creating an initial object") dep, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) @@ -2949,7 +3045,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC } metaList := &metav1.PartialObjectMetadataList{} metaList.SetGroupVersionKind(gvk) - Expect(cl.List(context.Background(), metaList)).NotTo(HaveOccurred()) + Expect(cl.List(ctx, metaList)).NotTo(HaveOccurred()) By("validating that the list GVK has been preserved") Expect(metaList.GroupVersionKind()).To(Equal(gvk)) @@ -2972,7 +3068,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(hasDep).To(BeTrue()) }) - It("should return an empty list if there are no matching objects", func() { + It("should return an empty list if there are no matching objects", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) @@ -2983,14 +3079,14 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Version: "v1", Kind: "DeploymentList", }) - Expect(cl.List(context.Background(), metaList)).NotTo(HaveOccurred()) + Expect(cl.List(ctx, metaList)).NotTo(HaveOccurred()) By("validating no Deployments are returned") Expect(metaList.Items).To(BeEmpty()) }) // TODO(seans): get label selector test working - It("should filter results by label selector", func() { + It("should filter results by label selector", func(ctx SpecContext) { By("creating a Deployment with the app=frontend label") depFrontend := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ @@ -3042,7 +3138,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Kind: "DeploymentList", }) labels := map[string]string{"app": "backend"} - err = cl.List(context.Background(), metaList, client.MatchingLabels(labels)) + err = cl.List(ctx, metaList, client.MatchingLabels(labels)) Expect(err).NotTo(HaveOccurred()) By("only the Deployment with the backend label is returned") @@ -3055,7 +3151,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC deleteDeployment(ctx, depBackend, ns) }) - It("should filter results by namespace selector", func() { + It("should filter results by namespace selector", func(ctx SpecContext) { By("creating a Deployment in test-namespace-1") tns1 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-1"}} _, err := clientset.CoreV1().Namespaces().Create(ctx, tns1, metav1.CreateOptions{}) @@ -3104,7 +3200,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Version: "v1", Kind: "DeploymentList", }) - err = cl.List(context.Background(), metaList, client.InNamespace("test-namespace-1")) + err = cl.List(ctx, metaList, client.InNamespace("test-namespace-1")) Expect(err).NotTo(HaveOccurred()) By("only the Deployment in test-namespace-1 is returned") @@ -3119,7 +3215,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC deleteNamespace(ctx, tns2) }) - It("should filter results by field selector", func() { + It("should filter results by field selector", func(ctx SpecContext) { By("creating a Deployment with name deployment-frontend") depFrontend := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "deployment-frontend", Namespace: ns}, @@ -3162,7 +3258,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Version: "v1", Kind: "DeploymentList", }) - err = cl.List(context.Background(), metaList, + err = cl.List(ctx, metaList, client.MatchingFields{"metadata.name": "deployment-backend"}) Expect(err).NotTo(HaveOccurred()) @@ -3176,7 +3272,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC deleteDeployment(ctx, depBackend, ns) }) - It("should filter results by namespace selector and label selector", func() { + It("should filter results by namespace selector and label selector", func(ctx SpecContext) { By("creating a Deployment in test-namespace-3 with the app=frontend label") tns3 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace-3"}} _, err := clientset.CoreV1().Namespaces().Create(ctx, tns3, metav1.CreateOptions{}) @@ -3254,7 +3350,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Kind: "DeploymentList", }) labels := map[string]string{"app": "frontend"} - err = cl.List(context.Background(), metaList, + err = cl.List(ctx, metaList, client.InNamespace("test-namespace-3"), client.MatchingLabels(labels), ) @@ -3274,8 +3370,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC deleteNamespace(ctx, tns4) }) - It("should filter results using limit and continue options", func() { - + It("should filter results using limit and continue options", func(ctx SpecContext) { makeDeployment := func(suffix string) *appsv1.Deployment { return &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ @@ -3324,7 +3419,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Version: "v1", Kind: "DeploymentList", }) - err = cl.List(context.Background(), metaList, + err = cl.List(ctx, metaList, client.Limit(1), ) Expect(err).NotTo(HaveOccurred()) @@ -3342,7 +3437,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Version: "v1", Kind: "DeploymentList", }) - err = cl.List(context.Background(), metaList, + err = cl.List(ctx, metaList, client.Limit(1), client.Continue(continueToken), ) @@ -3361,7 +3456,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Version: "v1", Kind: "DeploymentList", }) - err = cl.List(context.Background(), metaList, + err = cl.List(ctx, metaList, client.Continue(continueToken), ) Expect(err).NotTo(HaveOccurred()) @@ -3660,7 +3755,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC var _ = Describe("ClientWithCache", func() { Describe("Get", func() { - It("should call cache reader when structured object", func() { + It("should call cache reader when structured object", func(ctx SpecContext) { cachedReader := &fakeReader{} cl, err := client.New(cfg, client.Options{ Cache: &client.CacheOptions{ @@ -3670,14 +3765,14 @@ var _ = Describe("ClientWithCache", func() { Expect(err).NotTo(HaveOccurred()) var actual appsv1.Deployment key := client.ObjectKey{Namespace: "ns", Name: "name"} - Expect(cl.Get(context.TODO(), key, &actual)).To(Succeed()) + Expect(cl.Get(ctx, key, &actual)).To(Succeed()) Expect(1).To(Equal(cachedReader.Called)) }) When("getting unstructured objects", func() { var dep *appsv1.Deployment - BeforeEach(func() { + BeforeEach(func(ctx SpecContext) { dep = &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "deployment1", @@ -3694,17 +3789,17 @@ var _ = Describe("ClientWithCache", func() { }, } var err error - dep, err = clientset.AppsV1().Deployments("default").Create(context.Background(), dep, metav1.CreateOptions{}) + dep, err = clientset.AppsV1().Deployments("default").Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) }) - AfterEach(func() { + AfterEach(func(ctx SpecContext) { Expect(clientset.AppsV1().Deployments("default").Delete( - context.Background(), + ctx, dep.Name, metav1.DeleteOptions{}, )).To(Succeed()) }) - It("should call client reader when not cached", func() { + It("should call client reader when not cached", func(ctx SpecContext) { cachedReader := &fakeReader{} cl, err := client.New(cfg, client.Options{ Cache: &client.CacheOptions{ @@ -3721,10 +3816,10 @@ var _ = Describe("ClientWithCache", func() { }) actual.SetName(dep.Name) key := client.ObjectKey{Namespace: dep.Namespace, Name: dep.Name} - Expect(cl.Get(context.TODO(), key, actual)).To(Succeed()) + Expect(cl.Get(ctx, key, actual)).To(Succeed()) Expect(0).To(Equal(cachedReader.Called)) }) - It("should call cache reader when cached", func() { + It("should call cache reader when cached", func(ctx SpecContext) { cachedReader := &fakeReader{} cl, err := client.New(cfg, client.Options{ Cache: &client.CacheOptions{ @@ -3742,13 +3837,13 @@ var _ = Describe("ClientWithCache", func() { }) actual.SetName(dep.Name) key := client.ObjectKey{Namespace: dep.Namespace, Name: dep.Name} - Expect(cl.Get(context.TODO(), key, actual)).To(Succeed()) + Expect(cl.Get(ctx, key, actual)).To(Succeed()) Expect(1).To(Equal(cachedReader.Called)) }) }) }) Describe("List", func() { - It("should call cache reader when structured object", func() { + It("should call cache reader when structured object", func(ctx SpecContext) { cachedReader := &fakeReader{} cl, err := client.New(cfg, client.Options{ Cache: &client.CacheOptions{ @@ -3757,12 +3852,12 @@ var _ = Describe("ClientWithCache", func() { }) Expect(err).NotTo(HaveOccurred()) var actual appsv1.DeploymentList - Expect(cl.List(context.Background(), &actual)).To(Succeed()) + Expect(cl.List(ctx, &actual)).To(Succeed()) Expect(1).To(Equal(cachedReader.Called)) }) When("listing unstructured objects", func() { - It("should call client reader when not cached", func() { + It("should call client reader when not cached", func(ctx SpecContext) { cachedReader := &fakeReader{} cl, err := client.New(cfg, client.Options{ Cache: &client.CacheOptions{ @@ -3777,10 +3872,10 @@ var _ = Describe("ClientWithCache", func() { Kind: "DeploymentList", Version: "v1", }) - Expect(cl.List(context.Background(), actual)).To(Succeed()) + Expect(cl.List(ctx, actual)).To(Succeed()) Expect(0).To(Equal(cachedReader.Called)) }) - It("should call cache reader when cached", func() { + It("should call cache reader when cached", func(ctx SpecContext) { cachedReader := &fakeReader{} cl, err := client.New(cfg, client.Options{ Cache: &client.CacheOptions{ @@ -3796,7 +3891,7 @@ var _ = Describe("ClientWithCache", func() { Kind: "DeploymentList", Version: "v1", }) - Expect(cl.List(context.Background(), actual)).To(Succeed()) + Expect(cl.List(ctx, actual)).To(Succeed()) Expect(1).To(Equal(cachedReader.Called)) }) }) diff --git a/pkg/client/dryrun.go b/pkg/client/dryrun.go index bbcdd38321..a185860d33 100644 --- a/pkg/client/dryrun.go +++ b/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/pkg/client/dryrun_test.go b/pkg/client/dryrun_test.go index 0d370e0576..912a4a10dc 100644 --- a/pkg/client/dryrun_test.go +++ b/pkg/client/dryrun_test.go @@ -17,7 +17,6 @@ limitations under the License. package client_test import ( - "context" "fmt" "sync/atomic" @@ -38,7 +37,6 @@ var _ = Describe("DryRunClient", func() { var count uint64 = 0 var replicaCount int32 = 2 var ns = "default" - ctx := context.Background() getClient := func() client.Client { cl, err := client.New(cfg, client.Options{DryRun: ptr.To(true)}) @@ -47,7 +45,7 @@ var _ = Describe("DryRunClient", func() { return cl } - BeforeEach(func() { + BeforeEach(func(ctx SpecContext) { atomic.AddUint64(&count, 1) dep = &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ @@ -72,11 +70,11 @@ var _ = Describe("DryRunClient", func() { Expect(err).NotTo(HaveOccurred()) }) - AfterEach(func() { + AfterEach(func(ctx SpecContext) { deleteDeployment(ctx, dep, ns) }) - It("should successfully Get an object", func() { + It("should successfully Get an object", func(ctx SpecContext) { name := types.NamespacedName{Namespace: ns, Name: dep.Name} result := &appsv1.Deployment{} @@ -84,7 +82,7 @@ var _ = Describe("DryRunClient", func() { Expect(result).To(BeEquivalentTo(dep)) }) - It("should successfully List objects", func() { + It("should successfully List objects", func(ctx SpecContext) { result := &appsv1.DeploymentList{} opts := client.MatchingLabels(dep.Labels) @@ -94,7 +92,7 @@ var _ = Describe("DryRunClient", func() { Expect(result.Items[0]).To(BeEquivalentTo(*dep)) }) - It("should not create an object", func() { + It("should not create an object", func(ctx SpecContext) { newDep := dep.DeepCopy() newDep.Name = "new-deployment" @@ -104,7 +102,7 @@ var _ = Describe("DryRunClient", func() { Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) - It("should not create an object with opts", func() { + It("should not create an object with opts", func(ctx SpecContext) { newDep := dep.DeepCopy() newDep.Name = "new-deployment" opts := &client.CreateOptions{DryRun: []string{"Bye", "Pippa"}} @@ -115,7 +113,7 @@ var _ = Describe("DryRunClient", func() { Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) - It("should refuse a create request for an invalid object", func() { + It("should refuse a create request for an invalid object", func(ctx SpecContext) { changedDep := dep.DeepCopy() changedDep.Spec.Template.Spec.Containers = nil @@ -123,7 +121,7 @@ var _ = Describe("DryRunClient", func() { Expect(apierrors.IsInvalid(err)).To(BeTrue()) }) - It("should not change objects via update", func() { + It("should not change objects via update", func(ctx SpecContext) { changedDep := dep.DeepCopy() *changedDep.Spec.Replicas = 2 @@ -135,7 +133,7 @@ var _ = Describe("DryRunClient", func() { Expect(actual).To(BeEquivalentTo(dep)) }) - It("should not change objects via update with opts", func() { + It("should not change objects via update with opts", func(ctx SpecContext) { changedDep := dep.DeepCopy() *changedDep.Spec.Replicas = 2 opts := &client.UpdateOptions{DryRun: []string{"Bye", "Pippa"}} @@ -148,7 +146,7 @@ var _ = Describe("DryRunClient", func() { Expect(actual).To(BeEquivalentTo(dep)) }) - It("should refuse an update request for an invalid change", func() { + It("should refuse an update request for an invalid change", func(ctx SpecContext) { changedDep := dep.DeepCopy() changedDep.Spec.Template.Spec.Containers = nil @@ -156,7 +154,7 @@ var _ = Describe("DryRunClient", func() { Expect(apierrors.IsInvalid(err)).To(BeTrue()) }) - It("should not change objects via patch", func() { + It("should not change objects via patch", func(ctx SpecContext) { changedDep := dep.DeepCopy() *changedDep.Spec.Replicas = 2 @@ -168,7 +166,7 @@ var _ = Describe("DryRunClient", func() { Expect(actual).To(BeEquivalentTo(dep)) }) - It("should not change objects via patch with opts", func() { + It("should not change objects via patch with opts", func(ctx SpecContext) { changedDep := dep.DeepCopy() *changedDep.Spec.Replicas = 2 opts := &client.PatchOptions{DryRun: []string{"Bye", "Pippa"}} @@ -181,7 +179,7 @@ var _ = Describe("DryRunClient", func() { Expect(actual).To(BeEquivalentTo(dep)) }) - It("should not delete objects", func() { + It("should not delete objects", func(ctx SpecContext) { Expect(getClient().Delete(ctx, dep)).NotTo(HaveOccurred()) actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) @@ -190,7 +188,7 @@ var _ = Describe("DryRunClient", func() { Expect(actual).To(BeEquivalentTo(dep)) }) - It("should not delete objects with opts", func() { + It("should not delete objects with opts", func(ctx SpecContext) { opts := &client.DeleteOptions{DryRun: []string{"Bye", "Pippa"}} Expect(getClient().Delete(ctx, dep, opts)).NotTo(HaveOccurred()) @@ -201,7 +199,7 @@ var _ = Describe("DryRunClient", func() { Expect(actual).To(BeEquivalentTo(dep)) }) - It("should not delete objects via deleteAllOf", func() { + It("should not delete objects via deleteAllOf", func(ctx SpecContext) { opts := []client.DeleteAllOfOption{client.InNamespace(ns), client.MatchingLabels(dep.Labels)} Expect(getClient().DeleteAllOf(ctx, dep, opts...)).NotTo(HaveOccurred()) @@ -212,7 +210,7 @@ var _ = Describe("DryRunClient", func() { Expect(actual).To(BeEquivalentTo(dep)) }) - It("should not change objects via update status", func() { + It("should not change objects via update status", func(ctx SpecContext) { changedDep := dep.DeepCopy() changedDep.Status.Replicas = 99 @@ -224,7 +222,7 @@ var _ = Describe("DryRunClient", func() { Expect(actual).To(BeEquivalentTo(dep)) }) - It("should not change objects via update status with opts", func() { + It("should not change objects via update status with opts", func(ctx SpecContext) { changedDep := dep.DeepCopy() changedDep.Status.Replicas = 99 opts := &client.SubResourceUpdateOptions{UpdateOptions: client.UpdateOptions{DryRun: []string{"Bye", "Pippa"}}} @@ -237,7 +235,7 @@ var _ = Describe("DryRunClient", func() { Expect(actual).To(BeEquivalentTo(dep)) }) - It("should not change objects via status patch", func() { + It("should not change objects via status patch", func(ctx SpecContext) { changedDep := dep.DeepCopy() changedDep.Status.Replicas = 99 @@ -249,7 +247,7 @@ var _ = Describe("DryRunClient", func() { Expect(actual).To(BeEquivalentTo(dep)) }) - It("should not change objects via status patch with opts", func() { + It("should not change objects via status patch with opts", func(ctx SpecContext) { changedDep := dep.DeepCopy() changedDep.Status.Replicas = 99 diff --git a/pkg/client/fake/client.go b/pkg/client/fake/client.go index 16e2cba512..380c728b0d 100644 --- a/pkg/client/fake/client.go +++ b/pkg/client/fake/client.go @@ -52,17 +52,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" @@ -98,6 +102,8 @@ type fakeClient struct { indexes map[schema.GroupVersionKind]map[string]client.IndexerFunc // indexesLock must be held when accessing indexes. indexesLock sync.RWMutex + + returnManagedFields bool } var _ client.WithWatch = &fakeClient{} @@ -131,6 +137,8 @@ type ClientBuilder struct { withStatusSubresource []client.Object objectTracker testing.ObjectTracker interceptorFuncs *interceptor.Funcs + typeConverters []managedfields.TypeConverter + returnManagedFields bool // indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK. // The inner map maps from index name to IndexerFunc. @@ -172,6 +180,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 @@ -228,6 +238,31 @@ 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.scheme == nil { @@ -237,8 +272,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) @@ -248,10 +281,33 @@ 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")) + } + 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}, + ) + } + tracker := versionedTracker{ + ObjectTracker: f.objectTracker, + scheme: f.scheme, + withStatusSubresource: withStatusSubResource, } for _, obj := range f.initObject { @@ -276,6 +332,7 @@ func (f *ClientBuilder) Build() client.WithWatch { restMapper: f.restMapper, indexes: f.indexes, withStatusSubresource: withStatusSubResource, + returnManagedFields: f.returnManagedFields, } if f.interceptorFuncs != nil { @@ -332,8 +389,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")}) } @@ -372,6 +430,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 { @@ -394,7 +455,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 } @@ -402,6 +467,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) } @@ -433,8 +502,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")}) } @@ -521,42 +591,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 @@ -572,6 +660,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) @@ -579,11 +671,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() @@ -602,39 +695,30 @@ 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 := ensureTypeMeta(obj, originalGVK); err != nil { + return err + } objCopy := obj.DeepCopyObject().(client.ObjectList) if err := json.Unmarshal(j, objCopy); err != nil { return err } - if _, isUnstructured := obj.(runtime.Unstructured); isUnstructured { - ta, err := meta.TypeAccessor(obj) - if err != nil { - return err - } - ta.SetKind(originalKind) - ta.SetAPIVersion(gvk.GroupVersion().String()) - } - objs, err := meta.ExtractList(objCopy) if err != nil { return err } + for _, o := range objs { + if err := ensureTypeMeta(o, gvk); err != nil { + return err + } + } + if listOpts.LabelSelector == nil && listOpts.FieldSelector == nil { return meta.SetList(obj, objs) } @@ -741,8 +825,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) @@ -773,14 +862,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()); 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 @@ -826,8 +936,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 @@ -877,8 +992,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) @@ -892,6 +1012,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 @@ -899,19 +1023,77 @@ 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() + + return c.patch(u, applyPatch, patchOpts) +} + +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 @@ -922,16 +1104,16 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client if err != nil { return err } - accessor, err := meta.Accessor(obj) + gvk, err := apiutil.GVKForObject(obj, c.scheme) if err != nil { return err } - data, err := patch.Data(obj) + accessor, err := meta.Accessor(obj) if err != nil { return err } - gvk, err := apiutil.GVKForObject(obj, c.scheme) + data, err := patch.Data(obj) if err != nil { return err } @@ -940,7 +1122,10 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client 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{} } oldAccessor, err := meta.Accessor(oldObj) if err != nil { @@ -955,7 +1140,7 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client // 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) + o, err := dryPatch(action, c.tracker, obj) if err != nil { return err } @@ -978,21 +1163,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. @@ -1014,12 +1206,15 @@ func deletionTimestampEqual(newObj metav1.Object, obj metav1.Object) bool { // This results in some code duplication, but was found to be a cleaner alternative than unmarshalling and introspecting the patch data // and easier than refactoring the k8s client-go method upstream. // Duplicate of upstream: https://github.com/kubernetes/client-go/blob/783d0d33626e59d55d52bfd7696b775851f92107/testing/fixture.go#L146-L194 -func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (runtime.Object, error) { +func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker, newObj runtime.Object) (runtime.Object, error) { ns := action.GetNamespace() gvr := action.GetResource() 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 } @@ -1064,10 +1259,23 @@ func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (ru if err = json.Unmarshal(mergedByte, obj); err != nil { return nil, err } - 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") case types.ApplyCBORPatchType: return nil, errors.New("apply CBOR patches are not supported in the fake client") + case types.ApplyPatchType: + // There doesn't seem to be a way to test this without actually applying it as apply is implemented in the tracker. + // We have to make sure no reader sees this and we can not handle errors resetting the obj to the original state. + defer func() { + if unstructured, isUnstructured := obj.(*unstructured.Unstructured); isUnstructured && unstructured.GetKind() == "" { + unstructured.SetGroupVersionKind(newObj.GetObjectKind().GroupVersionKind()) + } + if err := tracker.Add(obj); err != nil { + panic(err) + } + }() + if err := tracker.Apply(gvr, newObj, ns, action.PatchOptions); err != nil { + return nil, err + } + return tracker.Get(gvr, ns, action.GetName()) default: return nil, fmt.Errorf("%s PatchType is not supported", action.GetPatchType()) } @@ -1600,3 +1808,47 @@ func AddIndex(c client.Client, obj runtime.Object, field string, extractValue cl 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/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go index 4d794b14d8..0d1589c2ec 100644 --- a/pkg/client/fake/client_test.go +++ b/pkg/client/fake/client_test.go @@ -44,7 +44,10 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" + clientgoapplyconfigurations "k8s.io/client-go/applyconfigurations" + corev1applyconfigurations "k8s.io/client-go/applyconfigurations/core/v1" "k8s.io/client-go/kubernetes/fake" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" @@ -66,10 +69,6 @@ var _ = Describe("Fake client", func() { BeforeEach(func() { replicas := int32(1) dep = &appsv1.Deployment{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "apps/v1", - Kind: "Deployment", - }, ObjectMeta: metav1.ObjectMeta{ Name: "test-deployment", Namespace: "ns1", @@ -83,10 +82,6 @@ var _ = Describe("Fake client", func() { }, } dep2 = &appsv1.Deployment{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "apps/v1", - Kind: "Deployment", - }, ObjectMeta: metav1.ObjectMeta{ Name: "test-deployment-2", Namespace: "ns1", @@ -100,10 +95,6 @@ var _ = Describe("Fake client", func() { }, } cm = &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "ConfigMap", - }, ObjectMeta: metav1.ObjectMeta{ Name: "test-cm", Namespace: "ns2", @@ -116,19 +107,19 @@ var _ = Describe("Fake client", func() { }) AssertClientWithoutIndexBehavior := func() { - It("should be able to Get", func() { + It("should be able to Get", func(ctx SpecContext) { By("Getting a deployment") namespacedName := types.NamespacedName{ Name: "test-deployment", Namespace: "ns1", } obj := &appsv1.Deployment{} - err := cl.Get(context.Background(), namespacedName, obj) + err := cl.Get(ctx, namespacedName, obj) Expect(err).ToNot(HaveOccurred()) Expect(obj).To(Equal(dep)) }) - It("should be able to Get using unstructured", func() { + It("should be able to Get using unstructured", func(ctx SpecContext) { By("Getting a deployment") namespacedName := types.NamespacedName{ Name: "test-deployment", @@ -137,50 +128,50 @@ var _ = Describe("Fake client", func() { obj := &unstructured.Unstructured{} obj.SetAPIVersion("apps/v1") obj.SetKind("Deployment") - err := cl.Get(context.Background(), namespacedName, obj) + err := cl.Get(ctx, namespacedName, obj) Expect(err).ToNot(HaveOccurred()) }) - It("should be able to List", func() { + It("should be able to List", func(ctx SpecContext) { By("Listing all deployments in a namespace") list := &appsv1.DeploymentList{} - err := cl.List(context.Background(), list, client.InNamespace("ns1")) + err := cl.List(ctx, list, client.InNamespace("ns1")) Expect(err).ToNot(HaveOccurred()) Expect(list.Items).To(HaveLen(2)) Expect(list.Items).To(ConsistOf(*dep, *dep2)) }) - It("should be able to List using unstructured list", func() { + It("should be able to List using unstructured list", func(ctx SpecContext) { By("Listing all deployments in a namespace") list := &unstructured.UnstructuredList{} list.SetAPIVersion("apps/v1") list.SetKind("DeploymentList") - err := cl.List(context.Background(), list, client.InNamespace("ns1")) + err := cl.List(ctx, list, client.InNamespace("ns1")) Expect(err).ToNot(HaveOccurred()) Expect(list.GroupVersionKind().GroupVersion().String()).To(Equal("apps/v1")) Expect(list.GetKind()).To(Equal("DeploymentList")) Expect(list.Items).To(HaveLen(2)) }) - It("should be able to List using unstructured list when setting a non-list kind", func() { + It("should be able to List using unstructured list when setting a non-list kind", func(ctx SpecContext) { By("Listing all deployments in a namespace") list := &unstructured.UnstructuredList{} list.SetAPIVersion("apps/v1") list.SetKind("Deployment") - err := cl.List(context.Background(), list, client.InNamespace("ns1")) + err := cl.List(ctx, list, client.InNamespace("ns1")) Expect(err).ToNot(HaveOccurred()) Expect(list.GroupVersionKind().GroupVersion().String()).To(Equal("apps/v1")) Expect(list.GetKind()).To(Equal("Deployment")) Expect(list.Items).To(HaveLen(2)) }) - It("should be able to retrieve registered objects that got manipulated as unstructured", func() { + It("should be able to retrieve registered objects that got manipulated as unstructured", func(ctx SpecContext) { list := func() { By("Listing all endpoints in a namespace") list := &unstructured.UnstructuredList{} list.SetAPIVersion("v1") list.SetKind("EndpointsList") - err := cl.List(context.Background(), list, client.InNamespace("ns1")) + err := cl.List(ctx, list, client.InNamespace("ns1")) Expect(err).ToNot(HaveOccurred()) Expect(list.GroupVersionKind().GroupVersion().String()).To(Equal("v1")) Expect(list.GetKind()).To(Equal("EndpointsList")) @@ -199,44 +190,44 @@ var _ = Describe("Fake client", func() { By("Adding the object during client initialization") cl = NewClientBuilder().WithRuntimeObjects(unstructuredEndpoint()).Build() list() - Expect(cl.Delete(context.Background(), unstructuredEndpoint())).To(Succeed()) + Expect(cl.Delete(ctx, unstructuredEndpoint())).To(Succeed()) By("Creating an object") item := unstructuredEndpoint() - err := cl.Create(context.Background(), item) + err := cl.Create(ctx, item) Expect(err).ToNot(HaveOccurred()) list() By("Updating the object") item.SetAnnotations(map[string]string{"foo": "bar"}) - err = cl.Update(context.Background(), item) + err = cl.Update(ctx, item) Expect(err).ToNot(HaveOccurred()) list() By("Patching the object") old := item.DeepCopy() item.SetAnnotations(map[string]string{"bar": "baz"}) - err = cl.Patch(context.Background(), item, client.MergeFrom(old)) + err = cl.Patch(ctx, item, client.MergeFrom(old)) Expect(err).ToNot(HaveOccurred()) list() }) - It("should be able to Create an unregistered type using unstructured", func() { + It("should be able to Create an unregistered type using unstructured", func(ctx SpecContext) { item := &unstructured.Unstructured{} item.SetAPIVersion("custom/v1") item.SetKind("Image") item.SetName("my-item") - err := cl.Create(context.Background(), item) + err := cl.Create(ctx, item) Expect(err).ToNot(HaveOccurred()) }) - It("should be able to Get an unregisted type using unstructured", func() { + It("should be able to Get an unregisted type using unstructured", func(ctx SpecContext) { By("Creating an object of an unregistered type") item := &unstructured.Unstructured{} item.SetAPIVersion("custom/v2") item.SetKind("Image") item.SetName("my-item") - err := cl.Create(context.Background(), item) + err := cl.Create(ctx, item) Expect(err).ToNot(HaveOccurred()) By("Getting and the object") @@ -244,43 +235,43 @@ var _ = Describe("Fake client", func() { item.SetAPIVersion("custom/v2") item.SetKind("Image") item.SetName("my-item") - err = cl.Get(context.Background(), client.ObjectKeyFromObject(item), item) + err = cl.Get(ctx, client.ObjectKeyFromObject(item), item) Expect(err).ToNot(HaveOccurred()) }) - It("should be able to List an unregistered type using unstructured", func() { + It("should be able to List an unregistered type using unstructured with ListKind", func(ctx SpecContext) { list := &unstructured.UnstructuredList{} list.SetAPIVersion("custom/v3") list.SetKind("ImageList") - err := cl.List(context.Background(), list) + err := cl.List(ctx, list) Expect(list.GroupVersionKind().GroupVersion().String()).To(Equal("custom/v3")) Expect(list.GetKind()).To(Equal("ImageList")) Expect(err).ToNot(HaveOccurred()) }) - It("should be able to List an unregistered type using unstructured", func() { + It("should be able to List an unregistered type using unstructured with Kind", func(ctx SpecContext) { list := &unstructured.UnstructuredList{} list.SetAPIVersion("custom/v4") list.SetKind("Image") - err := cl.List(context.Background(), list) + err := cl.List(ctx, list) + Expect(err).ToNot(HaveOccurred()) Expect(list.GroupVersionKind().GroupVersion().String()).To(Equal("custom/v4")) Expect(list.GetKind()).To(Equal("Image")) - Expect(err).ToNot(HaveOccurred()) }) - It("should be able to Update an unregistered type using unstructured", func() { + It("should be able to Update an unregistered type using unstructured", func(ctx SpecContext) { By("Creating an object of an unregistered type") item := &unstructured.Unstructured{} item.SetAPIVersion("custom/v5") item.SetKind("Image") item.SetName("my-item") - err := cl.Create(context.Background(), item) + err := cl.Create(ctx, item) Expect(err).ToNot(HaveOccurred()) By("Updating the object") err = unstructured.SetNestedField(item.Object, int64(2), "spec", "replicas") Expect(err).ToNot(HaveOccurred()) - err = cl.Update(context.Background(), item) + err = cl.Update(ctx, item) Expect(err).ToNot(HaveOccurred()) By("Getting the object") @@ -288,7 +279,7 @@ var _ = Describe("Fake client", func() { item.SetAPIVersion("custom/v5") item.SetKind("Image") item.SetName("my-item") - err = cl.Get(context.Background(), client.ObjectKeyFromObject(item), item) + err = cl.Get(ctx, client.ObjectKeyFromObject(item), item) Expect(err).ToNot(HaveOccurred()) By("Inspecting the object") @@ -298,20 +289,20 @@ var _ = Describe("Fake client", func() { Expect(value).To(Equal(int64(2))) }) - It("should be able to Patch an unregistered type using unstructured", func() { + It("should be able to Patch an unregistered type using unstructured", func(ctx SpecContext) { By("Creating an object of an unregistered type") item := &unstructured.Unstructured{} item.SetAPIVersion("custom/v6") item.SetKind("Image") item.SetName("my-item") - err := cl.Create(context.Background(), item) + err := cl.Create(ctx, item) Expect(err).ToNot(HaveOccurred()) By("Updating the object") original := item.DeepCopy() err = unstructured.SetNestedField(item.Object, int64(2), "spec", "replicas") Expect(err).ToNot(HaveOccurred()) - err = cl.Patch(context.Background(), item, client.MergeFrom(original)) + err = cl.Patch(ctx, item, client.MergeFrom(original)) Expect(err).ToNot(HaveOccurred()) By("Getting the object") @@ -319,7 +310,7 @@ var _ = Describe("Fake client", func() { item.SetAPIVersion("custom/v6") item.SetKind("Image") item.SetName("my-item") - err = cl.Get(context.Background(), client.ObjectKeyFromObject(item), item) + err = cl.Get(ctx, client.ObjectKeyFromObject(item), item) Expect(err).ToNot(HaveOccurred()) By("Inspecting the object") @@ -329,17 +320,17 @@ var _ = Describe("Fake client", func() { Expect(value).To(Equal(int64(2))) }) - It("should be able to Delete an unregistered type using unstructured", func() { + It("should be able to Delete an unregistered type using unstructured", func(ctx SpecContext) { By("Creating an object of an unregistered type") item := &unstructured.Unstructured{} item.SetAPIVersion("custom/v7") item.SetKind("Image") item.SetName("my-item") - err := cl.Create(context.Background(), item) + err := cl.Create(ctx, item) Expect(err).ToNot(HaveOccurred()) By("Deleting the object") - err = cl.Delete(context.Background(), item) + err = cl.Delete(ctx, item) Expect(err).ToNot(HaveOccurred()) By("Getting the object") @@ -347,11 +338,11 @@ var _ = Describe("Fake client", func() { item.SetAPIVersion("custom/v7") item.SetKind("Image") item.SetName("my-item") - err = cl.Get(context.Background(), client.ObjectKeyFromObject(item), item) + err = cl.Get(ctx, client.ObjectKeyFromObject(item), item) Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) - It("should be able to retrieve objects by PartialObjectMetadata", func() { + It("should be able to retrieve objects by PartialObjectMetadata", func(ctx SpecContext) { By("Creating a Resource") secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -359,7 +350,7 @@ var _ = Describe("Fake client", func() { Namespace: "bar", }, } - err := cl.Create(context.Background(), secret) + err := cl.Create(ctx, secret) Expect(err).ToNot(HaveOccurred()) By("Fetching the resource using a PartialObjectMeta") @@ -371,17 +362,17 @@ var _ = Describe("Fake client", func() { } partialObjMeta.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Secret")) - err = cl.Get(context.Background(), client.ObjectKeyFromObject(partialObjMeta), partialObjMeta) + err = cl.Get(ctx, client.ObjectKeyFromObject(partialObjMeta), partialObjMeta) Expect(err).ToNot(HaveOccurred()) Expect(partialObjMeta.Kind).To(Equal("Secret")) Expect(partialObjMeta.APIVersion).To(Equal("v1")) }) - It("should support filtering by labels and their values", func() { + It("should support filtering by labels and their values", func(ctx SpecContext) { By("Listing deployments with a particular label and value") list := &appsv1.DeploymentList{} - err := cl.List(context.Background(), list, client.InNamespace("ns1"), + err := cl.List(ctx, list, client.InNamespace("ns1"), client.MatchingLabels(map[string]string{ "test-label": "label-value", })) @@ -390,49 +381,25 @@ var _ = Describe("Fake client", func() { Expect(list.Items).To(ConsistOf(*dep2)) }) - It("should support filtering by label existence", func() { + It("should support filtering by label existence", func(ctx SpecContext) { By("Listing deployments with a particular label") list := &appsv1.DeploymentList{} - err := cl.List(context.Background(), list, client.InNamespace("ns1"), + err := cl.List(ctx, list, client.InNamespace("ns1"), client.HasLabels{"test-label"}) Expect(err).ToNot(HaveOccurred()) Expect(list.Items).To(HaveLen(1)) Expect(list.Items).To(ConsistOf(*dep2)) }) - It("should reject apply patches, they are not supported in the fake client", func() { - By("Creating a new configmap") - cm := &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "ConfigMap", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "new-test-cm", - Namespace: "ns2", - }, - } - err := cl.Create(context.Background(), cm) - Expect(err).ToNot(HaveOccurred()) - - cm.Data = map[string]string{"foo": "bar"} - err = cl.Patch(context.Background(), cm, client.Apply, client.ForceOwnership) - Expect(err).To(MatchError(ContainSubstring("apply patches are not supported in the fake client"))) - }) - - It("should be able to Create", func() { + It("should be able to Create", func(ctx SpecContext) { By("Creating a new configmap") newcm := &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "ConfigMap", - }, ObjectMeta: metav1.ObjectMeta{ Name: "new-test-cm", Namespace: "ns2", }, } - err := cl.Create(context.Background(), newcm) + err := cl.Create(ctx, newcm) Expect(err).ToNot(HaveOccurred()) By("Getting the new configmap") @@ -441,13 +408,13 @@ var _ = Describe("Fake client", func() { Namespace: "ns2", } obj := &corev1.ConfigMap{} - err = cl.Get(context.Background(), namespacedName, obj) + err = cl.Get(ctx, namespacedName, obj) Expect(err).ToNot(HaveOccurred()) Expect(obj).To(Equal(newcm)) Expect(obj.ObjectMeta.ResourceVersion).To(Equal("1")) }) - It("should error on create with set resourceVersion", func() { + It("should error on create with set resourceVersion", func(ctx SpecContext) { By("Creating a new configmap") newcm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -456,58 +423,46 @@ var _ = Describe("Fake client", func() { ResourceVersion: "1", }, } - err := cl.Create(context.Background(), newcm) + err := cl.Create(ctx, newcm) Expect(apierrors.IsBadRequest(err)).To(BeTrue()) }) - It("should not change the submitted object if Create failed", func() { + It("should not change the submitted object if Create failed", func(ctx SpecContext) { By("Trying to create an existing configmap") submitted := cm.DeepCopy() submitted.ResourceVersion = "" submittedReference := submitted.DeepCopy() - err := cl.Create(context.Background(), submitted) + err := cl.Create(ctx, submitted) Expect(err).To(HaveOccurred()) Expect(apierrors.IsAlreadyExists(err)).To(BeTrue()) - Expect(submitted).To(Equal(submittedReference)) + Expect(submitted).To(BeComparableTo(submittedReference)) }) - It("should error on Create with empty Name", func() { + It("should error on Create with empty Name", func(ctx SpecContext) { By("Creating a new configmap") newcm := &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "ConfigMap", - }, ObjectMeta: metav1.ObjectMeta{ Namespace: "ns2", }, } - err := cl.Create(context.Background(), newcm) + err := cl.Create(ctx, newcm) Expect(err.Error()).To(Equal("ConfigMap \"\" is invalid: metadata.name: Required value: name is required")) }) - It("should error on Update with empty Name", func() { + It("should error on Update with empty Name", func(ctx SpecContext) { By("Creating a new configmap") newcm := &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "ConfigMap", - }, ObjectMeta: metav1.ObjectMeta{ Namespace: "ns2", }, } - err := cl.Update(context.Background(), newcm) + err := cl.Update(ctx, newcm) Expect(err.Error()).To(Equal("ConfigMap \"\" is invalid: metadata.name: Required value: name is required")) }) - It("should be able to Create with GenerateName", func() { + It("should be able to Create with GenerateName", func(ctx SpecContext) { By("Creating a new configmap") newcm := &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "ConfigMap", - }, ObjectMeta: metav1.ObjectMeta{ GenerateName: "new-test-cm", Namespace: "ns2", @@ -516,12 +471,12 @@ var _ = Describe("Fake client", func() { }, }, } - err := cl.Create(context.Background(), newcm) + err := cl.Create(ctx, newcm) Expect(err).ToNot(HaveOccurred()) By("Listing configmaps with a particular label") list := &corev1.ConfigMapList{} - err = cl.List(context.Background(), list, client.InNamespace("ns2"), + err = cl.List(ctx, list, client.InNamespace("ns2"), client.MatchingLabels(map[string]string{ "test-label": "label-value", })) @@ -530,13 +485,9 @@ var _ = Describe("Fake client", func() { Expect(list.Items[0].Name).NotTo(BeEmpty()) }) - It("should be able to Update", func() { + It("should be able to Update", func(ctx SpecContext) { By("Updating a new configmap") newcm := &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "ConfigMap", - }, ObjectMeta: metav1.ObjectMeta{ Name: "test-cm", Namespace: "ns2", @@ -546,7 +497,7 @@ var _ = Describe("Fake client", func() { "test-key": "new-value", }, } - err := cl.Update(context.Background(), newcm) + err := cl.Update(ctx, newcm) Expect(err).ToNot(HaveOccurred()) By("Getting the new configmap") @@ -555,19 +506,15 @@ var _ = Describe("Fake client", func() { Namespace: "ns2", } obj := &corev1.ConfigMap{} - err = cl.Get(context.Background(), namespacedName, obj) + err = cl.Get(ctx, namespacedName, obj) Expect(err).ToNot(HaveOccurred()) Expect(obj).To(Equal(newcm)) Expect(obj.ObjectMeta.ResourceVersion).To(Equal("1000")) }) - It("should allow updates with non-set ResourceVersion for a resource that allows unconditional updates", func() { + It("should allow updates with non-set ResourceVersion for a resource that allows unconditional updates", func(ctx SpecContext) { By("Updating a new configmap") newcm := &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "ConfigMap", - }, ObjectMeta: metav1.ObjectMeta{ Name: "test-cm", Namespace: "ns2", @@ -576,7 +523,7 @@ var _ = Describe("Fake client", func() { "test-key": "new-value", }, } - err := cl.Update(context.Background(), newcm) + err := cl.Update(ctx, newcm) Expect(err).ToNot(HaveOccurred()) By("Getting the configmap") @@ -585,31 +532,25 @@ var _ = Describe("Fake client", func() { Namespace: "ns2", } obj := &corev1.ConfigMap{} - err = cl.Get(context.Background(), namespacedName, obj) + err = cl.Get(ctx, namespacedName, obj) Expect(err).ToNot(HaveOccurred()) Expect(obj).To(Equal(newcm)) Expect(obj.ObjectMeta.ResourceVersion).To(Equal("1000")) }) - It("should allow patch when the patch sets RV to 'null'", func() { - schemeBuilder := &scheme.Builder{GroupVersion: schema.GroupVersion{Group: "test", Version: "v1"}} - schemeBuilder.Register(&WithPointerMeta{}, &WithPointerMetaList{}) - - scheme := runtime.NewScheme() - Expect(schemeBuilder.AddToScheme(scheme)).NotTo(HaveOccurred()) - - cl := NewClientBuilder().WithScheme(scheme).Build() - original := &WithPointerMeta{ - ObjectMeta: &metav1.ObjectMeta{ + It("should allow patch when the patch sets RV to 'null'", func(ctx SpecContext) { + cl := NewClientBuilder().Build() + original := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ Name: "obj", Namespace: "ns2", }} - err := cl.Create(context.Background(), original) + err := cl.Create(ctx, original) Expect(err).ToNot(HaveOccurred()) - newObj := &WithPointerMeta{ - ObjectMeta: &metav1.ObjectMeta{ + newObj := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ Name: original.Name, Namespace: original.Namespace, Annotations: map[string]string{ @@ -617,20 +558,16 @@ var _ = Describe("Fake client", func() { }, }} - Expect(cl.Patch(context.Background(), newObj, client.MergeFrom(original))).To(Succeed()) + Expect(cl.Patch(ctx, newObj, client.MergeFrom(original))).To(Succeed()) - patched := &WithPointerMeta{} - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(original), patched)).To(Succeed()) + patched := &corev1.ConfigMap{} + Expect(cl.Get(ctx, client.ObjectKeyFromObject(original), patched)).To(Succeed()) Expect(patched.Annotations).To(Equal(map[string]string{"foo": "bar"})) }) - It("should reject updates with non-set ResourceVersion for a resource that doesn't allow unconditional updates", func() { + It("should reject updates with non-set ResourceVersion for a resource that doesn't allow unconditional updates", func(ctx SpecContext) { By("Creating a new binding") binding := &corev1.Binding{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Binding", - }, ObjectMeta: metav1.ObjectMeta{ Name: "test-binding", Namespace: "ns2", @@ -642,14 +579,10 @@ var _ = Describe("Fake client", func() { Name: cm.Name, }, } - Expect(cl.Create(context.Background(), binding)).To(Succeed()) + Expect(cl.Create(ctx, binding)).To(Succeed()) By("Updating the binding with a new resource lacking resource version") newBinding := &corev1.Binding{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Binding", - }, ObjectMeta: metav1.ObjectMeta{ Name: binding.Name, Namespace: binding.Namespace, @@ -659,23 +592,19 @@ var _ = Describe("Fake client", func() { Name: "blue", }, } - Expect(cl.Update(context.Background(), newBinding)).NotTo(Succeed()) + Expect(cl.Update(ctx, newBinding)).NotTo(Succeed()) }) - It("should allow create on update for a resource that allows create on update", func() { + It("should allow create on update for a resource that allows create on update", func(ctx SpecContext) { By("Creating a new lease with update") lease := &coordinationv1.Lease{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "coordination.k8s.io/v1", - Kind: "Lease", - }, ObjectMeta: metav1.ObjectMeta{ Name: "test-lease", Namespace: "ns2", }, Spec: coordinationv1.LeaseSpec{}, } - Expect(cl.Create(context.Background(), lease)).To(Succeed()) + Expect(cl.Create(ctx, lease)).To(Succeed()) By("Getting the lease") namespacedName := types.NamespacedName{ @@ -683,18 +612,14 @@ var _ = Describe("Fake client", func() { Namespace: lease.Namespace, } obj := &coordinationv1.Lease{} - Expect(cl.Get(context.Background(), namespacedName, obj)).To(Succeed()) + Expect(cl.Get(ctx, namespacedName, obj)).To(Succeed()) Expect(obj).To(Equal(lease)) Expect(obj.ObjectMeta.ResourceVersion).To(Equal("1")) }) - It("should reject create on update for a resource that does not allow create on update", func() { + It("should reject create on update for a resource that does not allow create on update", func(ctx SpecContext) { By("Attemping to create a new configmap with update") newcm := &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "ConfigMap", - }, ObjectMeta: metav1.ObjectMeta{ Name: "different-test-cm", Namespace: "ns2", @@ -703,10 +628,10 @@ var _ = Describe("Fake client", func() { "test-key": "new-value", }, } - Expect(cl.Update(context.Background(), newcm)).NotTo(Succeed()) + Expect(cl.Update(ctx, newcm)).NotTo(Succeed()) }) - It("should reject updates with non-matching ResourceVersion", func() { + It("should reject updates with non-matching ResourceVersion", func(ctx SpecContext) { By("Updating a new configmap") newcm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -718,7 +643,7 @@ var _ = Describe("Fake client", func() { "test-key": "new-value", }, } - err := cl.Update(context.Background(), newcm) + err := cl.Update(ctx, newcm) Expect(apierrors.IsConflict(err)).To(BeTrue()) By("Getting the configmap") @@ -727,67 +652,67 @@ var _ = Describe("Fake client", func() { Namespace: "ns2", } obj := &corev1.ConfigMap{} - err = cl.Get(context.Background(), namespacedName, obj) + err = cl.Get(ctx, namespacedName, obj) Expect(err).ToNot(HaveOccurred()) Expect(obj).To(Equal(cm)) Expect(obj.ObjectMeta.ResourceVersion).To(Equal(trackerAddResourceVersion)) }) - It("should reject Delete with a mismatched ResourceVersion", func() { + It("should reject Delete with a mismatched ResourceVersion", func(ctx SpecContext) { bogusRV := "bogus" By("Deleting with a mismatched ResourceVersion Precondition") - err := cl.Delete(context.Background(), dep, client.Preconditions{ResourceVersion: &bogusRV}) + err := cl.Delete(ctx, dep, client.Preconditions{ResourceVersion: &bogusRV}) Expect(apierrors.IsConflict(err)).To(BeTrue()) list := &appsv1.DeploymentList{} - err = cl.List(context.Background(), list, client.InNamespace("ns1")) + err = cl.List(ctx, list, client.InNamespace("ns1")) Expect(err).ToNot(HaveOccurred()) Expect(list.Items).To(HaveLen(2)) Expect(list.Items).To(ConsistOf(*dep, *dep2)) }) - It("should successfully Delete with a matching ResourceVersion", func() { + It("should successfully Delete with a matching ResourceVersion", func(ctx SpecContext) { goodRV := trackerAddResourceVersion By("Deleting with a matching ResourceVersion Precondition") - err := cl.Delete(context.Background(), dep, client.Preconditions{ResourceVersion: &goodRV}) + err := cl.Delete(ctx, dep, client.Preconditions{ResourceVersion: &goodRV}) Expect(err).ToNot(HaveOccurred()) list := &appsv1.DeploymentList{} - err = cl.List(context.Background(), list, client.InNamespace("ns1")) + err = cl.List(ctx, list, client.InNamespace("ns1")) Expect(err).ToNot(HaveOccurred()) Expect(list.Items).To(HaveLen(1)) Expect(list.Items).To(ConsistOf(*dep2)) }) - It("should be able to Delete with no ResourceVersion Precondition", func() { + It("should be able to Delete with no ResourceVersion Precondition", func(ctx SpecContext) { By("Deleting a deployment") - err := cl.Delete(context.Background(), dep) + err := cl.Delete(ctx, dep) Expect(err).ToNot(HaveOccurred()) By("Listing all deployments in the namespace") list := &appsv1.DeploymentList{} - err = cl.List(context.Background(), list, client.InNamespace("ns1")) + err = cl.List(ctx, list, client.InNamespace("ns1")) Expect(err).ToNot(HaveOccurred()) Expect(list.Items).To(HaveLen(1)) Expect(list.Items).To(ConsistOf(*dep2)) }) - It("should be able to Delete with no opts even if object's ResourceVersion doesn't match server", func() { + It("should be able to Delete with no opts even if object's ResourceVersion doesn't match server", func(ctx SpecContext) { By("Deleting a deployment") depCopy := dep.DeepCopy() depCopy.ResourceVersion = "bogus" - err := cl.Delete(context.Background(), depCopy) + err := cl.Delete(ctx, depCopy) Expect(err).ToNot(HaveOccurred()) By("Listing all deployments in the namespace") list := &appsv1.DeploymentList{} - err = cl.List(context.Background(), list, client.InNamespace("ns1")) + err = cl.List(ctx, list, client.InNamespace("ns1")) Expect(err).ToNot(HaveOccurred()) Expect(list.Items).To(HaveLen(1)) Expect(list.Items).To(ConsistOf(*dep2)) }) - It("should handle finalizers on Update", func() { + It("should handle finalizers on Update", func(ctx SpecContext) { namespacedName := types.NamespacedName{ Name: "test-cm", Namespace: "delete-with-finalizers", @@ -803,31 +728,31 @@ var _ = Describe("Fake client", func() { "test-key": "new-value", }, } - err := cl.Create(context.Background(), newObj) + err := cl.Create(ctx, newObj) Expect(err).ToNot(HaveOccurred()) By("Deleting the object") - err = cl.Delete(context.Background(), newObj) + err = cl.Delete(ctx, newObj) Expect(err).ToNot(HaveOccurred()) By("Getting the object") obj := &corev1.ConfigMap{} - err = cl.Get(context.Background(), namespacedName, obj) + err = cl.Get(ctx, namespacedName, obj) Expect(err).ToNot(HaveOccurred()) Expect(obj.DeletionTimestamp).NotTo(BeNil()) By("Removing the finalizer") obj.Finalizers = []string{} - err = cl.Update(context.Background(), obj) + err = cl.Update(ctx, obj) Expect(err).ToNot(HaveOccurred()) By("Getting the object") obj = &corev1.ConfigMap{} - err = cl.Get(context.Background(), namespacedName, obj) + err = cl.Get(ctx, namespacedName, obj) Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) - It("should reject changes to deletionTimestamp on Update", func() { + It("should reject changes to deletionTimestamp on Update", func(ctx SpecContext) { namespacedName := types.NamespacedName{ Name: "test-cm", Namespace: "reject-with-deletiontimestamp", @@ -842,52 +767,52 @@ var _ = Describe("Fake client", func() { "test-key": "new-value", }, } - err := cl.Create(context.Background(), newObj) + err := cl.Create(ctx, newObj) Expect(err).ToNot(HaveOccurred()) By("Getting the object") obj := &corev1.ConfigMap{} - err = cl.Get(context.Background(), namespacedName, obj) + err = cl.Get(ctx, namespacedName, obj) Expect(err).ToNot(HaveOccurred()) Expect(obj.DeletionTimestamp).To(BeNil()) By("Adding deletionTimestamp") now := metav1.Now() obj.DeletionTimestamp = &now - err = cl.Update(context.Background(), obj) + err = cl.Update(ctx, obj) Expect(err).To(HaveOccurred()) By("Deleting the object") - err = cl.Delete(context.Background(), newObj) + err = cl.Delete(ctx, newObj) Expect(err).ToNot(HaveOccurred()) By("Changing the deletionTimestamp to new value") obj = &corev1.ConfigMap{} t := metav1.NewTime(time.Now().Add(time.Second)) obj.DeletionTimestamp = &t - err = cl.Update(context.Background(), obj) + err = cl.Update(ctx, obj) Expect(err).To(HaveOccurred()) By("Removing deletionTimestamp") obj.DeletionTimestamp = nil - err = cl.Update(context.Background(), obj) + err = cl.Update(ctx, obj) Expect(err).To(HaveOccurred()) }) - It("should be able to Delete a Collection", func() { + It("should be able to Delete a Collection", func(ctx SpecContext) { By("Deleting a deploymentList") - err := cl.DeleteAllOf(context.Background(), &appsv1.Deployment{}, client.InNamespace("ns1")) + err := cl.DeleteAllOf(ctx, &appsv1.Deployment{}, client.InNamespace("ns1")) Expect(err).ToNot(HaveOccurred()) By("Listing all deployments in the namespace") list := &appsv1.DeploymentList{} - err = cl.List(context.Background(), list, client.InNamespace("ns1")) + err = cl.List(ctx, list, client.InNamespace("ns1")) Expect(err).ToNot(HaveOccurred()) Expect(list.Items).To(BeEmpty()) }) - It("should handle finalizers deleting a collection", func() { + It("should handle finalizers deleting a collection", func(ctx SpecContext) { for i := 0; i < 5; i++ { namespacedName := types.NamespacedName{ Name: fmt.Sprintf("test-cm-%d", i), @@ -904,16 +829,16 @@ var _ = Describe("Fake client", func() { "test-key": "new-value", }, } - err := cl.Create(context.Background(), newObj) + err := cl.Create(ctx, newObj) Expect(err).ToNot(HaveOccurred()) } By("Deleting the object") - err := cl.DeleteAllOf(context.Background(), &corev1.ConfigMap{}, client.InNamespace("delete-collection-with-finalizers")) + err := cl.DeleteAllOf(ctx, &corev1.ConfigMap{}, client.InNamespace("delete-collection-with-finalizers")) Expect(err).ToNot(HaveOccurred()) configmaps := corev1.ConfigMapList{} - err = cl.List(context.Background(), &configmaps, client.InNamespace("delete-collection-with-finalizers")) + err = cl.List(ctx, &configmaps, client.InNamespace("delete-collection-with-finalizers")) Expect(err).ToNot(HaveOccurred()) Expect(configmaps.Items).To(HaveLen(5)) @@ -922,9 +847,9 @@ var _ = Describe("Fake client", func() { } }) - It("should be able to watch", func() { + It("should be able to watch", func(ctx SpecContext) { By("Creating a watch") - objWatch, err := cl.Watch(context.Background(), &corev1.ServiceList{}) + objWatch, err := cl.Watch(ctx, &corev1.ServiceList{}) Expect(err).NotTo(HaveOccurred()) defer objWatch.Stop() @@ -935,7 +860,7 @@ var _ = Describe("Fake client", func() { // in the outer routine, sleep to make sure this is always true time.Sleep(100 * time.Millisecond) - err := cl.Create(context.Background(), &corev1.Service{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "for-watch"}}) + err := cl.Create(ctx, &corev1.Service{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "for-watch"}}) Expect(err).ToNot(HaveOccurred()) }() @@ -949,7 +874,7 @@ var _ = Describe("Fake client", func() { }) Context("with the DryRun option", func() { - It("should not create a new object", func() { + It("should not create a new object", func(ctx SpecContext) { By("Creating a new configmap with DryRun") newcm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -957,7 +882,7 @@ var _ = Describe("Fake client", func() { Namespace: "ns2", }, } - err := cl.Create(context.Background(), newcm, client.DryRunAll) + err := cl.Create(ctx, newcm, client.DryRunAll) Expect(err).ToNot(HaveOccurred()) By("Getting the new configmap") @@ -966,13 +891,13 @@ var _ = Describe("Fake client", func() { Namespace: "ns2", } obj := &corev1.ConfigMap{} - err = cl.Get(context.Background(), namespacedName, obj) + err = cl.Get(ctx, namespacedName, obj) Expect(err).To(HaveOccurred()) Expect(apierrors.IsNotFound(err)).To(BeTrue()) Expect(obj).NotTo(Equal(newcm)) }) - It("should not Update the object", func() { + It("should not Update the object", func(ctx SpecContext) { By("Updating a new configmap with DryRun") newcm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -984,7 +909,7 @@ var _ = Describe("Fake client", func() { "test-key": "new-value", }, } - err := cl.Update(context.Background(), newcm, client.DryRunAll) + err := cl.Update(ctx, newcm, client.DryRunAll) Expect(err).ToNot(HaveOccurred()) By("Getting the new configmap") @@ -993,19 +918,19 @@ var _ = Describe("Fake client", func() { Namespace: "ns2", } obj := &corev1.ConfigMap{} - err = cl.Get(context.Background(), namespacedName, obj) + err = cl.Get(ctx, namespacedName, obj) Expect(err).ToNot(HaveOccurred()) Expect(obj).To(Equal(cm)) Expect(obj.ObjectMeta.ResourceVersion).To(Equal(trackerAddResourceVersion)) }) - It("Should not Delete the object", func() { + It("Should not Delete the object", func(ctx SpecContext) { By("Deleting a configmap with DryRun with Delete()") - err := cl.Delete(context.Background(), cm, client.DryRunAll) + err := cl.Delete(ctx, cm, client.DryRunAll) Expect(err).ToNot(HaveOccurred()) By("Deleting a configmap with DryRun with DeleteAllOf()") - err = cl.DeleteAllOf(context.Background(), cm, client.DryRunAll) + err = cl.DeleteAllOf(ctx, cm, client.DryRunAll) Expect(err).ToNot(HaveOccurred()) By("Getting the configmap") @@ -1014,14 +939,14 @@ var _ = Describe("Fake client", func() { Namespace: "ns2", } obj := &corev1.ConfigMap{} - err = cl.Get(context.Background(), namespacedName, obj) + err = cl.Get(ctx, namespacedName, obj) Expect(err).ToNot(HaveOccurred()) Expect(obj).To(Equal(cm)) Expect(obj.ObjectMeta.ResourceVersion).To(Equal(trackerAddResourceVersion)) }) }) - It("should be able to Patch", func() { + It("should be able to Patch", func(ctx SpecContext) { By("Patching a deployment") mergePatch, err := json.Marshal(map[string]interface{}{ "metadata": map[string]interface{}{ @@ -1031,7 +956,7 @@ var _ = Describe("Fake client", func() { }, }) Expect(err).NotTo(HaveOccurred()) - err = cl.Patch(context.Background(), dep, client.RawPatch(types.StrategicMergePatchType, mergePatch)) + err = cl.Patch(ctx, dep, client.RawPatch(types.StrategicMergePatchType, mergePatch)) Expect(err).NotTo(HaveOccurred()) By("Getting the patched deployment") @@ -1040,13 +965,13 @@ var _ = Describe("Fake client", func() { Namespace: "ns1", } obj := &appsv1.Deployment{} - err = cl.Get(context.Background(), namespacedName, obj) + err = cl.Get(ctx, namespacedName, obj) Expect(err).NotTo(HaveOccurred()) Expect(obj.Annotations["foo"]).To(Equal("bar")) Expect(obj.ObjectMeta.ResourceVersion).To(Equal("1000")) }) - It("should ignore deletionTimestamp without finalizer on Create", func() { + It("should ignore deletionTimestamp without finalizer on Create", func(ctx SpecContext) { namespacedName := types.NamespacedName{ Name: "test-cm", Namespace: "ignore-deletiontimestamp", @@ -1065,18 +990,18 @@ var _ = Describe("Fake client", func() { }, } - err := cl.Create(context.Background(), newObj) + err := cl.Create(ctx, newObj) Expect(err).ToNot(HaveOccurred()) By("Getting the object") obj := &corev1.ConfigMap{} - err = cl.Get(context.Background(), namespacedName, obj) + err = cl.Get(ctx, namespacedName, obj) Expect(err).ToNot(HaveOccurred()) Expect(obj.DeletionTimestamp).To(BeNil()) }) - It("should reject deletionTimestamp without finalizers on Build", func() { + It("should reject deletionTimestamp without finalizers on Build", func(ctx SpecContext) { namespacedName := types.NamespacedName{ Name: "test-cm", Namespace: "reject-deletiontimestamp-no-finalizers", @@ -1113,12 +1038,12 @@ var _ = Describe("Fake client", func() { By("Getting the object") obj = &corev1.ConfigMap{} - err := cl.Get(context.Background(), namespacedName, obj) + err := cl.Get(ctx, namespacedName, obj) Expect(err).ToNot(HaveOccurred()) }) - It("should reject changes to deletionTimestamp on Patch", func() { + It("should reject changes to deletionTimestamp on Patch", func(ctx SpecContext) { namespacedName := types.NamespacedName{ Name: "test-cm", Namespace: "reject-deletiontimestamp", @@ -1135,7 +1060,7 @@ var _ = Describe("Fake client", func() { "test-key": "new-value", }, } - err := cl.Create(context.Background(), newObj) + err := cl.Create(ctx, newObj) Expect(err).ToNot(HaveOccurred()) By("Add a deletionTimestamp") @@ -1147,16 +1072,16 @@ var _ = Describe("Fake client", func() { DeletionTimestamp: &now, }, } - err = cl.Patch(context.Background(), obj, client.MergeFrom(newObj)) + err = cl.Patch(ctx, obj, client.MergeFrom(newObj)) Expect(err).To(HaveOccurred()) By("Deleting the object") - err = cl.Delete(context.Background(), newObj) + err = cl.Delete(ctx, newObj) Expect(err).ToNot(HaveOccurred()) By("Getting the object") obj = &corev1.ConfigMap{} - err = cl.Get(context.Background(), namespacedName, obj) + err = cl.Get(ctx, namespacedName, obj) Expect(err).ToNot(HaveOccurred()) Expect(obj.DeletionTimestamp).NotTo(BeNil()) @@ -1164,7 +1089,7 @@ var _ = Describe("Fake client", func() { newObj = &corev1.ConfigMap{} t := metav1.NewTime(time.Now().Add(time.Second)) newObj.DeletionTimestamp = &t - err = cl.Patch(context.Background(), newObj, client.MergeFrom(obj)) + err = cl.Patch(ctx, newObj, client.MergeFrom(obj)) Expect(err).To(HaveOccurred()) By("Removing deletionTimestamp") @@ -1175,12 +1100,12 @@ var _ = Describe("Fake client", func() { DeletionTimestamp: nil, }, } - err = cl.Patch(context.Background(), newObj, client.MergeFrom(obj)) + err = cl.Patch(ctx, newObj, client.MergeFrom(obj)) Expect(err).To(HaveOccurred()) }) - It("should handle finalizers on Patch", func() { + It("should handle finalizers on Patch", func(ctx SpecContext) { namespacedName := types.NamespacedName{ Name: "test-cm", Namespace: "delete-with-finalizers", @@ -1196,11 +1121,11 @@ var _ = Describe("Fake client", func() { "test-key": "new-value", }, } - err := cl.Create(context.Background(), newObj) + err := cl.Create(ctx, newObj) Expect(err).ToNot(HaveOccurred()) By("Deleting the object") - err = cl.Delete(context.Background(), newObj) + err = cl.Delete(ctx, newObj) Expect(err).ToNot(HaveOccurred()) By("Removing the finalizer") @@ -1211,16 +1136,16 @@ var _ = Describe("Fake client", func() { Finalizers: []string{}, }, } - err = cl.Patch(context.Background(), obj, client.MergeFrom(newObj)) + err = cl.Patch(ctx, obj, client.MergeFrom(newObj)) Expect(err).ToNot(HaveOccurred()) By("Getting the object") obj = &corev1.ConfigMap{} - err = cl.Get(context.Background(), namespacedName, obj) + err = cl.Get(ctx, namespacedName, obj) Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) - It("should remove finalizers of the object on Patch", func() { + It("should remove finalizers of the object on Patch", func(ctx SpecContext) { namespacedName := types.NamespacedName{ Name: "test-cm", Namespace: "patch-finalizers-in-obj", @@ -1236,7 +1161,7 @@ var _ = Describe("Fake client", func() { "test-key": "new-value", }, } - err := cl.Create(context.Background(), obj) + err := cl.Create(ctx, obj) Expect(err).ToNot(HaveOccurred()) By("Removing the finalizer") @@ -1248,7 +1173,7 @@ var _ = Describe("Fake client", func() { }, }) Expect(err).ToNot(HaveOccurred()) - err = cl.Patch(context.Background(), obj, client.RawPatch(types.StrategicMergePatchType, mergePatch)) + err = cl.Patch(ctx, obj, client.RawPatch(types.StrategicMergePatchType, mergePatch)) Expect(err).ToNot(HaveOccurred()) By("Check the finalizer has been removed in the object") @@ -1256,7 +1181,7 @@ var _ = Describe("Fake client", func() { By("Check the finalizer has been removed in client") newObj := &corev1.ConfigMap{} - err = cl.Get(context.Background(), namespacedName, newObj) + err = cl.Get(ctx, namespacedName, newObj) Expect(err).ToNot(HaveOccurred()) Expect(newObj.Finalizers).To(BeEmpty()) }) @@ -1325,106 +1250,106 @@ var _ = Describe("Fake client", func() { }) Context("filtered List using field selector", func() { - It("errors when there's no Index for the GroupVersionResource", func() { + It("errors when there's no Index for the GroupVersionResource", func(ctx SpecContext) { listOpts := &client.ListOptions{ FieldSelector: fields.OneTermEqualSelector("key", "val"), } list := &corev1.ConfigMapList{} - err := cl.List(context.Background(), list, listOpts) + err := cl.List(ctx, list, listOpts) Expect(err).To(HaveOccurred()) Expect(list.Items).To(BeEmpty()) }) - It("errors when there's no Index for the GroupVersionResource with UnstructuredList", func() { + It("errors when there's no Index for the GroupVersionResource with UnstructuredList", func(ctx SpecContext) { listOpts := &client.ListOptions{ FieldSelector: fields.OneTermEqualSelector("key", "val"), } list := &unstructured.UnstructuredList{} list.SetAPIVersion("v1") list.SetKind("ConfigMapList") - err := cl.List(context.Background(), list, listOpts) + err := cl.List(ctx, list, listOpts) Expect(err).To(HaveOccurred()) Expect(list.GroupVersionKind().GroupVersion().String()).To(Equal("v1")) Expect(list.GetKind()).To(Equal("ConfigMapList")) Expect(list.Items).To(BeEmpty()) }) - It("errors when there's no Index matching the field name", func() { + It("errors when there's no Index matching the field name", func(ctx SpecContext) { listOpts := &client.ListOptions{ FieldSelector: fields.OneTermEqualSelector("spec.paused", "false"), } list := &appsv1.DeploymentList{} - err := cl.List(context.Background(), list, listOpts) + err := cl.List(ctx, list, listOpts) Expect(err).To(HaveOccurred()) Expect(list.Items).To(BeEmpty()) }) - It("errors when field selector uses two requirements", func() { + It("errors when field selector uses two requirements", func(ctx SpecContext) { listOpts := &client.ListOptions{ FieldSelector: fields.AndSelectors( fields.OneTermEqualSelector("spec.replicas", "1"), fields.OneTermEqualSelector("spec.strategy.type", string(appsv1.RecreateDeploymentStrategyType)), )} list := &appsv1.DeploymentList{} - err := cl.List(context.Background(), list, listOpts) + err := cl.List(ctx, list, listOpts) Expect(err).To(HaveOccurred()) Expect(list.Items).To(BeEmpty()) }) - It("returns two deployments that match the only field selector requirement", func() { + It("returns two deployments that match the only field selector requirement", func(ctx SpecContext) { listOpts := &client.ListOptions{ FieldSelector: fields.OneTermEqualSelector("spec.replicas", "1"), } list := &appsv1.DeploymentList{} - Expect(cl.List(context.Background(), list, listOpts)).To(Succeed()) + Expect(cl.List(ctx, list, listOpts)).To(Succeed()) Expect(list.Items).To(ConsistOf(*dep, *dep2)) }) - It("returns no object because no object matches the only field selector requirement", func() { + It("returns no object because no object matches the only field selector requirement", func(ctx SpecContext) { listOpts := &client.ListOptions{ FieldSelector: fields.OneTermEqualSelector("spec.replicas", "2"), } list := &appsv1.DeploymentList{} - Expect(cl.List(context.Background(), list, listOpts)).To(Succeed()) + Expect(cl.List(ctx, list, listOpts)).To(Succeed()) Expect(list.Items).To(BeEmpty()) }) - It("returns deployment that matches both the field and label selectors", func() { + It("returns deployment that matches both the field and label selectors", func(ctx SpecContext) { listOpts := &client.ListOptions{ FieldSelector: fields.OneTermEqualSelector("spec.replicas", "1"), LabelSelector: labels.SelectorFromSet(dep2.Labels), } list := &appsv1.DeploymentList{} - Expect(cl.List(context.Background(), list, listOpts)).To(Succeed()) + Expect(cl.List(ctx, list, listOpts)).To(Succeed()) Expect(list.Items).To(ConsistOf(*dep2)) }) - It("returns no object even if field selector matches because label selector doesn't", func() { + It("returns no object even if field selector matches because label selector doesn't", func(ctx SpecContext) { listOpts := &client.ListOptions{ FieldSelector: fields.OneTermEqualSelector("spec.replicas", "1"), LabelSelector: labels.Nothing(), } list := &appsv1.DeploymentList{} - Expect(cl.List(context.Background(), list, listOpts)).To(Succeed()) + Expect(cl.List(ctx, list, listOpts)).To(Succeed()) Expect(list.Items).To(BeEmpty()) }) - It("returns no object even if label selector matches because field selector doesn't", func() { + It("returns no object even if label selector matches because field selector doesn't", func(ctx SpecContext) { listOpts := &client.ListOptions{ FieldSelector: fields.OneTermEqualSelector("spec.replicas", "2"), LabelSelector: labels.Everything(), } list := &appsv1.DeploymentList{} - Expect(cl.List(context.Background(), list, listOpts)).To(Succeed()) + Expect(cl.List(ctx, list, listOpts)).To(Succeed()) Expect(list.Items).To(BeEmpty()) }) - It("supports adding an index at runtime", func() { + It("supports adding an index at runtime", func(ctx SpecContext) { listOpts := &client.ListOptions{ FieldSelector: fields.OneTermEqualSelector("metadata.name", "test-deployment-2"), } list := &appsv1.DeploymentList{} - err := cl.List(context.Background(), list, listOpts) + err := cl.List(ctx, list, listOpts) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("no index with name metadata.name has been registered")) @@ -1433,11 +1358,11 @@ var _ = Describe("Fake client", func() { }) Expect(err).To(Succeed()) - Expect(cl.List(context.Background(), list, listOpts)).To(Succeed()) + Expect(cl.List(ctx, list, listOpts)).To(Succeed()) Expect(list.Items).To(ConsistOf(*dep2)) }) - It("Is not a datarace to add and use indexes in parallel", func() { + It("Is not a datarace to add and use indexes in parallel", func(ctx SpecContext) { wg := sync.WaitGroup{} wg.Add(2) @@ -1447,7 +1372,7 @@ var _ = Describe("Fake client", func() { go func() { defer wg.Done() defer GinkgoRecover() - Expect(cl.List(context.Background(), &appsv1.DeploymentList{}, listOpts)).To(Succeed()) + Expect(cl.List(ctx, &appsv1.DeploymentList{}, listOpts)).To(Succeed()) }() go func() { defer wg.Done() @@ -1472,43 +1397,43 @@ var _ = Describe("Fake client", func() { }) Context("filtered List using field selector", func() { - It("uses the second index to retrieve the indexed objects when there are matches", func() { + It("uses the second index to retrieve the indexed objects when there are matches", func(ctx SpecContext) { listOpts := &client.ListOptions{ FieldSelector: fields.OneTermEqualSelector("spec.strategy.type", string(appsv1.RecreateDeploymentStrategyType)), } list := &appsv1.DeploymentList{} - Expect(cl.List(context.Background(), list, listOpts)).To(Succeed()) + Expect(cl.List(ctx, list, listOpts)).To(Succeed()) Expect(list.Items).To(ConsistOf(*dep)) }) - It("uses the second index to retrieve the indexed objects when there are no matches", func() { + It("uses the second index to retrieve the indexed objects when there are no matches", func(ctx SpecContext) { listOpts := &client.ListOptions{ FieldSelector: fields.OneTermEqualSelector("spec.strategy.type", string(appsv1.RollingUpdateDeploymentStrategyType)), } list := &appsv1.DeploymentList{} - Expect(cl.List(context.Background(), list, listOpts)).To(Succeed()) + Expect(cl.List(ctx, list, listOpts)).To(Succeed()) Expect(list.Items).To(BeEmpty()) }) - It("no error when field selector uses two requirements", func() { + It("no error when field selector uses two requirements", func(ctx SpecContext) { listOpts := &client.ListOptions{ FieldSelector: fields.AndSelectors( fields.OneTermEqualSelector("spec.replicas", "1"), fields.OneTermEqualSelector("spec.strategy.type", string(appsv1.RecreateDeploymentStrategyType)), )} list := &appsv1.DeploymentList{} - Expect(cl.List(context.Background(), list, listOpts)).To(Succeed()) + Expect(cl.List(ctx, list, listOpts)).To(Succeed()) Expect(list.Items).To(ConsistOf(*dep)) }) }) }) }) - It("should set the ResourceVersion to 999 when adding an object to the tracker", func() { + It("should set the ResourceVersion to 999 when adding an object to the tracker", func(ctx SpecContext) { cl := NewClientBuilder().WithObjects(&corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "cm"}}).Build() retrieved := &corev1.Secret{} - Expect(cl.Get(context.Background(), types.NamespacedName{Name: "cm"}, retrieved)).To(Succeed()) + Expect(cl.Get(ctx, types.NamespacedName{Name: "cm"}, retrieved)).To(Succeed()) reference := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -1519,7 +1444,7 @@ var _ = Describe("Fake client", func() { Expect(retrieved).To(Equal(reference)) }) - It("should be able to build with given tracker and get resource", func() { + It("should be able to build with given tracker and get resource", func(ctx SpecContext) { clientSet := fake.NewSimpleClientset(dep) cl := NewClientBuilder().WithRuntimeObjects(dep2).WithObjectTracker(clientSet.Tracker()).Build() @@ -1529,12 +1454,12 @@ var _ = Describe("Fake client", func() { Namespace: "ns1", } obj := &appsv1.Deployment{} - err := cl.Get(context.Background(), namespacedName, obj) + err := cl.Get(ctx, namespacedName, obj) Expect(err).ToNot(HaveOccurred()) - Expect(obj).To(Equal(dep)) + Expect(obj).To(BeComparableTo(dep)) By("Getting a deployment from clientSet") - csDep2, err := clientSet.AppsV1().Deployments("ns1").Get(context.Background(), "test-deployment-2", metav1.GetOptions{}) + csDep2, err := clientSet.AppsV1().Deployments("ns1").Get(ctx, "test-deployment-2", metav1.GetOptions{}) Expect(err).ToNot(HaveOccurred()) Expect(csDep2).To(Equal(dep2)) @@ -1545,10 +1470,6 @@ var _ = Describe("Fake client", func() { } dep3 := &appsv1.Deployment{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "apps/v1", - Kind: "Deployment", - }, ObjectMeta: metav1.ObjectMeta{ Name: "test-deployment-3", Namespace: "ns1", @@ -1559,16 +1480,16 @@ var _ = Describe("Fake client", func() { }, } - _, err = clientSet.AppsV1().Deployments("ns1").Create(context.Background(), dep3, metav1.CreateOptions{}) + _, err = clientSet.AppsV1().Deployments("ns1").Create(ctx, dep3, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) obj = &appsv1.Deployment{} - err = cl.Get(context.Background(), namespacedName3, obj) + err = cl.Get(ctx, namespacedName3, obj) Expect(err).ToNot(HaveOccurred()) - Expect(obj).To(Equal(dep3)) + Expect(obj).To(BeComparableTo(dep3)) }) - It("should not change the status of typed objects that have a status subresource on update", func() { + It("should not change the status of typed objects that have a status subresource on update", func(ctx SpecContext) { obj := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "pod", @@ -1577,14 +1498,14 @@ var _ = Describe("Fake client", func() { cl := NewClientBuilder().WithStatusSubresource(obj).WithObjects(obj).Build() obj.Status.Phase = "Running" - Expect(cl.Update(context.Background(), obj)).To(Succeed()) + Expect(cl.Update(ctx, obj)).To(Succeed()) - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), obj)).To(Succeed()) + Expect(cl.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed()) Expect(obj.Status).To(BeEquivalentTo(corev1.PodStatus{})) }) - It("should return a conflict error when an incorrect RV is used on status update", func() { + It("should return a conflict error when an incorrect RV is used on status update", func(ctx SpecContext) { obj := &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node", @@ -1595,11 +1516,11 @@ var _ = Describe("Fake client", func() { obj.Status.Phase = corev1.NodeRunning obj.ResourceVersion = "invalid" - err := cl.Status().Update(context.Background(), obj) + err := cl.Status().Update(ctx, obj) Expect(apierrors.IsConflict(err)).To(BeTrue()) }) - It("should not change non-status field of typed objects that have a status subresource on status update", func() { + It("should not change non-status field of typed objects that have a status subresource on status update", func(ctx SpecContext) { obj := &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node", @@ -1625,10 +1546,10 @@ var _ = Describe("Fake client", func() { } obj.Status.NodeInfo.MachineID = machineIDFromStatusUpdate - Expect(cl.Status().Update(context.Background(), obj)).NotTo(HaveOccurred()) + Expect(cl.Status().Update(ctx, obj)).NotTo(HaveOccurred()) actual := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: obj.Name}} - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(actual), actual)).NotTo(HaveOccurred()) + Expect(cl.Get(ctx, client.ObjectKeyFromObject(actual), actual)).NotTo(HaveOccurred()) objOriginal.APIVersion = actual.APIVersion objOriginal.Kind = actual.Kind @@ -1637,7 +1558,7 @@ var _ = Describe("Fake client", func() { Expect(cmp.Diff(objOriginal, actual)).To(BeEmpty()) }) - It("should be able to update an object after updating an object's status", func() { + It("should be able to update an object after updating an object's status", func(ctx SpecContext) { obj := &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node", @@ -1655,7 +1576,7 @@ var _ = Describe("Fake client", func() { expectedObj := obj.DeepCopy() obj.Status.NodeInfo.MachineID = machineIDFromStatusUpdate - Expect(cl.Status().Update(context.Background(), obj)).NotTo(HaveOccurred()) + Expect(cl.Status().Update(ctx, obj)).NotTo(HaveOccurred()) obj.Annotations = map[string]string{ "some-annotation-key": "some", @@ -1663,10 +1584,10 @@ var _ = Describe("Fake client", func() { expectedObj.Annotations = map[string]string{ "some-annotation-key": "some", } - Expect(cl.Update(context.Background(), obj)).NotTo(HaveOccurred()) + Expect(cl.Update(ctx, obj)).NotTo(HaveOccurred()) actual := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: obj.Name}} - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(actual), actual)).NotTo(HaveOccurred()) + Expect(cl.Get(ctx, client.ObjectKeyFromObject(actual), actual)).NotTo(HaveOccurred()) expectedObj.APIVersion = actual.APIVersion expectedObj.Kind = actual.Kind @@ -1675,7 +1596,7 @@ var _ = Describe("Fake client", func() { Expect(cmp.Diff(expectedObj, actual)).To(BeEmpty()) }) - It("should be able to update an object's status after updating an object", func() { + It("should be able to update an object's status after updating an object", func(ctx SpecContext) { obj := &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node", @@ -1698,14 +1619,14 @@ var _ = Describe("Fake client", func() { expectedObj.Annotations = map[string]string{ "some-annotation-key": "some", } - Expect(cl.Update(context.Background(), obj)).NotTo(HaveOccurred()) + Expect(cl.Update(ctx, obj)).NotTo(HaveOccurred()) obj.Spec.PodCIDR = cidrFromStatusUpdate obj.Status.NodeInfo.MachineID = machineIDFromStatusUpdate - Expect(cl.Status().Update(context.Background(), obj)).NotTo(HaveOccurred()) + Expect(cl.Status().Update(ctx, obj)).NotTo(HaveOccurred()) actual := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: obj.Name}} - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(actual), actual)).NotTo(HaveOccurred()) + Expect(cl.Get(ctx, client.ObjectKeyFromObject(actual), actual)).NotTo(HaveOccurred()) expectedObj.APIVersion = actual.APIVersion expectedObj.Kind = actual.Kind @@ -1714,7 +1635,7 @@ var _ = Describe("Fake client", func() { Expect(cmp.Diff(expectedObj, actual)).To(BeEmpty()) }) - It("Should only override status fields of typed objects that have a status subresource on status update", func() { + It("Should only override status fields of typed objects that have a status subresource on status update", func(ctx SpecContext) { obj := &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node", @@ -1732,10 +1653,10 @@ var _ = Describe("Fake client", func() { objOriginal := obj.DeepCopy() obj.Status.Phase = corev1.NodeRunning - Expect(cl.Status().Update(context.Background(), obj)).NotTo(HaveOccurred()) + Expect(cl.Status().Update(ctx, obj)).NotTo(HaveOccurred()) actual := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: obj.Name}} - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(actual), actual)).NotTo(HaveOccurred()) + Expect(cl.Get(ctx, client.ObjectKeyFromObject(actual), actual)).NotTo(HaveOccurred()) objOriginal.APIVersion = actual.APIVersion objOriginal.Kind = actual.Kind @@ -1745,7 +1666,7 @@ var _ = Describe("Fake client", func() { Expect(objOriginal.Status.Phase).ToNot(Equal(actual.Status.Phase)) }) - It("should be able to change typed objects that have a scale subresource on patch", func() { + It("should be able to change typed objects that have a scale subresource on patch", func(ctx SpecContext) { obj := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "deploy", @@ -1755,10 +1676,10 @@ var _ = Describe("Fake client", func() { objOriginal := obj.DeepCopy() patch := []byte(fmt.Sprintf(`{"spec":{"replicas":%d}}`, 2)) - Expect(cl.SubResource("scale").Patch(context.Background(), obj, client.RawPatch(types.MergePatchType, patch))).NotTo(HaveOccurred()) + Expect(cl.SubResource("scale").Patch(ctx, obj, client.RawPatch(types.MergePatchType, patch))).NotTo(HaveOccurred()) actual := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: obj.Name}} - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(actual), actual)).To(Succeed()) + Expect(cl.Get(ctx, client.ObjectKeyFromObject(actual), actual)).To(Succeed()) objOriginal.APIVersion = actual.APIVersion objOriginal.Kind = actual.Kind @@ -1767,24 +1688,24 @@ var _ = Describe("Fake client", func() { Expect(cmp.Diff(objOriginal, actual)).To(BeEmpty()) }) - It("should not change the status of typed objects that have a status subresource on patch", func() { + It("should not change the status of typed objects that have a status subresource on patch", func(ctx SpecContext) { obj := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "node", }, } - Expect(cl.Create(context.Background(), obj)).To(Succeed()) + Expect(cl.Create(ctx, obj)).To(Succeed()) original := obj.DeepCopy() obj.Status.Phase = "Running" - Expect(cl.Patch(context.Background(), obj, client.MergeFrom(original))).To(Succeed()) + Expect(cl.Patch(ctx, obj, client.MergeFrom(original))).To(Succeed()) - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), obj)).To(Succeed()) + Expect(cl.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed()) Expect(obj.Status).To(BeEquivalentTo(corev1.PodStatus{})) }) - It("should not change non-status field of typed objects that have a status subresource on status patch", func() { + It("should not change non-status field of typed objects that have a status subresource on status patch", func(ctx SpecContext) { obj := &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node", @@ -1798,10 +1719,10 @@ var _ = Describe("Fake client", func() { obj.Spec.PodCIDR = cidrFromStatusUpdate obj.Status.NodeInfo.MachineID = "machine-id" - Expect(cl.Status().Patch(context.Background(), obj, client.MergeFrom(objOriginal))).NotTo(HaveOccurred()) + Expect(cl.Status().Patch(ctx, obj, client.MergeFrom(objOriginal))).NotTo(HaveOccurred()) actual := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: obj.Name}} - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(actual), actual)).NotTo(HaveOccurred()) + Expect(cl.Get(ctx, client.ObjectKeyFromObject(actual), actual)).NotTo(HaveOccurred()) objOriginal.APIVersion = actual.APIVersion objOriginal.Kind = actual.Kind @@ -1810,7 +1731,7 @@ var _ = Describe("Fake client", func() { Expect(cmp.Diff(objOriginal, actual)).To(BeEmpty()) }) - It("should Unmarshal the schemaless object with int64 to preserve ints", func() { + It("should Unmarshal the schemaless object with int64 to preserve ints", func(ctx SpecContext) { schemeBuilder := &scheme.Builder{GroupVersion: schema.GroupVersion{Group: "test", Version: "v1"}} schemeBuilder.Register(&WithSchemalessSpec{}) @@ -1829,13 +1750,13 @@ var _ = Describe("Fake client", func() { } cl := NewClientBuilder().WithScheme(scheme).WithStatusSubresource(obj).WithObjects(obj).Build() - Expect(cl.Update(context.Background(), obj)).To(Succeed()) + Expect(cl.Update(ctx, obj)).To(Succeed()) Expect(obj.Spec).To(BeEquivalentTo(spec)) - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), obj)).To(Succeed()) + Expect(cl.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed()) Expect(obj.Spec).To(BeEquivalentTo(spec)) }) - It("should Unmarshal the schemaless object with float64 to preserve ints", func() { + It("should Unmarshal the schemaless object with float64 to preserve ints", func(ctx SpecContext) { schemeBuilder := &scheme.Builder{GroupVersion: schema.GroupVersion{Group: "test", Version: "v1"}} schemeBuilder.Register(&WithSchemalessSpec{}) @@ -1854,13 +1775,13 @@ var _ = Describe("Fake client", func() { } cl := NewClientBuilder().WithScheme(scheme).WithStatusSubresource(obj).WithObjects(obj).Build() - Expect(cl.Update(context.Background(), obj)).To(Succeed()) + Expect(cl.Update(ctx, obj)).To(Succeed()) Expect(obj.Spec).To(BeEquivalentTo(spec)) - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), obj)).To(Succeed()) + Expect(cl.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed()) Expect(obj.Spec).To(BeEquivalentTo(spec)) }) - It("should not change the status of unstructured objects that are configured to have a status subresource on update", func() { + It("should not change the status of unstructured objects that are configured to have a status subresource on update", func(ctx SpecContext) { obj := &unstructured.Unstructured{} obj.SetAPIVersion("foo/v1") obj.SetKind("Foo") @@ -1874,14 +1795,14 @@ var _ = Describe("Fake client", func() { err = unstructured.SetNestedField(obj.Object, map[string]any{"state": "new"}, "status") Expect(err).ToNot(HaveOccurred()) - Expect(cl.Update(context.Background(), obj)).To(Succeed()) + Expect(cl.Update(ctx, obj)).To(Succeed()) - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), obj)).To(Succeed()) + Expect(cl.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed()) Expect(obj.Object["status"]).To(BeEquivalentTo(map[string]any{"state": "old"})) }) - It("should not change non-status fields of unstructured objects that are configured to have a status subresource on status update", func() { + It("should not change non-status fields of unstructured objects that are configured to have a status subresource on status update", func(ctx SpecContext) { obj := &unstructured.Unstructured{} obj.SetAPIVersion("foo/v1") obj.SetKind("Foo") @@ -1897,14 +1818,14 @@ var _ = Describe("Fake client", func() { err = unstructured.SetNestedField(obj.Object, map[string]any{"state": "new"}, "status") Expect(err).ToNot(HaveOccurred()) - Expect(cl.Status().Update(context.Background(), obj)).To(Succeed()) - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), obj)).To(Succeed()) + Expect(cl.Status().Update(ctx, obj)).To(Succeed()) + Expect(cl.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed()) Expect(obj.Object["status"]).To(BeEquivalentTo(map[string]any{"state": "new"})) Expect(obj.Object["spec"]).To(BeEquivalentTo("original")) }) - It("should not change the status of known unstructured objects that have a status subresource on update", func() { + It("should not change the status of known unstructured objects that have a status subresource on update", func(ctx SpecContext) { obj := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "pod", @@ -1923,7 +1844,7 @@ var _ = Describe("Fake client", func() { u.SetAPIVersion("v1") u.SetKind("Pod") u.SetName(obj.Name) - err := cl.Get(context.Background(), client.ObjectKeyFromObject(u), u) + err := cl.Get(ctx, client.ObjectKeyFromObject(u), u) Expect(err).NotTo(HaveOccurred()) err = unstructured.SetNestedField(u.Object, string(corev1.RestartPolicyNever), "spec", "restartPolicy") @@ -1931,19 +1852,17 @@ var _ = Describe("Fake client", func() { err = unstructured.SetNestedField(u.Object, string(corev1.PodRunning), "status", "phase") Expect(err).NotTo(HaveOccurred()) - Expect(cl.Update(context.Background(), u)).To(Succeed()) + Expect(cl.Update(ctx, u)).To(Succeed()) actual := &corev1.Pod{} - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), actual)).To(Succeed()) - obj.APIVersion = u.GetAPIVersion() - obj.Kind = u.GetKind() + Expect(cl.Get(ctx, client.ObjectKeyFromObject(obj), actual)).To(Succeed()) obj.ResourceVersion = actual.ResourceVersion // only the spec mutation should persist obj.Spec.RestartPolicy = corev1.RestartPolicyNever Expect(cmp.Diff(obj, actual)).To(BeEmpty()) }) - It("should not change non-status field of known unstructured objects that have a status subresource on status update", func() { + It("should not change non-status field of known unstructured objects that have a status subresource on status update", func(ctx SpecContext) { obj := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "pod", @@ -1962,7 +1881,7 @@ var _ = Describe("Fake client", func() { u.SetAPIVersion("v1") u.SetKind("Pod") u.SetName(obj.Name) - err := cl.Get(context.Background(), client.ObjectKeyFromObject(u), u) + err := cl.Get(ctx, client.ObjectKeyFromObject(u), u) Expect(err).NotTo(HaveOccurred()) err = unstructured.SetNestedField(u.Object, string(corev1.RestartPolicyNever), "spec", "restartPolicy") @@ -1970,37 +1889,37 @@ var _ = Describe("Fake client", func() { err = unstructured.SetNestedField(u.Object, string(corev1.PodRunning), "status", "phase") Expect(err).NotTo(HaveOccurred()) - Expect(cl.Status().Update(context.Background(), u)).To(Succeed()) + Expect(cl.Status().Update(ctx, u)).To(Succeed()) actual := &corev1.Pod{} - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), actual)).To(Succeed()) + Expect(cl.Get(ctx, client.ObjectKeyFromObject(obj), actual)).To(Succeed()) obj.ResourceVersion = actual.ResourceVersion // only the status mutation should persist obj.Status.Phase = corev1.PodRunning Expect(cmp.Diff(obj, actual)).To(BeEmpty()) }) - It("should not change the status of unstructured objects that are configured to have a status subresource on patch", func() { + It("should not change the status of unstructured objects that are configured to have a status subresource on patch", func(ctx SpecContext) { obj := &unstructured.Unstructured{} obj.SetAPIVersion("foo/v1") obj.SetKind("Foo") obj.SetName("a-foo") cl := NewClientBuilder().WithStatusSubresource(obj).Build() - Expect(cl.Create(context.Background(), obj)).To(Succeed()) + Expect(cl.Create(ctx, obj)).To(Succeed()) original := obj.DeepCopy() err := unstructured.SetNestedField(obj.Object, map[string]interface{}{"count": int64(2)}, "status") Expect(err).ToNot(HaveOccurred()) - Expect(cl.Patch(context.Background(), obj, client.MergeFrom(original))).To(Succeed()) + Expect(cl.Patch(ctx, obj, client.MergeFrom(original))).To(Succeed()) - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), obj)).To(Succeed()) + Expect(cl.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed()) Expect(obj.Object["status"]).To(BeNil()) }) - It("should not change non-status fields of unstructured objects that are configured to have a status subresource on status patch", func() { + It("should not change non-status fields of unstructured objects that are configured to have a status subresource on status patch", func(ctx SpecContext) { obj := &unstructured.Unstructured{} obj.SetAPIVersion("foo/v1") obj.SetKind("Foo") @@ -2017,14 +1936,14 @@ var _ = Describe("Fake client", func() { err = unstructured.SetNestedField(obj.Object, map[string]any{"state": "new"}, "status") Expect(err).ToNot(HaveOccurred()) - Expect(cl.Status().Patch(context.Background(), obj, client.MergeFrom(original))).To(Succeed()) - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(obj), obj)).To(Succeed()) + Expect(cl.Status().Patch(ctx, obj, client.MergeFrom(original))).To(Succeed()) + Expect(cl.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed()) Expect(obj.Object["status"]).To(BeEquivalentTo(map[string]any{"state": "new"})) Expect(obj.Object["spec"]).To(BeEquivalentTo("original")) }) - It("should return not found on status update of resources that don't have a status subresource", func() { + It("should return not found on status update of resources that don't have a status subresource", func(ctx SpecContext) { obj := &unstructured.Unstructured{} obj.SetAPIVersion("foo/v1") obj.SetKind("Foo") @@ -2032,7 +1951,7 @@ var _ = Describe("Fake client", func() { cl := NewClientBuilder().WithObjects(obj).Build() - err := cl.Status().Update(context.Background(), obj) + err := cl.Status().Update(ctx, obj) Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) @@ -2041,223 +1960,108 @@ var _ = Describe("Fake client", func() { &policyv1.Eviction{}, } for _, tp := range evictionTypes { - It("should delete a pod through the eviction subresource", func() { + It("should delete a pod through the eviction subresource", func(ctx SpecContext) { pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} cl := NewClientBuilder().WithObjects(pod).Build() - err := cl.SubResource("eviction").Create(context.Background(), pod, tp) + err := cl.SubResource("eviction").Create(ctx, pod, tp) Expect(err).NotTo(HaveOccurred()) - err = cl.Get(context.Background(), client.ObjectKeyFromObject(pod), pod) + err = cl.Get(ctx, client.ObjectKeyFromObject(pod), pod) Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) - It("should return not found when attempting to evict a pod that doesn't exist", func() { + It("should return not found when attempting to evict a pod that doesn't exist", func(ctx SpecContext) { cl := NewClientBuilder().Build() pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} - err := cl.SubResource("eviction").Create(context.Background(), pod, tp) + err := cl.SubResource("eviction").Create(ctx, pod, tp) Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) - It("should return not found when attempting to evict something other than a pod", func() { + It("should return not found when attempting to evict something other than a pod", func(ctx SpecContext) { ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} cl := NewClientBuilder().WithObjects(ns).Build() - err := cl.SubResource("eviction").Create(context.Background(), ns, tp) + err := cl.SubResource("eviction").Create(ctx, ns, tp) Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) - It("should return an error when using the wrong subresource", func() { + It("should return an error when using the wrong subresource", func(ctx SpecContext) { cl := NewClientBuilder().Build() - err := cl.SubResource("eviction-subresource").Create(context.Background(), &corev1.Namespace{}, tp) + err := cl.SubResource("eviction-subresource").Create(ctx, &corev1.Namespace{}, tp) Expect(err).To(HaveOccurred()) }) } - It("should error when creating an eviction with the wrong type", func() { + It("should error when creating an eviction with the wrong type", func(ctx SpecContext) { cl := NewClientBuilder().Build() - err := cl.SubResource("eviction").Create(context.Background(), &corev1.Pod{}, &corev1.Namespace{}) + err := cl.SubResource("eviction").Create(ctx, &corev1.Pod{}, &corev1.Namespace{}) Expect(apierrors.IsBadRequest(err)).To(BeTrue()) }) - It("should create a ServiceAccount token through the token subresource", func() { + It("should create a ServiceAccount token through the token subresource", func(ctx SpecContext) { sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} cl := NewClientBuilder().WithObjects(sa).Build() tokenRequest := &authenticationv1.TokenRequest{} - err := cl.SubResource("token").Create(context.Background(), sa, tokenRequest) + err := cl.SubResource("token").Create(ctx, sa, tokenRequest) Expect(err).NotTo(HaveOccurred()) Expect(tokenRequest.Status.Token).NotTo(Equal("")) Expect(tokenRequest.Status.ExpirationTimestamp).NotTo(Equal(metav1.Time{})) }) - It("should return not found when creating a token for a ServiceAccount that doesn't exist", func() { + It("should return not found when creating a token for a ServiceAccount that doesn't exist", func(ctx SpecContext) { sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} cl := NewClientBuilder().Build() - err := cl.SubResource("token").Create(context.Background(), sa, &authenticationv1.TokenRequest{}) + err := cl.SubResource("token").Create(ctx, sa, &authenticationv1.TokenRequest{}) Expect(err).To(HaveOccurred()) Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) - It("should error when creating a token with the wrong subresource type", func() { + It("should error when creating a token with the wrong subresource type", func(ctx SpecContext) { cl := NewClientBuilder().Build() - err := cl.SubResource("token").Create(context.Background(), &corev1.ServiceAccount{}, &corev1.Namespace{}) + err := cl.SubResource("token").Create(ctx, &corev1.ServiceAccount{}, &corev1.Namespace{}) Expect(err).To(HaveOccurred()) Expect(apierrors.IsBadRequest(err)).To(BeTrue()) }) - It("should error when creating a token with the wrong type", func() { + It("should error when creating a token with the wrong type", func(ctx SpecContext) { cl := NewClientBuilder().Build() - err := cl.SubResource("token").Create(context.Background(), &corev1.Secret{}, &authenticationv1.TokenRequest{}) + err := cl.SubResource("token").Create(ctx, &corev1.Secret{}, &authenticationv1.TokenRequest{}) Expect(err).To(HaveOccurred()) Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) - It("should leave typemeta empty on typed get", func() { + It("should leave typemeta empty on typed get", func(ctx SpecContext) { cl := NewClientBuilder().WithObjects(&corev1.Pod{ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "foo", }}).Build() var pod corev1.Pod - Expect(cl.Get(context.Background(), client.ObjectKey{Namespace: "default", Name: "foo"}, &pod)).NotTo(HaveOccurred()) + Expect(cl.Get(ctx, client.ObjectKey{Namespace: "default", Name: "foo"}, &pod)).NotTo(HaveOccurred()) Expect(pod.TypeMeta).To(Equal(metav1.TypeMeta{})) }) - It("should leave typemeta empty on typed list", func() { + It("should leave typemeta empty on typed list", func(ctx SpecContext) { cl := NewClientBuilder().WithObjects(&corev1.Pod{ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "foo", }}).Build() var podList corev1.PodList - Expect(cl.List(context.Background(), &podList)).NotTo(HaveOccurred()) + Expect(cl.List(ctx, &podList)).NotTo(HaveOccurred()) Expect(podList.ListMeta).To(Equal(metav1.ListMeta{})) Expect(podList.Items[0].TypeMeta).To(Equal(metav1.TypeMeta{})) }) - It("should be able to Get an object that has pointer fields for metadata", func() { - schemeBuilder := &scheme.Builder{GroupVersion: schema.GroupVersion{Group: "test", Version: "v1"}} - schemeBuilder.Register(&WithPointerMeta{}, &WithPointerMetaList{}) - scheme := runtime.NewScheme() - Expect(schemeBuilder.AddToScheme(scheme)).NotTo(HaveOccurred()) - - cl := NewClientBuilder(). - WithScheme(scheme). - WithObjects(&WithPointerMeta{ObjectMeta: &metav1.ObjectMeta{ - Name: "foo", - }}). - Build() - - var object WithPointerMeta - Expect(cl.Get(context.Background(), client.ObjectKey{Name: "foo"}, &object)).NotTo(HaveOccurred()) - }) - - It("should be able to List an object type that has pointer fields for metadata", func() { - schemeBuilder := &scheme.Builder{GroupVersion: schema.GroupVersion{Group: "test", Version: "v1"}} - schemeBuilder.Register(&WithPointerMeta{}, &WithPointerMetaList{}) - scheme := runtime.NewScheme() - Expect(schemeBuilder.AddToScheme(scheme)).NotTo(HaveOccurred()) - - cl := NewClientBuilder(). - WithScheme(scheme). - WithObjects(&WithPointerMeta{ObjectMeta: &metav1.ObjectMeta{ - Name: "foo", - }}). - Build() - - var objectList WithPointerMetaList - Expect(cl.List(context.Background(), &objectList)).NotTo(HaveOccurred()) - Expect(objectList.Items).To(HaveLen(1)) - }) - - It("should be able to List an object type that has pointer fields for metadata with no results", func() { - schemeBuilder := &scheme.Builder{GroupVersion: schema.GroupVersion{Group: "test", Version: "v1"}} - schemeBuilder.Register(&WithPointerMeta{}, &WithPointerMetaList{}) - scheme := runtime.NewScheme() - Expect(schemeBuilder.AddToScheme(scheme)).NotTo(HaveOccurred()) - - cl := NewClientBuilder(). - WithScheme(scheme). - Build() - - var objectList WithPointerMetaList - Expect(cl.List(context.Background(), &objectList)).NotTo(HaveOccurred()) - Expect(objectList.Items).To(BeEmpty()) - }) - - It("should be able to Patch an object type that has pointer fields for metadata", func() { - schemeBuilder := &scheme.Builder{GroupVersion: schema.GroupVersion{Group: "test", Version: "v1"}} - schemeBuilder.Register(&WithPointerMeta{}, &WithPointerMetaList{}) - scheme := runtime.NewScheme() - Expect(schemeBuilder.AddToScheme(scheme)).NotTo(HaveOccurred()) - - obj := &WithPointerMeta{ObjectMeta: &metav1.ObjectMeta{ - Name: "foo", - }} - cl := NewClientBuilder(). - WithScheme(scheme). - WithObjects(obj). - Build() - - original := obj.DeepCopy() - obj.Labels = map[string]string{"foo": "bar"} - Expect(cl.Patch(context.Background(), obj, client.MergeFrom(original))).NotTo(HaveOccurred()) - - Expect(cl.Get(context.Background(), client.ObjectKey{Name: "foo"}, obj)).NotTo(HaveOccurred()) - Expect(obj.Labels).To(Equal(map[string]string{"foo": "bar"})) - }) - - It("should be able to Update an object type that has pointer fields for metadata", func() { - schemeBuilder := &scheme.Builder{GroupVersion: schema.GroupVersion{Group: "test", Version: "v1"}} - schemeBuilder.Register(&WithPointerMeta{}, &WithPointerMetaList{}) - scheme := runtime.NewScheme() - Expect(schemeBuilder.AddToScheme(scheme)).NotTo(HaveOccurred()) - - obj := &WithPointerMeta{ObjectMeta: &metav1.ObjectMeta{ - Name: "foo", - }} - cl := NewClientBuilder(). - WithScheme(scheme). - WithObjects(obj). - Build() - - Expect(cl.Get(context.Background(), client.ObjectKey{Name: "foo"}, obj)).NotTo(HaveOccurred()) - - obj.Labels = map[string]string{"foo": "bar"} - Expect(cl.Update(context.Background(), obj)).NotTo(HaveOccurred()) - - Expect(cl.Get(context.Background(), client.ObjectKey{Name: "foo"}, obj)).NotTo(HaveOccurred()) - Expect(obj.Labels).To(Equal(map[string]string{"foo": "bar"})) - }) - - It("should be able to Delete an object type that has pointer fields for metadata", func() { - schemeBuilder := &scheme.Builder{GroupVersion: schema.GroupVersion{Group: "test", Version: "v1"}} - schemeBuilder.Register(&WithPointerMeta{}, &WithPointerMetaList{}) - scheme := runtime.NewScheme() - Expect(schemeBuilder.AddToScheme(scheme)).NotTo(HaveOccurred()) - - obj := &WithPointerMeta{ObjectMeta: &metav1.ObjectMeta{ - Name: "foo", - }} - cl := NewClientBuilder(). - WithScheme(scheme). - WithObjects(obj). - Build() - - Expect(cl.Delete(context.Background(), obj)).NotTo(HaveOccurred()) - - err := cl.Get(context.Background(), client.ObjectKey{Name: "foo"}, obj) - Expect(apierrors.IsNotFound(err)).To(BeTrue()) - }) - - It("should allow concurrent patches to a configMap", func() { + It("should allow concurrent patches to a configMap", func(ctx SpecContext) { scheme := runtime.NewScheme() Expect(corev1.AddToScheme(scheme)).To(Succeed()) @@ -2280,18 +2084,18 @@ var _ = Describe("Fake client", func() { newObj := obj.DeepCopy() newObj.Data = map[string]string{"foo": strconv.Itoa(i)} - Expect(cl.Patch(context.Background(), newObj, client.MergeFrom(obj))).To(Succeed()) + Expect(cl.Patch(ctx, newObj, client.MergeFrom(obj))).To(Succeed()) }() } wg.Wait() // While the order is not deterministic, there must be $tries distinct updates // that each increment the resource version by one - Expect(cl.Get(context.Background(), client.ObjectKey{Name: "foo"}, obj)).To(Succeed()) + Expect(cl.Get(ctx, client.ObjectKey{Name: "foo"}, obj)).To(Succeed()) Expect(obj.ResourceVersion).To(Equal(strconv.Itoa(tries))) }) - It("should not allow concurrent patches to a configMap if the patch contains a ResourceVersion", func() { + It("should not allow concurrent patches to a configMap if the patch contains a ResourceVersion", func(ctx SpecContext) { scheme := runtime.NewScheme() Expect(corev1.AddToScheme(scheme)).To(Succeed()) @@ -2313,13 +2117,13 @@ var _ = Describe("Fake client", func() { newObj := obj.DeepCopy() newObj.ResourceVersion = "1" // include an invalid RV to cause a conflict newObj.Data = map[string]string{"foo": strconv.Itoa(i)} - Expect(apierrors.IsConflict(cl.Patch(context.Background(), newObj, client.MergeFrom(obj)))).To(BeTrue()) + Expect(apierrors.IsConflict(cl.Patch(ctx, newObj, client.MergeFrom(obj)))).To(BeTrue()) }() } wg.Wait() }) - It("should allow concurrent updates to an object that allows unconditionalUpdate if the incoming request has no RV", func() { + It("should allow concurrent updates to an object that allows unconditionalUpdate if the incoming request has no RV", func(ctx SpecContext) { scheme := runtime.NewScheme() Expect(corev1.AddToScheme(scheme)).To(Succeed()) @@ -2343,18 +2147,18 @@ var _ = Describe("Fake client", func() { newObj := obj.DeepCopy() newObj.Data = map[string]string{"foo": strconv.Itoa(i)} newObj.ResourceVersion = "" - Expect(cl.Update(context.Background(), newObj)).To(Succeed()) + Expect(cl.Update(ctx, newObj)).To(Succeed()) }() } wg.Wait() // While the order is not deterministic, there must be $tries distinct updates // that each increment the resource version by one - Expect(cl.Get(context.Background(), client.ObjectKey{Name: "foo"}, obj)).To(Succeed()) + Expect(cl.Get(ctx, client.ObjectKey{Name: "foo"}, obj)).To(Succeed()) Expect(obj.ResourceVersion).To(Equal(strconv.Itoa(tries))) }) - It("If a create races with an update for an object that allows createOnUpdate, the update should always succeed", func() { + It("If a create races with an update for an object that allows createOnUpdate, the update should always succeed", func(ctx SpecContext) { scheme := runtime.NewScheme() Expect(corev1.AddToScheme(scheme)).To(Succeed()) @@ -2376,7 +2180,7 @@ var _ = Describe("Fake client", func() { // this may or may not succeed depending on if we win the race. Either is acceptable, // but if it fails, it must fail due to an AlreadyExists. - err := cl.Create(context.Background(), obj.DeepCopy()) + err := cl.Create(ctx, obj.DeepCopy()) if err != nil { Expect(apierrors.IsAlreadyExists(err)).To(BeTrue()) } @@ -2387,14 +2191,14 @@ var _ = Describe("Fake client", func() { defer GinkgoRecover() // This must always succeed, regardless of the outcome of the create. - Expect(cl.Update(context.Background(), obj.DeepCopy())).To(Succeed()) + Expect(cl.Update(ctx, obj.DeepCopy())).To(Succeed()) }() } wg.Wait() }) - It("If a delete races with an update for an object that allows createOnUpdate, the update should always succeed", func() { + It("If a delete races with an update for an object that allows createOnUpdate, the update should always succeed", func(ctx SpecContext) { scheme := runtime.NewScheme() Expect(corev1.AddToScheme(scheme)).To(Succeed()) @@ -2410,13 +2214,13 @@ var _ = Describe("Fake client", func() { Name: strconv.Itoa(i), }, } - Expect(cl.Create(context.Background(), obj.DeepCopy())).To(Succeed()) + Expect(cl.Create(ctx, obj.DeepCopy())).To(Succeed()) go func() { defer wg.Done() defer GinkgoRecover() - Expect(cl.Delete(context.Background(), obj.DeepCopy())).To(Succeed()) + Expect(cl.Delete(ctx, obj.DeepCopy())).To(Succeed()) }() go func() { @@ -2425,14 +2229,14 @@ var _ = Describe("Fake client", func() { // This must always succeed, regardless of if the delete came before or // after us. - Expect(cl.Update(context.Background(), obj.DeepCopy())).To(Succeed()) + Expect(cl.Update(ctx, obj.DeepCopy())).To(Succeed()) }() } wg.Wait() }) - It("If a DeleteAllOf races with a delete, the DeleteAllOf should always succeed", func() { + It("If a DeleteAllOf races with a delete, the DeleteAllOf should always succeed", func(ctx SpecContext) { scheme := runtime.NewScheme() Expect(corev1.AddToScheme(scheme)).To(Succeed()) @@ -2448,7 +2252,7 @@ var _ = Describe("Fake client", func() { Name: strconv.Itoa(i), }, } - Expect(cl.Create(context.Background(), obj.DeepCopy())).To(Succeed()) + Expect(cl.Create(ctx, obj.DeepCopy())).To(Succeed()) } for i := range objects { @@ -2464,18 +2268,18 @@ var _ = Describe("Fake client", func() { // This may or may not succeed depending on if the DeleteAllOf is faster, // but if it fails, it should be a not found. - err := cl.Delete(context.Background(), obj) + err := cl.Delete(ctx, obj) if err != nil { Expect(apierrors.IsNotFound(err)).To(BeTrue()) } }() } - Expect(cl.DeleteAllOf(context.Background(), &corev1.Service{})).To(Succeed()) + Expect(cl.DeleteAllOf(ctx, &corev1.Service{})).To(Succeed()) wg.Wait() }) - It("If an update races with a scale update, only one of them succeeds", func() { + It("If an update races with a scale update, only one of them succeeds", func(ctx SpecContext) { scheme := runtime.NewScheme() Expect(appsv1.AddToScheme(scheme)).To(Succeed()) @@ -2488,7 +2292,7 @@ var _ = Describe("Fake client", func() { Name: strconv.Itoa(i), }, } - Expect(cl.Create(context.Background(), dep)).To(Succeed()) + Expect(cl.Create(ctx, dep)).To(Succeed()) wg := sync.WaitGroup{} wg.Add(2) @@ -2503,7 +2307,7 @@ var _ = Describe("Fake client", func() { dep.Annotations = map[string]string{"foo": "bar"} // This may or may not fail. If it does fail, it must be a conflict. - err := cl.Update(context.Background(), dep) + err := cl.Update(ctx, dep) if err != nil { Expect(apierrors.IsConflict(err)).To(BeTrue()) } else { @@ -2517,7 +2321,7 @@ var _ = Describe("Fake client", func() { // This may or may not fail. If it does fail, it must be a conflict. scale := &autoscalingv1.Scale{Spec: autoscalingv1.ScaleSpec{Replicas: 10}} - err := cl.SubResource("scale").Update(context.Background(), dep.DeepCopy(), client.WithSubResourceBody(scale)) + err := cl.SubResource("scale").Update(ctx, dep.DeepCopy(), client.WithSubResourceBody(scale)) if err != nil { Expect(apierrors.IsConflict(err)).To(BeTrue()) } else { @@ -2531,7 +2335,7 @@ var _ = Describe("Fake client", func() { }) - It("disallows scale subresources on unsupported built-in types", func() { + It("disallows scale subresources on unsupported built-in types", func(ctx SpecContext) { scheme := runtime.NewScheme() Expect(corev1.AddToScheme(scheme)).To(Succeed()) Expect(apiextensions.AddToScheme(scheme)).To(Succeed()) @@ -2545,11 +2349,11 @@ var _ = Describe("Fake client", func() { scale := &autoscalingv1.Scale{Spec: autoscalingv1.ScaleSpec{Replicas: 2}} expectedErr := "unimplemented scale subresource for resource *v1.Pod" - Expect(cl.SubResource(subResourceScale).Get(context.Background(), obj, scale).Error()).To(Equal(expectedErr)) - Expect(cl.SubResource(subResourceScale).Update(context.Background(), obj, client.WithSubResourceBody(scale)).Error()).To(Equal(expectedErr)) + Expect(cl.SubResource(subResourceScale).Get(ctx, obj, scale).Error()).To(Equal(expectedErr)) + Expect(cl.SubResource(subResourceScale).Update(ctx, obj, client.WithSubResourceBody(scale)).Error()).To(Equal(expectedErr)) }) - It("disallows scale subresources on non-existing objects", func() { + It("disallows scale subresources on non-existing objects", func(ctx SpecContext) { obj := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", @@ -2562,11 +2366,95 @@ var _ = Describe("Fake client", func() { scale := &autoscalingv1.Scale{Spec: autoscalingv1.ScaleSpec{Replicas: 2}} expectedErr := "deployments.apps \"foo\" not found" - Expect(cl.SubResource(subResourceScale).Get(context.Background(), obj, scale).Error()).To(Equal(expectedErr)) - Expect(cl.SubResource(subResourceScale).Update(context.Background(), obj, client.WithSubResourceBody(scale)).Error()).To(Equal(expectedErr)) + Expect(cl.SubResource(subResourceScale).Get(ctx, obj, scale).Error()).To(Equal(expectedErr)) + Expect(cl.SubResource(subResourceScale).Update(ctx, obj, client.WithSubResourceBody(scale)).Error()).To(Equal(expectedErr)) }) - It("is threadsafe", func() { + It("clears typemeta from structured objects on create", func(ctx SpecContext) { + obj := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + } + cl := NewClientBuilder().Build() + Expect(cl.Create(ctx, obj)).To(Succeed()) + Expect(obj.TypeMeta).To(Equal(metav1.TypeMeta{})) + }) + + It("clears typemeta from structured objects on update", func(ctx SpecContext) { + obj := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + } + cl := NewClientBuilder().WithObjects(obj).Build() + Expect(cl.Update(ctx, obj)).To(Succeed()) + Expect(obj.TypeMeta).To(Equal(metav1.TypeMeta{})) + }) + + It("clears typemeta from structured objects on patch", func(ctx SpecContext) { + obj := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + } + cl := NewClientBuilder().WithObjects(obj).Build() + original := obj.DeepCopy() + obj.TypeMeta = metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + } + Expect(cl.Patch(ctx, obj, client.MergeFrom(original))).To(Succeed()) + Expect(obj.TypeMeta).To(Equal(metav1.TypeMeta{})) + }) + + It("clears typemeta from structured objects on get", func(ctx SpecContext) { + obj := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + } + cl := NewClientBuilder().WithObjects(obj).Build() + target := &corev1.ConfigMap{} + Expect(cl.Get(ctx, client.ObjectKeyFromObject(obj), target)).To(Succeed()) + Expect(target.TypeMeta).To(Equal(metav1.TypeMeta{})) + }) + + It("clears typemeta from structured objects on list", func(ctx SpecContext) { + obj := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + } + cl := NewClientBuilder().WithObjects(obj).Build() + target := &corev1.ConfigMapList{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + } + Expect(cl.List(ctx, target)).To(Succeed()) + Expect(target.TypeMeta).To(Equal(metav1.TypeMeta{})) + Expect(target.Items[0].TypeMeta).To(Equal(metav1.TypeMeta{})) + }) + + It("is threadsafe", func(ctx SpecContext) { cl := NewClientBuilder().Build() u := func() *unstructured.Unstructured { @@ -2614,7 +2502,6 @@ var _ = Describe("Fake client", func() { }} } - ctx := context.Background() ops := []func(){ func() { _ = cl.Create(ctx, u()) }, func() { _ = cl.Get(ctx, client.ObjectKeyFromObject(u()), u()) }, @@ -2653,6 +2540,202 @@ var _ = Describe("Fake client", func() { wg.Wait() }) + It("supports server-side apply of a client-go resource", func(ctx SpecContext) { + cl := NewClientBuilder().Build() + obj := &unstructured.Unstructured{} + obj.SetAPIVersion("v1") + obj.SetKind("ConfigMap") + obj.SetName("foo") + Expect(unstructured.SetNestedField(obj.Object, map[string]any{"some": "data"}, "data")).To(Succeed()) + + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) + + cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} + + Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm), cm)).To(Succeed()) + Expect(cm.Data).To(Equal(map[string]string{"some": "data"})) + + Expect(unstructured.SetNestedField(obj.Object, map[string]any{"other": "data"}, "data")).To(Succeed()) + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) + + Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm), cm)).To(Succeed()) + Expect(cm.Data).To(Equal(map[string]string{"other": "data"})) + }) + + It("supports server-side apply of a custom resource", func(ctx SpecContext) { + cl := NewClientBuilder().Build() + obj := &unstructured.Unstructured{} + obj.SetAPIVersion("custom/v1") + obj.SetKind("FakeResource") + obj.SetName("foo") + result := obj.DeepCopy() + + Expect(unstructured.SetNestedField(obj.Object, map[string]any{"some": "data"}, "spec")).To(Succeed()) + + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) + + Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed()) + Expect(result.Object["spec"]).To(Equal(map[string]any{"some": "data"})) + + Expect(unstructured.SetNestedField(obj.Object, map[string]any{"other": "data"}, "spec")).To(Succeed()) + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) + + Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed()) + Expect(result.Object["spec"]).To(Equal(map[string]any{"other": "data"})) + }) + + It("errors out when doing SSA with managedFields set", func(ctx SpecContext) { + cl := NewClientBuilder().WithReturnManagedFields().Build() + obj := &unstructured.Unstructured{} + obj.SetAPIVersion("v1") + obj.SetKind("ConfigMap") + obj.SetName("foo") + Expect(unstructured.SetNestedField(obj.Object, map[string]any{"some": "data"}, "data")).To(Succeed()) + + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) + + err := cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo")) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("metadata.managedFields must be nil")) + }) + + It("supports server-side apply using a custom type converter", func(ctx SpecContext) { + cl := NewClientBuilder(). + WithTypeConverters(clientgoapplyconfigurations.NewTypeConverter(clientgoscheme.Scheme)). + Build() + obj := &unstructured.Unstructured{} + obj.SetAPIVersion("v1") + obj.SetKind("ConfigMap") + obj.SetName("foo") + + Expect(unstructured.SetNestedField(obj.Object, map[string]any{"some": "data"}, "data")).To(Succeed()) + + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) + + cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} + + Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm), cm)).To(Succeed()) + Expect(cm.Data).To(Equal(map[string]string{"some": "data"})) + + Expect(unstructured.SetNestedField(obj.Object, map[string]any{"other": "data"}, "data")).To(Succeed()) + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) + + Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm), cm)).To(Succeed()) + Expect(cm.Data).To(Equal(map[string]string{"other": "data"})) + }) + + It("returns managedFields if configured to do so", func(ctx SpecContext) { + cl := NewClientBuilder().WithReturnManagedFields().Build() + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-cm", + Namespace: "default", + }, + Data: map[string]string{ + "initial": "data", + }, + } + Expect(cl.Create(ctx, cm)).NotTo(HaveOccurred()) + Expect(cm.ManagedFields).NotTo(BeNil()) + + retrieved := &corev1.ConfigMap{} + Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm), retrieved)).NotTo(HaveOccurred()) + Expect(retrieved.ManagedFields).NotTo(BeNil()) + + cm.Data["another"] = "value" + cm.SetManagedFields(nil) + Expect(cl.Update(ctx, cm)).NotTo(HaveOccurred()) + Expect(cm.ManagedFields).NotTo(BeNil()) + + cm.SetManagedFields(nil) + beforePatch := cm.DeepCopy() + cm.Data["a-third"] = "value" + Expect(cl.Patch(ctx, cm, client.MergeFrom(beforePatch))).NotTo(HaveOccurred()) + Expect(cm.ManagedFields).NotTo(BeNil()) + + u := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]any{ + "name": cm.Name, + "namespace": cm.Namespace, + }, + "data": map[string]any{ + "ssa": "value", + }, + }} + Expect(cl.Patch(ctx, u, client.Apply, client.FieldOwner("foo"))).NotTo(HaveOccurred()) + _, exists, err := unstructured.NestedFieldNoCopy(u.Object, "metadata", "managedFields") + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeTrue()) + }) + + It("supports server-side apply of a client-go resource via Apply method", func(ctx SpecContext) { + cl := NewClientBuilder().Build() + obj := corev1applyconfigurations. + ConfigMap("foo", "default"). + WithData(map[string]string{"some": "data"}) + + Expect(cl.Apply(ctx, obj, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed()) + + cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}} + + Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm), cm)).To(Succeed()) + Expect(cm.Data).To(BeComparableTo(map[string]string{"some": "data"})) + + obj.Data = map[string]string{"other": "data"} + Expect(cl.Apply(ctx, obj, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed()) + + Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm), cm)).To(Succeed()) + Expect(cm.Data).To(BeComparableTo(map[string]string{"other": "data"})) + }) + + It("errors when trying to server-side apply an object without configuring a FieldManager", func(ctx SpecContext) { + cl := NewClientBuilder().Build() + obj := corev1applyconfigurations. + ConfigMap("foo", "default"). + WithData(map[string]string{"some": "data"}) + + err := cl.Apply(ctx, obj) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsInvalid(err)).To(BeTrue(), "Expected error to be an invalid error") + }) + + It("errors when trying to server-side apply an object with an invalid FieldManager", func(ctx SpecContext) { + cl := NewClientBuilder().Build() + obj := corev1applyconfigurations. + ConfigMap("foo", "default"). + WithData(map[string]string{"some": "data"}) + + err := cl.Apply(ctx, obj, client.FieldOwner("\x00")) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsInvalid(err)).To(BeTrue(), "Expected error to be an invalid error") + }) + + It("supports server-side apply of a custom resource via Apply method", func(ctx SpecContext) { + cl := NewClientBuilder().Build() + obj := &unstructured.Unstructured{} + obj.SetAPIVersion("custom/v1") + obj.SetKind("FakeResource") + obj.SetName("foo") + result := obj.DeepCopy() + + Expect(unstructured.SetNestedField(obj.Object, map[string]any{"some": "data"}, "spec")).To(Succeed()) + + applyConfig := client.ApplyConfigurationFromUnstructured(obj) + Expect(cl.Apply(ctx, applyConfig, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed()) + + Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed()) + Expect(result.Object["spec"]).To(Equal(map[string]any{"some": "data"})) + + Expect(unstructured.SetNestedField(obj.Object, map[string]any{"other": "data"}, "spec")).To(Succeed()) + applyConfig2 := client.ApplyConfigurationFromUnstructured(obj) + Expect(cl.Apply(ctx, applyConfig2, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed()) + + Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed()) + Expect(result.Object["spec"]).To(Equal(map[string]any{"other": "data"})) + }) + scalableObjs := []client.Object{ &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ @@ -2688,11 +2771,11 @@ var _ = Describe("Fake client", func() { }, } for _, obj := range scalableObjs { - It(fmt.Sprintf("should be able to Get scale subresources for resource %T", obj), func() { + It(fmt.Sprintf("should be able to Get scale subresources for resource %T", obj), func(ctx SpecContext) { cl := NewClientBuilder().WithObjects(obj).Build() scaleActual := &autoscalingv1.Scale{} - Expect(cl.SubResource(subResourceScale).Get(context.Background(), obj, scaleActual)).NotTo(HaveOccurred()) + Expect(cl.SubResource(subResourceScale).Get(ctx, obj, scaleActual)).NotTo(HaveOccurred()) scaleExpected := &autoscalingv1.Scale{ ObjectMeta: metav1.ObjectMeta{ @@ -2707,14 +2790,14 @@ var _ = Describe("Fake client", func() { Expect(cmp.Diff(scaleExpected, scaleActual)).To(BeEmpty()) }) - It(fmt.Sprintf("should be able to Update scale subresources for resource %T", obj), func() { + It(fmt.Sprintf("should be able to Update scale subresources for resource %T", obj), func(ctx SpecContext) { cl := NewClientBuilder().WithObjects(obj).Build() scaleExpected := &autoscalingv1.Scale{Spec: autoscalingv1.ScaleSpec{Replicas: 3}} - Expect(cl.SubResource(subResourceScale).Update(context.Background(), obj, client.WithSubResourceBody(scaleExpected))).NotTo(HaveOccurred()) + Expect(cl.SubResource(subResourceScale).Update(ctx, obj, client.WithSubResourceBody(scaleExpected))).NotTo(HaveOccurred()) objActual := obj.DeepCopyObject().(client.Object) - Expect(cl.Get(context.Background(), client.ObjectKeyFromObject(objActual), objActual)).To(Succeed()) + Expect(cl.Get(ctx, client.ObjectKeyFromObject(objActual), objActual)).To(Succeed()) objExpected := obj.DeepCopyObject().(client.Object) switch expected := objExpected.(type) { @@ -2734,7 +2817,7 @@ var _ = Describe("Fake client", func() { Expect(cmp.Diff(objExpected, objActual)).To(BeEmpty()) scaleActual := &autoscalingv1.Scale{} - Expect(cl.SubResource(subResourceScale).Get(context.Background(), obj, scaleActual)).NotTo(HaveOccurred()) + Expect(cl.SubResource(subResourceScale).Get(ctx, obj, scaleActual)).NotTo(HaveOccurred()) // When we called Update, these were derived but we need them now to compare. scaleExpected.Name = scaleActual.Name @@ -2791,58 +2874,8 @@ func (in *Schemaless) DeepCopy() *Schemaless { return out } -type WithPointerMetaList struct { - *metav1.ListMeta - *metav1.TypeMeta - Items []*WithPointerMeta -} - -func (t *WithPointerMetaList) DeepCopy() *WithPointerMetaList { - l := &WithPointerMetaList{ - ListMeta: t.ListMeta.DeepCopy(), - } - if t.TypeMeta != nil { - l.TypeMeta = &metav1.TypeMeta{ - APIVersion: t.APIVersion, - Kind: t.Kind, - } - } - for _, item := range t.Items { - l.Items = append(l.Items, item.DeepCopy()) - } - - return l -} - -func (t *WithPointerMetaList) DeepCopyObject() runtime.Object { - return t.DeepCopy() -} - -type WithPointerMeta struct { - *metav1.TypeMeta `json:",inline"` - *metav1.ObjectMeta `json:"metadata,omitempty"` -} - -func (t *WithPointerMeta) DeepCopy() *WithPointerMeta { - w := &WithPointerMeta{ - ObjectMeta: t.ObjectMeta.DeepCopy(), - } - if t.TypeMeta != nil { - w.TypeMeta = &metav1.TypeMeta{ - APIVersion: t.APIVersion, - Kind: t.Kind, - } - } - - return w -} - -func (t *WithPointerMeta) DeepCopyObject() runtime.Object { - return t.DeepCopy() -} - var _ = Describe("Fake client builder", func() { - It("panics when an index with the same name and GroupVersionKind is registered twice", func() { + It("panics when an index with the same name and GroupVersionKind is registered twice", func(ctx SpecContext) { // We need any realistic GroupVersionKind, the choice of apps/v1 Deployment is arbitrary. cb := NewClientBuilder().WithIndex(&appsv1.Deployment{}, "test-name", @@ -2855,7 +2888,7 @@ var _ = Describe("Fake client builder", func() { }).To(Panic()) }) - It("should wrap the fake client with an interceptor when WithInterceptorFuncs is called", func() { + It("should wrap the fake client with an interceptor when WithInterceptorFuncs is called", func(ctx SpecContext) { var called bool cli := NewClientBuilder().WithInterceptorFuncs(interceptor.Funcs{ Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { @@ -2863,7 +2896,7 @@ var _ = Describe("Fake client builder", func() { return nil }, }).Build() - err := cli.Get(context.Background(), client.ObjectKey{}, &corev1.Pod{}) + err := cli.Get(ctx, client.ObjectKey{}, &corev1.Pod{}) Expect(err).NotTo(HaveOccurred()) Expect(called).To(BeTrue()) }) diff --git a/pkg/client/fake/typeconverter.go b/pkg/client/fake/typeconverter.go new file mode 100644 index 0000000000..3cb3a0dc77 --- /dev/null +++ b/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/pkg/client/fake/typeconverter_test.go b/pkg/client/fake/typeconverter_test.go new file mode 100644 index 0000000000..8acba79f88 --- /dev/null +++ b/pkg/client/fake/typeconverter_test.go @@ -0,0 +1,200 @@ +/* +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 ( + "errors" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/managedfields" + "sigs.k8s.io/structured-merge-diff/v6/typed" +) + +var _ = Describe("multiTypeConverter", func() { + Describe("ObjectToTyped", func() { + It("should use first converter when it succeeds", func() { + testObj := &corev1.ConfigMap{Data: map[string]string{"key": "value"}} + testTyped := &typed.TypedValue{} + + firstConverter := &mockTypeConverter{ + objectToTypedResult: testTyped, + } + secondConverter := &mockTypeConverter{ + objectToTypedError: errors.New("second converter should not be called"), + } + + converter := multiTypeConverter{ + upstream: []managedfields.TypeConverter{firstConverter, secondConverter}, + } + + result, err := converter.ObjectToTyped(testObj) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal(testTyped)) + }) + + It("should use second converter when first fails", func() { + testObj := &corev1.ConfigMap{Data: map[string]string{"key": "value"}} + testTyped := &typed.TypedValue{} + + firstConverter := &mockTypeConverter{ + objectToTypedError: errors.New("first converter error"), + } + secondConverter := &mockTypeConverter{ + objectToTypedResult: testTyped, + } + + converter := multiTypeConverter{ + upstream: []managedfields.TypeConverter{firstConverter, secondConverter}, + } + + result, err := converter.ObjectToTyped(testObj) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal(testTyped)) + }) + + It("should return aggregate error when all converters fail", func() { + testObj := &corev1.ConfigMap{Data: map[string]string{"key": "value"}} + + firstConverter := &mockTypeConverter{ + objectToTypedError: errors.New("first converter error"), + } + secondConverter := &mockTypeConverter{ + objectToTypedError: errors.New("second converter error"), + } + + converter := multiTypeConverter{ + upstream: []managedfields.TypeConverter{firstConverter, secondConverter}, + } + + result, err := converter.ObjectToTyped(testObj) + Expect(err).To(HaveOccurred()) + Expect(result).To(BeNil()) + Expect(err.Error()).To(ContainSubstring("failed to convert Object to Typed")) + Expect(err.Error()).To(ContainSubstring("first converter error")) + Expect(err.Error()).To(ContainSubstring("second converter error")) + }) + + It("should return error when no converters provided", func() { + testObj := &corev1.ConfigMap{Data: map[string]string{"key": "value"}} + + converter := multiTypeConverter{ + upstream: []managedfields.TypeConverter{}, + } + + result, err := converter.ObjectToTyped(testObj) + Expect(err).To(HaveOccurred()) + Expect(result).To(BeNil()) + Expect(err.Error()).To(ContainSubstring("failed to convert Object to Typed")) + }) + }) + + Describe("TypedToObject", func() { + It("should use first converter when it succeeds", func() { + testTyped := &typed.TypedValue{} + testObj := &corev1.ConfigMap{Data: map[string]string{"key": "value"}} + + firstConverter := &mockTypeConverter{ + typedToObjectResult: testObj, + } + secondConverter := &mockTypeConverter{ + typedToObjectError: errors.New("second converter should not be called"), + } + + converter := multiTypeConverter{ + upstream: []managedfields.TypeConverter{firstConverter, secondConverter}, + } + + result, err := converter.TypedToObject(testTyped) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal(testObj)) + }) + + It("should use second converter when first fails", func() { + testTyped := &typed.TypedValue{} + testObj := &corev1.ConfigMap{Data: map[string]string{"key": "value"}} + + firstConverter := &mockTypeConverter{ + typedToObjectError: errors.New("first converter error"), + } + secondConverter := &mockTypeConverter{ + typedToObjectResult: testObj, + } + + converter := multiTypeConverter{ + upstream: []managedfields.TypeConverter{firstConverter, secondConverter}, + } + + result, err := converter.TypedToObject(testTyped) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal(testObj)) + }) + + It("should return aggregate error when all converters fail", func() { + testTyped := &typed.TypedValue{} + + firstConverter := &mockTypeConverter{ + typedToObjectError: errors.New("first converter error"), + } + secondConverter := &mockTypeConverter{ + typedToObjectError: errors.New("second converter error"), + } + + converter := multiTypeConverter{ + upstream: []managedfields.TypeConverter{firstConverter, secondConverter}, + } + + result, err := converter.TypedToObject(testTyped) + Expect(err).To(HaveOccurred()) + Expect(result).To(BeNil()) + Expect(err.Error()).To(ContainSubstring("failed to convert TypedValue to Object")) + Expect(err.Error()).To(ContainSubstring("first converter error")) + Expect(err.Error()).To(ContainSubstring("second converter error")) + }) + + It("should return error when no converters provided", func() { + testTyped := &typed.TypedValue{} + + converter := multiTypeConverter{ + upstream: []managedfields.TypeConverter{}, + } + + result, err := converter.TypedToObject(testTyped) + Expect(err).To(HaveOccurred()) + Expect(result).To(BeNil()) + Expect(err.Error()).To(ContainSubstring("failed to convert TypedValue to Object")) + }) + }) +}) + +type mockTypeConverter struct { + objectToTypedResult *typed.TypedValue + objectToTypedError error + + typedToObjectResult runtime.Object + typedToObjectError error +} + +func (m *mockTypeConverter) ObjectToTyped(r runtime.Object, o ...typed.ValidationOptions) (*typed.TypedValue, error) { + return m.objectToTypedResult, m.objectToTypedError +} + +func (m *mockTypeConverter) TypedToObject(v *typed.TypedValue) (runtime.Object, error) { + return m.typedToObjectResult, m.typedToObjectError +} diff --git a/pkg/client/fieldowner.go b/pkg/client/fieldowner.go index 07183cd192..93274f9500 100644 --- a/pkg/client/fieldowner.go +++ b/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/pkg/client/fieldowner_test.go b/pkg/client/fieldowner_test.go index 7c3d752316..95cb4e0f91 100644 --- a/pkg/client/fieldowner_test.go +++ b/pkg/client/fieldowner_test.go @@ -31,7 +31,7 @@ func TestWithFieldOwner(t *testing.T) { fakeClient := testClient(t, "custom-field-mgr", func() { calls++ }) wrappedClient := client.WithFieldOwner(fakeClient, "custom-field-mgr") - ctx := context.Background() + ctx := t.Context() dummyObj := &corev1.Namespace{} _ = wrappedClient.Create(ctx, dummyObj) @@ -55,7 +55,7 @@ func TestWithFieldOwnerOverridden(t *testing.T) { fakeClient := testClient(t, "new-field-manager", func() { calls++ }) wrappedClient := client.WithFieldOwner(fakeClient, "old-field-manager") - ctx := context.Background() + ctx := t.Context() dummyObj := &corev1.Namespace{} _ = wrappedClient.Create(ctx, dummyObj, client.FieldOwner("new-field-manager")) diff --git a/pkg/client/fieldvalidation.go b/pkg/client/fieldvalidation.go index 659b3d44c9..ce8d0576c7 100644 --- a/pkg/client/fieldvalidation.go +++ b/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/pkg/client/fieldvalidation_test.go b/pkg/client/fieldvalidation_test.go index 4d06e5d96f..d32ee5717d 100644 --- a/pkg/client/fieldvalidation_test.go +++ b/pkg/client/fieldvalidation_test.go @@ -25,20 +25,21 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + corev1applyconfigurations "k8s.io/client-go/applyconfigurations/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/client/interceptor" ) var _ = Describe("ClientWithFieldValidation", func() { - It("should return errors for invalid fields when using strict validation", func() { + It("should return errors for invalid fields when using strict validation", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) wrappedClient := client.WithFieldValidation(cl, metav1.FieldValidationStrict) - ctx := context.Background() baseNode := &unstructured.Unstructured{} baseNode.SetGroupVersionKind(schema.GroupVersionKind{ @@ -99,11 +100,12 @@ func TestWithStrictFieldValidation(t *testing.T) { fakeClient := testFieldValidationClient(t, metav1.FieldValidationStrict, func() { calls++ }) wrappedClient := client.WithFieldValidation(fakeClient, metav1.FieldValidationStrict) - ctx := context.Background() + ctx := t.Context() dummyObj := &corev1.Namespace{} _ = wrappedClient.Create(ctx, dummyObj) _ = wrappedClient.Update(ctx, dummyObj) + _ = wrappedClient.Apply(ctx, corev1applyconfigurations.ConfigMap("foo", "bar")) _ = wrappedClient.Patch(ctx, dummyObj, nil) _ = wrappedClient.Status().Create(ctx, dummyObj, dummyObj) _ = wrappedClient.Status().Update(ctx, dummyObj) @@ -112,7 +114,7 @@ func TestWithStrictFieldValidation(t *testing.T) { _ = wrappedClient.SubResource("some-subresource").Update(ctx, dummyObj) _ = wrappedClient.SubResource("some-subresource").Patch(ctx, dummyObj, nil) - if expectedCalls := 9; calls != expectedCalls { + if expectedCalls := 10; calls != expectedCalls { t.Fatalf("wrong number of calls to assertions: expected=%d; got=%d", expectedCalls, calls) } } @@ -123,7 +125,7 @@ func TestWithStrictFieldValidationOverridden(t *testing.T) { fakeClient := testFieldValidationClient(t, metav1.FieldValidationWarn, func() { calls++ }) wrappedClient := client.WithFieldValidation(fakeClient, metav1.FieldValidationStrict) - ctx := context.Background() + ctx := t.Context() dummyObj := &corev1.Namespace{} _ = wrappedClient.Create(ctx, dummyObj, client.FieldValidation(metav1.FieldValidationWarn)) @@ -188,6 +190,10 @@ func testFieldValidationClient(t *testing.T, expectedFieldValidation string, cal } return nil }, + Apply: func(ctx context.Context, client client.WithWatch, obj runtime.ApplyConfiguration, opts ...client.ApplyOption) error { + callback() + return nil + }, Patch: func(ctx context.Context, c client.WithWatch, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { callback() out := &client.PatchOptions{} diff --git a/pkg/client/interceptor/intercept.go b/pkg/client/interceptor/intercept.go index 3d3f3cb011..7ff73bd8da 100644 --- a/pkg/client/interceptor/intercept.go +++ b/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/pkg/client/interceptor/intercept_test.go b/pkg/client/interceptor/intercept_test.go index a0536789b1..26ea5b057e 100644 --- a/pkg/client/interceptor/intercept_test.go +++ b/pkg/client/interceptor/intercept_test.go @@ -16,8 +16,7 @@ import ( var _ = Describe("NewClient", func() { wrappedClient := dummyClient{} - ctx := context.Background() - It("should call the provided Get function", func() { + It("should call the provided Get function", func(ctx SpecContext) { var called bool client := NewClient(wrappedClient, Funcs{ Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { @@ -28,7 +27,7 @@ var _ = Describe("NewClient", func() { _ = client.Get(ctx, types.NamespacedName{}, nil) Expect(called).To(BeTrue()) }) - It("should call the underlying client if the provided Get function is nil", func() { + It("should call the underlying client if the provided Get function is nil", func(ctx SpecContext) { var called bool client1 := NewClient(wrappedClient, Funcs{ Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { @@ -40,7 +39,7 @@ var _ = Describe("NewClient", func() { _ = client2.Get(ctx, types.NamespacedName{}, nil) Expect(called).To(BeTrue()) }) - It("should call the provided List function", func() { + It("should call the provided List function", func(ctx SpecContext) { var called bool client := NewClient(wrappedClient, Funcs{ List: func(ctx context.Context, client client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { @@ -51,7 +50,7 @@ var _ = Describe("NewClient", func() { _ = client.List(ctx, nil) Expect(called).To(BeTrue()) }) - It("should call the underlying client if the provided List function is nil", func() { + It("should call the underlying client if the provided List function is nil", func(ctx SpecContext) { var called bool client1 := NewClient(wrappedClient, Funcs{ List: func(ctx context.Context, client client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { @@ -63,7 +62,30 @@ var _ = Describe("NewClient", func() { _ = client2.List(ctx, nil) Expect(called).To(BeTrue()) }) - It("should call the provided Create function", func() { + It("should call the provided Apply function", func(ctx SpecContext) { + var called bool + client := NewClient(wrappedClient, Funcs{ + Apply: func(ctx context.Context, client client.WithWatch, obj runtime.ApplyConfiguration, opts ...client.ApplyOption) error { + called = true + return nil + }, + }) + _ = client.Apply(ctx, nil) + Expect(called).To(BeTrue()) + }) + It("should call the underlying client if the provided Apply function is nil", func(ctx SpecContext) { + var called bool + client1 := NewClient(wrappedClient, Funcs{ + Apply: func(ctx context.Context, client client.WithWatch, obj runtime.ApplyConfiguration, opts ...client.ApplyOption) error { + called = true + return nil + }, + }) + client2 := NewClient(client1, Funcs{}) + _ = client2.Apply(ctx, nil) + Expect(called).To(BeTrue()) + }) + It("should call the provided Create function", func(ctx SpecContext) { var called bool client := NewClient(wrappedClient, Funcs{ Create: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.CreateOption) error { @@ -74,7 +96,7 @@ var _ = Describe("NewClient", func() { _ = client.Create(ctx, nil) Expect(called).To(BeTrue()) }) - It("should call the underlying client if the provided Create function is nil", func() { + It("should call the underlying client if the provided Create function is nil", func(ctx SpecContext) { var called bool client1 := NewClient(wrappedClient, Funcs{ Create: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.CreateOption) error { @@ -86,7 +108,7 @@ var _ = Describe("NewClient", func() { _ = client2.Create(ctx, nil) Expect(called).To(BeTrue()) }) - It("should call the provided Delete function", func() { + It("should call the provided Delete function", func(ctx SpecContext) { var called bool client := NewClient(wrappedClient, Funcs{ Delete: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.DeleteOption) error { @@ -97,7 +119,7 @@ var _ = Describe("NewClient", func() { _ = client.Delete(ctx, nil) Expect(called).To(BeTrue()) }) - It("should call the underlying client if the provided Delete function is nil", func() { + It("should call the underlying client if the provided Delete function is nil", func(ctx SpecContext) { var called bool client1 := NewClient(wrappedClient, Funcs{ Delete: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.DeleteOption) error { @@ -109,7 +131,7 @@ var _ = Describe("NewClient", func() { _ = client2.Delete(ctx, nil) Expect(called).To(BeTrue()) }) - It("should call the provided DeleteAllOf function", func() { + It("should call the provided DeleteAllOf function", func(ctx SpecContext) { var called bool client := NewClient(wrappedClient, Funcs{ DeleteAllOf: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.DeleteAllOfOption) error { @@ -120,7 +142,7 @@ var _ = Describe("NewClient", func() { _ = client.DeleteAllOf(ctx, nil) Expect(called).To(BeTrue()) }) - It("should call the underlying client if the provided DeleteAllOf function is nil", func() { + It("should call the underlying client if the provided DeleteAllOf function is nil", func(ctx SpecContext) { var called bool client1 := NewClient(wrappedClient, Funcs{ DeleteAllOf: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.DeleteAllOfOption) error { @@ -132,7 +154,7 @@ var _ = Describe("NewClient", func() { _ = client2.DeleteAllOf(ctx, nil) Expect(called).To(BeTrue()) }) - It("should call the provided Update function", func() { + It("should call the provided Update function", func(ctx SpecContext) { var called bool client := NewClient(wrappedClient, Funcs{ Update: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.UpdateOption) error { @@ -143,7 +165,7 @@ var _ = Describe("NewClient", func() { _ = client.Update(ctx, nil) Expect(called).To(BeTrue()) }) - It("should call the underlying client if the provided Update function is nil", func() { + It("should call the underlying client if the provided Update function is nil", func(ctx SpecContext) { var called bool client1 := NewClient(wrappedClient, Funcs{ Update: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.UpdateOption) error { @@ -155,7 +177,7 @@ var _ = Describe("NewClient", func() { _ = client2.Update(ctx, nil) Expect(called).To(BeTrue()) }) - It("should call the provided Patch function", func() { + It("should call the provided Patch function", func(ctx SpecContext) { var called bool client := NewClient(wrappedClient, Funcs{ Patch: func(ctx context.Context, client client.WithWatch, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { @@ -166,7 +188,7 @@ var _ = Describe("NewClient", func() { _ = client.Patch(ctx, nil, nil) Expect(called).To(BeTrue()) }) - It("should call the underlying client if the provided Patch function is nil", func() { + It("should call the underlying client if the provided Patch function is nil", func(ctx SpecContext) { var called bool client1 := NewClient(wrappedClient, Funcs{ Patch: func(ctx context.Context, client client.WithWatch, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { @@ -178,7 +200,7 @@ var _ = Describe("NewClient", func() { _ = client2.Patch(ctx, nil, nil) Expect(called).To(BeTrue()) }) - It("should call the provided Watch function", func() { + It("should call the provided Watch function", func(ctx SpecContext) { var called bool client := NewClient(wrappedClient, Funcs{ Watch: func(ctx context.Context, client client.WithWatch, obj client.ObjectList, opts ...client.ListOption) (watch.Interface, error) { @@ -189,7 +211,7 @@ var _ = Describe("NewClient", func() { _, _ = client.Watch(ctx, nil) Expect(called).To(BeTrue()) }) - It("should call the underlying client if the provided Watch function is nil", func() { + It("should call the underlying client if the provided Watch function is nil", func(ctx SpecContext) { var called bool client1 := NewClient(wrappedClient, Funcs{ Watch: func(ctx context.Context, client client.WithWatch, obj client.ObjectList, opts ...client.ListOption) (watch.Interface, error) { @@ -229,8 +251,7 @@ var _ = Describe("NewClient", func() { var _ = Describe("NewSubResourceClient", func() { c := dummyClient{} - ctx := context.Background() - It("should call the provided Get function", func() { + It("should call the provided Get function", func(ctx SpecContext) { var called bool c := NewClient(c, Funcs{ SubResourceGet: func(_ context.Context, client client.Client, subResourceName string, obj, subResource client.Object, opts ...client.SubResourceGetOption) error { @@ -242,7 +263,7 @@ var _ = Describe("NewSubResourceClient", func() { _ = c.SubResource("foo").Get(ctx, nil, nil) Expect(called).To(BeTrue()) }) - It("should call the underlying client if the provided Get function is nil", func() { + It("should call the underlying client if the provided Get function is nil", func(ctx SpecContext) { var called bool client1 := NewClient(c, Funcs{ SubResourceGet: func(_ context.Context, client client.Client, subResourceName string, obj, subResource client.Object, opts ...client.SubResourceGetOption) error { @@ -255,7 +276,7 @@ var _ = Describe("NewSubResourceClient", func() { _ = client2.SubResource("foo").Get(ctx, nil, nil) Expect(called).To(BeTrue()) }) - It("should call the provided Update function", func() { + It("should call the provided Update function", func(ctx SpecContext) { var called bool client := NewClient(c, Funcs{ SubResourceUpdate: func(_ context.Context, client client.Client, subResourceName string, obj client.Object, opts ...client.SubResourceUpdateOption) error { @@ -267,7 +288,7 @@ var _ = Describe("NewSubResourceClient", func() { _ = client.SubResource("foo").Update(ctx, nil, nil) Expect(called).To(BeTrue()) }) - It("should call the underlying client if the provided Update function is nil", func() { + It("should call the underlying client if the provided Update function is nil", func(ctx SpecContext) { var called bool client1 := NewClient(c, Funcs{ SubResourceUpdate: func(_ context.Context, client client.Client, subResourceName string, obj client.Object, opts ...client.SubResourceUpdateOption) error { @@ -280,7 +301,7 @@ var _ = Describe("NewSubResourceClient", func() { _ = client2.SubResource("foo").Update(ctx, nil, nil) Expect(called).To(BeTrue()) }) - It("should call the provided Patch function", func() { + It("should call the provided Patch function", func(ctx SpecContext) { var called bool client := NewClient(c, Funcs{ SubResourcePatch: func(_ context.Context, client client.Client, subResourceName string, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error { @@ -292,7 +313,7 @@ var _ = Describe("NewSubResourceClient", func() { _ = client.SubResource("foo").Patch(ctx, nil, nil) Expect(called).To(BeTrue()) }) - It("should call the underlying client if the provided Patch function is nil", func() { + It("should call the underlying client if the provided Patch function is nil", func(ctx SpecContext) { var called bool client1 := NewClient(c, Funcs{ SubResourcePatch: func(ctx context.Context, client client.Client, subResourceName string, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error { @@ -305,7 +326,7 @@ var _ = Describe("NewSubResourceClient", func() { _ = client2.SubResource("foo").Patch(ctx, nil, nil) Expect(called).To(BeTrue()) }) - It("should call the provided Create function", func() { + It("should call the provided Create function", func(ctx SpecContext) { var called bool client := NewClient(c, Funcs{ SubResourceCreate: func(_ context.Context, client client.Client, subResourceName string, obj, subResource client.Object, opts ...client.SubResourceCreateOption) error { @@ -317,7 +338,7 @@ var _ = Describe("NewSubResourceClient", func() { _ = client.SubResource("foo").Create(ctx, nil, nil) Expect(called).To(BeTrue()) }) - It("should call the underlying client if the provided Create function is nil", func() { + It("should call the underlying client if the provided Create function is nil", func(ctx SpecContext) { var called bool client1 := NewClient(c, Funcs{ SubResourceCreate: func(_ context.Context, client client.Client, subResourceName string, obj, subResource client.Object, opts ...client.SubResourceCreateOption) error { @@ -360,6 +381,10 @@ func (d dummyClient) Patch(ctx context.Context, obj client.Object, patch client. return nil } +func (d dummyClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.ApplyOption) error { + return nil +} + func (d dummyClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { return nil } diff --git a/pkg/client/interfaces.go b/pkg/client/interfaces.go index 3b282fc2c5..61559ecbe1 100644 --- a/pkg/client/interfaces.go +++ b/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 diff --git a/pkg/client/namespaced_client.go b/pkg/client/namespaced_client.go index 222dc79579..cacba4a9c6 100644 --- a/pkg/client/namespaced_client.go +++ b/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) diff --git a/pkg/client/namespaced_client_test.go b/pkg/client/namespaced_client_test.go index 6e1c2641a3..cf28289e72 100644 --- a/pkg/client/namespaced_client_test.go +++ b/pkg/client/namespaced_client_test.go @@ -25,21 +25,26 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - rbacv1 "k8s.io/api/rbac/v1" - appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + appsv1applyconfigurations "k8s.io/client-go/applyconfigurations/apps/v1" + corev1applyconfigurations "k8s.io/client-go/applyconfigurations/core/v1" + metav1applyconfigurations "k8s.io/client-go/applyconfigurations/meta/v1" + rbacv1applyconfigurations "k8s.io/client-go/applyconfigurations/rbac/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ) var _ = Describe("NamespacedClient", func() { var dep *appsv1.Deployment + var acDep *appsv1applyconfigurations.DeploymentApplyConfiguration var ns = "default" - ctx := context.Background() var count uint64 = 0 var replicaCount int32 = 2 @@ -75,20 +80,36 @@ var _ = Describe("NamespacedClient", func() { }, }, } + acDep = appsv1applyconfigurations.Deployment(dep.Name, ""). + WithLabels(dep.Labels). + WithSpec(appsv1applyconfigurations.DeploymentSpec(). + WithReplicas(*dep.Spec.Replicas). + WithSelector(metav1applyconfigurations.LabelSelector().WithMatchLabels(dep.Spec.Selector.MatchLabels)). + WithTemplate(corev1applyconfigurations.PodTemplateSpec(). + WithLabels(dep.Spec.Template.Labels). + WithSpec(corev1applyconfigurations.PodSpec(). + WithContainers(corev1applyconfigurations.Container(). + WithName(dep.Spec.Template.Spec.Containers[0].Name). + WithImage(dep.Spec.Template.Spec.Containers[0].Image), + ), + ), + ), + ) + }) Describe("Get", func() { - - BeforeEach(func() { + BeforeEach(func(ctx SpecContext) { var err error dep, err = clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) }) - AfterEach(func() { + AfterEach(func(ctx SpecContext) { deleteDeployment(ctx, dep, ns) }) - It("should successfully Get a namespace-scoped object", func() { + + It("should successfully Get a namespace-scoped object", func(ctx SpecContext) { name := types.NamespacedName{Name: dep.Name} result := &appsv1.Deployment{} @@ -97,7 +118,7 @@ var _ = Describe("NamespacedClient", func() { }) It("should error when namespace provided in the object is different than the one "+ - "specified in client", func() { + "specified in client", func(ctx SpecContext) { name := types.NamespacedName{Name: dep.Name, Namespace: "non-default"} result := &appsv1.Deployment{} @@ -106,17 +127,17 @@ var _ = Describe("NamespacedClient", func() { }) Describe("List", func() { - BeforeEach(func() { + BeforeEach(func(ctx SpecContext) { var err error dep, err = clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) }) - AfterEach(func() { + AfterEach(func(ctx SpecContext) { deleteDeployment(ctx, dep, ns) }) - It("should successfully List objects when namespace is not specified with the object", func() { + It("should successfully List objects when namespace is not specified with the object", func(ctx SpecContext) { result := &appsv1.DeploymentList{} opts := client.MatchingLabels(dep.Labels) @@ -125,7 +146,7 @@ var _ = Describe("NamespacedClient", func() { Expect(result.Items[0]).To(BeEquivalentTo(*dep)) }) - It("should List objects from the namespace specified in the client", func() { + It("should List objects from the namespace specified in the client", func(ctx SpecContext) { result := &appsv1.DeploymentList{} opts := client.InNamespace("non-default") @@ -135,12 +156,72 @@ var _ = Describe("NamespacedClient", func() { }) }) + Describe("Apply", func() { + AfterEach(func(ctx SpecContext) { + deleteDeployment(ctx, dep, ns) + }) + + It("should successfully apply an object in the right namespace", func(ctx SpecContext) { + err := getClient().Apply(ctx, acDep, client.FieldOwner("test")) + Expect(err).NotTo(HaveOccurred()) + + res, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(res.GetNamespace()).To(BeEquivalentTo(ns)) + }) + + It("should successfully apply an object in the right namespace through unstructured", func(ctx SpecContext) { + serialized, err := json.Marshal(acDep) + Expect(err).NotTo(HaveOccurred()) + u := &unstructured.Unstructured{} + Expect(json.Unmarshal(serialized, &u.Object)).To(Succeed()) + err = getClient().Apply(ctx, client.ApplyConfigurationFromUnstructured(u), client.FieldOwner("test")) + Expect(err).NotTo(HaveOccurred()) + + res, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(res.GetNamespace()).To(BeEquivalentTo(ns)) + }) + + It("should not create an object if the namespace of the object is different", func(ctx SpecContext) { + acDep.WithNamespace("non-default") + err := getClient().Apply(ctx, acDep, client.FieldOwner("test")) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("does not match the namespace")) + }) + + It("should not create an object through unstructured if the namespace of the object is different", func(ctx SpecContext) { + acDep.WithNamespace("non-default") + serialized, err := json.Marshal(acDep) + Expect(err).NotTo(HaveOccurred()) + u := &unstructured.Unstructured{} + Expect(json.Unmarshal(serialized, &u.Object)).To(Succeed()) + err = getClient().Apply(ctx, client.ApplyConfigurationFromUnstructured(u), client.FieldOwner("test")) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("does not match the namespace")) + }) + + It("should create a cluster scoped object", func(ctx SpecContext) { + cr := rbacv1applyconfigurations.ClusterRole(fmt.Sprintf("clusterRole-%v", count)) + + err := getClient().Apply(ctx, cr, client.FieldOwner("test")) + Expect(err).NotTo(HaveOccurred()) + + By("checking if the object was created") + res, err := clientset.RbacV1().ClusterRoles().Get(ctx, *cr.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(res).NotTo(BeNil()) + + deleteClusterRole(ctx, &rbacv1.ClusterRole{ObjectMeta: metav1.ObjectMeta{Name: *cr.Name}}) + }) + }) + Describe("Create", func() { - AfterEach(func() { + AfterEach(func(ctx SpecContext) { deleteDeployment(ctx, dep, ns) }) - It("should successfully create object in the right namespace", func() { + It("should successfully create object in the right namespace", func(ctx SpecContext) { By("creating the object initially") err := getClient().Create(ctx, dep) Expect(err).NotTo(HaveOccurred()) @@ -151,13 +232,13 @@ var _ = Describe("NamespacedClient", func() { Expect(res.GetNamespace()).To(BeEquivalentTo(ns)) }) - It("should not create object if the namespace of the object is different", func() { + It("should not create object if the namespace of the object is different", func(ctx SpecContext) { By("creating the object initially") dep.SetNamespace("non-default") err := getClient().Create(ctx, dep) Expect(err).To(HaveOccurred()) }) - It("should create a cluster scoped object", func() { + It("should create a cluster scoped object", func(ctx SpecContext) { cr := &rbacv1.ClusterRole{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("clusterRole-%v", count), @@ -186,16 +267,16 @@ var _ = Describe("NamespacedClient", func() { Describe("Update", func() { var err error - BeforeEach(func() { + BeforeEach(func(ctx SpecContext) { dep, err = clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) dep.Annotations = map[string]string{"foo": "bar"} Expect(err).NotTo(HaveOccurred()) }) - AfterEach(func() { + AfterEach(func(ctx SpecContext) { deleteDeployment(ctx, dep, ns) }) - It("should successfully update the provided object", func() { + It("should successfully update the provided object", func(ctx SpecContext) { By("updating the Deployment") err = getClient().Update(ctx, dep) Expect(err).NotTo(HaveOccurred()) @@ -208,7 +289,7 @@ var _ = Describe("NamespacedClient", func() { Expect(actual.Annotations["foo"]).To(Equal("bar")) }) - It("should successfully update the provided object when namespace is not provided", func() { + It("should successfully update the provided object when namespace is not provided", func(ctx SpecContext) { By("updating the Deployment") dep.SetNamespace("") err = getClient().Update(ctx, dep) @@ -222,14 +303,14 @@ var _ = Describe("NamespacedClient", func() { Expect(actual.Annotations["foo"]).To(Equal("bar")) }) - It("should not update when object namespace is different", func() { + It("should not update when object namespace is different", func(ctx SpecContext) { By("updating the Deployment") dep.SetNamespace("non-default") err = getClient().Update(ctx, dep) Expect(err).To(HaveOccurred()) }) - It("should not update any object from other namespace", func() { + It("should not update any object from other namespace", func(ctx SpecContext) { By("creating a new namespace") tns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "non-default-1"}} _, err := clientset.CoreV1().Namespaces().Create(ctx, tns, metav1.CreateOptions{}) @@ -266,7 +347,7 @@ var _ = Describe("NamespacedClient", func() { deleteNamespace(ctx, tns) }) - It("should update a cluster scoped resource", func() { + It("should update a cluster scoped resource", func(ctx SpecContext) { changedCR := &rbacv1.ClusterRole{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("clusterRole-%v", count), @@ -302,16 +383,16 @@ var _ = Describe("NamespacedClient", func() { Describe("Patch", func() { var err error - BeforeEach(func() { + BeforeEach(func(ctx SpecContext) { dep, err = clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) }) - AfterEach(func() { + AfterEach(func(ctx SpecContext) { deleteDeployment(ctx, dep, ns) }) - It("should successfully modify the object using Patch", func() { + It("should successfully modify the object using Patch", func(ctx SpecContext) { By("Applying Patch") err = getClient().Patch(ctx, dep, client.RawPatch(types.MergePatchType, generatePatch())) Expect(err).NotTo(HaveOccurred()) @@ -323,7 +404,7 @@ var _ = Describe("NamespacedClient", func() { Expect(actual.GetNamespace()).To(Equal(ns)) }) - It("should successfully modify the object using Patch when namespace is not provided", func() { + It("should successfully modify the object using Patch when namespace is not provided", func(ctx SpecContext) { By("Applying Patch") dep.SetNamespace("") err = getClient().Patch(ctx, dep, client.RawPatch(types.MergePatchType, generatePatch())) @@ -336,13 +417,13 @@ var _ = Describe("NamespacedClient", func() { Expect(actual.GetNamespace()).To(Equal(ns)) }) - It("should not modify the object when namespace of the object is different", func() { + It("should not modify the object when namespace of the object is different", func(ctx SpecContext) { dep.SetNamespace("non-default") err = getClient().Patch(ctx, dep, client.RawPatch(types.MergePatchType, generatePatch())) Expect(err).To(HaveOccurred()) }) - It("should not modify an object from a different namespace", func() { + It("should not modify an object from a different namespace", func(ctx SpecContext) { By("creating a new namespace") tns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "non-default-2"}} _, err := clientset.CoreV1().Namespaces().Create(ctx, tns, metav1.CreateOptions{}) @@ -377,7 +458,7 @@ var _ = Describe("NamespacedClient", func() { deleteNamespace(ctx, tns) }) - It("should successfully modify cluster scoped resource", func() { + It("should successfully modify cluster scoped resource", func(ctx SpecContext) { cr := &rbacv1.ClusterRole{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("clusterRole-%v", count), @@ -411,15 +492,15 @@ var _ = Describe("NamespacedClient", func() { Describe("Delete and DeleteAllOf", func() { var err error - BeforeEach(func() { + BeforeEach(func(ctx SpecContext) { dep, err = clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) }) - AfterEach(func() { + AfterEach(func(ctx SpecContext) { deleteDeployment(ctx, dep, ns) }) - It("should successfully delete an object when namespace is not specified", func() { + It("should successfully delete an object when namespace is not specified", func(ctx SpecContext) { By("deleting the object") dep.SetNamespace("") err = getClient().Delete(ctx, dep) @@ -430,7 +511,7 @@ var _ = Describe("NamespacedClient", func() { Expect(err).To(HaveOccurred()) }) - It("should successfully delete all of the deployments in the given namespace", func() { + It("should successfully delete all of the deployments in the given namespace", func(ctx SpecContext) { By("Deleting all objects in the namespace") err = getClient().DeleteAllOf(ctx, dep) Expect(err).NotTo(HaveOccurred()) @@ -440,7 +521,7 @@ var _ = Describe("NamespacedClient", func() { Expect(err).To(HaveOccurred()) }) - It("should not delete deployments in other namespaces", func() { + It("should not delete deployments in other namespaces", func(ctx SpecContext) { tns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "non-default-3"}} _, err = clientset.CoreV1().Namespaces().Create(ctx, tns, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) @@ -482,16 +563,16 @@ var _ = Describe("NamespacedClient", func() { Describe("SubResourceWriter", func() { var err error - BeforeEach(func() { + BeforeEach(func(ctx SpecContext) { dep, err = clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) }) - AfterEach(func() { + AfterEach(func(ctx SpecContext) { deleteDeployment(ctx, dep, ns) }) - It("should change objects via update status", func() { + It("should change objects via update status", func(ctx SpecContext) { changedDep := dep.DeepCopy() changedDep.Status.Replicas = 99 @@ -504,7 +585,7 @@ var _ = Describe("NamespacedClient", func() { Expect(actual.Status.Replicas).To(BeEquivalentTo(99)) }) - It("should not change objects via update status when object namespace is different", func() { + It("should not change objects via update status when object namespace is different", func(ctx SpecContext) { changedDep := dep.DeepCopy() changedDep.SetNamespace("test") changedDep.Status.Replicas = 99 @@ -512,7 +593,7 @@ var _ = Describe("NamespacedClient", func() { Expect(getClient().SubResource("status").Update(ctx, changedDep)).To(HaveOccurred()) }) - It("should change objects via status patch", func() { + It("should change objects via status patch", func(ctx SpecContext) { changedDep := dep.DeepCopy() changedDep.Status.Replicas = 99 @@ -525,7 +606,7 @@ var _ = Describe("NamespacedClient", func() { Expect(actual.Status.Replicas).To(BeEquivalentTo(99)) }) - It("should not change objects via status patch when object namespace is different", func() { + It("should not change objects via status patch when object namespace is different", func(ctx SpecContext) { changedDep := dep.DeepCopy() changedDep.Status.Replicas = 99 changedDep.SetNamespace("test") @@ -535,7 +616,7 @@ var _ = Describe("NamespacedClient", func() { }) Describe("Test on invalid objects", func() { - It("should refuse to perform operations on invalid object", func() { + It("should refuse to perform operations on invalid object", func(ctx SpecContext) { err := getClient().Create(ctx, nil) Expect(err).To(HaveOccurred()) diff --git a/pkg/client/options.go b/pkg/client/options.go index db50ed8feb..86da5ddfc3 100644 --- a/pkg/client/options.go +++ b/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. @@ -692,15 +718,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 +888,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 +932,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 +974,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/pkg/client/options_test.go b/pkg/client/options_test.go index c6dc09b676..e50669c691 100644 --- a/pkg/client/options_test.go +++ b/pkg/client/options_test.go @@ -66,6 +66,19 @@ var _ = Describe("ListOptions", func() { o.ApplyToList(newListOpts) Expect(newListOpts).To(Equal(o)) }) + It("Should set UnsafeDisableDeepCopy", func() { + definitelyTrue := true + o := &client.ListOptions{UnsafeDisableDeepCopy: &definitelyTrue} + newListOpts := &client.ListOptions{} + o.ApplyToList(newListOpts) + Expect(newListOpts).To(Equal(o)) + }) + It("Should set UnsafeDisableDeepCopy through option", func() { + listOpts := &client.ListOptions{} + client.UnsafeDisableDeepCopy.ApplyToList(listOpts) + Expect(listOpts.UnsafeDisableDeepCopy).ToNot(BeNil()) + Expect(*listOpts.UnsafeDisableDeepCopy).To(BeTrue()) + }) It("Should not set anything", func() { o := &client.ListOptions{} newListOpts := &client.ListOptions{} @@ -81,6 +94,40 @@ var _ = Describe("GetOptions", func() { o.ApplyToGet(newGetOpts) Expect(newGetOpts).To(Equal(o)) }) + It("Should set UnsafeDisableDeepCopy", func() { + definitelyTrue := true + o := &client.GetOptions{UnsafeDisableDeepCopy: &definitelyTrue} + newGetOpts := &client.GetOptions{} + o.ApplyToGet(newGetOpts) + Expect(newGetOpts).To(Equal(o)) + }) + It("Should set UnsafeDisableDeepCopy through option", func() { + getOpts := &client.GetOptions{} + client.UnsafeDisableDeepCopy.ApplyToGet(getOpts) + Expect(getOpts.UnsafeDisableDeepCopy).ToNot(BeNil()) + Expect(*getOpts.UnsafeDisableDeepCopy).To(BeTrue()) + }) +}) + +var _ = Describe("ApplyOptions", func() { + It("Should set DryRun", func() { + o := &client.ApplyOptions{DryRun: []string{"Hello", "Theodore"}} + newApplyOpts := &client.ApplyOptions{} + o.ApplyToApply(newApplyOpts) + Expect(newApplyOpts).To(Equal(o)) + }) + It("Should set Force", func() { + o := &client.ApplyOptions{Force: ptr.To(true)} + newApplyOpts := &client.ApplyOptions{} + o.ApplyToApply(newApplyOpts) + Expect(newApplyOpts).To(Equal(o)) + }) + It("Should set FieldManager", func() { + o := &client.ApplyOptions{FieldManager: "field-manager"} + newApplyOpts := &client.ApplyOptions{} + o.ApplyToApply(newApplyOpts) + Expect(newApplyOpts).To(Equal(o)) + }) }) var _ = Describe("CreateOptions", func() { @@ -252,6 +299,57 @@ var _ = Describe("MatchingLabels", func() { }) }) +var _ = Describe("DryRunAll", func() { + It("Should apply to ApplyOptions", func() { + o := &client.ApplyOptions{DryRun: []string{"server"}} + t := client.DryRunAll + t.ApplyToApply(o) + Expect(o.DryRun).To(Equal([]string{metav1.DryRunAll})) + }) + It("Should apply to CreateOptions", func() { + o := &client.CreateOptions{DryRun: []string{"server"}} + t := client.DryRunAll + t.ApplyToCreate(o) + Expect(o.DryRun).To(Equal([]string{metav1.DryRunAll})) + }) + It("Should apply to UpdateOptions", func() { + o := &client.UpdateOptions{DryRun: []string{"server"}} + t := client.DryRunAll + t.ApplyToUpdate(o) + Expect(o.DryRun).To(Equal([]string{metav1.DryRunAll})) + }) + It("Should apply to PatchOptions", func() { + o := &client.PatchOptions{DryRun: []string{"server"}} + t := client.DryRunAll + t.ApplyToPatch(o) + Expect(o.DryRun).To(Equal([]string{metav1.DryRunAll})) + }) + It("Should apply to DeleteOptions", func() { + o := &client.DeleteOptions{DryRun: []string{"server"}} + t := client.DryRunAll + t.ApplyToDelete(o) + Expect(o.DryRun).To(Equal([]string{metav1.DryRunAll})) + }) + It("Should apply to SubResourcePatchOptions", func() { + o := &client.SubResourcePatchOptions{PatchOptions: client.PatchOptions{DryRun: []string{"server"}}} + t := client.DryRunAll + t.ApplyToSubResourcePatch(o) + Expect(o.DryRun).To(Equal([]string{metav1.DryRunAll})) + }) + It("Should apply to SubResourceCreateOptions", func() { + o := &client.SubResourceCreateOptions{CreateOptions: client.CreateOptions{DryRun: []string{"server"}}} + t := client.DryRunAll + t.ApplyToSubResourceCreate(o) + Expect(o.DryRun).To(Equal([]string{metav1.DryRunAll})) + }) + It("Should apply to SubResourceUpdateOptions", func() { + o := &client.SubResourceUpdateOptions{UpdateOptions: client.UpdateOptions{DryRun: []string{"server"}}} + t := client.DryRunAll + t.ApplyToSubResourceUpdate(o) + Expect(o.DryRun).To(Equal([]string{metav1.DryRunAll})) + }) +}) + var _ = Describe("FieldOwner", func() { It("Should apply to PatchOptions", func() { o := &client.PatchOptions{FieldManager: "bar"} @@ -259,6 +357,12 @@ var _ = Describe("FieldOwner", func() { t.ApplyToPatch(o) Expect(o.FieldManager).To(Equal("foo")) }) + It("Should apply to ApplyOptions", func() { + o := &client.ApplyOptions{FieldManager: "bar"} + t := client.FieldOwner("foo") + t.ApplyToApply(o) + Expect(o.FieldManager).To(Equal("foo")) + }) It("Should apply to CreateOptions", func() { o := &client.CreateOptions{FieldManager: "bar"} t := client.FieldOwner("foo") @@ -304,6 +408,12 @@ var _ = Describe("ForceOwnership", func() { t.ApplyToSubResourcePatch(o) Expect(*o.Force).To(BeTrue()) }) + It("Should apply to ApplyOptions", func() { + o := &client.ApplyOptions{} + t := client.ForceOwnership + t.ApplyToApply(o) + Expect(*o.Force).To(BeTrue()) + }) }) var _ = Describe("HasLabels", func() { diff --git a/pkg/client/patch.go b/pkg/client/patch.go index 11d6083885..95cb331e7d 100644 --- a/pkg/client/patch.go +++ b/pkg/client/patch.go @@ -27,6 +27,7 @@ import ( var ( // Apply uses server-side apply to patch the given object. + // Deprecated: Use client.Apply instead. Apply Patch = applyPatch{} // Merge uses the raw object as a merge patch, without modifications. diff --git a/pkg/client/typed_client.go b/pkg/client/typed_client.go index 92afd9a9c2..3bd762a638 100644 --- a/pkg/client/typed_client.go +++ b/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/pkg/client/unstructured_client.go b/pkg/client/unstructured_client.go index 0d96951780..e636c3beef 100644 --- a/pkg/client/unstructured_client.go +++ b/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/pkg/client/watch_test.go b/pkg/client/watch_test.go index 26d90f6550..8d5b3344d3 100644 --- a/pkg/client/watch_test.go +++ b/pkg/client/watch_test.go @@ -38,9 +38,8 @@ var _ = Describe("ClientWithWatch", func() { var count uint64 = 0 var replicaCount int32 = 2 var ns = "kube-public" - ctx := context.TODO() - BeforeEach(func() { + BeforeEach(func(ctx SpecContext) { atomic.AddUint64(&count, 1) dep = &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("watch-deployment-name-%v", count), Namespace: ns, Labels: map[string]string{"app": fmt.Sprintf("bar-%v", count)}}, @@ -61,18 +60,18 @@ var _ = Describe("ClientWithWatch", func() { Expect(err).NotTo(HaveOccurred()) }) - AfterEach(func() { + AfterEach(func(ctx SpecContext) { deleteDeployment(ctx, dep, ns) }) Describe("NewWithWatch", func() { - It("should return a new Client", func() { + It("should return a new Client", func(ctx SpecContext) { cl, err := client.NewWithWatch(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) }) - watchSuite := func(through client.ObjectList, expectedType client.Object, checkGvk bool) { + watchSuite := func(ctx context.Context, through client.ObjectList, expectedType client.Object, checkGvk bool) { cl, err := client.NewWithWatch(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) Expect(cl).NotTo(BeNil()) @@ -110,23 +109,23 @@ var _ = Describe("ClientWithWatch", func() { } } - It("should receive a create event when watching the typed object", func() { - watchSuite(&appsv1.DeploymentList{}, &appsv1.Deployment{}, false) + It("should receive a create event when watching the typed object", func(ctx SpecContext) { + watchSuite(ctx, &appsv1.DeploymentList{}, &appsv1.Deployment{}, false) }) - It("should receive a create event when watching the unstructured object", func() { + It("should receive a create event when watching the unstructured object", func(ctx SpecContext) { u := &unstructured.UnstructuredList{} u.SetGroupVersionKind(schema.GroupVersionKind{ Group: "apps", Kind: "Deployment", Version: "v1", }) - watchSuite(u, &unstructured.Unstructured{}, true) + watchSuite(ctx, u, &unstructured.Unstructured{}, true) }) - It("should receive a create event when watching the metadata object", func() { + It("should receive a create event when watching the metadata object", func(ctx SpecContext) { m := &metav1.PartialObjectMetadataList{TypeMeta: metav1.TypeMeta{Kind: "Deployment", APIVersion: "apps/v1"}} - watchSuite(m, &metav1.PartialObjectMetadata{}, false) + watchSuite(ctx, m, &metav1.PartialObjectMetadata{}, false) }) }) diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go index dc52b2d9b3..c08a742403 100644 --- a/pkg/cluster/cluster_test.go +++ b/pkg/cluster/cluster_test.go @@ -100,22 +100,22 @@ var _ = Describe("cluster.Cluster", func() { }) Describe("Start", func() { - It("should stop when context is cancelled", func() { + It("should stop when context is cancelled", func(specCtx SpecContext) { c, err := New(cfg) Expect(err).NotTo(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) cancel() Expect(c.Start(ctx)).NotTo(HaveOccurred()) }) }) - It("should not leak goroutines when stopped", func() { + It("should not leak goroutines when stopped", func(specCtx SpecContext) { currentGRs := goleak.IgnoreCurrent() c, err := New(cfg) Expect(err).NotTo(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) cancel() Expect(c.Start(ctx)).NotTo(HaveOccurred()) diff --git a/pkg/config/controller.go b/pkg/config/controller.go index a5655593ef..8920480319 100644 --- a/pkg/config/controller.go +++ b/pkg/config/controller.go @@ -60,6 +60,19 @@ type Controller struct { // 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. + EnableWarmup *bool + // UsePriorityQueue configures the controllers queue to use the controller-runtime provided // priority queue. // diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 9de959b48f..03e51abcc5 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -93,6 +93,19 @@ type TypedOptions[request comparable] struct { // // Note: This flag is disabled by default until a future version. It's currently in beta. 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. + EnableWarmup *bool } // DefaultFromConfig defaults the config from a config.Controller @@ -124,6 +137,10 @@ func (options *TypedOptions[request]) DefaultFromConfig(config config.Controller if options.NeedLeaderElection == nil { options.NeedLeaderElection = config.NeedLeaderElection } + + if options.EnableWarmup == nil { + options.EnableWarmup = config.EnableWarmup + } } // Controller implements an API. A Controller manages a work queue fed reconcile.Requests @@ -243,7 +260,7 @@ func NewTypedUnmanaged[request comparable](name string, options TypedOptions[req } // 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, @@ -253,7 +270,8 @@ func NewTypedUnmanaged[request comparable](name string, options TypedOptions[req LogConstructor: options.LogConstructor, RecoverPanic: options.RecoverPanic, LeaderElected: options.NeedLeaderElection, - }, nil + EnableWarmup: options.EnableWarmup, + }), nil } // ReconcileIDFromContext gets the reconcileID from the current context. diff --git a/pkg/controller/controller_integration_test.go b/pkg/controller/controller_integration_test.go index c8e8a790fc..e09813eee2 100644 --- a/pkg/controller/controller_integration_test.go +++ b/pkg/controller/controller_integration_test.go @@ -18,16 +18,21 @@ package controller_test import ( "context" + "fmt" + "strconv" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllertest" "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" @@ -38,7 +43,6 @@ import ( var _ = Describe("controller", func() { var reconciled chan reconcile.Request - ctx := context.Background() BeforeEach(func() { reconciled = make(chan reconcile.Request) @@ -48,30 +52,48 @@ var _ = Describe("controller", func() { Describe("controller", func() { // TODO(directxman12): write a whole suite of controller-client interaction tests - It("should reconcile", func() { + // The watches in this test are setup with a namespace predicate to avoid each table entry + // from interfering with the others. We cannot add a delete call for the pods created in the + // test, as it causes flakes with the api-server termination timing out. + // See https://github.com/kubernetes-sigs/controller-runtime/issues/1571 for a description + // of the issue, and a discussion here: https://github.com/kubernetes-sigs/controller-runtime/pull/3192#discussion_r2186967799 + DescribeTable("should reconcile", func(ctx SpecContext, enableWarmup bool) { By("Creating the Manager") cm, err := manager.New(cfg, manager.Options{}) Expect(err).NotTo(HaveOccurred()) By("Creating the Controller") - instance, err := controller.New("foo-controller", cm, controller.Options{ - Reconciler: reconcile.Func( - func(_ context.Context, request reconcile.Request) (reconcile.Result, error) { - reconciled <- request - return reconcile.Result{}, nil - }), - }) + instance, err := controller.New( + fmt.Sprintf("foo-controller-%t", enableWarmup), + cm, + controller.Options{ + Reconciler: reconcile.Func( + func(_ context.Context, request reconcile.Request) (reconcile.Result, error) { + reconciled <- request + return reconcile.Result{}, nil + }), + EnableWarmup: ptr.To(enableWarmup), + }, + ) Expect(err).NotTo(HaveOccurred()) + testNamespace := strconv.FormatBool(enableWarmup) + By("Watching Resources") err = instance.Watch( source.Kind(cm.GetCache(), &appsv1.ReplicaSet{}, handler.TypedEnqueueRequestForOwner[*appsv1.ReplicaSet](cm.GetScheme(), cm.GetRESTMapper(), &appsv1.Deployment{}), + makeNamespacePredicate[*appsv1.ReplicaSet](testNamespace), ), ) Expect(err).NotTo(HaveOccurred()) - err = instance.Watch(source.Kind(cm.GetCache(), &appsv1.Deployment{}, &handler.TypedEnqueueRequestForObject[*appsv1.Deployment]{})) + err = instance.Watch( + source.Kind(cm.GetCache(), &appsv1.Deployment{}, + &handler.TypedEnqueueRequestForObject[*appsv1.Deployment]{}, + makeNamespacePredicate[*appsv1.Deployment](testNamespace), + ), + ) Expect(err).NotTo(HaveOccurred()) err = cm.GetClient().Get(ctx, types.NamespacedName{Name: "foo"}, &corev1.Namespace{}) @@ -80,8 +102,6 @@ var _ = Describe("controller", func() { Expect(err).To(Equal(&cache.ErrCacheNotStarted{})) By("Starting the Manager") - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(cm.Start(ctx)).NotTo(HaveOccurred()) @@ -110,19 +130,25 @@ var _ = Describe("controller", func() { }, } expectedReconcileRequest := reconcile.Request{NamespacedName: types.NamespacedName{ - Namespace: "default", + Namespace: testNamespace, Name: "deployment-name", }} + By("Creating the test namespace") + _, err = clientset.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: testNamespace}, + }, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + By("Invoking Reconciling for Create") - deployment, err = clientset.AppsV1().Deployments("default").Create(ctx, deployment, metav1.CreateOptions{}) + deployment, err = clientset.AppsV1().Deployments(testNamespace).Create(ctx, deployment, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) Expect(<-reconciled).To(Equal(expectedReconcileRequest)) By("Invoking Reconciling for Update") newDeployment := deployment.DeepCopy() newDeployment.Labels = map[string]string{"foo": "bar"} - _, err = clientset.AppsV1().Deployments("default").Update(ctx, newDeployment, metav1.UpdateOptions{}) + _, err = clientset.AppsV1().Deployments(testNamespace).Update(ctx, newDeployment, metav1.UpdateOptions{}) Expect(err).NotTo(HaveOccurred()) Expect(<-reconciled).To(Equal(expectedReconcileRequest)) @@ -145,36 +171,41 @@ var _ = Describe("controller", func() { Template: deployment.Spec.Template, }, } - replicaset, err = clientset.AppsV1().ReplicaSets("default").Create(ctx, replicaset, metav1.CreateOptions{}) + replicaset, err = clientset.AppsV1().ReplicaSets(testNamespace).Create(ctx, replicaset, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) Expect(<-reconciled).To(Equal(expectedReconcileRequest)) By("Invoking Reconciling for an OwnedObject when it is updated") newReplicaset := replicaset.DeepCopy() newReplicaset.Labels = map[string]string{"foo": "bar"} - _, err = clientset.AppsV1().ReplicaSets("default").Update(ctx, newReplicaset, metav1.UpdateOptions{}) + _, err = clientset.AppsV1().ReplicaSets(testNamespace).Update(ctx, newReplicaset, metav1.UpdateOptions{}) Expect(err).NotTo(HaveOccurred()) Expect(<-reconciled).To(Equal(expectedReconcileRequest)) By("Invoking Reconciling for an OwnedObject when it is deleted") - err = clientset.AppsV1().ReplicaSets("default").Delete(ctx, replicaset.Name, metav1.DeleteOptions{}) + err = clientset.AppsV1().ReplicaSets(testNamespace).Delete(ctx, replicaset.Name, metav1.DeleteOptions{}) Expect(err).NotTo(HaveOccurred()) Expect(<-reconciled).To(Equal(expectedReconcileRequest)) By("Invoking Reconciling for Delete") - err = clientset.AppsV1().Deployments("default"). + err = clientset.AppsV1().Deployments(testNamespace). Delete(ctx, "deployment-name", metav1.DeleteOptions{}) Expect(err).NotTo(HaveOccurred()) Expect(<-reconciled).To(Equal(expectedReconcileRequest)) By("Listing a type with a slice of pointers as items field") err = cm.GetClient(). - List(context.Background(), &controllertest.UnconventionalListTypeList{}) + List(ctx, &controllertest.UnconventionalListTypeList{}) Expect(err).NotTo(HaveOccurred()) By("Invoking Reconciling for a pod when it is created when adding watcher dynamically") // Add new watcher dynamically - err = instance.Watch(source.Kind(cm.GetCache(), &corev1.Pod{}, &handler.TypedEnqueueRequestForObject[*corev1.Pod]{})) + err = instance.Watch( + source.Kind(cm.GetCache(), &corev1.Pod{}, + &handler.TypedEnqueueRequestForObject[*corev1.Pod]{}, + makeNamespacePredicate[*corev1.Pod](testNamespace), + ), + ) Expect(err).NotTo(HaveOccurred()) pod := &corev1.Pod{ @@ -194,16 +225,27 @@ var _ = Describe("controller", func() { }, } expectedReconcileRequest = reconcile.Request{NamespacedName: types.NamespacedName{ - Namespace: "default", + Namespace: testNamespace, Name: "pod-name", }} - _, err = clientset.CoreV1().Pods("default").Create(ctx, pod, metav1.CreateOptions{}) + _, err = clientset.CoreV1().Pods(testNamespace).Create(ctx, pod, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) Expect(<-reconciled).To(Equal(expectedReconcileRequest)) - }) + }, + Entry("with controller warmup enabled", true), + Entry("with controller warmup not enabled", false), + ) }) }) +// makeNamespacePredicate returns a predicate that filters out all objects not in the passed in +// namespace. +func makeNamespacePredicate[object client.Object](namespace string) predicate.TypedPredicate[object] { + return predicate.NewTypedPredicateFuncs[object](func(obj object) bool { + return obj.GetNamespace() == namespace + }) +} + func truePtr() *bool { t := true return &t diff --git a/pkg/controller/controller_test.go b/pkg/controller/controller_test.go index 1c5b11d709..1e125a0f58 100644 --- a/pkg/controller/controller_test.go +++ b/pkg/controller/controller_test.go @@ -136,10 +136,10 @@ var _ = Describe("controller.Controller", func() { Expect(c2).ToNot(BeNil()) }) - It("should not leak goroutines when stopped", func() { + It("should not leak goroutines when stopped", func(specCtx SpecContext) { currentGRs := goleak.IgnoreCurrent() - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) watchChan := make(chan event.GenericEvent, 1) watch := source.Channel(watchChan, &handler.EnqueueRequestForObject{}) watchChan <- event.GenericEvent{Object: &corev1.Pod{}} @@ -474,5 +474,71 @@ var _ = Describe("controller.Controller", func() { _, ok = q.(priorityqueue.PriorityQueue[reconcile.Request]) Expect(ok).To(BeFalse()) }) + + It("should set EnableWarmup correctly", func() { + m, err := manager.New(cfg, manager.Options{}) + Expect(err).NotTo(HaveOccurred()) + + // Test with EnableWarmup set to true + ctrlWithWarmup, err := controller.New("warmup-enabled-ctrl", m, controller.Options{ + Reconciler: reconcile.Func(nil), + EnableWarmup: ptr.To(true), + }) + Expect(err).NotTo(HaveOccurred()) + + internalCtrlWithWarmup, ok := ctrlWithWarmup.(*internalcontroller.Controller[reconcile.Request]) + Expect(ok).To(BeTrue()) + Expect(internalCtrlWithWarmup.EnableWarmup).To(HaveValue(BeTrue())) + + // Test with EnableWarmup set to false + ctrlWithoutWarmup, err := controller.New("warmup-disabled-ctrl", m, controller.Options{ + Reconciler: reconcile.Func(nil), + EnableWarmup: ptr.To(false), + }) + Expect(err).NotTo(HaveOccurred()) + + internalCtrlWithoutWarmup, ok := ctrlWithoutWarmup.(*internalcontroller.Controller[reconcile.Request]) + Expect(ok).To(BeTrue()) + Expect(internalCtrlWithoutWarmup.EnableWarmup).To(HaveValue(BeFalse())) + + // Test with EnableWarmup not set (should default to nil) + ctrlWithDefaultWarmup, err := controller.New("warmup-default-ctrl", m, controller.Options{ + Reconciler: reconcile.Func(nil), + }) + Expect(err).NotTo(HaveOccurred()) + + internalCtrlWithDefaultWarmup, ok := ctrlWithDefaultWarmup.(*internalcontroller.Controller[reconcile.Request]) + Expect(ok).To(BeTrue()) + Expect(internalCtrlWithDefaultWarmup.EnableWarmup).To(BeNil()) + }) + + It("should inherit EnableWarmup from manager config", func() { + // Test with manager default setting EnableWarmup to true + managerWithWarmup, err := manager.New(cfg, manager.Options{ + Controller: config.Controller{ + EnableWarmup: ptr.To(true), + }, + }) + Expect(err).NotTo(HaveOccurred()) + ctrlInheritingWarmup, err := controller.New("inherit-warmup-enabled", managerWithWarmup, controller.Options{ + Reconciler: reconcile.Func(nil), + }) + Expect(err).NotTo(HaveOccurred()) + + internalCtrlInheritingWarmup, ok := ctrlInheritingWarmup.(*internalcontroller.Controller[reconcile.Request]) + Expect(ok).To(BeTrue()) + Expect(internalCtrlInheritingWarmup.EnableWarmup).To(HaveValue(BeTrue())) + + // Test that explicit controller setting overrides manager setting + ctrlOverridingWarmup, err := controller.New("override-warmup-disabled", managerWithWarmup, controller.Options{ + Reconciler: reconcile.Func(nil), + EnableWarmup: ptr.To(false), + }) + Expect(err).NotTo(HaveOccurred()) + + internalCtrlOverridingWarmup, ok := ctrlOverridingWarmup.(*internalcontroller.Controller[reconcile.Request]) + Expect(ok).To(BeTrue()) + Expect(internalCtrlOverridingWarmup.EnableWarmup).To(HaveValue(BeFalse())) + }) }) }) diff --git a/pkg/controller/controllerutil/controllerutil_test.go b/pkg/controller/controllerutil/controllerutil_test.go index 89bd65bfd0..a716667f6a 100644 --- a/pkg/controller/controllerutil/controllerutil_test.go +++ b/pkg/controller/controllerutil/controllerutil_test.go @@ -491,8 +491,8 @@ var _ = Describe("Controllerutil", func() { specr = deploymentSpecr(deploy, deplSpec) }) - It("creates a new object if one doesn't exists", func() { - op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, specr) + It("creates a new object if one doesn't exists", func(ctx SpecContext) { + op, err := controllerutil.CreateOrUpdate(ctx, c, deploy, specr) By("returning no error") Expect(err).NotTo(HaveOccurred()) @@ -502,7 +502,7 @@ var _ = Describe("Controllerutil", func() { By("actually having the deployment created") fetched := &appsv1.Deployment{} - Expect(c.Get(context.TODO(), deplKey, fetched)).To(Succeed()) + Expect(c.Get(ctx, deplKey, fetched)).To(Succeed()) By("being mutated by MutateFn") Expect(fetched.Spec.Template.Spec.Containers).To(HaveLen(1)) @@ -510,13 +510,13 @@ var _ = Describe("Controllerutil", func() { Expect(fetched.Spec.Template.Spec.Containers[0].Image).To(Equal(deplSpec.Template.Spec.Containers[0].Image)) }) - It("updates existing object", func() { + It("updates existing object", func(ctx SpecContext) { var scale int32 = 2 - op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, specr) + op, err := controllerutil.CreateOrUpdate(ctx, c, deploy, specr) Expect(err).NotTo(HaveOccurred()) Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated)) - op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentScaler(deploy, scale)) + op, err = controllerutil.CreateOrUpdate(ctx, c, deploy, deploymentScaler(deploy, scale)) By("returning no error") Expect(err).NotTo(HaveOccurred()) @@ -525,17 +525,17 @@ var _ = Describe("Controllerutil", func() { By("actually having the deployment scaled") fetched := &appsv1.Deployment{} - Expect(c.Get(context.TODO(), deplKey, fetched)).To(Succeed()) + Expect(c.Get(ctx, deplKey, fetched)).To(Succeed()) Expect(*fetched.Spec.Replicas).To(Equal(scale)) }) - It("updates only changed objects", func() { - op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, specr) + It("updates only changed objects", func(ctx SpecContext) { + op, err := controllerutil.CreateOrUpdate(ctx, c, deploy, specr) Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated)) Expect(err).NotTo(HaveOccurred()) - op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentIdentity) + op, err = controllerutil.CreateOrUpdate(ctx, c, deploy, deploymentIdentity) By("returning no error") Expect(err).NotTo(HaveOccurred()) @@ -543,8 +543,8 @@ var _ = Describe("Controllerutil", func() { Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone)) }) - It("errors when MutateFn changes object name on creation", func() { - op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, func() error { + It("errors when MutateFn changes object name on creation", func(ctx SpecContext) { + op, err := controllerutil.CreateOrUpdate(ctx, c, deploy, func() error { Expect(specr()).To(Succeed()) return deploymentRenamer(deploy)() }) @@ -556,13 +556,13 @@ var _ = Describe("Controllerutil", func() { Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone)) }) - It("errors when MutateFn renames an object", func() { - op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, specr) + It("errors when MutateFn renames an object", func(ctx SpecContext) { + op, err := controllerutil.CreateOrUpdate(ctx, c, deploy, specr) Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated)) Expect(err).NotTo(HaveOccurred()) - op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentRenamer(deploy)) + op, err = controllerutil.CreateOrUpdate(ctx, c, deploy, deploymentRenamer(deploy)) By("returning error") Expect(err).To(HaveOccurred()) @@ -571,13 +571,13 @@ var _ = Describe("Controllerutil", func() { Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone)) }) - It("errors when object namespace changes", func() { - op, err := controllerutil.CreateOrUpdate(context.TODO(), c, deploy, specr) + It("errors when object namespace changes", func(ctx SpecContext) { + op, err := controllerutil.CreateOrUpdate(ctx, c, deploy, specr) Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated)) Expect(err).NotTo(HaveOccurred()) - op, err = controllerutil.CreateOrUpdate(context.TODO(), c, deploy, deploymentNamespaceChanger(deploy)) + op, err = controllerutil.CreateOrUpdate(ctx, c, deploy, deploymentNamespaceChanger(deploy)) By("returning error") Expect(err).To(HaveOccurred()) @@ -586,8 +586,8 @@ var _ = Describe("Controllerutil", func() { Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone)) }) - It("aborts immediately if there was an error initially retrieving the object", func() { - op, err := controllerutil.CreateOrUpdate(context.TODO(), errorReader{c}, deploy, func() error { + It("aborts immediately if there was an error initially retrieving the object", func(ctx SpecContext) { + op, err := controllerutil.CreateOrUpdate(ctx, errorReader{c}, deploy, func() error { Fail("Mutation method should not run") return nil }) @@ -640,22 +640,22 @@ var _ = Describe("Controllerutil", func() { specr = deploymentSpecr(deploy, deplSpec) }) - assertLocalDeployWasUpdated := func(fetched *appsv1.Deployment) { + assertLocalDeployWasUpdated := func(ctx context.Context, fetched *appsv1.Deployment) { By("local deploy object was updated during patch & has same spec, status, resource version as fetched") if fetched == nil { fetched = &appsv1.Deployment{} - ExpectWithOffset(1, c.Get(context.TODO(), deplKey, fetched)).To(Succeed()) + ExpectWithOffset(1, c.Get(ctx, deplKey, fetched)).To(Succeed()) } ExpectWithOffset(1, fetched.ResourceVersion).To(Equal(deploy.ResourceVersion)) ExpectWithOffset(1, fetched.Spec).To(BeEquivalentTo(deploy.Spec)) ExpectWithOffset(1, fetched.Status).To(BeEquivalentTo(deploy.Status)) } - assertLocalDeployStatusWasUpdated := func(fetched *appsv1.Deployment) { + assertLocalDeployStatusWasUpdated := func(ctx context.Context, fetched *appsv1.Deployment) { By("local deploy object was updated during patch & has same spec, status, resource version as fetched") if fetched == nil { fetched = &appsv1.Deployment{} - ExpectWithOffset(1, c.Get(context.TODO(), deplKey, fetched)).To(Succeed()) + ExpectWithOffset(1, c.Get(ctx, deplKey, fetched)).To(Succeed()) } ExpectWithOffset(1, fetched.ResourceVersion).To(Equal(deploy.ResourceVersion)) ExpectWithOffset(1, *fetched.Spec.Replicas).To(BeEquivalentTo(int32(5))) @@ -663,8 +663,8 @@ var _ = Describe("Controllerutil", func() { ExpectWithOffset(1, len(fetched.Status.Conditions)).To(BeEquivalentTo(1)) } - It("creates a new object if one doesn't exists", func() { - op, err := controllerutil.CreateOrPatch(context.TODO(), c, deploy, specr) + It("creates a new object if one doesn't exists", func(ctx SpecContext) { + op, err := controllerutil.CreateOrPatch(ctx, c, deploy, specr) By("returning no error") Expect(err).NotTo(HaveOccurred()) @@ -674,7 +674,7 @@ var _ = Describe("Controllerutil", func() { By("actually having the deployment created") fetched := &appsv1.Deployment{} - Expect(c.Get(context.TODO(), deplKey, fetched)).To(Succeed()) + Expect(c.Get(ctx, deplKey, fetched)).To(Succeed()) By("being mutated by MutateFn") Expect(fetched.Spec.Template.Spec.Containers).To(HaveLen(1)) @@ -682,13 +682,13 @@ var _ = Describe("Controllerutil", func() { Expect(fetched.Spec.Template.Spec.Containers[0].Image).To(Equal(deplSpec.Template.Spec.Containers[0].Image)) }) - It("patches existing object", func() { + It("patches existing object", func(ctx SpecContext) { var scale int32 = 2 - op, err := controllerutil.CreateOrPatch(context.TODO(), c, deploy, specr) + op, err := controllerutil.CreateOrPatch(ctx, c, deploy, specr) Expect(err).NotTo(HaveOccurred()) Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated)) - op, err = controllerutil.CreateOrPatch(context.TODO(), c, deploy, deploymentScaler(deploy, scale)) + op, err = controllerutil.CreateOrPatch(ctx, c, deploy, deploymentScaler(deploy, scale)) By("returning no error") Expect(err).NotTo(HaveOccurred()) @@ -697,29 +697,29 @@ var _ = Describe("Controllerutil", func() { By("actually having the deployment scaled") fetched := &appsv1.Deployment{} - Expect(c.Get(context.TODO(), deplKey, fetched)).To(Succeed()) + Expect(c.Get(ctx, deplKey, fetched)).To(Succeed()) Expect(*fetched.Spec.Replicas).To(Equal(scale)) - assertLocalDeployWasUpdated(fetched) + assertLocalDeployWasUpdated(ctx, fetched) }) - It("patches only changed objects", func() { - op, err := controllerutil.CreateOrPatch(context.TODO(), c, deploy, specr) + It("patches only changed objects", func(ctx SpecContext) { + op, err := controllerutil.CreateOrPatch(ctx, c, deploy, specr) Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated)) Expect(err).NotTo(HaveOccurred()) - op, err = controllerutil.CreateOrPatch(context.TODO(), c, deploy, deploymentIdentity) + op, err = controllerutil.CreateOrPatch(ctx, c, deploy, deploymentIdentity) By("returning no error") Expect(err).NotTo(HaveOccurred()) By("returning OperationResultNone") Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone)) - assertLocalDeployWasUpdated(nil) + assertLocalDeployWasUpdated(ctx, nil) }) - It("patches only changed status", func() { - op, err := controllerutil.CreateOrPatch(context.TODO(), c, deploy, specr) + It("patches only changed status", func(ctx SpecContext) { + op, err := controllerutil.CreateOrPatch(ctx, c, deploy, specr) Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated)) Expect(err).NotTo(HaveOccurred()) @@ -728,18 +728,18 @@ var _ = Describe("Controllerutil", func() { ReadyReplicas: 1, Replicas: 3, } - op, err = controllerutil.CreateOrPatch(context.TODO(), c, deploy, deploymentStatusr(deploy, deployStatus)) + op, err = controllerutil.CreateOrPatch(ctx, c, deploy, deploymentStatusr(deploy, deployStatus)) By("returning no error") Expect(err).NotTo(HaveOccurred()) By("returning OperationResultUpdatedStatusOnly") Expect(op).To(BeEquivalentTo(controllerutil.OperationResultUpdatedStatusOnly)) - assertLocalDeployWasUpdated(nil) + assertLocalDeployWasUpdated(ctx, nil) }) - It("patches resource and status", func() { - op, err := controllerutil.CreateOrPatch(context.TODO(), c, deploy, specr) + It("patches resource and status", func(ctx SpecContext) { + op, err := controllerutil.CreateOrPatch(ctx, c, deploy, specr) Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated)) Expect(err).NotTo(HaveOccurred()) @@ -749,7 +749,7 @@ var _ = Describe("Controllerutil", func() { ReadyReplicas: 1, Replicas: replicas, } - op, err = controllerutil.CreateOrPatch(context.TODO(), c, deploy, func() error { + op, err = controllerutil.CreateOrPatch(ctx, c, deploy, func() error { Expect(deploymentScaler(deploy, replicas)()).To(Succeed()) return deploymentStatusr(deploy, deployStatus)() }) @@ -759,11 +759,11 @@ var _ = Describe("Controllerutil", func() { By("returning OperationResultUpdatedStatus") Expect(op).To(BeEquivalentTo(controllerutil.OperationResultUpdatedStatus)) - assertLocalDeployWasUpdated(nil) + assertLocalDeployWasUpdated(ctx, nil) }) - It("patches resource and not empty status", func() { - op, err := controllerutil.CreateOrPatch(context.TODO(), c, deploy, specr) + It("patches resource and not empty status", func(ctx SpecContext) { + op, err := controllerutil.CreateOrPatch(ctx, c, deploy, specr) Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated)) Expect(err).NotTo(HaveOccurred()) @@ -773,7 +773,7 @@ var _ = Describe("Controllerutil", func() { ReadyReplicas: 1, Replicas: replicas, } - op, err = controllerutil.CreateOrPatch(context.TODO(), c, deploy, func() error { + op, err = controllerutil.CreateOrPatch(ctx, c, deploy, func() error { Expect(deploymentScaler(deploy, replicas)()).To(Succeed()) return deploymentStatusr(deploy, deployStatus)() }) @@ -783,9 +783,9 @@ var _ = Describe("Controllerutil", func() { By("returning OperationResultUpdatedStatus") Expect(op).To(BeEquivalentTo(controllerutil.OperationResultUpdatedStatus)) - assertLocalDeployWasUpdated(nil) + assertLocalDeployWasUpdated(ctx, nil) - op, err = controllerutil.CreateOrPatch(context.TODO(), c, deploy, func() error { + op, err = controllerutil.CreateOrPatch(ctx, c, deploy, func() error { deploy.Spec.Replicas = ptr.To(int32(5)) deploy.Status.Conditions = []appsv1.DeploymentCondition{{ Type: appsv1.DeploymentProgressing, @@ -799,11 +799,11 @@ var _ = Describe("Controllerutil", func() { By("returning OperationResultUpdatedStatus") Expect(op).To(BeEquivalentTo(controllerutil.OperationResultUpdatedStatus)) - assertLocalDeployStatusWasUpdated(nil) + assertLocalDeployStatusWasUpdated(ctx, nil) }) - It("errors when MutateFn changes object name on creation", func() { - op, err := controllerutil.CreateOrPatch(context.TODO(), c, deploy, func() error { + It("errors when MutateFn changes object name on creation", func(ctx SpecContext) { + op, err := controllerutil.CreateOrPatch(ctx, c, deploy, func() error { Expect(specr()).To(Succeed()) return deploymentRenamer(deploy)() }) @@ -815,13 +815,13 @@ var _ = Describe("Controllerutil", func() { Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone)) }) - It("errors when MutateFn renames an object", func() { - op, err := controllerutil.CreateOrPatch(context.TODO(), c, deploy, specr) + It("errors when MutateFn renames an object", func(ctx SpecContext) { + op, err := controllerutil.CreateOrPatch(ctx, c, deploy, specr) Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated)) Expect(err).NotTo(HaveOccurred()) - op, err = controllerutil.CreateOrPatch(context.TODO(), c, deploy, deploymentRenamer(deploy)) + op, err = controllerutil.CreateOrPatch(ctx, c, deploy, deploymentRenamer(deploy)) By("returning error") Expect(err).To(HaveOccurred()) @@ -830,13 +830,13 @@ var _ = Describe("Controllerutil", func() { Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone)) }) - It("errors when object namespace changes", func() { - op, err := controllerutil.CreateOrPatch(context.TODO(), c, deploy, specr) + It("errors when object namespace changes", func(ctx SpecContext) { + op, err := controllerutil.CreateOrPatch(ctx, c, deploy, specr) Expect(op).To(BeEquivalentTo(controllerutil.OperationResultCreated)) Expect(err).NotTo(HaveOccurred()) - op, err = controllerutil.CreateOrPatch(context.TODO(), c, deploy, deploymentNamespaceChanger(deploy)) + op, err = controllerutil.CreateOrPatch(ctx, c, deploy, deploymentNamespaceChanger(deploy)) By("returning error") Expect(err).To(HaveOccurred()) @@ -845,8 +845,8 @@ var _ = Describe("Controllerutil", func() { Expect(op).To(BeEquivalentTo(controllerutil.OperationResultNone)) }) - It("aborts immediately if there was an error initially retrieving the object", func() { - op, err := controllerutil.CreateOrPatch(context.TODO(), errorReader{c}, deploy, func() error { + It("aborts immediately if there was an error initially retrieving the object", func(ctx SpecContext) { + op, err := controllerutil.CreateOrPatch(ctx, errorReader{c}, deploy, func() error { Fail("Mutation method should not run") return nil }) diff --git a/pkg/controller/priorityqueue/priorityqueue.go b/pkg/controller/priorityqueue/priorityqueue.go index c3f77a6f39..1f1a245849 100644 --- a/pkg/controller/priorityqueue/priorityqueue.go +++ b/pkg/controller/priorityqueue/priorityqueue.go @@ -129,6 +129,10 @@ type priorityqueue[T comparable] struct { } func (w *priorityqueue[T]) AddWithOpts(o AddOpts, items ...T) { + if w.shutdown.Load() { + return + } + w.lock.Lock() defer w.lock.Unlock() @@ -274,9 +278,15 @@ func (w *priorityqueue[T]) AddRateLimited(item T) { } func (w *priorityqueue[T]) GetWithPriority() (_ T, priority int, shutdown bool) { + if w.shutdown.Load() { + var zero T + return zero, 0, true + } + w.waiters.Add(1) w.notifyItemOrWaiterAdded() + item := <-w.get return item.Key, item.Priority, w.shutdown.Load() diff --git a/pkg/controller/priorityqueue/priorityqueue_test.go b/pkg/controller/priorityqueue/priorityqueue_test.go index ec0f36e95f..7e5d3ba3ed 100644 --- a/pkg/controller/priorityqueue/priorityqueue_test.go +++ b/pkg/controller/priorityqueue/priorityqueue_test.go @@ -300,6 +300,26 @@ var _ = Describe("Controllerworkqueue", func() { Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 2})) }) + // ref: https://github.com/kubernetes-sigs/controller-runtime/issues/3239 + It("Get from priority queue might get stuck when the priority queue is shut down", func() { + q, _ := newQueue() + + q.Add("baz") + // shut down + q.ShutDown() + q.AddWithOpts(AddOpts{After: time.Second}, "foo") + + item, priority, isShutDown := q.GetWithPriority() + Expect(item).To(Equal("")) + Expect(priority).To(Equal(0)) + Expect(isShutDown).To(BeTrue()) + + item1, priority1, isShutDown := q.GetWithPriority() + Expect(item1).To(Equal("")) + Expect(priority1).To(Equal(0)) + Expect(isShutDown).To(BeTrue()) + }) + It("items are included in Len() and the queueDepth metric once they are ready", func() { q, metrics := newQueue() defer q.ShutDown() diff --git a/pkg/doc.go b/pkg/doc.go index 89b380c108..64693b4829 100644 --- a/pkg/doc.go +++ b/pkg/doc.go @@ -137,11 +137,11 @@ Source provides event: EventHandler enqueues Request: -* &handler.EnqueueRequestForObject{} -> (reconcile.Request{types.NamespaceName{Name: "foo", Namespace: "bar"}}) +* &handler.EnqueueRequestForObject{} -> (reconcile.Request{types.NamespaceName{Namespace: "foo", Name: "bar"}}) Reconciler is called with the Request: -* Reconciler(reconcile.Request{types.NamespaceName{Name: "foo", Namespace: "bar"}}) +* Reconciler(reconcile.Request{types.NamespaceName{Namespace: "foo", Name: "bar"}}) # Usage diff --git a/pkg/envtest/binaries_test.go b/pkg/envtest/binaries_test.go index 70493c29c5..e5865cbc70 100644 --- a/pkg/envtest/binaries_test.go +++ b/pkg/envtest/binaries_test.go @@ -20,7 +20,6 @@ import ( "archive/tar" "bytes" "compress/gzip" - "context" "crypto/rand" "crypto/sha512" "encoding/hex" @@ -50,8 +49,8 @@ var _ = Describe("Test download binaries", func() { setupServer(server) }) - It("should download binaries of latest stable version", func() { - apiServerPath, etcdPath, kubectlPath, err := downloadBinaryAssets(context.Background(), downloadDirectory, "", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml")) + It("should download binaries of latest stable version", func(ctx SpecContext) { + apiServerPath, etcdPath, kubectlPath, err := downloadBinaryAssets(ctx, downloadDirectory, "", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml")) Expect(err).ToNot(HaveOccurred()) // Verify latest stable version (v1.32.0) was downloaded @@ -69,8 +68,8 @@ var _ = Describe("Test download binaries", func() { Expect(actualFiles).To(ConsistOf("some-file")) }) - It("should download v1.32.0 binaries", func() { - apiServerPath, etcdPath, kubectlPath, err := downloadBinaryAssets(context.Background(), downloadDirectory, "v1.31.0", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml")) + It("should download v1.32.0 binaries", func(ctx SpecContext) { + apiServerPath, etcdPath, kubectlPath, err := downloadBinaryAssets(ctx, downloadDirectory, "v1.31.0", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml")) Expect(err).ToNot(HaveOccurred()) // Verify latest stable version (v1.32.0) was downloaded diff --git a/pkg/envtest/envtest_test.go b/pkg/envtest/envtest_test.go index ae30a8405a..ce3e9a4d3f 100644 --- a/pkg/envtest/envtest_test.go +++ b/pkg/envtest/envtest_test.go @@ -17,7 +17,6 @@ limitations under the License. package envtest import ( - "context" "path/filepath" "time" @@ -55,29 +54,29 @@ var _ = Describe("Test", func() { }) // Cleanup CRDs - AfterEach(func() { + AfterEach(func(ctx SpecContext) { for _, crd := range crds { // Delete only if CRD exists. crdObjectKey := client.ObjectKey{ Name: crd.GetName(), } var placeholder apiextensionsv1.CustomResourceDefinition - if err = c.Get(context.TODO(), crdObjectKey, &placeholder); err != nil && + if err = c.Get(ctx, crdObjectKey, &placeholder); err != nil && apierrors.IsNotFound(err) { // CRD doesn't need to be deleted. continue } Expect(err).NotTo(HaveOccurred()) - Expect(c.Delete(context.TODO(), crd)).To(Succeed()) + Expect(c.Delete(ctx, crd)).To(Succeed()) Eventually(func() bool { - err := c.Get(context.TODO(), crdObjectKey, &placeholder) + err := c.Get(ctx, crdObjectKey, &placeholder) return apierrors.IsNotFound(err) }, 5*time.Second).Should(BeTrue()) } }, teardownTimeoutSeconds) Describe("InstallCRDs", func() { - It("should install the unserved CRDs into the cluster", func() { + It("should install the unserved CRDs into the cluster", func(ctx SpecContext) { crds, err = InstallCRDs(env.Config, CRDInstallOptions{ Paths: []string{filepath.Join(".", "testdata", "crds", "examplecrd_unserved.yaml")}, }) @@ -86,7 +85,7 @@ var _ = Describe("Test", func() { // Expect to find the CRDs crd := &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "frigates.ship.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "frigates.ship.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Frigate")) @@ -115,7 +114,7 @@ var _ = Describe("Test", func() { ) Expect(err).NotTo(HaveOccurred()) }) - It("should install the CRDs into the cluster using directory", func() { + It("should install the CRDs into the cluster using directory", func(ctx SpecContext) { crds, err = InstallCRDs(env.Config, CRDInstallOptions{ Paths: []string{validDirectory}, }) @@ -124,27 +123,27 @@ var _ = Describe("Test", func() { // Expect to find the CRDs crd := &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "foos.bar.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "foos.bar.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Foo")) crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "bazs.qux.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "bazs.qux.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Baz")) crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "captains.crew.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "captains.crew.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Captain")) crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "firstmates.crew.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "firstmates.crew.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("FirstMate")) crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "drivers.crew.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "drivers.crew.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Driver")) @@ -244,14 +243,14 @@ var _ = Describe("Test", func() { Expect(err).NotTo(HaveOccurred()) }) - It("should install the CRDs into the cluster using file", func() { + It("should install the CRDs into the cluster using file", func(ctx SpecContext) { crds, err = InstallCRDs(env.Config, CRDInstallOptions{ Paths: []string{filepath.Join(".", "testdata", "crds", "examplecrd3.yaml")}, }) Expect(err).NotTo(HaveOccurred()) crd := &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "configs.foo.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "configs.foo.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Config")) @@ -290,7 +289,7 @@ var _ = Describe("Test", func() { Expect(crds).To(HaveLen(2)) }) - It("should filter out already existent CRD", func() { + It("should filter out already existent CRD", func(ctx SpecContext) { crds, err = InstallCRDs(env.Config, CRDInstallOptions{ Paths: []string{ filepath.Join(".", "testdata"), @@ -300,7 +299,7 @@ var _ = Describe("Test", func() { Expect(err).NotTo(HaveOccurred()) crd := &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "foos.bar.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "foos.bar.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Foo")) @@ -422,7 +421,7 @@ var _ = Describe("Test", func() { Expect(err).To(HaveOccurred()) }) - It("should reinstall the CRDs if already present in the cluster", func() { + It("should reinstall the CRDs if already present in the cluster", func(ctx SpecContext) { crds, err = InstallCRDs(env.Config, CRDInstallOptions{ Paths: []string{filepath.Join(".", "testdata")}, @@ -432,27 +431,27 @@ var _ = Describe("Test", func() { // Expect to find the CRDs crd := &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "foos.bar.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "foos.bar.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Foo")) crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "bazs.qux.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "bazs.qux.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Baz")) crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "captains.crew.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "captains.crew.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Captain")) crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "firstmates.crew.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "firstmates.crew.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("FirstMate")) crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "drivers.crew.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "drivers.crew.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Driver")) @@ -561,27 +560,27 @@ var _ = Describe("Test", func() { // Expect to find the CRDs crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "foos.bar.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "foos.bar.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Foo")) crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "bazs.qux.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "bazs.qux.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Baz")) crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "captains.crew.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "captains.crew.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Captain")) crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "firstmates.crew.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "firstmates.crew.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("FirstMate")) crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "drivers.crew.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "drivers.crew.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Driver")) @@ -682,15 +681,15 @@ var _ = Describe("Test", func() { }) }) - It("should set a working KubeConfig", func() { + It("should set a working KubeConfig", func(ctx SpecContext) { kubeconfigRESTConfig, err := clientcmd.RESTConfigFromKubeConfig(env.KubeConfig) Expect(err).ToNot(HaveOccurred()) kubeconfigClient, err := client.New(kubeconfigRESTConfig, client.Options{Scheme: s}) Expect(err).NotTo(HaveOccurred()) - Expect(kubeconfigClient.List(context.Background(), &apiextensionsv1.CustomResourceDefinitionList{})).To(Succeed()) + Expect(kubeconfigClient.List(ctx, &apiextensionsv1.CustomResourceDefinitionList{})).To(Succeed()) }) - It("should update CRDs if already present in the cluster", func() { + It("should update CRDs if already present in the cluster", func(ctx SpecContext) { // Install only the CRDv1 multi-version example crds, err = InstallCRDs(env.Config, CRDInstallOptions{ @@ -701,7 +700,7 @@ var _ = Describe("Test", func() { // Expect to find the CRDs crd := &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "drivers.crew.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "drivers.crew.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Driver")) Expect(len(crd.Spec.Versions)).To(BeEquivalentTo(2)) @@ -743,7 +742,7 @@ var _ = Describe("Test", func() { // Expect to find updated CRD crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "drivers.crew.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "drivers.crew.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Driver")) Expect(len(crd.Spec.Versions)).To(BeEquivalentTo(3)) @@ -781,8 +780,7 @@ var _ = Describe("Test", func() { }) Describe("UninstallCRDs", func() { - It("should uninstall the CRDs from the cluster", func() { - + It("should uninstall the CRDs from the cluster", func(ctx SpecContext) { crds, err = InstallCRDs(env.Config, CRDInstallOptions{ Paths: []string{validDirectory}, }) @@ -791,27 +789,27 @@ var _ = Describe("Test", func() { // Expect to find the CRDs crd := &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "foos.bar.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "foos.bar.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Foo")) crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "bazs.qux.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "bazs.qux.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Baz")) crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "captains.crew.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "captains.crew.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Captain")) crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "firstmates.crew.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "firstmates.crew.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("FirstMate")) crd = &apiextensionsv1.CustomResourceDefinition{} - err = c.Get(context.TODO(), types.NamespacedName{Name: "drivers.crew.example.com"}, crd) + err = c.Get(ctx, types.NamespacedName{Name: "drivers.crew.example.com"}, crd) Expect(err).NotTo(HaveOccurred()) Expect(crd.Spec.Names.Kind).To(Equal("Driver")) @@ -927,7 +925,7 @@ var _ = Describe("Test", func() { placeholder := &apiextensionsv1.CustomResourceDefinition{} Eventually(func() bool { for _, crd := range crds { - err = c.Get(context.TODO(), types.NamespacedName{Name: crd}, placeholder) + err = c.Get(ctx, types.NamespacedName{Name: crd}, placeholder) notFound := err != nil && apierrors.IsNotFound(err) if !notFound { return false diff --git a/pkg/envtest/webhook_test.go b/pkg/envtest/webhook_test.go index 79a5227088..47550fa147 100644 --- a/pkg/envtest/webhook_test.go +++ b/pkg/envtest/webhook_test.go @@ -37,9 +37,8 @@ import ( ) var _ = Describe("Test", func() { - Describe("Webhook", func() { - It("should reject create request for webhook that rejects all requests", func() { + It("should reject create request for webhook that rejects all requests", func(specCtx SpecContext) { m, err := manager.New(env.Config, manager.Options{ WebhookServer: webhook.NewServer(webhook.Options{ Port: env.WebhookInstallOptions.LocalServingPort, @@ -52,7 +51,7 @@ var _ = Describe("Test", func() { server := m.GetWebhookServer() server.Register("/failing", &webhook.Admission{Handler: &rejectingValidator{}}) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) go func() { _ = server.Start(ctx) }() @@ -88,7 +87,7 @@ var _ = Describe("Test", func() { } Eventually(func() bool { - err = c.Create(context.TODO(), obj) + err = c.Create(ctx, obj) return err != nil && strings.HasSuffix(err.Error(), "Always denied") && apierrors.ReasonForError(err) == metav1.StatusReasonForbidden }, 1*time.Second).Should(BeTrue()) diff --git a/pkg/finalizer/finalizer_test.go b/pkg/finalizer/finalizer_test.go index 02fbdf003c..c6848f6473 100644 --- a/pkg/finalizer/finalizer_test.go +++ b/pkg/finalizer/finalizer_test.go @@ -57,14 +57,14 @@ var _ = Describe("TestFinalizer", func() { }) Describe("Finalize", func() { - It("successfully finalizes and returns true for Updated when deletion timestamp is nil and finalizer does not exist", func() { + It("successfully finalizes and returns true for Updated when deletion timestamp is nil and finalizer does not exist", func(ctx SpecContext) { err = finalizers.Register("finalizers.sigs.k8s.io/testfinalizer", f) Expect(err).ToNot(HaveOccurred()) pod.DeletionTimestamp = nil pod.Finalizers = []string{} - result, err := finalizers.Finalize(context.TODO(), pod) + result, err := finalizers.Finalize(ctx, pod) Expect(err).ToNot(HaveOccurred()) Expect(result.Updated).To(BeTrue()) // when deletion timestamp is nil and finalizer is not present, the registered finalizer would be added to the obj @@ -73,7 +73,7 @@ var _ = Describe("TestFinalizer", func() { }) - It("successfully finalizes and returns true for Updated when deletion timestamp is not nil and the finalizer exists", func() { + It("successfully finalizes and returns true for Updated when deletion timestamp is not nil and the finalizer exists", func(ctx SpecContext) { now := metav1.Now() pod.DeletionTimestamp = &now @@ -82,37 +82,37 @@ var _ = Describe("TestFinalizer", func() { pod.Finalizers = []string{"finalizers.sigs.k8s.io/testfinalizer"} - result, err := finalizers.Finalize(context.TODO(), pod) + result, err := finalizers.Finalize(ctx, pod) Expect(err).ToNot(HaveOccurred()) Expect(result.Updated).To(BeTrue()) // finalizer will be removed from the obj upon successful finalization Expect(pod.Finalizers).To(BeEmpty()) }) - It("should return no error and return false for Updated when deletion timestamp is nil and finalizer doesn't exist", func() { + It("should return no error and return false for Updated when deletion timestamp is nil and finalizer doesn't exist", func(ctx SpecContext) { pod.DeletionTimestamp = nil pod.Finalizers = []string{} - result, err := finalizers.Finalize(context.TODO(), pod) + result, err := finalizers.Finalize(ctx, pod) Expect(err).ToNot(HaveOccurred()) Expect(result.Updated).To(BeFalse()) Expect(pod.Finalizers).To(BeEmpty()) }) - It("should return no error and return false for Updated when deletion timestamp is not nil and the finalizer doesn't exist", func() { + It("should return no error and return false for Updated when deletion timestamp is not nil and the finalizer doesn't exist", func(ctx SpecContext) { now := metav1.Now() pod.DeletionTimestamp = &now pod.Finalizers = []string{} - result, err := finalizers.Finalize(context.TODO(), pod) + result, err := finalizers.Finalize(ctx, pod) Expect(err).ToNot(HaveOccurred()) Expect(result.Updated).To(BeFalse()) Expect(pod.Finalizers).To(BeEmpty()) }) - It("successfully finalizes multiple finalizers and returns true for Updated when deletion timestamp is not nil and the finalizer exists", func() { + It("successfully finalizes multiple finalizers and returns true for Updated when deletion timestamp is not nil and the finalizer exists", func(ctx SpecContext) { now := metav1.Now() pod.DeletionTimestamp = &now @@ -124,14 +124,14 @@ var _ = Describe("TestFinalizer", func() { pod.Finalizers = []string{"finalizers.sigs.k8s.io/testfinalizer", "finalizers.sigs.k8s.io/newtestfinalizer"} - result, err := finalizers.Finalize(context.TODO(), pod) + result, err := finalizers.Finalize(ctx, pod) Expect(err).ToNot(HaveOccurred()) Expect(result.Updated).To(BeTrue()) Expect(result.StatusUpdated).To(BeFalse()) Expect(pod.Finalizers).To(BeEmpty()) }) - It("should return result as false and a non-nil error", func() { + It("should return result as false and a non-nil error", func(ctx SpecContext) { now := metav1.Now() pod.DeletionTimestamp = &now pod.Finalizers = []string{"finalizers.sigs.k8s.io/testfinalizer"} @@ -143,7 +143,7 @@ var _ = Describe("TestFinalizer", func() { err = finalizers.Register("finalizers.sigs.k8s.io/testfinalizer", f) Expect(err).ToNot(HaveOccurred()) - result, err := finalizers.Finalize(context.TODO(), pod) + result, err := finalizers.Finalize(ctx, pod) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("finalizer failed")) Expect(result.Updated).To(BeFalse()) @@ -152,7 +152,7 @@ var _ = Describe("TestFinalizer", func() { Expect(pod.Finalizers[0]).To(Equal("finalizers.sigs.k8s.io/testfinalizer")) }) - It("should return expected result values and error values when registering multiple finalizers", func() { + It("should return expected result values and error values when registering multiple finalizers", func(ctx SpecContext) { now := metav1.Now() pod.DeletionTimestamp = &now pod.Finalizers = []string{ @@ -169,7 +169,7 @@ var _ = Describe("TestFinalizer", func() { err = finalizers.Register("finalizers.sigs.k8s.io/testfinalizer1", f) Expect(err).ToNot(HaveOccurred()) - result, err := finalizers.Finalize(context.TODO(), pod) + result, err := finalizers.Finalize(ctx, pod) Expect(err).ToNot(HaveOccurred()) Expect(result.Updated).To(BeTrue()) Expect(result.StatusUpdated).To(BeFalse()) @@ -186,7 +186,7 @@ var _ = Describe("TestFinalizer", func() { err = finalizers.Register("finalizers.sigs.k8s.io/testfinalizer2", f) Expect(err).ToNot(HaveOccurred()) - result, err = finalizers.Finalize(context.TODO(), pod) + result, err = finalizers.Finalize(ctx, pod) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("finalizer failed")) Expect(result.Updated).To(BeFalse()) @@ -202,7 +202,7 @@ var _ = Describe("TestFinalizer", func() { err = finalizers.Register("finalizers.sigs.k8s.io/testfinalizer3", f) Expect(err).ToNot(HaveOccurred()) - result, err = finalizers.Finalize(context.TODO(), pod) + result, err = finalizers.Finalize(ctx, pod) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("finalizer failed")) Expect(result.Updated).To(BeTrue()) diff --git a/pkg/handler/eventhandler_test.go b/pkg/handler/eventhandler_test.go index 7cf5076e7c..5e9bf9974b 100644 --- a/pkg/handler/eventhandler_test.go +++ b/pkg/handler/eventhandler_test.go @@ -41,7 +41,6 @@ import ( ) var _ = Describe("Eventhandler", func() { - var ctx = context.Background() var q workqueue.TypedRateLimitingInterface[reconcile.Request] var instance handler.EnqueueRequestForObject var pod *corev1.Pod @@ -60,7 +59,7 @@ var _ = Describe("Eventhandler", func() { }) Describe("EnqueueRequestForObject", func() { - It("should enqueue a Request with the Name / Namespace of the object in the CreateEvent.", func() { + It("should enqueue a Request with the Name / Namespace of the object in the CreateEvent.", func(ctx SpecContext) { evt := event.CreateEvent{ Object: pod, } @@ -71,7 +70,7 @@ var _ = Describe("Eventhandler", func() { Expect(req.NamespacedName).To(Equal(types.NamespacedName{Namespace: "biz", Name: "baz"})) }) - It("should enqueue a Request with the Name / Namespace of the object in the DeleteEvent.", func() { + It("should enqueue a Request with the Name / Namespace of the object in the DeleteEvent.", func(ctx SpecContext) { evt := event.DeleteEvent{ Object: pod, } @@ -83,7 +82,7 @@ var _ = Describe("Eventhandler", func() { }) It("should enqueue a Request with the Name / Namespace of one object in the UpdateEvent.", - func() { + func(ctx SpecContext) { newPod := pod.DeepCopy() newPod.Name = "baz2" newPod.Namespace = "biz2" @@ -99,7 +98,7 @@ var _ = Describe("Eventhandler", func() { Expect(req.NamespacedName).To(Equal(types.NamespacedName{Namespace: "biz2", Name: "baz2"})) }) - It("should enqueue a Request with the Name / Namespace of the object in the GenericEvent.", func() { + It("should enqueue a Request with the Name / Namespace of the object in the GenericEvent.", func(ctx SpecContext) { evt := event.GenericEvent{ Object: pod, } @@ -110,7 +109,7 @@ var _ = Describe("Eventhandler", func() { }) Context("for a runtime.Object without Object", func() { - It("should do nothing if the Object is missing for a CreateEvent.", func() { + It("should do nothing if the Object is missing for a CreateEvent.", func(ctx SpecContext) { evt := event.CreateEvent{ Object: nil, } @@ -118,7 +117,7 @@ var _ = Describe("Eventhandler", func() { Expect(q.Len()).To(Equal(0)) }) - It("should do nothing if the Object is missing for a UpdateEvent.", func() { + It("should do nothing if the Object is missing for a UpdateEvent.", func(ctx SpecContext) { newPod := pod.DeepCopy() newPod.Name = "baz2" newPod.Namespace = "biz2" @@ -140,7 +139,7 @@ var _ = Describe("Eventhandler", func() { Expect(req.NamespacedName).To(Equal(types.NamespacedName{Namespace: "biz", Name: "baz"})) }) - It("should do nothing if the Object is missing for a DeleteEvent.", func() { + It("should do nothing if the Object is missing for a DeleteEvent.", func(ctx SpecContext) { evt := event.DeleteEvent{ Object: nil, } @@ -148,7 +147,7 @@ var _ = Describe("Eventhandler", func() { Expect(q.Len()).To(Equal(0)) }) - It("should do nothing if the Object is missing for a GenericEvent.", func() { + It("should do nothing if the Object is missing for a GenericEvent.", func(ctx SpecContext) { evt := event.GenericEvent{ Object: nil, } @@ -159,7 +158,7 @@ var _ = Describe("Eventhandler", func() { }) Describe("EnqueueRequestsFromMapFunc", func() { - It("should enqueue a Request with the function applied to the CreateEvent.", func() { + It("should enqueue a Request with the function applied to the CreateEvent.", func(ctx SpecContext) { req := []reconcile.Request{} instance := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, a client.Object) []reconcile.Request { defer GinkgoRecover() @@ -191,7 +190,7 @@ var _ = Describe("Eventhandler", func() { )) }) - It("should enqueue a Request with the function applied to the DeleteEvent.", func() { + It("should enqueue a Request with the function applied to the DeleteEvent.", func(ctx SpecContext) { req := []reconcile.Request{} instance := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, a client.Object) []reconcile.Request { defer GinkgoRecover() @@ -224,7 +223,7 @@ var _ = Describe("Eventhandler", func() { }) It("should enqueue a Request with the function applied to both objects in the UpdateEvent.", - func() { + func(ctx SpecContext) { newPod := pod.DeepCopy() req := []reconcile.Request{} @@ -256,7 +255,7 @@ var _ = Describe("Eventhandler", func() { Expect(i).To(Equal(reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "biz", Name: "baz-baz"}})) }) - It("should enqueue a Request with the function applied to the GenericEvent.", func() { + It("should enqueue a Request with the function applied to the GenericEvent.", func(ctx SpecContext) { req := []reconcile.Request{} instance := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, a client.Object) []reconcile.Request { defer GinkgoRecover() @@ -290,7 +289,7 @@ var _ = Describe("Eventhandler", func() { }) Describe("EnqueueRequestForOwner", func() { - It("should enqueue a Request with the Owner of the object in the CreateEvent.", func() { + It("should enqueue a Request with the Owner of the object in the CreateEvent.", func(ctx SpecContext) { instance := handler.EnqueueRequestForOwner(scheme.Scheme, mapper, &appsv1.ReplicaSet{}) pod.OwnerReferences = []metav1.OwnerReference{ @@ -311,7 +310,7 @@ var _ = Describe("Eventhandler", func() { NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "foo-parent"}})) }) - It("should enqueue a Request with the Owner of the object in the DeleteEvent.", func() { + It("should enqueue a Request with the Owner of the object in the DeleteEvent.", func(ctx SpecContext) { instance := handler.EnqueueRequestForOwner(scheme.Scheme, mapper, &appsv1.ReplicaSet{}) pod.OwnerReferences = []metav1.OwnerReference{ @@ -332,7 +331,7 @@ var _ = Describe("Eventhandler", func() { NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "foo-parent"}})) }) - It("should enqueue a Request with the Owners of both objects in the UpdateEvent.", func() { + It("should enqueue a Request with the Owners of both objects in the UpdateEvent.", func(ctx SpecContext) { newPod := pod.DeepCopy() newPod.Name = pod.Name + "2" newPod.Namespace = pod.Namespace + "2" @@ -370,7 +369,7 @@ var _ = Describe("Eventhandler", func() { )) }) - It("should enqueue a Request with the one duplicate Owner of both objects in the UpdateEvent.", func() { + It("should enqueue a Request with the one duplicate Owner of both objects in the UpdateEvent.", func(ctx SpecContext) { newPod := pod.DeepCopy() newPod.Name = pod.Name + "2" @@ -402,7 +401,7 @@ var _ = Describe("Eventhandler", func() { NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "foo-parent"}})) }) - It("should enqueue a Request with the Owner of the object in the GenericEvent.", func() { + It("should enqueue a Request with the Owner of the object in the GenericEvent.", func(ctx SpecContext) { instance := handler.EnqueueRequestForOwner(scheme.Scheme, mapper, &appsv1.ReplicaSet{}) pod.OwnerReferences = []metav1.OwnerReference{ { @@ -422,7 +421,7 @@ var _ = Describe("Eventhandler", func() { NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "foo-parent"}})) }) - It("should not enqueue a Request if there are no owners matching Group and Kind.", func() { + It("should not enqueue a Request if there are no owners matching Group and Kind.", func(ctx SpecContext) { instance := handler.EnqueueRequestForOwner(scheme.Scheme, mapper, &appsv1.ReplicaSet{}, handler.OnlyControllerOwner()) pod.OwnerReferences = []metav1.OwnerReference{ { // Wrong group @@ -444,7 +443,7 @@ var _ = Describe("Eventhandler", func() { }) It("should enqueue a Request if there are owners matching Group "+ - "and Kind with a different version.", func() { + "and Kind with a different version.", func(ctx SpecContext) { instance := handler.EnqueueRequestForOwner(scheme.Scheme, mapper, &autoscalingv1.HorizontalPodAutoscaler{}) pod.OwnerReferences = []metav1.OwnerReference{ { @@ -464,7 +463,7 @@ var _ = Describe("Eventhandler", func() { NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "foo-parent"}})) }) - It("should enqueue a Request for a owner that is cluster scoped", func() { + It("should enqueue a Request for a owner that is cluster scoped", func(ctx SpecContext) { instance := handler.EnqueueRequestForOwner(scheme.Scheme, mapper, &corev1.Node{}) pod.OwnerReferences = []metav1.OwnerReference{ { @@ -485,7 +484,7 @@ var _ = Describe("Eventhandler", func() { }) - It("should not enqueue a Request if there are no owners.", func() { + It("should not enqueue a Request if there are no owners.", func(ctx SpecContext) { instance := handler.EnqueueRequestForOwner(scheme.Scheme, mapper, &appsv1.ReplicaSet{}) evt := event.CreateEvent{ Object: pod, @@ -496,7 +495,7 @@ var _ = Describe("Eventhandler", func() { Context("with the Controller field set to true", func() { It("should enqueue reconcile.Requests for only the first the Controller if there are "+ - "multiple Controller owners.", func() { + "multiple Controller owners.", func(ctx SpecContext) { instance := handler.EnqueueRequestForOwner(scheme.Scheme, mapper, &appsv1.ReplicaSet{}, handler.OnlyControllerOwner()) pod.OwnerReferences = []metav1.OwnerReference{ { @@ -537,7 +536,7 @@ var _ = Describe("Eventhandler", func() { NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "foo2-parent"}})) }) - It("should not enqueue reconcile.Requests if there are no Controller owners.", func() { + It("should not enqueue reconcile.Requests if there are no Controller owners.", func(ctx SpecContext) { instance := handler.EnqueueRequestForOwner(scheme.Scheme, mapper, &appsv1.ReplicaSet{}, handler.OnlyControllerOwner()) pod.OwnerReferences = []metav1.OwnerReference{ { @@ -563,7 +562,7 @@ var _ = Describe("Eventhandler", func() { Expect(q.Len()).To(Equal(0)) }) - It("should not enqueue reconcile.Requests if there are no owners.", func() { + It("should not enqueue reconcile.Requests if there are no owners.", func(ctx SpecContext) { instance := handler.EnqueueRequestForOwner(scheme.Scheme, mapper, &appsv1.ReplicaSet{}, handler.OnlyControllerOwner()) evt := event.CreateEvent{ Object: pod, @@ -574,7 +573,7 @@ var _ = Describe("Eventhandler", func() { }) Context("with the Controller field set to false", func() { - It("should enqueue a reconcile.Requests for all owners.", func() { + It("should enqueue a reconcile.Requests for all owners.", func(ctx SpecContext) { instance := handler.EnqueueRequestForOwner(scheme.Scheme, mapper, &appsv1.ReplicaSet{}) pod.OwnerReferences = []metav1.OwnerReference{ { @@ -614,7 +613,7 @@ var _ = Describe("Eventhandler", func() { }) Context("with a nil object", func() { - It("should do nothing.", func() { + It("should do nothing.", func(ctx SpecContext) { instance := handler.EnqueueRequestForOwner(scheme.Scheme, mapper, &appsv1.ReplicaSet{}) pod.OwnerReferences = []metav1.OwnerReference{ { @@ -640,7 +639,7 @@ var _ = Describe("Eventhandler", func() { }) Context("with an invalid APIVersion in the OwnerReference", func() { - It("should do nothing.", func() { + It("should do nothing.", func(ctx SpecContext) { instance := handler.EnqueueRequestForOwner(scheme.Scheme, mapper, &appsv1.ReplicaSet{}) pod.OwnerReferences = []metav1.OwnerReference{ { @@ -678,7 +677,7 @@ var _ = Describe("Eventhandler", func() { }, } - It("should call CreateFunc for a CreateEvent if provided.", func() { + It("should call CreateFunc for a CreateEvent if provided.", func(ctx SpecContext) { instance := failingFuncs evt := event.CreateEvent{ Object: pod, @@ -691,7 +690,7 @@ var _ = Describe("Eventhandler", func() { instance.Create(ctx, evt, q) }) - It("should NOT call CreateFunc for a CreateEvent if NOT provided.", func() { + It("should NOT call CreateFunc for a CreateEvent if NOT provided.", func(ctx SpecContext) { instance := failingFuncs instance.CreateFunc = nil evt := event.CreateEvent{ @@ -700,7 +699,7 @@ var _ = Describe("Eventhandler", func() { instance.Create(ctx, evt, q) }) - It("should call UpdateFunc for an UpdateEvent if provided.", func() { + It("should call UpdateFunc for an UpdateEvent if provided.", func(ctx SpecContext) { newPod := pod.DeepCopy() newPod.Name = pod.Name + "2" newPod.Namespace = pod.Namespace + "2" @@ -719,7 +718,7 @@ var _ = Describe("Eventhandler", func() { instance.Update(ctx, evt, q) }) - It("should NOT call UpdateFunc for an UpdateEvent if NOT provided.", func() { + It("should NOT call UpdateFunc for an UpdateEvent if NOT provided.", func(ctx SpecContext) { newPod := pod.DeepCopy() newPod.Name = pod.Name + "2" newPod.Namespace = pod.Namespace + "2" @@ -730,7 +729,7 @@ var _ = Describe("Eventhandler", func() { instance.Update(ctx, evt, q) }) - It("should call DeleteFunc for a DeleteEvent if provided.", func() { + It("should call DeleteFunc for a DeleteEvent if provided.", func(ctx SpecContext) { instance := failingFuncs evt := event.DeleteEvent{ Object: pod, @@ -743,7 +742,7 @@ var _ = Describe("Eventhandler", func() { instance.Delete(ctx, evt, q) }) - It("should NOT call DeleteFunc for a DeleteEvent if NOT provided.", func() { + It("should NOT call DeleteFunc for a DeleteEvent if NOT provided.", func(ctx SpecContext) { instance := failingFuncs instance.DeleteFunc = nil evt := event.DeleteEvent{ @@ -752,7 +751,7 @@ var _ = Describe("Eventhandler", func() { instance.Delete(ctx, evt, q) }) - It("should call GenericFunc for a GenericEvent if provided.", func() { + It("should call GenericFunc for a GenericEvent if provided.", func(ctx SpecContext) { instance := failingFuncs evt := event.GenericEvent{ Object: pod, @@ -765,7 +764,7 @@ var _ = Describe("Eventhandler", func() { instance.Generic(ctx, evt, q) }) - It("should NOT call GenericFunc for a GenericEvent if NOT provided.", func() { + It("should NOT call GenericFunc for a GenericEvent if NOT provided.", func(ctx SpecContext) { instance := failingFuncs instance.GenericFunc = nil evt := event.GenericEvent{ @@ -841,7 +840,7 @@ var _ = Describe("Eventhandler", func() { } for _, test := range handlerPriorityTests { When("handler is "+test.name, func() { - It("should lower the priority of a create request for an object that was part of the initial list", func() { + It("should lower the priority of a create request for an object that was part of the initial list", func(ctx SpecContext) { actualOpts := priorityqueue.AddOpts{} var actualRequests []reconcile.Request wq := &fakePriorityQueue{ @@ -867,7 +866,7 @@ var _ = Describe("Eventhandler", func() { Expect(actualRequests).To(Equal([]reconcile.Request{{NamespacedName: types.NamespacedName{Name: "my-pod"}}})) }) - It("should not lower the priority of a create request for an object that was not part of the initial list", func() { + It("should not lower the priority of a create request for an object that was not part of the initial list", func(ctx SpecContext) { actualOpts := priorityqueue.AddOpts{} var actualRequests []reconcile.Request wq := &fakePriorityQueue{ @@ -893,7 +892,7 @@ var _ = Describe("Eventhandler", func() { Expect(actualRequests).To(Equal([]reconcile.Request{{NamespacedName: types.NamespacedName{Name: "my-pod"}}})) }) - It("should lower the priority of an update request with unchanged RV", func() { + It("should lower the priority of an update request with unchanged RV", func(ctx SpecContext) { actualOpts := priorityqueue.AddOpts{} var actualRequests []reconcile.Request wq := &fakePriorityQueue{ @@ -924,7 +923,7 @@ var _ = Describe("Eventhandler", func() { Expect(actualRequests).To(Equal([]reconcile.Request{{NamespacedName: types.NamespacedName{Name: "my-pod"}}})) }) - It("should not lower the priority of an update request with changed RV", func() { + It("should not lower the priority of an update request with changed RV", func(ctx SpecContext) { actualOpts := priorityqueue.AddOpts{} var actualRequests []reconcile.Request wq := &fakePriorityQueue{ @@ -956,7 +955,7 @@ var _ = Describe("Eventhandler", func() { Expect(actualRequests).To(Equal([]reconcile.Request{{NamespacedName: types.NamespacedName{Name: "my-pod"}}})) }) - It("should have no effect on create if the workqueue is not a priorityqueue", func() { + It("should have no effect on create if the workqueue is not a priorityqueue", func(ctx SpecContext) { test.handler().Create(ctx, event.CreateEvent{ Object: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{ Name: "my-pod", @@ -972,7 +971,7 @@ var _ = Describe("Eventhandler", func() { Expect(item).To(Equal(reconcile.Request{NamespacedName: types.NamespacedName{Name: "my-pod"}})) }) - It("should have no effect on Update if the workqueue is not a priorityqueue", func() { + It("should have no effect on Update if the workqueue is not a priorityqueue", func(ctx SpecContext) { test.handler().Update(ctx, event.UpdateEvent{ ObjectOld: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{ Name: "my-pod", diff --git a/pkg/internal/controller/controller.go b/pkg/internal/controller/controller.go index 9fa7ec71e1..18042e41f6 100644 --- a/pkg/internal/controller/controller.go +++ b/pkg/internal/controller/controller.go @@ -38,6 +38,51 @@ import ( "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 +} + // 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. @@ -83,6 +128,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 @@ -95,6 +148,35 @@ 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 +} + +// 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, + } } // Reconcile implements reconcile.Reconciler. @@ -124,10 +206,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 } @@ -144,6 +225,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 @@ -158,17 +254,6 @@ func (c *Controller[request]) Start(ctx context.Context) error { // Set the internal context. c.ctx = ctx - 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() - }() - wg := &sync.WaitGroup{} err := func() error { defer c.mu.Unlock() @@ -179,18 +264,12 @@ func (c *Controller[request]) Start(ctx context.Context) error { // NB(directxman12): launch the sources *before* trying to wait for the // caches to sync so that they have a chance to register their intended // caches. - if err := c.startEventSources(ctx); err != nil { + if err := c.startEventSourcesAndQueueLocked(ctx); err != nil { return err } c.LogConstructor(nil).Info("Starting Controller") - // 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) @@ -218,63 +297,90 @@ func (c *Controller[request]) Start(ctx context.Context) error { return nil } -// startEventSources launches all the sources registered with this controller and waits +// 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]) startEventSources(ctx context.Context) error { - 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)) +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 { - log = log.WithValues("source", fmt.Sprintf("%s", watch)) + c.Queue = &priorityQueueWrapper[request]{TypedRateLimitingInterface: queue} } - 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 + 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() - 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) - } - }) - } - return 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 diff --git a/pkg/internal/controller/controller_test.go b/pkg/internal/controller/controller_test.go index 3fde5da9c8..f30b060528 100644 --- a/pkg/internal/controller/controller_test.go +++ b/pkg/internal/controller/controller_test.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "sync" + "sync/atomic" "time" "github.com/go-logr/logr" @@ -28,6 +29,7 @@ import ( . "github.com/onsi/gomega" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "go.uber.org/goleak" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -39,10 +41,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllertest" "sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue" + "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics" "sigs.k8s.io/controller-runtime/pkg/internal/log" + "sigs.k8s.io/controller-runtime/pkg/leaderelection" + fakeleaderelection "sigs.k8s.io/controller-runtime/pkg/leaderelection/fake" + "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" ) @@ -51,6 +57,8 @@ type TestRequest struct { Key string } +const testControllerName = "testcontroller" + var _ = Describe("controller", func() { var fakeReconcile *fakeReconciler var ctrl *Controller[reconcile.Request] @@ -69,7 +77,7 @@ var _ = Describe("controller", func() { queue = &controllertest.Queue{ TypedInterface: workqueue.NewTyped[reconcile.Request](), } - ctrl = &Controller[reconcile.Request]{ + ctrl = New[reconcile.Request](Options[reconcile.Request]{ MaxConcurrentReconciles: 1, Do: fakeReconcile, NewQueue: func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] { @@ -78,14 +86,11 @@ var _ = Describe("controller", func() { LogConstructor: func(_ *reconcile.Request) logr.Logger { return log.RuntimeLog.WithName("controller").WithName("test") }, - } + }) }) Describe("Reconciler", func() { - It("should call the Reconciler function", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - + It("should call the Reconciler function", func(ctx SpecContext) { ctrl.Do = reconcile.Func(func(context.Context, reconcile.Request) (reconcile.Result, error) { return reconcile.Result{Requeue: true}, nil }) @@ -95,10 +100,7 @@ var _ = Describe("controller", func() { Expect(result).To(Equal(reconcile.Result{Requeue: true})) }) - It("should not recover panic if RecoverPanic is false", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - + It("should not recover panic if RecoverPanic is false", func(ctx SpecContext) { defer func() { Expect(recover()).ShouldNot(BeNil()) }() @@ -111,10 +113,7 @@ var _ = Describe("controller", func() { reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"}}) }) - It("should recover panic if RecoverPanic is true by default", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - + It("should recover panic if RecoverPanic is true by default", func(ctx SpecContext) { defer func() { Expect(recover()).To(BeNil()) }() @@ -129,10 +128,7 @@ var _ = Describe("controller", func() { Expect(err.Error()).To(ContainSubstring("[recovered]")) }) - It("should recover panic if RecoverPanic is true", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - + It("should recover panic if RecoverPanic is true", func(ctx SpecContext) { defer func() { Expect(recover()).To(BeNil()) }() @@ -149,21 +145,19 @@ var _ = Describe("controller", func() { }) Describe("Start", func() { - It("should return an error if there is an error waiting for the informers", func() { + It("should return an error if there is an error waiting for the informers", func(ctx SpecContext) { ctrl.CacheSyncTimeout = time.Second f := false ctrl.startWatches = []source.TypedSource[reconcile.Request]{ source.Kind(&informertest.FakeInformers{Synced: &f}, &corev1.Pod{}, &handler.TypedEnqueueRequestForObject[*corev1.Pod]{}), } ctrl.Name = "foo" - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() err := ctrl.Start(ctx) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to wait for foo caches to sync")) }) - It("should error when cache sync timeout occurs", func() { + It("should error when cache sync timeout occurs", func(ctx SpecContext) { c, err := cache.New(cfg, cache.Options{}) Expect(err).NotTo(HaveOccurred()) c = &cacheWithIndefinitelyBlockingGetInformer{c} @@ -172,14 +166,14 @@ var _ = Describe("controller", func() { ctrl.startWatches = []source.TypedSource[reconcile.Request]{ source.Kind(c, &appsv1.Deployment{}, &handler.TypedEnqueueRequestForObject[*appsv1.Deployment]{}), } - ctrl.Name = "testcontroller" + ctrl.Name = testControllerName - err = ctrl.Start(context.TODO()) + err = ctrl.Start(ctx) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to wait for testcontroller caches to sync kind source: *v1.Deployment: timed out waiting for cache to be synced")) }) - It("should not error when controller Start context is cancelled during Sources WaitForSync", func() { + It("should not error when controller Start context is cancelled during Sources WaitForSync", func(specCtx SpecContext) { ctrl.CacheSyncTimeout = 1 * time.Second sourceSynced := make(chan struct{}) @@ -192,9 +186,9 @@ var _ = Describe("controller", func() { cacheSyncDone: sourceSynced, }, } - ctrl.Name = "testcontroller" + ctrl.Name = testControllerName - ctx, cancel := context.WithCancel(context.TODO()) + ctx, cancel := context.WithCancel(specCtx) go func() { defer GinkgoRecover() err = ctrl.Start(ctx) @@ -205,7 +199,7 @@ var _ = Describe("controller", func() { <-sourceSynced }) - It("should error when Start() is blocking forever", func() { + It("should error when Start() is blocking forever", func(specCtx SpecContext) { ctrl.CacheSyncTimeout = time.Second controllerDone := make(chan struct{}) @@ -215,7 +209,7 @@ var _ = Describe("controller", func() { return ctx.Err() })} - ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second) + ctx, cancel := context.WithTimeout(specCtx, 10*time.Second) defer cancel() err := ctrl.Start(ctx) @@ -225,10 +219,8 @@ var _ = Describe("controller", func() { close(controllerDone) }) - It("should not error when cache sync timeout is of sufficiently high", func() { + It("should not error when cache sync timeout is of sufficiently high", func(ctx SpecContext) { ctrl.CacheSyncTimeout = 10 * time.Second - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() sourceSynced := make(chan struct{}) c := &informertest.FakeInformers{} @@ -247,16 +239,13 @@ var _ = Describe("controller", func() { <-sourceSynced }) - It("should process events from source.Channel", func() { + It("should process events from source.Channel", func(ctx SpecContext) { ctrl.CacheSyncTimeout = 10 * time.Second // channel to be closed when event is processed processed := make(chan struct{}) // source channel ch := make(chan event.GenericEvent, 1) - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - // event to be sent to the channel p := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}, @@ -287,10 +276,8 @@ var _ = Describe("controller", func() { <-processed }) - It("should error when channel source is not specified", func() { + It("should error when channel source is not specified", func(ctx SpecContext) { ctrl.CacheSyncTimeout = 10 * time.Second - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() ins := source.Channel[string](nil, nil) ctrl.startWatches = []source.TypedSource[reconcile.Request]{ins} @@ -300,10 +287,10 @@ var _ = Describe("controller", func() { Expect(e.Error()).To(ContainSubstring("must specify Channel.Source")) }) - It("should call Start on sources with the appropriate EventHandler, Queue, and Predicates", func() { + It("should call Start on sources with the appropriate EventHandler, Queue, and Predicates", func(specCtx SpecContext) { ctrl.CacheSyncTimeout = 10 * time.Second started := false - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) src := source.Func(func(ctx context.Context, q workqueue.TypedRateLimitingInterface[reconcile.Request]) error { defer GinkgoRecover() Expect(q).To(Equal(ctrl.Queue)) @@ -319,7 +306,7 @@ var _ = Describe("controller", func() { Expect(started).To(BeTrue()) }) - It("should return an error if there is an error starting sources", func() { + It("should return an error if there is an error starting sources", func(ctx SpecContext) { ctrl.CacheSyncTimeout = 10 * time.Second err := fmt.Errorf("Expected Error: could not start source") src := source.Func(func(context.Context, @@ -329,15 +316,12 @@ var _ = Describe("controller", func() { return err }) Expect(ctrl.Watch(src)).To(Succeed()) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() Expect(ctrl.Start(ctx)).To(Equal(err)) }) - It("should return an error if it gets started more than once", func() { + It("should return an error if it gets started more than once", func(specCtx SpecContext) { // Use a cancelled context so Start doesn't block - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) cancel() Expect(ctrl.Start(ctx)).To(Succeed()) err := ctrl.Start(ctx) @@ -345,19 +329,19 @@ var _ = Describe("controller", func() { Expect(err.Error()).To(Equal("controller was started more than once. This is likely to be caused by being added to a manager multiple times")) }) - It("should check for correct TypedSyncingSource if custom types are used", func() { + It("should check for correct TypedSyncingSource if custom types are used", func(specCtx SpecContext) { queue := &priorityQueueWrapper[TestRequest]{ TypedRateLimitingInterface: &controllertest.TypedQueue[TestRequest]{ TypedInterface: workqueue.NewTyped[TestRequest](), }} - ctrl := &Controller[TestRequest]{ + ctrl := New[TestRequest](Options[TestRequest]{ NewQueue: func(string, workqueue.TypedRateLimiter[TestRequest]) workqueue.TypedRateLimitingInterface[TestRequest] { return queue }, LogConstructor: func(*TestRequest) logr.Logger { return log.RuntimeLog.WithName("controller").WithName("test") }, - } + }) ctrl.CacheSyncTimeout = time.Second src := &bisignallingSource[TestRequest]{ startCall: make(chan workqueue.TypedRateLimitingInterface[TestRequest]), @@ -367,7 +351,7 @@ var _ = Describe("controller", func() { } ctrl.startWatches = []source.TypedSource[TestRequest]{src} ctrl.Name = "foo" - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) defer cancel() startCh := make(chan error) go func() { @@ -383,52 +367,47 @@ var _ = Describe("controller", func() { }) }) - Describe("startEventSources", func() { - It("should return nil when no sources are provided", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - + Describe("startEventSourcesAndQueueLocked", func() { + It("should return nil when no sources are provided", func(ctx SpecContext) { ctrl.startWatches = []source.TypedSource[reconcile.Request]{} - err := ctrl.startEventSources(ctx) + err := ctrl.startEventSourcesAndQueueLocked(ctx) Expect(err).NotTo(HaveOccurred()) }) - It("should return an error if a source fails to start", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + It("should initialize controller queue when called", func(ctx SpecContext) { + ctrl.startWatches = []source.TypedSource[reconcile.Request]{} + err := ctrl.startEventSourcesAndQueueLocked(ctx) + Expect(err).NotTo(HaveOccurred()) + Expect(ctrl.Queue).NotTo(BeNil()) + }) + It("should return an error if a source fails to start", func(ctx SpecContext) { expectedErr := fmt.Errorf("failed to start source") src := source.Func(func(ctx context.Context, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) error { // Return the error immediately so we don't get a timeout return expectedErr }) - // // Set a sufficiently long timeout to avoid timeouts interfering with the error being returned + // Set a sufficiently long timeout to avoid timeouts interfering with the error being returned ctrl.CacheSyncTimeout = 5 * time.Second ctrl.startWatches = []source.TypedSource[reconcile.Request]{src} - err := ctrl.startEventSources(ctx) + err := ctrl.startEventSourcesAndQueueLocked(ctx) Expect(err).To(Equal(expectedErr)) }) - It("should return an error if a source fails to sync", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - + It("should return an error if a source fails to sync", func(ctx SpecContext) { ctrl.startWatches = []source.TypedSource[reconcile.Request]{ source.Kind(&informertest.FakeInformers{Synced: ptr.To(false)}, &corev1.Pod{}, &handler.TypedEnqueueRequestForObject[*corev1.Pod]{}), } ctrl.Name = "test-controller" ctrl.CacheSyncTimeout = 5 * time.Second - err := ctrl.startEventSources(ctx) + err := ctrl.startEventSourcesAndQueueLocked(ctx) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to wait for test-controller caches to sync")) }) - It("should not return an error when sources start and sync successfully", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - + It("should not return an error when sources start and sync successfully", func(ctx SpecContext) { // Create a source that starts and syncs successfully ctrl.startWatches = []source.TypedSource[reconcile.Request]{ source.Kind(&informertest.FakeInformers{Synced: ptr.To(true)}, &corev1.Pod{}, &handler.TypedEnqueueRequestForObject[*corev1.Pod]{}), @@ -436,12 +415,12 @@ var _ = Describe("controller", func() { ctrl.Name = "test-controller" ctrl.CacheSyncTimeout = 5 * time.Second - err := ctrl.startEventSources(ctx) + err := ctrl.startEventSourcesAndQueueLocked(ctx) Expect(err).NotTo(HaveOccurred()) }) - It("should not return an error when context is cancelled during source sync", func() { - sourceCtx, sourceCancel := context.WithCancel(context.Background()) + It("should not return an error when context is cancelled during source sync", func(ctx SpecContext) { + sourceCtx, sourceCancel := context.WithCancel(ctx) defer sourceCancel() ctrl.CacheSyncTimeout = 5 * time.Second @@ -459,7 +438,8 @@ var _ = Describe("controller", func() { // Start the sources in a goroutine startErrCh := make(chan error) go func() { - startErrCh <- ctrl.startEventSources(sourceCtx) + defer GinkgoRecover() + startErrCh <- ctrl.startEventSourcesAndQueueLocked(sourceCtx) }() // Allow source to start successfully @@ -480,10 +460,7 @@ var _ = Describe("controller", func() { Expect(err).To(MatchError(context.Canceled)) }) - It("should timeout if source Start blocks for too long", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - + It("should timeout if source Start blocks for too long", func(ctx SpecContext) { ctrl.CacheSyncTimeout = 1 * time.Millisecond // Create a source that blocks forever in Start @@ -494,16 +471,100 @@ var _ = Describe("controller", func() { ctrl.startWatches = []source.TypedSource[reconcile.Request]{blockingSrc} - err := ctrl.startEventSources(ctx) + err := ctrl.startEventSourcesAndQueueLocked(ctx) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("timed out waiting for source")) }) + + It("should only start sources once when called multiple times concurrently", func(ctx SpecContext) { + ctrl.CacheSyncTimeout = 1 * time.Millisecond + + var startCount atomic.Int32 + src := source.Func(func(ctx context.Context, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + startCount.Add(1) + return nil + }) + + ctrl.startWatches = []source.TypedSource[reconcile.Request]{src} + + By("Calling startEventSourcesAndQueueLocked multiple times in parallel") + var wg sync.WaitGroup + for i := 1; i <= 5; i++ { + wg.Add(1) + go func() { + defer wg.Done() + err := ctrl.startEventSourcesAndQueueLocked(ctx) + // All calls should return the same nil error + Expect(err).NotTo(HaveOccurred()) + }() + } + + wg.Wait() + Expect(startCount.Load()).To(Equal(int32(1)), "Source should only be started once even when called multiple times") + }) + + It("should block subsequent calls from returning until the first call to startEventSourcesAndQueueLocked has returned", func(ctx SpecContext) { + ctrl.CacheSyncTimeout = 5 * time.Second + + // finishSourceChan is closed to unblock startEventSourcesAndQueueLocked from returning + finishSourceChan := make(chan struct{}) + + src := source.Func(func(ctx context.Context, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + <-finishSourceChan + return nil + }) + ctrl.startWatches = []source.TypedSource[reconcile.Request]{src} + + By("Calling startEventSourcesAndQueueLocked asynchronously") + wg := sync.WaitGroup{} + go func() { + defer GinkgoRecover() + defer wg.Done() + + wg.Add(1) + Expect(ctrl.startEventSourcesAndQueueLocked(ctx)).To(Succeed()) + }() + + By("Calling startEventSourcesAndQueueLocked again") + var didSubsequentCallComplete atomic.Bool + go func() { + defer GinkgoRecover() + defer wg.Done() + + wg.Add(1) + Expect(ctrl.startEventSourcesAndQueueLocked(ctx)).To(Succeed()) + didSubsequentCallComplete.Store(true) + }() + + // Assert that second call to startEventSourcesAndQueueLocked is blocked while source has not finished + Consistently(didSubsequentCallComplete.Load).Should(BeFalse()) + + By("Finishing source start + sync") + finishSourceChan <- struct{}{} + + // Assert that second call to startEventSourcesAndQueueLocked is now complete + Eventually(didSubsequentCallComplete.Load).Should(BeTrue(), "startEventSourcesAndQueueLocked should complete after source is started and synced") + wg.Wait() + }) + + It("should reset c.startWatches to nil after returning and startedEventSourcesAndQueue", func(ctx SpecContext) { + ctrl.CacheSyncTimeout = 1 * time.Millisecond + + src := source.Func(func(ctx context.Context, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + return nil + }) + + ctrl.startWatches = []source.TypedSource[reconcile.Request]{src} + + err := ctrl.startEventSourcesAndQueueLocked(ctx) + Expect(err).NotTo(HaveOccurred()) + Expect(ctrl.startWatches).To(BeNil(), "startWatches should be reset to nil after returning") + Expect(ctrl.startedEventSourcesAndQueue).To(BeTrue(), "startedEventSourcesAndQueue should be set to true after startEventSourcesAndQueueLocked returns without error") + }) }) Describe("Processing queue items from a Controller", func() { - It("should call Reconciler if an item is enqueued", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + It("should call Reconciler if an item is enqueued", func(ctx SpecContext) { go func() { defer GinkgoRecover() Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) @@ -519,9 +580,7 @@ var _ = Describe("controller", func() { Eventually(func() int { return queue.NumRequeues(request) }).Should(Equal(0)) }) - It("should requeue a Request if there is an error and continue processing items", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + It("should requeue a Request if there is an error and continue processing items", func(ctx SpecContext) { go func() { defer GinkgoRecover() Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) @@ -545,9 +604,7 @@ var _ = Describe("controller", func() { Eventually(func() int { return queue.NumRequeues(request) }, 1.0).Should(Equal(0)) }) - It("should not requeue a Request if there is a terminal error", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + It("should not requeue a Request if there is a terminal error", func(ctx SpecContext) { go func() { defer GinkgoRecover() Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) @@ -568,14 +625,12 @@ var _ = Describe("controller", func() { // TODO(directxman12): we should ensure that backoff occurrs with error requeue - It("should not reset backoff until there's a non-error result", func() { + It("should not reset backoff until there's a non-error result", func(ctx SpecContext) { dq := &DelegatingQueue{TypedRateLimitingInterface: ctrl.NewQueue("controller1", nil)} ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] { return dq } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) @@ -606,14 +661,12 @@ var _ = Describe("controller", func() { Eventually(func() int { return dq.NumRequeues(request) }).Should(Equal(0)) }) - It("should requeue a Request with rate limiting if the Result sets Requeue:true and continue processing items", func() { + It("should requeue a Request with rate limiting if the Result sets Requeue:true and continue processing items", func(ctx SpecContext) { dq := &DelegatingQueue{TypedRateLimitingInterface: ctrl.NewQueue("controller1", nil)} ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] { return dq } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) @@ -638,14 +691,12 @@ var _ = Describe("controller", func() { Eventually(func() int { return dq.NumRequeues(request) }).Should(Equal(0)) }) - It("should retain the priority when the reconciler requests a requeue", func() { + It("should retain the priority when the reconciler requests a requeue", func(ctx SpecContext) { q := &fakePriorityQueue{PriorityQueue: priorityqueue.New[reconcile.Request]("controller1")} ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] { return q } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) @@ -669,14 +720,12 @@ var _ = Describe("controller", func() { }})) }) - It("should requeue a Request after a duration (but not rate-limitted) if the Result sets RequeueAfter (regardless of Requeue)", func() { + It("should requeue a Request after a duration (but not rate-limitted) if the Result sets RequeueAfter (regardless of Requeue)", func(ctx SpecContext) { dq := &DelegatingQueue{TypedRateLimitingInterface: ctrl.NewQueue("controller1", nil)} ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] { return dq } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) @@ -701,14 +750,12 @@ var _ = Describe("controller", func() { Eventually(func() int { return dq.NumRequeues(request) }).Should(Equal(0)) }) - It("should retain the priority with RequeAfter", func() { + It("should retain the priority with RequeAfter", func(ctx SpecContext) { q := &fakePriorityQueue{PriorityQueue: priorityqueue.New[reconcile.Request]("controller1")} ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] { return q } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) @@ -732,14 +779,12 @@ var _ = Describe("controller", func() { }})) }) - It("should perform error behavior if error is not nil, regardless of RequeueAfter", func() { + It("should perform error behavior if error is not nil, regardless of RequeueAfter", func(ctx SpecContext) { dq := &DelegatingQueue{TypedRateLimitingInterface: ctrl.NewQueue("controller1", nil)} ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] { return dq } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) @@ -763,14 +808,12 @@ var _ = Describe("controller", func() { Eventually(func() int { return dq.NumRequeues(request) }).Should(Equal(0)) }) - It("should retain the priority when there was an error", func() { + It("should retain the priority when there was an error", func(ctx SpecContext) { q := &fakePriorityQueue{PriorityQueue: priorityqueue.New[reconcile.Request]("controller1")} ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] { return q } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) @@ -814,7 +857,7 @@ var _ = Describe("controller", func() { reconcileTotal.Reset() }) - It("should get updated on successful reconciliation", func() { + It("should get updated on successful reconciliation", func(ctx SpecContext) { Expect(func() error { Expect(ctrlmetrics.ReconcileTotal.WithLabelValues(ctrl.Name, "success").Write(&reconcileTotal)).To(Succeed()) if reconcileTotal.GetCounter().GetValue() != 0.0 { @@ -823,8 +866,6 @@ var _ = Describe("controller", func() { return nil }()).Should(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) @@ -843,7 +884,7 @@ var _ = Describe("controller", func() { }, 2.0).Should(Succeed()) }) - It("should get updated on reconcile errors", func() { + It("should get updated on reconcile errors", func(ctx SpecContext) { Expect(func() error { Expect(ctrlmetrics.ReconcileTotal.WithLabelValues(ctrl.Name, "error").Write(&reconcileTotal)).To(Succeed()) if reconcileTotal.GetCounter().GetValue() != 0.0 { @@ -852,8 +893,6 @@ var _ = Describe("controller", func() { return nil }()).Should(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) @@ -872,7 +911,7 @@ var _ = Describe("controller", func() { }, 2.0).Should(Succeed()) }) - It("should get updated when reconcile returns with retry enabled", func() { + It("should get updated when reconcile returns with retry enabled", func(ctx SpecContext) { Expect(func() error { Expect(ctrlmetrics.ReconcileTotal.WithLabelValues(ctrl.Name, "retry").Write(&reconcileTotal)).To(Succeed()) if reconcileTotal.GetCounter().GetValue() != 0.0 { @@ -881,8 +920,6 @@ var _ = Describe("controller", func() { return nil }()).Should(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) @@ -902,7 +939,7 @@ var _ = Describe("controller", func() { }, 2.0).Should(Succeed()) }) - It("should get updated when reconcile returns with retryAfter enabled", func() { + It("should get updated when reconcile returns with retryAfter enabled", func(ctx SpecContext) { Expect(func() error { Expect(ctrlmetrics.ReconcileTotal.WithLabelValues(ctrl.Name, "retry_after").Write(&reconcileTotal)).To(Succeed()) if reconcileTotal.GetCounter().GetValue() != 0.0 { @@ -911,8 +948,6 @@ var _ = Describe("controller", func() { return nil }()).Should(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) @@ -933,7 +968,7 @@ var _ = Describe("controller", func() { }) Context("should update prometheus metrics", func() { - It("should requeue a Request if there is an error and continue processing items", func() { + It("should requeue a Request if there is an error and continue processing items", func(ctx SpecContext) { var reconcileErrs dto.Metric ctrlmetrics.ReconcileErrors.Reset() Expect(func() error { @@ -944,8 +979,6 @@ var _ = Describe("controller", func() { return nil }()).Should(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) @@ -972,7 +1005,7 @@ var _ = Describe("controller", func() { Eventually(func() int { return queue.NumRequeues(request) }).Should(Equal(0)) }) - It("should add a reconcile time to the reconcile time histogram", func() { + It("should add a reconcile time to the reconcile time histogram", func(ctx SpecContext) { var reconcileTime dto.Metric ctrlmetrics.ReconcileTime.Reset() @@ -986,8 +1019,6 @@ var _ = Describe("controller", func() { return nil }()).Should(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) @@ -1014,19 +1045,520 @@ var _ = Describe("controller", func() { }) }) }) + + Describe("Warmup", func() { + JustBeforeEach(func() { + ctrl.EnableWarmup = ptr.To(true) + }) + + It("should track warmup status correctly with successful sync", func(ctx SpecContext) { + // Setup controller with sources that complete successfully + ctrl.CacheSyncTimeout = time.Second + ctrl.startWatches = []source.TypedSource[reconcile.Request]{ + source.Func(func(ctx context.Context, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + return nil + }), + } + + Expect(ctrl.Warmup(ctx)).To(Succeed()) + }) + + It("should return an error if there is an error waiting for the informers", func(ctx SpecContext) { + ctrl.CacheSyncTimeout = time.Second + ctrl.startWatches = []source.TypedSource[reconcile.Request]{ + source.Kind(&informertest.FakeInformers{Synced: ptr.To(false)}, &corev1.Pod{}, &handler.TypedEnqueueRequestForObject[*corev1.Pod]{}), + } + ctrl.Name = testControllerName + err := ctrl.Warmup(ctx) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to wait for testcontroller caches to sync")) + }) + + It("should error when cache sync timeout occurs", func(ctx SpecContext) { + c, err := cache.New(cfg, cache.Options{}) + Expect(err).NotTo(HaveOccurred()) + c = &cacheWithIndefinitelyBlockingGetInformer{c} + + ctrl.CacheSyncTimeout = time.Second + ctrl.startWatches = []source.TypedSource[reconcile.Request]{ + source.Kind(c, &appsv1.Deployment{}, &handler.TypedEnqueueRequestForObject[*appsv1.Deployment]{}), + } + ctrl.Name = testControllerName + + err = ctrl.Warmup(ctx) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to wait for testcontroller caches to sync kind source: *v1.Deployment: timed out waiting for cache to be synced")) + }) + + It("should not error when controller Warmup context is cancelled during Sources WaitForSync", func(specCtx SpecContext) { + ctrl.CacheSyncTimeout = 1 * time.Second + + sourceSynced := make(chan struct{}) + c, err := cache.New(cfg, cache.Options{}) + Expect(err).NotTo(HaveOccurred()) + c = &cacheWithIndefinitelyBlockingGetInformer{c} + ctrl.startWatches = []source.TypedSource[reconcile.Request]{ + &singnallingSourceWrapper{ + SyncingSource: source.Kind[client.Object](c, &appsv1.Deployment{}, &handler.EnqueueRequestForObject{}), + cacheSyncDone: sourceSynced, + }, + } + ctrl.Name = testControllerName + + ctx, cancel := context.WithCancel(specCtx) + go func() { + defer GinkgoRecover() + err = ctrl.Warmup(ctx) + Expect(err).To(Succeed()) + }() + + cancel() + <-sourceSynced + }) + + It("should error when Warmup() is blocking forever", func(specCtx SpecContext) { + ctrl.CacheSyncTimeout = time.Second + + controllerDone := make(chan struct{}) + ctrl.startWatches = []source.TypedSource[reconcile.Request]{ + source.Func(func(ctx context.Context, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + <-controllerDone + return ctx.Err() + })} + + ctx, cancel := context.WithTimeout(specCtx, 10*time.Second) + defer cancel() + + err := ctrl.Warmup(ctx) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Please ensure that its Start() method is non-blocking")) + + close(controllerDone) + }) + + It("should not error when cache sync timeout is of sufficiently high", func(ctx SpecContext) { + ctrl.CacheSyncTimeout = 10 * time.Second + + sourceSynced := make(chan struct{}) + c := &informertest.FakeInformers{} + ctrl.startWatches = []source.TypedSource[reconcile.Request]{ + &singnallingSourceWrapper{ + SyncingSource: source.Kind[client.Object](c, &appsv1.Deployment{}, &handler.EnqueueRequestForObject{}), + cacheSyncDone: sourceSynced, + }, + } + + go func() { + defer GinkgoRecover() + Expect(ctrl.Warmup(ctx)).To(Succeed()) + }() + + <-sourceSynced + }) + + It("should process events from source.Channel", func(ctx SpecContext) { + ctrl.CacheSyncTimeout = 10 * time.Second + // channel to be closed when event is processed + processed := make(chan struct{}) + // source channel + ch := make(chan event.GenericEvent, 1) + + // event to be sent to the channel + p := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}, + } + evt := event.GenericEvent{ + Object: p, + } + + ins := source.Channel( + ch, + handler.Funcs{ + GenericFunc: func(ctx context.Context, evt event.GenericEvent, q workqueue.TypedRateLimitingInterface[reconcile.Request]) { + defer GinkgoRecover() + close(processed) + }, + }, + ) + + // send the event to the channel + ch <- evt + + ctrl.startWatches = []source.TypedSource[reconcile.Request]{ins} + + go func() { + defer GinkgoRecover() + Expect(ctrl.Warmup(ctx)).To(Succeed()) + }() + <-processed + }) + + It("should error when channel source is not specified", func(ctx SpecContext) { + ctrl.CacheSyncTimeout = 10 * time.Second + + ins := source.Channel[string](nil, nil) + ctrl.startWatches = []source.TypedSource[reconcile.Request]{ins} + + e := ctrl.Warmup(ctx) + Expect(e).To(HaveOccurred()) + Expect(e.Error()).To(ContainSubstring("must specify Channel.Source")) + }) + + It("should call Start on sources with the appropriate EventHandler, Queue, and Predicates", func(specCtx SpecContext) { + ctrl.CacheSyncTimeout = 10 * time.Second + started := false + ctx, cancel := context.WithCancel(specCtx) + src := source.Func(func(ctx context.Context, q workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + defer GinkgoRecover() + Expect(q).To(Equal(ctrl.Queue)) + + started = true + cancel() // Cancel the context so ctrl.Warmup() doesn't block forever + return nil + }) + Expect(ctrl.Watch(src)).NotTo(HaveOccurred()) + + err := ctrl.Warmup(ctx) + Expect(err).To(Succeed()) + Expect(started).To(BeTrue()) + }) + + It("should return an error if there is an error starting sources", func(ctx SpecContext) { + ctrl.CacheSyncTimeout = 10 * time.Second + err := fmt.Errorf("Expected Error: could not start source") + src := source.Func(func(context.Context, + workqueue.TypedRateLimitingInterface[reconcile.Request], + ) error { + defer GinkgoRecover() + return err + }) + Expect(ctrl.Watch(src)).To(Succeed()) + + Expect(ctrl.Warmup(ctx)).To(Equal(err)) + }) + + It("should track warmup status correctly with unsuccessful sync", func(ctx SpecContext) { + // Setup controller with sources that complete with error + ctrl.CacheSyncTimeout = time.Second + ctrl.startWatches = []source.TypedSource[reconcile.Request]{ + source.Func(func(ctx context.Context, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + return errors.New("sync error") + }), + } + + err := ctrl.Warmup(ctx) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("sync error")) + }) + + It("should call Start on sources with the appropriate non-nil queue", func(specCtx SpecContext) { + ctrl.CacheSyncTimeout = 10 * time.Second + started := false + ctx, cancel := context.WithCancel(specCtx) + src := source.Func(func(ctx context.Context, q workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + defer GinkgoRecover() + Expect(q).ToNot(BeNil()) + Expect(q).To(Equal(ctrl.Queue)) + + started = true + cancel() // Cancel the context so ctrl.Start() doesn't block forever + return nil + }) + Expect(ctrl.Watch(src)).To(Succeed()) + Expect(ctrl.Warmup(ctx)).To(Succeed()) + Expect(ctrl.Queue).ToNot(BeNil()) + Expect(started).To(BeTrue()) + }) + + It("should return true if context is cancelled while waiting for source to start", func(specCtx SpecContext) { + // Setup controller with sources that complete with error + ctx, cancel := context.WithCancel(specCtx) + defer cancel() + + ctrl.CacheSyncTimeout = time.Second + ctrl.startWatches = []source.TypedSource[reconcile.Request]{ + source.Func(func(ctx context.Context, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + <-ctx.Done() + return nil + }), + } + + // channel to prevent the goroutine from outliving the It test + waitChan := make(chan struct{}) + + // Invoked in a goroutine because Warmup will block + go func() { + defer GinkgoRecover() + defer close(waitChan) + Expect(ctrl.Warmup(ctx)).To(Succeed()) + }() + + cancel() + <-waitChan + }) + + It("should be called before leader election runnables if warmup is enabled", func(specCtx SpecContext) { + // This unit test exists to ensure that a warmup enabled controller will actually be + // called in the warmup phase before the leader election runnables are started. It + // catches regressions in the controller that would not implement warmupRunnable from + // pkg/manager. + ctx, cancel := context.WithCancel(specCtx) + + By("Creating a channel to track execution order") + runnableExecutionOrderChan := make(chan string, 2) + const nonWarmupRunnableName = "nonWarmupRunnable" + const warmupRunnableName = "warmupRunnable" + + ctrl.CacheSyncTimeout = time.Second + ctrl.startWatches = []source.TypedSource[reconcile.Request]{ + source.Func(func(ctx context.Context, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + runnableExecutionOrderChan <- warmupRunnableName + return nil + }), + } + + nonWarmupCtrl := New[reconcile.Request](Options[reconcile.Request]{ + MaxConcurrentReconciles: 1, + Do: fakeReconcile, + NewQueue: func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] { + return queue + }, + LogConstructor: func(_ *reconcile.Request) logr.Logger { + return log.RuntimeLog.WithName("controller").WithName("test") + }, + CacheSyncTimeout: time.Second, + EnableWarmup: ptr.To(false), + LeaderElected: ptr.To(true), + }) + nonWarmupCtrl.startWatches = []source.TypedSource[reconcile.Request]{ + source.Func(func(ctx context.Context, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + runnableExecutionOrderChan <- nonWarmupRunnableName + return nil + }), + } + + By("Creating a test resource lock with hooks") + resourceLock, err := fakeleaderelection.NewResourceLock(nil, nil, leaderelection.Options{}) + Expect(err).ToNot(HaveOccurred()) + + By("Creating a manager") + testenv = &envtest.Environment{} + cfg, err := testenv.Start() + Expect(err).NotTo(HaveOccurred()) + m, err := manager.New(cfg, manager.Options{ + LeaderElection: true, + LeaderElectionID: "some-leader-election-id", + LeaderElectionNamespace: "default", + LeaderElectionResourceLockInterface: resourceLock, + }) + Expect(err).NotTo(HaveOccurred()) + + By("Adding warmup and non-warmup controllers to the manager") + Expect(m.Add(ctrl)).To(Succeed()) + Expect(m.Add(nonWarmupCtrl)).To(Succeed()) + + By("Blocking leader election") + resourceLockWithHooks, ok := resourceLock.(fakeleaderelection.ControllableResourceLockInterface) + Expect(ok).To(BeTrue(), "resource lock should implement ResourceLockInterfaceWithHooks") + resourceLockWithHooks.BlockLeaderElection() + + By("Starting the manager") + waitChan := make(chan struct{}) + go func() { + defer GinkgoRecover() + defer close(waitChan) + Expect(m.Start(ctx)).To(Succeed()) + }() + Expect(<-runnableExecutionOrderChan).To(Equal(warmupRunnableName)) + + By("Unblocking leader election") + resourceLockWithHooks.UnblockLeaderElection() + <-m.Elected() + Expect(<-runnableExecutionOrderChan).To(Equal(nonWarmupRunnableName)) + + cancel() + <-waitChan + }) + + It("should not cause a data race when called concurrently", func(ctx SpecContext) { + + ctrl.CacheSyncTimeout = time.Second + + ctrl.startWatches = []source.TypedSource[reconcile.Request]{ + source.Func(func(ctx context.Context, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + return nil + }), + } + + var wg sync.WaitGroup + for i := 0; i < 5; i++ { + wg.Add(1) + go func() { + defer GinkgoRecover() + defer wg.Done() + Expect(ctrl.Warmup(ctx)).To(Succeed()) + }() + } + + wg.Wait() + }) + + It("should not cause a data race when called concurrently with Start and only start sources once", func(ctx SpecContext) { + + ctrl.CacheSyncTimeout = time.Second + numWatches := 10 + + var watchStartedCount atomic.Int32 + for range numWatches { + ctrl.startWatches = append(ctrl.startWatches, source.Func(func(ctx context.Context, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + watchStartedCount.Add(1) + return nil + })) + } + + By("calling Warmup and Start concurrently") + go func() { + defer GinkgoRecover() + Expect(ctrl.Start(ctx)).To(Succeed()) + }() + + blockOnWarmupChan := make(chan struct{}) + go func() { + defer GinkgoRecover() + Expect(ctrl.Warmup(ctx)).To(Succeed()) + close(blockOnWarmupChan) + }() + + <-blockOnWarmupChan + + Expect(watchStartedCount.Load()).To(Equal(int32(numWatches)), "source should only be started once") + Expect(ctrl.startWatches).To(BeNil(), "startWatches should be reset to nil after they are started") + }) + + It("should start sources added after Warmup is called", func(specCtx SpecContext) { + ctx, cancel := context.WithCancel(specCtx) + + ctrl.CacheSyncTimeout = time.Second + + Expect(ctrl.Warmup(ctx)).To(Succeed()) + + By("starting a watch after warmup is added") + var didWatchStart atomic.Bool + Expect(ctrl.Watch(source.Func(func(ctx context.Context, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + didWatchStart.Store(true) + return nil + }))).To(Succeed()) + + waitChan := make(chan struct{}) + go func() { + defer GinkgoRecover() + Expect(ctrl.Start(ctx)).To(Succeed()) + close(waitChan) + }() + + Eventually(didWatchStart.Load).Should(BeTrue(), "watch should be started if it is added after Warmup") + + cancel() + <-waitChan + }) + + DescribeTable("should not leak goroutines when manager is stopped with warmup runnable", + func(specContext SpecContext, leaderElection bool) { + ctx, cancel := context.WithCancel(specContext) + defer cancel() + + ctrl.CacheSyncTimeout = time.Second + + By("Creating a manager") + testenv = &envtest.Environment{} + cfg, err := testenv.Start() + Expect(err).NotTo(HaveOccurred()) + m, err := manager.New(cfg, manager.Options{ + LeaderElection: leaderElection, + LeaderElectionID: "some-leader-election-id", + LeaderElectionNamespace: "default", + }) + Expect(err).NotTo(HaveOccurred()) + + ctrl.startWatches = []source.TypedSource[reconcile.Request]{ + source.Func(func(ctx context.Context, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + <-ctx.Done() + return nil + }), + } + Expect(m.Add(ctrl)).To(Succeed()) + + // ignore needs to go after the testenv.Start() call to ignore the apiserver + // process + currentGRs := goleak.IgnoreCurrent() + waitChan := make(chan struct{}) + go func() { + defer GinkgoRecover() + Expect(m.Start(ctx)).To(Succeed()) + close(waitChan) + }() + + <-m.Elected() + By("stopping the manager via context") + cancel() + + Eventually(func() error { return goleak.Find(currentGRs) }).Should(Succeed()) + <-waitChan + }, + Entry("and with leader election enabled", true), + Entry("and without leader election enabled", false), + ) + }) + + Describe("Warmup with warmup disabled", func() { + JustBeforeEach(func() { + ctrl.EnableWarmup = ptr.To(false) + }) + + It("should not start sources when Warmup is called if warmup is disabled but start it when Start is called.", func(specCtx SpecContext) { + // Setup controller with sources that complete successfully + ctx, cancel := context.WithCancel(specCtx) + + ctrl.CacheSyncTimeout = time.Second + var isSourceStarted atomic.Bool + isSourceStarted.Store(false) + ctrl.startWatches = []source.TypedSource[reconcile.Request]{ + source.Func(func(ctx context.Context, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + isSourceStarted.Store(true) + return nil + }), + } + + By("Calling Warmup when EnableWarmup is false") + err := ctrl.Warmup(ctx) + Expect(err).NotTo(HaveOccurred()) + Expect(isSourceStarted.Load()).To(BeFalse()) + + By("Calling Start when EnableWarmup is false") + waitChan := make(chan struct{}) + + go func() { + defer GinkgoRecover() + Expect(ctrl.Start(ctx)).To(Succeed()) + close(waitChan) + }() + Eventually(isSourceStarted.Load).Should(BeTrue()) + cancel() + <-waitChan + }) + }) }) var _ = Describe("ReconcileIDFromContext function", func() { - It("should return an empty string if there is nothing in the context", func() { - ctx := context.Background() + It("should return an empty string if there is nothing in the context", func(ctx SpecContext) { reconcileID := ReconcileIDFromContext(ctx) Expect(reconcileID).To(Equal(types.UID(""))) }) - It("should return the correct reconcileID from context", func() { + It("should return the correct reconcileID from context", func(specContext SpecContext) { const expectedReconcileID = types.UID("uuid") - ctx := addReconcileID(context.Background(), expectedReconcileID) + ctx := addReconcileID(specContext, expectedReconcileID) reconcileID := ReconcileIDFromContext(ctx) Expect(reconcileID).To(Equal(expectedReconcileID)) diff --git a/pkg/internal/recorder/recorder_integration_test.go b/pkg/internal/recorder/recorder_integration_test.go index 48097872c5..c278fbde79 100644 --- a/pkg/internal/recorder/recorder_integration_test.go +++ b/pkg/internal/recorder/recorder_integration_test.go @@ -37,7 +37,7 @@ import ( var _ = Describe("recorder", func() { Describe("recorder", func() { - It("should publish events", func() { + It("should publish events", func(ctx SpecContext) { By("Creating the Manager") cm, err := manager.New(cfg, manager.Options{}) Expect(err).NotTo(HaveOccurred()) @@ -60,8 +60,6 @@ var _ = Describe("recorder", func() { Expect(err).NotTo(HaveOccurred()) By("Starting the Manager") - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(cm.Start(ctx)).NotTo(HaveOccurred()) diff --git a/pkg/internal/source/internal_test.go b/pkg/internal/source/internal_test.go index 6e4e8924da..73eb1a1d28 100644 --- a/pkg/internal/source/internal_test.go +++ b/pkg/internal/source/internal_test.go @@ -38,11 +38,10 @@ import ( ) var _ = Describe("Internal", func() { - var ctx = context.Background() var instance *internal.EventHandler[client.Object, reconcile.Request] var funcs, setfuncs *handler.Funcs var set bool - BeforeEach(func() { + BeforeEach(func(ctx SpecContext) { funcs = &handler.Funcs{ CreateFunc: func(context.Context, event.CreateEvent, workqueue.TypedRateLimitingInterface[reconcile.Request]) { defer GinkgoRecover() @@ -92,7 +91,7 @@ var _ = Describe("Internal", func() { newPod.Labels = map[string]string{"foo": "bar"} }) - It("should create a CreateEvent", func() { + It("should create a CreateEvent", func(ctx SpecContext) { funcs.CreateFunc = func(ctx context.Context, evt event.CreateEvent, q workqueue.TypedRateLimitingInterface[reconcile.Request]) { defer GinkgoRecover() Expect(evt.Object).To(Equal(pod)) @@ -100,7 +99,7 @@ var _ = Describe("Internal", func() { instance.OnAdd(pod, false) }) - It("should used Predicates to filter CreateEvents", func() { + It("should used Predicates to filter CreateEvents", func(ctx SpecContext) { instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return false }}, }) @@ -148,7 +147,7 @@ var _ = Describe("Internal", func() { instance.OnAdd(FooRuntimeObject{}, false) }) - It("should create an UpdateEvent", func() { + It("should create an UpdateEvent", func(ctx SpecContext) { funcs.UpdateFunc = func(ctx context.Context, evt event.UpdateEvent, q workqueue.TypedRateLimitingInterface[reconcile.Request]) { defer GinkgoRecover() Expect(evt.ObjectOld).To(Equal(pod)) @@ -157,7 +156,7 @@ var _ = Describe("Internal", func() { instance.OnUpdate(pod, newPod) }) - It("should used Predicates to filter UpdateEvents", func() { + It("should used Predicates to filter UpdateEvents", func(ctx SpecContext) { set = false instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{UpdateFunc: func(updateEvent event.UpdateEvent) bool { return false }}, @@ -215,7 +214,7 @@ var _ = Describe("Internal", func() { instance.OnDelete(pod) }) - It("should used Predicates to filter DeleteEvents", func() { + It("should used Predicates to filter DeleteEvents", func(ctx SpecContext) { set = false instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return false }}, diff --git a/pkg/internal/testing/controlplane/plane_test.go b/pkg/internal/testing/controlplane/plane_test.go index cd0359dbca..a228e5a51c 100644 --- a/pkg/internal/testing/controlplane/plane_test.go +++ b/pkg/internal/testing/controlplane/plane_test.go @@ -17,8 +17,6 @@ limitations under the License. package controlplane_test import ( - "context" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" kauthn "k8s.io/api/authorization/v1" @@ -69,7 +67,7 @@ var _ = Describe("Control Plane", func() { Expect(plane.Stop()).To(Succeed()) }) - It("should provision a working legacy user and legacy kubectl", func() { + It("should provision a working legacy user and legacy kubectl", func(ctx SpecContext) { By("grabbing the legacy kubectl") Expect(plane.KubeCtl()).NotTo(BeNil()) @@ -89,7 +87,7 @@ var _ = Describe("Control Plane", func() { }, }, } - Expect(cl.Create(context.Background(), sar)).To(Succeed(), "should be able to make a Self-SAR") + Expect(cl.Create(ctx, sar)).To(Succeed(), "should be able to make a Self-SAR") Expect(sar.Status.Allowed).To(BeTrue(), "admin user should be able to do everything") }) diff --git a/pkg/leaderelection/fake/leader_election.go b/pkg/leaderelection/fake/leader_election.go index 5a82cf43b8..ab816a19a7 100644 --- a/pkg/leaderelection/fake/leader_election.go +++ b/pkg/leaderelection/fake/leader_election.go @@ -19,7 +19,9 @@ package fake import ( "context" "encoding/json" + "errors" "os" + "sync/atomic" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -30,6 +32,19 @@ import ( "sigs.k8s.io/controller-runtime/pkg/recorder" ) +// ControllableResourceLockInterface is an interface that extends resourcelock.Interface to be +// controllable. +type ControllableResourceLockInterface interface { + resourcelock.Interface + + // BlockLeaderElection blocks the leader election process when called. It will not be unblocked + // until UnblockLeaderElection is called. + BlockLeaderElection() + + // UnblockLeaderElection unblocks the leader election. + UnblockLeaderElection() +} + // NewResourceLock creates a new ResourceLock for use in testing // leader election. func NewResourceLock(config *rest.Config, recorderProvider recorder.Provider, options leaderelection.Options) (resourcelock.Interface, error) { @@ -40,27 +55,31 @@ func NewResourceLock(config *rest.Config, recorderProvider recorder.Provider, op } id = id + "_" + string(uuid.NewUUID()) - return &ResourceLock{ + return &resourceLock{ id: id, record: resourcelock.LeaderElectionRecord{ HolderIdentity: id, - LeaseDurationSeconds: 15, + LeaseDurationSeconds: 1, AcquireTime: metav1.NewTime(time.Now()), - RenewTime: metav1.NewTime(time.Now().Add(15 * time.Second)), + RenewTime: metav1.NewTime(time.Now().Add(1 * time.Second)), LeaderTransitions: 1, }, }, nil } -// ResourceLock implements the ResourceLockInterface. +var _ ControllableResourceLockInterface = &resourceLock{} + +// resourceLock implements the ResourceLockInterface. // By default returns that the current identity holds the lock. -type ResourceLock struct { +type resourceLock struct { id string record resourcelock.LeaderElectionRecord + + blockedLeaderElection atomic.Bool } // Get implements the ResourceLockInterface. -func (f *ResourceLock) Get(ctx context.Context) (*resourcelock.LeaderElectionRecord, []byte, error) { +func (f *resourceLock) Get(ctx context.Context) (*resourcelock.LeaderElectionRecord, []byte, error) { recordBytes, err := json.Marshal(f.record) if err != nil { return nil, nil, err @@ -69,28 +88,49 @@ func (f *ResourceLock) Get(ctx context.Context) (*resourcelock.LeaderElectionRec } // Create implements the ResourceLockInterface. -func (f *ResourceLock) Create(ctx context.Context, ler resourcelock.LeaderElectionRecord) error { +func (f *resourceLock) Create(ctx context.Context, ler resourcelock.LeaderElectionRecord) error { + if f.blockedLeaderElection.Load() { + // If leader election is blocked, we do not allow creating a new record. + return errors.New("leader election is blocked, cannot create new record") + } + f.record = ler return nil } // Update implements the ResourceLockInterface. -func (f *ResourceLock) Update(ctx context.Context, ler resourcelock.LeaderElectionRecord) error { +func (f *resourceLock) Update(ctx context.Context, ler resourcelock.LeaderElectionRecord) error { + if f.blockedLeaderElection.Load() { + // If leader election is blocked, we do not allow updating records + return errors.New("leader election is blocked, cannot update record") + } + f.record = ler + return nil } // RecordEvent implements the ResourceLockInterface. -func (f *ResourceLock) RecordEvent(s string) { +func (f *resourceLock) RecordEvent(s string) { } // Identity implements the ResourceLockInterface. -func (f *ResourceLock) Identity() string { +func (f *resourceLock) Identity() string { return f.id } // Describe implements the ResourceLockInterface. -func (f *ResourceLock) Describe() string { +func (f *resourceLock) Describe() string { return f.id } + +// BlockLeaderElection blocks the leader election process when called. +func (f *resourceLock) BlockLeaderElection() { + f.blockedLeaderElection.Store(true) +} + +// UnblockLeaderElection blocks the leader election process when called. +func (f *resourceLock) UnblockLeaderElection() { + f.blockedLeaderElection.Store(false) +} diff --git a/pkg/leaderelection/leader_election.go b/pkg/leaderelection/leader_election.go index 5cc253917a..6c013e7992 100644 --- a/pkg/leaderelection/leader_election.go +++ b/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/pkg/log/log_test.go b/pkg/log/log_test.go index f3bb4d6ec2..404a859e4d 100644 --- a/pkg/log/log_test.go +++ b/pkg/log/log_test.go @@ -17,7 +17,6 @@ limitations under the License. package log import ( - "context" "errors" "github.com/go-logr/logr" @@ -297,17 +296,17 @@ var _ = Describe("logging", func() { }) Describe("logger from context", func() { - It("should return default logger when context is empty", func() { - gotLog := FromContext(context.Background()) + It("should return default logger when context is empty", func(ctx SpecContext) { + gotLog := FromContext(ctx) Expect(gotLog).To(Not(BeNil())) }) - It("should return existing logger", func() { + It("should return existing logger", func(specCtx SpecContext) { root := &fakeLoggerRoot{} baseLog := &fakeLogger{root: root} wantLog := logr.New(baseLog).WithName("my-logger") - ctx := IntoContext(context.Background(), wantLog) + ctx := IntoContext(specCtx, wantLog) gotLog := FromContext(ctx) Expect(gotLog).To(Not(BeNil())) @@ -318,12 +317,12 @@ var _ = Describe("logging", func() { )) }) - It("should have added key-values", func() { + It("should have added key-values", func(specCtx SpecContext) { root := &fakeLoggerRoot{} baseLog := &fakeLogger{root: root} wantLog := logr.New(baseLog).WithName("my-logger") - ctx := IntoContext(context.Background(), wantLog) + ctx := IntoContext(specCtx, wantLog) gotLog := FromContext(ctx, "tag1", "value1") Expect(gotLog).To(Not(BeNil())) diff --git a/pkg/manager/internal.go b/pkg/manager/internal.go index e5204a7506..a9f91cbdd5 100644 --- a/pkg/manager/internal.go +++ b/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/pkg/manager/internal/integration/manager_test.go b/pkg/manager/internal/integration/manager_test.go index 346daa1e68..c83eead3c1 100644 --- a/pkg/manager/internal/integration/manager_test.go +++ b/pkg/manager/internal/integration/manager_test.go @@ -34,10 +34,12 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/utils/ptr" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -90,6 +92,8 @@ var ( }, }, } + + ctx = ctrl.SetupSignalHandler() ) var _ = Describe("manger.Manager Start", func() { @@ -107,9 +111,7 @@ var _ = Describe("manger.Manager Start", func() { // * Add an index on v2 Driver to ensure we start and wait for an informer during cache.Start (as part of manager.Start) // * Note: cache.Start would fail if the conversion webhook doesn't work (which in turn depends on the readiness probe) // * Note: Adding the index for v2 ensures the Driver list call during Informer sync goes through conversion. - It("should start all components without deadlock", func() { - ctx := ctrl.SetupSignalHandler() - + DescribeTable("should start all components without deadlock", func(warmupEnabled bool) { // Set up schema. Expect(clientgoscheme.AddToScheme(scheme)).To(Succeed()) Expect(apiextensionsv1.AddToScheme(scheme)).To(Succeed()) @@ -163,7 +165,13 @@ var _ = Describe("manger.Manager Start", func() { driverReconciler := &DriverReconciler{ Client: mgr.GetClient(), } - Expect(ctrl.NewControllerManagedBy(mgr).For(&crewv2.Driver{}).Complete(driverReconciler)).To(Succeed()) + Expect( + ctrl.NewControllerManagedBy(mgr). + For(&crewv2.Driver{}). + Named(fmt.Sprintf("driver_warmup_%t", warmupEnabled)). + WithOptions(controller.Options{EnableWarmup: ptr.To(warmupEnabled)}). + Complete(driverReconciler), + ).To(Succeed()) // Set up a conversion webhook. conversionWebhook := createConversionWebhook(mgr) @@ -211,7 +219,10 @@ var _ = Describe("manger.Manager Start", func() { // Shutdown the server cancel() - }) + }, + Entry("controller warmup enabled", true), + Entry("controller warmup not enabled", false), + ) }) type DriverReconciler struct { diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go index c3ae317b04..e0e94245e7 100644 --- a/pkg/manager/manager.go +++ b/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, diff --git a/pkg/manager/manager_test.go b/pkg/manager/manager_test.go index 247a33f9dc..4363d62f59 100644 --- a/pkg/manager/manager_test.go +++ b/pkg/manager/manager_test.go @@ -160,7 +160,7 @@ var _ = Describe("manger.Manager", func() { }) Context("with leader election enabled", func() { - It("should only cancel the leader election after all runnables are done", func() { + It("should only cancel the leader election after all runnables are done", func(specCtx SpecContext) { m, err := New(cfg, Options{ LeaderElection: true, LeaderElectionNamespace: "default", @@ -190,7 +190,7 @@ var _ = Describe("manger.Manager", func() { close(leaderElectionDone) } - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) mgrDone := make(chan struct{}) go func() { defer GinkgoRecover() @@ -209,7 +209,7 @@ var _ = Describe("manger.Manager", func() { <-mgrDone }) - It("should disable gracefulShutdown when stopping to lead", func() { + It("should disable gracefulShutdown when stopping to lead", func(ctx SpecContext) { m, err := New(cfg, Options{ LeaderElection: true, LeaderElectionNamespace: "default", @@ -220,8 +220,6 @@ var _ = Describe("manger.Manager", func() { }) Expect(err).ToNot(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() mgrDone := make(chan struct{}) go func() { defer GinkgoRecover() @@ -239,7 +237,7 @@ var _ = Describe("manger.Manager", func() { Expect(cm.gracefulShutdownTimeout.Nanoseconds()).To(Equal(int64(0))) }) - It("should prevent leader election when shutting down a non-elected manager", func() { + It("should prevent leader election when shutting down a non-elected manager", func(specCtx SpecContext) { var rl resourcelock.Interface m1, err := New(cfg, Options{ LeaderElection: true, @@ -286,12 +284,10 @@ var _ = Describe("manger.Manager", func() { return nil }))).To(Succeed()) - ctx1, cancel1 := context.WithCancel(context.Background()) - defer cancel1() go func() { defer GinkgoRecover() Expect(m1.Elected()).ShouldNot(BeClosed()) - Expect(m1.Start(ctx1)).NotTo(HaveOccurred()) + Expect(m1.Start(specCtx)).NotTo(HaveOccurred()) }() <-m1.Elected() <-m1done @@ -300,7 +296,7 @@ var _ = Describe("manger.Manager", func() { Expect(m2.Add(electionRunnable)).To(Succeed()) - ctx2, cancel2 := context.WithCancel(context.Background()) + ctx2, cancel2 := context.WithCancel(specCtx) m2done := make(chan struct{}) go func() { defer GinkgoRecover() @@ -339,7 +335,7 @@ var _ = Describe("manger.Manager", func() { Expect(m1).ToNot(BeNil()) }) - It("should default ID to controller-runtime if ID is not set", func() { + It("should default ID to controller-runtime if ID is not set", func(specCtx SpecContext) { var rl resourcelock.Interface m1, err := New(cfg, Options{ LeaderElection: true, @@ -390,12 +386,10 @@ var _ = Describe("manger.Manager", func() { return nil }))).To(Succeed()) - ctx1, cancel1 := context.WithCancel(context.Background()) - defer cancel1() go func() { defer GinkgoRecover() Expect(m1.Elected()).ShouldNot(BeClosed()) - Expect(m1.Start(ctx1)).NotTo(HaveOccurred()) + Expect(m1.Start(specCtx)).NotTo(HaveOccurred()) }() <-m1.Elected() <-c1 @@ -407,7 +401,7 @@ var _ = Describe("manger.Manager", func() { return nil }))).To(Succeed()) - ctx2, cancel := context.WithCancel(context.Background()) + ctx2, cancel := context.WithCancel(specCtx) m2done := make(chan struct{}) go func() { defer GinkgoRecover() @@ -465,7 +459,7 @@ var _ = Describe("manger.Manager", func() { _, isLeaseLock := cm.resourceLock.(*resourcelock.LeaseLock) Expect(isLeaseLock).To(BeTrue()) }) - It("should release lease if ElectionReleaseOnCancel is true", func() { + It("should release lease if ElectionReleaseOnCancel is true", func(specCtx SpecContext) { var rl resourcelock.Interface m, err := New(cfg, Options{ LeaderElection: true, @@ -481,7 +475,7 @@ var _ = Describe("manger.Manager", func() { }) Expect(err).ToNot(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) doneCh := make(chan struct{}) go func() { defer GinkgoRecover() @@ -492,12 +486,29 @@ var _ = Describe("manger.Manager", func() { cancel() <-doneCh - ctx, cancel = context.WithCancel(context.Background()) - defer cancel() - record, _, err := rl.Get(ctx) + record, _, err := rl.Get(specCtx) Expect(err).ToNot(HaveOccurred()) Expect(record.HolderIdentity).To(BeEmpty()) }) + It("should set the leaselocks's label field when LeaderElectionLabels is set", func() { + labels := map[string]string{"my-key": "my-val"} + m, err := New(cfg, Options{ + LeaderElection: true, + LeaderElectionResourceLock: resourcelock.LeasesResourceLock, + LeaderElectionID: "controller-runtime", + LeaderElectionNamespace: "default", + LeaderElectionLabels: labels, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(m).ToNot(BeNil()) + cm, ok := m.(*controllerManager) + Expect(ok).To(BeTrue()) + ll, isLeaseLock := cm.resourceLock.(*resourcelock.LeaseLock) + Expect(isLeaseLock).To(BeTrue()) + val, exists := ll.Labels["my-key"] + Expect(exists).To(BeTrue()) + Expect(val).To(Equal("my-val")) + }) When("using a custom LeaderElectionResourceLockInterface", func() { It("should use the custom LeaderElectionResourceLockInterface", func() { rl, err := fakeleaderelection.NewResourceLock(nil, nil, leaderelection.Options{}) @@ -519,7 +530,7 @@ var _ = Describe("manger.Manager", func() { }) }) - It("should create a metrics server if a valid address is provided", func() { + It("should create a metrics server if a valid address is provided", func(specCtx SpecContext) { var srv metricsserver.Server m, err := New(cfg, Options{ Metrics: metricsserver.Options{BindAddress: ":0"}, @@ -535,12 +546,12 @@ var _ = Describe("manger.Manager", func() { // Triggering the metric server start here manually to test if it works. // Usually this happens later during manager.Start(). - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + ctx, cancel := context.WithTimeout(specCtx, 5*time.Second) Expect(srv.Start(ctx)).To(Succeed()) cancel() }) - It("should create a metrics server if a valid address is provided and secure serving is enabled", func() { + It("should create a metrics server if a valid address is provided and secure serving is enabled", func(specCtx SpecContext) { var srv metricsserver.Server m, err := New(cfg, Options{ Metrics: metricsserver.Options{BindAddress: ":0", SecureServing: true}, @@ -556,7 +567,7 @@ var _ = Describe("manger.Manager", func() { // Triggering the metric server start here manually to test if it works. // Usually this happens later during manager.Start(). - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + ctx, cancel := context.WithTimeout(specCtx, 5*time.Second) Expect(srv.Start(ctx)).To(Succeed()) cancel() }) @@ -571,7 +582,7 @@ var _ = Describe("manger.Manager", func() { Expect(err).ToNot(HaveOccurred()) }) - It("should return an error if the metrics bind address is already in use", func() { + It("should return an error if the metrics bind address is already in use", func(ctx SpecContext) { ln, err := net.Listen("tcp", ":0") Expect(err).ShouldNot(HaveOccurred()) @@ -591,12 +602,12 @@ var _ = Describe("manger.Manager", func() { // Triggering the metric server start here manually to test if it works. // Usually this happens later during manager.Start(). - Expect(srv.Start(context.Background())).ToNot(Succeed()) + Expect(srv.Start(ctx)).ToNot(Succeed()) Expect(ln.Close()).To(Succeed()) }) - It("should return an error if the metrics bind address is already in use and secure serving enabled", func() { + It("should return an error if the metrics bind address is already in use and secure serving enabled", func(ctx SpecContext) { ln, err := net.Listen("tcp", ":0") Expect(err).ShouldNot(HaveOccurred()) @@ -617,7 +628,7 @@ var _ = Describe("manger.Manager", func() { // Triggering the metric server start here manually to test if it works. // Usually this happens later during manager.Start(). - Expect(srv.Start(context.Background())).ToNot(Succeed()) + Expect(srv.Start(ctx)).ToNot(Succeed()) Expect(ln.Close()).To(Succeed()) }) @@ -661,7 +672,7 @@ var _ = Describe("manger.Manager", func() { Describe("Start", func() { var startSuite = func(options Options, callbacks ...func(Manager)) { - It("should Start each Component", func() { + It("should Start each Component", func(ctx SpecContext) { m, err := New(cfg, options) Expect(err).NotTo(HaveOccurred()) for _, cb := range callbacks { @@ -681,8 +692,6 @@ var _ = Describe("manger.Manager", func() { return nil }))).To(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(m.Elected()).ShouldNot(BeClosed()) @@ -712,18 +721,18 @@ var _ = Describe("manger.Manager", func() { Expect(m.GetConfig()).To(Equal(originalCfg)) }) - It("should stop when context is cancelled", func() { + It("should stop when context is cancelled", func(specCtx SpecContext) { m, err := New(cfg, options) Expect(err).NotTo(HaveOccurred()) for _, cb := range callbacks { cb(m) } - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) cancel() Expect(m.Start(ctx)).NotTo(HaveOccurred()) }) - It("should return an error if it can't start the cache", func() { + It("should return an error if it can't start the cache", func(ctx SpecContext) { m, err := New(cfg, options) Expect(err).NotTo(HaveOccurred()) for _, cb := range callbacks { @@ -735,12 +744,10 @@ var _ = Describe("manger.Manager", func() { &cacheProvider{cache: &informertest.FakeInformers{Error: fmt.Errorf("expected error")}}, )).To(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() Expect(m.Start(ctx)).To(MatchError(ContainSubstring("expected error"))) }) - It("should start the cache before starting anything else", func() { + It("should start the cache before starting anything else", func(ctx SpecContext) { fakeCache := &startSignalingInformer{Cache: &informertest.FakeInformers{}} options.NewCache = func(_ *rest.Config, _ cache.Options) (cache.Cache, error) { return fakeCache, nil @@ -762,8 +769,6 @@ var _ = Describe("manger.Manager", func() { }) Expect(m.Add(runnable)).To(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(m.Start(ctx)).ToNot(HaveOccurred()) @@ -772,7 +777,7 @@ var _ = Describe("manger.Manager", func() { <-runnableWasStarted }) - It("should start additional clusters before anything else", func() { + It("should start additional clusters before anything else", func(ctx SpecContext) { fakeCache := &startSignalingInformer{Cache: &informertest.FakeInformers{}} options.NewCache = func(_ *rest.Config, _ cache.Options) (cache.Cache, error) { return fakeCache, nil @@ -805,8 +810,6 @@ var _ = Describe("manger.Manager", func() { return nil }))).To(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(m.Start(ctx)).ToNot(HaveOccurred()) @@ -815,7 +818,7 @@ var _ = Describe("manger.Manager", func() { <-runnableWasStarted }) - It("should return an error if any Components fail to Start", func() { + It("should return an error if any Components fail to Start", func(ctx SpecContext) { m, err := New(cfg, options) Expect(err).NotTo(HaveOccurred()) for _, cb := range callbacks { @@ -838,15 +841,12 @@ var _ = Describe("manger.Manager", func() { return nil }))).To(Succeed()) - defer GinkgoRecover() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() err = m.Start(ctx) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("expected error")) }) - It("should start caches added after Manager has started", func() { + It("should start caches added after Manager has started", func(ctx SpecContext) { fakeCache := &startSignalingInformer{Cache: &informertest.FakeInformers{}} options.NewCache = func(_ *rest.Config, _ cache.Options) (cache.Cache, error) { return fakeCache, nil @@ -867,8 +867,6 @@ var _ = Describe("manger.Manager", func() { return nil }))).To(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(m.Start(ctx)).ToNot(HaveOccurred()) @@ -889,7 +887,7 @@ var _ = Describe("manger.Manager", func() { }).Should(BeTrue()) }) - It("should wait for runnables to stop", func() { + It("should wait for runnables to stop", func(specCtx SpecContext) { m, err := New(cfg, options) Expect(err).NotTo(HaveOccurred()) for _, cb := range callbacks { @@ -923,7 +921,7 @@ var _ = Describe("manger.Manager", func() { }))).To(Succeed()) defer GinkgoRecover() - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) var wgManagerRunning sync.WaitGroup wgManagerRunning.Add(1) @@ -941,7 +939,7 @@ var _ = Describe("manger.Manager", func() { wgManagerRunning.Wait() }) - It("should return an error if any Components fail to Start and wait for runnables to stop", func() { + It("should return an error if any Components fail to Start and wait for runnables to stop", func(ctx SpecContext) { m, err := New(cfg, options) Expect(err).NotTo(HaveOccurred()) for _, cb := range callbacks { @@ -969,13 +967,11 @@ var _ = Describe("manger.Manager", func() { return nil }))).To(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() Expect(m.Start(ctx)).To(HaveOccurred()) Expect(runnableDoneCount).To(Equal(2)) }) - It("should refuse to add runnable if stop procedure is already engaged", func() { + It("should refuse to add runnable if stop procedure is already engaged", func(specCtx SpecContext) { m, err := New(cfg, options) Expect(err).NotTo(HaveOccurred()) for _, cb := range callbacks { @@ -992,7 +988,7 @@ var _ = Describe("manger.Manager", func() { return nil }))).To(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) go func() { Expect(m.Start(ctx)).NotTo(HaveOccurred()) }() @@ -1005,7 +1001,7 @@ var _ = Describe("manger.Manager", func() { }))).NotTo(Succeed()) }) - It("should not return runnables context.Canceled errors", func() { + It("should not return runnables context.Canceled errors", func(specCtx SpecContext) { Expect(options.Logger).To(BeZero(), "this test overrides Logger") var log struct { @@ -1033,7 +1029,7 @@ var _ = Describe("manger.Manager", func() { }))).To(Succeed()) stopped := make(chan error) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) go func() { stopped <- m.Start(ctx) }() @@ -1056,7 +1052,7 @@ var _ = Describe("manger.Manager", func() { ))) }) - It("should default controller logger from manager logger", func() { + It("should default controller logger from manager logger", func(specCtx SpecContext) { var lock sync.Mutex var messages []string options.Logger = funcr.NewJSON(func(object string) { @@ -1079,7 +1075,7 @@ var _ = Describe("manger.Manager", func() { }))).To(Succeed()) stopped := make(chan error) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) go func() { stopped <- m.Start(ctx) }() @@ -1102,7 +1098,7 @@ var _ = Describe("manger.Manager", func() { }).Should(Succeed()) }) - It("should return both runnables and stop errors when both error", func() { + It("should return both runnables and stop errors when both error", func(ctx SpecContext) { m, err := New(cfg, options) Expect(err).NotTo(HaveOccurred()) for _, cb := range callbacks { @@ -1125,8 +1121,6 @@ var _ = Describe("manger.Manager", func() { return nil } }))).To(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() err = m.Start(ctx) Expect(err).To(HaveOccurred()) eMsg := "[not feeling like that, failed waiting for all runnables to end within grace period of 1ns: context deadline exceeded]" @@ -1135,7 +1129,7 @@ var _ = Describe("manger.Manager", func() { Expect(errors.Is(err, runnableError{})).To(BeTrue()) }) - It("should return only stop errors if runnables dont error", func() { + It("should return only stop errors if runnables dont error", func(specCtx SpecContext) { m, err := New(cfg, options) Expect(err).NotTo(HaveOccurred()) for _, cb := range callbacks { @@ -1159,7 +1153,7 @@ var _ = Describe("manger.Manager", func() { return nil } }))).To(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) managerStopDone := make(chan struct{}) go func() { err = m.Start(ctx); close(managerStopDone) }() // Use the 'elected' channel to find out if startup was done, otherwise we stop @@ -1173,7 +1167,7 @@ var _ = Describe("manger.Manager", func() { Expect(errors.Is(err, runnableError{})).ToNot(BeTrue()) }) - It("should return only runnables error if stop doesn't error", func() { + It("should return only runnables error if stop doesn't error", func(ctx SpecContext) { m, err := New(cfg, options) Expect(err).NotTo(HaveOccurred()) for _, cb := range callbacks { @@ -1182,8 +1176,6 @@ var _ = Describe("manger.Manager", func() { Expect(m.Add(RunnableFunc(func(context.Context) error { return runnableError{} }))).To(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() err = m.Start(ctx) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("not feeling like that")) @@ -1191,7 +1183,7 @@ var _ = Describe("manger.Manager", func() { Expect(errors.Is(err, runnableError{})).To(BeTrue()) }) - It("should not wait for runnables if gracefulShutdownTimeout is 0", func() { + It("should not wait for runnables if gracefulShutdownTimeout is 0", func(specCtx SpecContext) { m, err := New(cfg, options) Expect(err).NotTo(HaveOccurred()) for _, cb := range callbacks { @@ -1207,7 +1199,7 @@ var _ = Describe("manger.Manager", func() { return nil }))).ToNot(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) managerStopDone := make(chan struct{}) go func() { defer GinkgoRecover() @@ -1221,7 +1213,7 @@ var _ = Describe("manger.Manager", func() { <-runnableStopped }) - It("should wait forever for runnables if gracefulShutdownTimeout is <0 (-1)", func() { + It("should wait forever for runnables if gracefulShutdownTimeout is <0 (-1)", func(specCtx SpecContext) { m, err := New(cfg, options) Expect(err).NotTo(HaveOccurred()) for _, cb := range callbacks { @@ -1250,7 +1242,7 @@ var _ = Describe("manger.Manager", func() { return nil }))).ToNot(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) managerStopDone := make(chan struct{}) go func() { defer GinkgoRecover() @@ -1286,7 +1278,7 @@ var _ = Describe("manger.Manager", func() { }, ) - It("should return an error if leader election param incorrect", func() { + It("should return an error if leader election param incorrect", func(specCtx SpecContext) { renewDeadline := time.Second * 20 m, err := New(cfg, Options{ LeaderElection: true, @@ -1296,7 +1288,7 @@ var _ = Describe("manger.Manager", func() { RenewDeadline: &renewDeadline, }) Expect(err).NotTo(HaveOccurred()) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + ctx, cancel := context.WithTimeout(specCtx, time.Second*10) defer cancel() err = m.Start(ctx) Expect(err).To(HaveOccurred()) @@ -1326,11 +1318,11 @@ var _ = Describe("manger.Manager", func() { } }) - It("should stop serving metrics when stop is called", func() { + It("should stop serving metrics when stop is called", func(specCtx SpecContext) { m, err := New(cfg, opts) Expect(err).NotTo(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) go func() { defer GinkgoRecover() Expect(m.Start(ctx)).NotTo(HaveOccurred()) @@ -1355,12 +1347,10 @@ var _ = Describe("manger.Manager", func() { }, 10*time.Second).ShouldNot(Succeed()) }) - It("should serve metrics endpoint", func() { + It("should serve metrics endpoint", func(ctx SpecContext) { m, err := New(cfg, opts) Expect(err).NotTo(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(m.Start(ctx)).NotTo(HaveOccurred()) @@ -1376,12 +1366,10 @@ var _ = Describe("manger.Manager", func() { Expect(resp.StatusCode).To(Equal(200)) }) - It("should not serve anything other than metrics endpoint by default", func() { + It("should not serve anything other than metrics endpoint by default", func(ctx SpecContext) { m, err := New(cfg, opts) Expect(err).NotTo(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(m.Start(ctx)).NotTo(HaveOccurred()) @@ -1398,7 +1386,7 @@ var _ = Describe("manger.Manager", func() { Expect(resp.StatusCode).To(Equal(404)) }) - It("should serve metrics in its registry", func() { + It("should serve metrics in its registry", func(ctx SpecContext) { one := prometheus.NewCounter(prometheus.CounterOpts{ Name: "test_one", Help: "test metric for testing", @@ -1410,8 +1398,6 @@ var _ = Describe("manger.Manager", func() { m, err := New(cfg, opts) Expect(err).NotTo(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(m.Start(ctx)).NotTo(HaveOccurred()) @@ -1440,7 +1426,7 @@ var _ = Describe("manger.Manager", func() { Expect(ok).To(BeTrue()) }) - It("should serve extra endpoints", func() { + It("should serve extra endpoints", func(ctx SpecContext) { opts.Metrics.ExtraHandlers = map[string]http.Handler{ "/debug": http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { _, _ = w.Write([]byte("Some debug info")) @@ -1455,8 +1441,6 @@ var _ = Describe("manger.Manager", func() { })) Expect(err).To(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(m.Start(ctx)).NotTo(HaveOccurred()) @@ -1500,12 +1484,12 @@ var _ = Describe("manger.Manager", func() { } }) - It("should stop serving health probes when stop is called", func() { + It("should stop serving health probes when stop is called", func(specCtx SpecContext) { opts.HealthProbeBindAddress = ":0" m, err := New(cfg, opts) Expect(err).NotTo(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) go func() { defer GinkgoRecover() Expect(m.Start(ctx)).NotTo(HaveOccurred()) @@ -1527,7 +1511,7 @@ var _ = Describe("manger.Manager", func() { }, 10*time.Second).ShouldNot(Succeed()) }) - It("should serve readiness endpoint", func() { + It("should serve readiness endpoint", func(ctx SpecContext) { opts.HealthProbeBindAddress = ":0" m, err := New(cfg, opts) Expect(err).NotTo(HaveOccurred()) @@ -1537,8 +1521,6 @@ var _ = Describe("manger.Manager", func() { err = m.AddReadyzCheck(namedCheck, func(_ *http.Request) error { return res }) Expect(err).NotTo(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(m.Start(ctx)).NotTo(HaveOccurred()) @@ -1582,7 +1564,7 @@ var _ = Describe("manger.Manager", func() { Expect(resp.StatusCode).To(Equal(http.StatusOK)) }) - It("should serve liveness endpoint", func() { + It("should serve liveness endpoint", func(ctx SpecContext) { opts.HealthProbeBindAddress = ":0" m, err := New(cfg, opts) Expect(err).NotTo(HaveOccurred()) @@ -1592,8 +1574,6 @@ var _ = Describe("manger.Manager", func() { err = m.AddHealthzCheck(namedCheck, func(_ *http.Request) error { return res }) Expect(err).NotTo(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(m.Start(ctx)).NotTo(HaveOccurred()) @@ -1659,12 +1639,12 @@ var _ = Describe("manger.Manager", func() { } }) - It("should stop serving pprof when stop is called", func() { + It("should stop serving pprof when stop is called", func(specCtx SpecContext) { opts.PprofBindAddress = ":0" m, err := New(cfg, opts) Expect(err).NotTo(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) go func() { defer GinkgoRecover() Expect(m.Start(ctx)).NotTo(HaveOccurred()) @@ -1686,13 +1666,11 @@ var _ = Describe("manger.Manager", func() { }, 10*time.Second).ShouldNot(Succeed()) }) - It("should serve pprof endpoints", func() { + It("should serve pprof endpoints", func(ctx SpecContext) { opts.PprofBindAddress = ":0" m, err := New(cfg, opts) Expect(err).NotTo(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(m.Start(ctx)).NotTo(HaveOccurred()) @@ -1733,7 +1711,7 @@ var _ = Describe("manger.Manager", func() { Describe("Add", func() { It("should immediately start the Component if the Manager has already Started another Component", - func() { + func(ctx SpecContext) { m, err := New(cfg, Options{}) Expect(err).NotTo(HaveOccurred()) mgr, ok := m.(*controllerManager) @@ -1747,8 +1725,6 @@ var _ = Describe("manger.Manager", func() { return nil }))).To(Succeed()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(m.Start(ctx)).NotTo(HaveOccurred()) @@ -1771,14 +1747,12 @@ var _ = Describe("manger.Manager", func() { <-c2 }) - It("should immediately start the Component if the Manager has already Started", func() { + It("should immediately start the Component if the Manager has already Started", func(ctx SpecContext) { m, err := New(cfg, Options{}) Expect(err).NotTo(HaveOccurred()) mgr, ok := m.(*controllerManager) Expect(ok).To(BeTrue()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(m.Start(ctx)).NotTo(HaveOccurred()) @@ -1798,12 +1772,10 @@ var _ = Describe("manger.Manager", func() { <-c1 }) - It("should fail if attempted to start a second time", func() { + It("should fail if attempted to start a second time", func(ctx SpecContext) { m, err := New(cfg, Options{}) Expect(err).NotTo(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(m.Start(ctx)).NotTo(HaveOccurred()) @@ -1822,13 +1794,13 @@ var _ = Describe("manger.Manager", func() { }) }) - It("should not leak goroutines when stopped", func() { + It("should not leak goroutines when stopped", func(specCtx SpecContext) { currentGRs := goleak.IgnoreCurrent() m, err := New(cfg, Options{}) Expect(err).NotTo(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) cancel() Expect(m.Start(ctx)).NotTo(HaveOccurred()) @@ -1838,7 +1810,7 @@ var _ = Describe("manger.Manager", func() { Eventually(func() error { return goleak.Find(currentGRs) }).Should(Succeed()) }) - It("should not leak goroutines if the default event broadcaster is used & events are emitted", func() { + It("should not leak goroutines if the default event broadcaster is used & events are emitted", func(specCtx SpecContext) { currentGRs := goleak.IgnoreCurrent() m, err := New(cfg, Options{ /* implicit: default setting for EventBroadcaster */ }) @@ -1855,7 +1827,7 @@ var _ = Describe("manger.Manager", func() { }))).To(Succeed()) By("starting the manager & waiting till we've sent our event") - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) doneCh := make(chan struct{}) go func() { defer GinkgoRecover() @@ -1886,6 +1858,54 @@ var _ = Describe("manger.Manager", func() { Eventually(func() error { return goleak.Find(currentGRs) }).Should(Succeed()) }) + It("should not leak goroutines when a runnable returns error slowly after being signaled to stop", func(specCtx SpecContext) { + // This test reproduces the race condition where the manager's Start method + // exits due to context cancellation, leaving no one to drain errChan + + currentGRs := goleak.IgnoreCurrent() + + // Create manager with a very short graceful shutdown timeout to reliablytrigger the race condition + shortGracefulShutdownTimeout := 10 * time.Millisecond + m, err := New(cfg, Options{ + GracefulShutdownTimeout: &shortGracefulShutdownTimeout, + }) + Expect(err).NotTo(HaveOccurred()) + + // Add the slow runnable that will return an error after some delay + for i := 0; i < 3; i++ { + slowRunnable := RunnableFunc(func(c context.Context) error { + <-c.Done() + + // Simulate some work that delays the error from being returned + // Choosing a large delay to reliably trigger the race condition + time.Sleep(100 * time.Millisecond) + + // This simulates the race condition where runnables try to send + // errors after the manager has stopped reading from errChan + return errors.New("slow runnable error") + }) + + Expect(m.Add(slowRunnable)).To(Succeed()) + } + + ctx, cancel := context.WithTimeout(specCtx, 50*time.Millisecond) + defer cancel() + go func() { + defer GinkgoRecover() + Expect(m.Start(ctx)).To(HaveOccurred()) // We expect error here because the slow runnables will return errors + }() + + // Wait for context to be cancelled + <-ctx.Done() + + // Give time for any leaks to become apparent. This makes sure that we don't false alarm on go routine leaks because runnables are still running. + time.Sleep(300 * time.Millisecond) + + // force-close keep-alive connections + clientTransport.CloseIdleConnections() + Eventually(func() error { return goleak.Find(currentGRs) }).Should(Succeed()) + }) + It("should provide a function to get the Config", func() { m, err := New(cfg, Options{}) Expect(err).NotTo(HaveOccurred()) @@ -1928,6 +1948,76 @@ var _ = Describe("manger.Manager", func() { Expect(err).NotTo(HaveOccurred()) Expect(m.GetAPIReader()).NotTo(BeNil()) }) + + It("should run warmup runnables before leader election is won", func(ctx SpecContext) { + By("Creating a channel to track execution order") + runnableExecutionOrderChan := make(chan string, 2) + const leaderElectionRunnableName = "leaderElectionRunnable" + const warmupRunnableName = "warmupRunnable" + + By("Creating a manager with leader election enabled") + m, err := New(cfg, Options{ + LeaderElection: true, + LeaderElectionNamespace: "default", + LeaderElectionID: "test-leader-election-warmup", + newResourceLock: fakeleaderelection.NewResourceLock, + HealthProbeBindAddress: "0", + Metrics: metricsserver.Options{BindAddress: "0"}, + PprofBindAddress: "0", + }) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a runnable that implements WarmupRunnable interface") + // Create a warmup runnable + warmupRunnable := newWarmupRunnableFunc( + func(ctx context.Context) error { + // This is the leader election runnable that will be executed after leader election + // It will block until context is done/cancelled + <-ctx.Done() + return nil + }, + func(ctx context.Context) error { + // This should be called during startup before leader election + runnableExecutionOrderChan <- warmupRunnableName + return nil + }, + ) + Expect(m.Add(warmupRunnable)).To(Succeed()) + + By("Creating a runnable that requires leader election") + leaderElectionRunnable := RunnableFunc( + func(ctx context.Context) error { + runnableExecutionOrderChan <- leaderElectionRunnableName + <-ctx.Done() + return nil + }, + ) + Expect(m.Add(leaderElectionRunnable)).To(Succeed()) + + cm, ok := m.(*controllerManager) + Expect(ok).To(BeTrue()) + resourceLockWithHooks, ok := cm.resourceLock.(fakeleaderelection.ControllableResourceLockInterface) + Expect(ok).To(BeTrue()) + + By("Blocking leader election") + resourceLockWithHooks.BlockLeaderElection() + + By("Starting the manager") + go func() { + defer GinkgoRecover() + Expect(m.Start(ctx)).To(Succeed()) + }() + + By("Waiting for the warmup runnable to be executed without leader election being won") + Expect(<-runnableExecutionOrderChan).To(Equal(warmupRunnableName)) + + By("Unblocking leader election") + resourceLockWithHooks.UnblockLeaderElection() + + By("Waiting for the leader election runnable to be executed after leader election was won") + <-m.Elected() + Expect(<-runnableExecutionOrderChan).To(Equal(leaderElectionRunnableName)) + }) }) type runnableError struct { diff --git a/pkg/manager/runnable_group.go b/pkg/manager/runnable_group.go index db5cda7c88..53e29fc56f 100644 --- a/pkg/manager/runnable_group.go +++ b/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/pkg/manager/runnable_group_test.go b/pkg/manager/runnable_group_test.go index f2f4119ba6..6f9b879e0e 100644 --- a/pkg/manager/runnable_group_test.go +++ b/pkg/manager/runnable_group_test.go @@ -27,6 +27,7 @@ var _ = Describe("runnables", func() { r := newRunnables(defaultBaseContext, errCh) Expect(r.Add(server)).To(Succeed()) Expect(r.HTTPServers.startQueue).To(HaveLen(1)) + Expect(r.Others.startQueue).To(BeEmpty()) }) It("should add caches to the appropriate group", func() { @@ -34,6 +35,7 @@ var _ = Describe("runnables", func() { r := newRunnables(defaultBaseContext, errCh) Expect(r.Add(cache)).To(Succeed()) Expect(r.Caches.startQueue).To(HaveLen(1)) + Expect(r.Others.startQueue).To(BeEmpty()) }) It("should add webhooks to the appropriate group", func() { @@ -41,6 +43,7 @@ var _ = Describe("runnables", func() { r := newRunnables(defaultBaseContext, errCh) Expect(r.Add(webhook)).To(Succeed()) Expect(r.Webhooks.startQueue).To(HaveLen(1)) + Expect(r.Others.startQueue).To(BeEmpty()) }) It("should add any runnable to the leader election group", func() { @@ -52,15 +55,122 @@ var _ = Describe("runnables", func() { r := newRunnables(defaultBaseContext, errCh) Expect(r.Add(runnable)).To(Succeed()) Expect(r.LeaderElection.startQueue).To(HaveLen(1)) + Expect(r.Others.startQueue).To(BeEmpty()) + }) + + It("should add WarmupRunnable to the Warmup and LeaderElection group", func() { + warmupRunnable := newWarmupRunnableFunc( + func(c context.Context) error { + <-c.Done() + return nil + }, + func(c context.Context) error { return nil }, + ) + + r := newRunnables(defaultBaseContext, errCh) + Expect(r.Add(warmupRunnable)).To(Succeed()) + Expect(r.Warmup.startQueue).To(HaveLen(1)) + Expect(r.LeaderElection.startQueue).To(HaveLen(1)) + Expect(r.Others.startQueue).To(BeEmpty()) + }) + + It("should add WarmupRunnable that doesn't needs leader election to warmup group only", func() { + warmupRunnable := newLeaderElectionAndWarmupRunnable( + func(c context.Context) error { + <-c.Done() + return nil + }, + func(c context.Context) error { return nil }, + false, + ) + + r := newRunnables(defaultBaseContext, errCh) + Expect(r.Add(warmupRunnable)).To(Succeed()) + + Expect(r.Warmup.startQueue).To(HaveLen(1)) + Expect(r.LeaderElection.startQueue).To(BeEmpty()) + Expect(r.Others.startQueue).To(HaveLen(1)) + }) + + It("should add WarmupRunnable that needs leader election to Warmup and LeaderElection group, not Others", func() { + warmupRunnable := newLeaderElectionAndWarmupRunnable( + func(c context.Context) error { + <-c.Done() + return nil + }, + func(c context.Context) error { return nil }, + true, + ) + + r := newRunnables(defaultBaseContext, errCh) + Expect(r.Add(warmupRunnable)).To(Succeed()) + + Expect(r.Warmup.startQueue).To(HaveLen(1)) + Expect(r.LeaderElection.startQueue).To(HaveLen(1)) + Expect(r.Others.startQueue).To(BeEmpty()) + }) + + It("should execute the Warmup function when Warmup group is started", func(ctx SpecContext) { + var warmupExecuted atomic.Bool + + warmupRunnable := newWarmupRunnableFunc( + func(c context.Context) error { + <-c.Done() + return nil + }, + func(c context.Context) error { + warmupExecuted.Store(true) + return nil + }, + ) + + r := newRunnables(defaultBaseContext, errCh) + Expect(r.Add(warmupRunnable)).To(Succeed()) + + // Start the Warmup group + Expect(r.Warmup.Start(ctx)).To(Succeed()) + + // Verify warmup function was called + Expect(warmupExecuted.Load()).To(BeTrue()) + }) + + It("should propagate errors from Warmup function to error channel", func(ctx SpecContext) { + expectedErr := fmt.Errorf("expected warmup error") + + warmupRunnable := newWarmupRunnableFunc( + func(c context.Context) error { + <-c.Done() + return nil + }, + func(c context.Context) error { return expectedErr }, + ) + + testErrChan := make(chan error, 1) + r := newRunnables(defaultBaseContext, testErrChan) + Expect(r.Add(warmupRunnable)).To(Succeed()) + + // Start the Warmup group in a goroutine + go func() { + Expect(r.Warmup.Start(ctx)).To(Succeed()) + }() + + // Error from Warmup should be sent to error channel + var receivedErr error + Eventually(func() error { + select { + case receivedErr = <-testErrChan: + return receivedErr + default: + return nil + } + }).Should(Equal(expectedErr)) }) }) var _ = Describe("runnableGroup", func() { errCh := make(chan error) - It("should be able to add new runnables before it starts", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + It("should be able to add new runnables before it starts", func(ctx SpecContext) { rg := newRunnableGroup(defaultBaseContext, errCh) Expect(rg.Add(RunnableFunc(func(c context.Context) error { <-ctx.Done() @@ -70,9 +180,7 @@ var _ = Describe("runnableGroup", func() { Expect(rg.Started()).To(BeFalse()) }) - It("should be able to add new runnables before and after start", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + It("should be able to add new runnables before and after start", func(ctx SpecContext) { rg := newRunnableGroup(defaultBaseContext, errCh) Expect(rg.Add(RunnableFunc(func(c context.Context) error { <-ctx.Done() @@ -86,9 +194,7 @@ var _ = Describe("runnableGroup", func() { }), nil)).To(Succeed()) }) - It("should be able to add new runnables before and after start concurrently", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + It("should be able to add new runnables before and after start concurrently", func(ctx SpecContext) { rg := newRunnableGroup(defaultBaseContext, errCh) go func() { @@ -110,8 +216,8 @@ var _ = Describe("runnableGroup", func() { } }) - It("should be able to close the group and wait for all runnables to finish", func() { - ctx, cancel := context.WithCancel(context.Background()) + It("should be able to close the group and wait for all runnables to finish", func(specCtx SpecContext) { + ctx, cancel := context.WithCancel(specCtx) exited := ptr.To(int64(0)) rg := newRunnableGroup(defaultBaseContext, errCh) @@ -127,7 +233,7 @@ var _ = Describe("runnableGroup", func() { // Cancel the context, asking the runnables to exit. cancel() - rg.StopAndWait(context.Background()) + rg.StopAndWait(specCtx) Expect(rg.Add(RunnableFunc(func(c context.Context) error { return nil @@ -136,8 +242,8 @@ var _ = Describe("runnableGroup", func() { Expect(atomic.LoadInt64(exited)).To(BeNumerically("==", 10)) }) - It("should be able to wait for all runnables to be ready at different intervals", func() { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + It("should be able to wait for all runnables to be ready at different intervals", func(specCtx SpecContext) { + ctx, cancel := context.WithTimeout(specCtx, 1*time.Second) defer cancel() rg := newRunnableGroup(defaultBaseContext, errCh) @@ -162,8 +268,8 @@ var _ = Describe("runnableGroup", func() { } }) - It("should be able to handle adding runnables while stopping", func() { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + It("should be able to handle adding runnables while stopping", func(specCtx SpecContext) { + ctx, cancel := context.WithTimeout(specCtx, 10*time.Second) defer cancel() rg := newRunnableGroup(defaultBaseContext, errCh) @@ -175,7 +281,7 @@ var _ = Describe("runnableGroup", func() { go func() { defer GinkgoRecover() <-time.After(1 * time.Millisecond) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(ctx) cancel() rg.StopAndWait(ctx) }() @@ -198,8 +304,8 @@ var _ = Describe("runnableGroup", func() { } }) - It("should not turn ready if some readiness check fail", func() { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + It("should not turn ready if some readiness check fail", func(specCtx SpecContext) { + ctx, cancel := context.WithTimeout(specCtx, 2*time.Second) defer cancel() rg := newRunnableGroup(defaultBaseContext, errCh) @@ -224,3 +330,57 @@ var _ = Describe("runnableGroup", func() { } }) }) + +var _ warmupRunnable = &warmupRunnableFunc{} + +func newWarmupRunnableFunc( + startFunc func(context.Context) error, + warmupFunc func(context.Context) error, +) *warmupRunnableFunc { + return &warmupRunnableFunc{ + startFunc: startFunc, + warmupFunc: warmupFunc, + } +} + +// warmupRunnableFunc is a helper struct that implements WarmupRunnable +// for testing purposes. +type warmupRunnableFunc struct { + startFunc func(context.Context) error + warmupFunc func(context.Context) error +} + +func (r *warmupRunnableFunc) Start(ctx context.Context) error { + return r.startFunc(ctx) +} + +func (r *warmupRunnableFunc) Warmup(ctx context.Context) error { + return r.warmupFunc(ctx) +} + +var _ LeaderElectionRunnable = &leaderElectionAndWarmupRunnable{} +var _ warmupRunnable = &leaderElectionAndWarmupRunnable{} + +// leaderElectionAndWarmupRunnable implements both WarmupRunnable and LeaderElectionRunnable +type leaderElectionAndWarmupRunnable struct { + *warmupRunnableFunc + needLeaderElection bool +} + +func newLeaderElectionAndWarmupRunnable( + startFunc func(context.Context) error, + warmupFunc func(context.Context) error, + needLeaderElection bool, +) *leaderElectionAndWarmupRunnable { + return &leaderElectionAndWarmupRunnable{ + warmupRunnableFunc: &warmupRunnableFunc{ + startFunc: startFunc, + warmupFunc: warmupFunc, + }, + needLeaderElection: needLeaderElection, + } +} + +func (r leaderElectionAndWarmupRunnable) NeedLeaderElection() bool { + return r.needLeaderElection +} diff --git a/pkg/manager/server.go b/pkg/manager/server.go index 76f6165b53..1983165da8 100644 --- a/pkg/manager/server.go +++ b/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/pkg/metrics/filters/filters_test.go b/pkg/metrics/filters/filters_test.go index fbf211e458..bd107fc56d 100644 --- a/pkg/metrics/filters/filters_test.go +++ b/pkg/metrics/filters/filters_test.go @@ -76,7 +76,7 @@ var _ = Describe("manger.Manager", func() { }} }) - It("should serve metrics in its registry", func() { + It("should serve metrics in its registry", func(ctx SpecContext) { one := prometheus.NewCounter(prometheus.CounterOpts{ Name: "test_one", Help: "test metric for testing", @@ -88,8 +88,6 @@ var _ = Describe("manger.Manager", func() { m, err := manager.New(cfg, opts) Expect(err).NotTo(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(m.Start(ctx)).NotTo(HaveOccurred()) @@ -128,7 +126,7 @@ var _ = Describe("manger.Manager", func() { Expect(ok).To(BeTrue()) }) - It("should serve extra endpoints", func() { + It("should serve extra endpoints", func(ctx SpecContext) { opts.Metrics.ExtraHandlers = map[string]http.Handler{ "/debug": http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { _, _ = w.Write([]byte("Some debug info")) @@ -137,8 +135,6 @@ var _ = Describe("manger.Manager", func() { m, err := manager.New(cfg, opts) Expect(err).NotTo(HaveOccurred()) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() go func() { defer GinkgoRecover() Expect(m.Start(ctx)).NotTo(HaveOccurred()) diff --git a/pkg/predicate/predicate.go b/pkg/predicate/predicate.go index ce33975f3b..9f24cb178c 100644 --- a/pkg/predicate/predicate.go +++ b/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] @@ -259,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/pkg/reconcile/reconcile_test.go b/pkg/reconcile/reconcile_test.go index fb6a88220a..bb5644b87c 100644 --- a/pkg/reconcile/reconcile_test.go +++ b/pkg/reconcile/reconcile_test.go @@ -63,7 +63,7 @@ var _ = Describe("reconcile", func() { }) Describe("Func", func() { - It("should call the function with the request and return a nil error.", func() { + It("should call the function with the request and return a nil error.", func(ctx SpecContext) { request := reconcile.Request{ NamespacedName: types.NamespacedName{Name: "foo", Namespace: "bar"}, } @@ -77,12 +77,12 @@ var _ = Describe("reconcile", func() { return result, nil }) - actualResult, actualErr := instance.Reconcile(context.Background(), request) + actualResult, actualErr := instance.Reconcile(ctx, request) Expect(actualResult).To(Equal(result)) Expect(actualErr).NotTo(HaveOccurred()) }) - It("should call the function with the request and return an error.", func() { + It("should call the function with the request and return an error.", func(ctx SpecContext) { request := reconcile.Request{ NamespacedName: types.NamespacedName{Name: "foo", Namespace: "bar"}, } @@ -97,7 +97,7 @@ var _ = Describe("reconcile", func() { return result, err }) - actualResult, actualErr := instance.Reconcile(context.Background(), request) + actualResult, actualErr := instance.Reconcile(ctx, request) Expect(actualResult).To(Equal(result)) Expect(actualErr).To(Equal(err)) }) @@ -136,7 +136,7 @@ var _ = Describe("reconcile", func() { Context("with an existing object", func() { var key client.ObjectKey - BeforeEach(func() { + BeforeEach(func(ctx SpecContext) { cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", @@ -145,11 +145,11 @@ var _ = Describe("reconcile", func() { } key = client.ObjectKeyFromObject(cm) - err := testClient.Create(context.Background(), cm) + err := testClient.Create(ctx, cm) Expect(err).NotTo(HaveOccurred()) }) - It("should Get the object and call the ObjectReconciler", func() { + It("should Get the object and call the ObjectReconciler", func(ctx SpecContext) { var actual *corev1.ConfigMap reconciler := reconcile.AsReconciler(testClient, &mockObjectReconciler{ reconcileFunc: func(ctx context.Context, cm *corev1.ConfigMap) (reconcile.Result, error) { @@ -158,7 +158,7 @@ var _ = Describe("reconcile", func() { }, }) - res, err := reconciler.Reconcile(context.Background(), reconcile.Request{NamespacedName: key}) + res, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key}) Expect(err).NotTo(HaveOccurred()) Expect(res).To(BeZero()) Expect(actual).NotTo(BeNil()) @@ -168,7 +168,7 @@ var _ = Describe("reconcile", func() { }) Context("with an object that doesn't exist", func() { - It("should not call the ObjectReconciler", func() { + It("should not call the ObjectReconciler", func(ctx SpecContext) { called := false reconciler := reconcile.AsReconciler(testClient, &mockObjectReconciler{ reconcileFunc: func(ctx context.Context, cm *corev1.ConfigMap) (reconcile.Result, error) { @@ -178,7 +178,7 @@ var _ = Describe("reconcile", func() { }) key := types.NamespacedName{Namespace: "default", Name: "fake-obj"} - res, err := reconciler.Reconcile(context.Background(), reconcile.Request{NamespacedName: key}) + res, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key}) Expect(err).NotTo(HaveOccurred()) Expect(res).To(BeZero()) Expect(called).To(BeFalse()) diff --git a/pkg/source/source_integration_test.go b/pkg/source/source_integration_test.go index 504a671c8a..cc0ba530ec 100644 --- a/pkg/source/source_integration_test.go +++ b/pkg/source/source_integration_test.go @@ -45,7 +45,7 @@ var _ = Describe("Source", func() { var ns string count := 0 - BeforeEach(func() { + BeforeEach(func(ctx SpecContext) { // Create the namespace for the test ns = fmt.Sprintf("controller-source-kindsource-%v", count) count++ @@ -63,7 +63,7 @@ var _ = Describe("Source", func() { c2 = make(chan interface{}) }) - AfterEach(func() { + AfterEach(func(ctx SpecContext) { err := clientset.CoreV1().Namespaces().Delete(ctx, ns, metav1.DeleteOptions{}) Expect(err).NotTo(HaveOccurred()) close(c1) @@ -74,7 +74,7 @@ var _ = Describe("Source", func() { Context("for a Deployment resource", func() { obj = &appsv1.Deployment{} - It("should provide Deployment Events", func() { + It("should provide Deployment Events", func(ctx SpecContext) { var created, updated, deleted *appsv1.Deployment var err error @@ -239,7 +239,7 @@ var _ = Describe("Source", func() { }) Context("for a ReplicaSet resource", func() { - It("should provide a ReplicaSet CreateEvent", func() { + It("should provide a ReplicaSet CreateEvent", func(ctx SpecContext) { c := make(chan struct{}) q := workqueue.NewTypedRateLimitingQueueWithConfig( @@ -282,7 +282,7 @@ var _ = Describe("Source", func() { <-c }) - It("should provide a ReplicaSet UpdateEvent", func() { + It("should provide a ReplicaSet UpdateEvent", func(ctx SpecContext) { var err error rs, err = clientset.AppsV1().ReplicaSets("default").Get(ctx, rs.Name, metav1.GetOptions{}) Expect(err).NotTo(HaveOccurred()) @@ -331,7 +331,7 @@ var _ = Describe("Source", func() { <-c }) - It("should provide a ReplicaSet DeletedEvent", func() { + It("should provide a ReplicaSet DeletedEvent", func(ctx SpecContext) { c := make(chan struct{}) q := workqueue.NewTypedRateLimitingQueueWithConfig( diff --git a/pkg/source/source_suite_test.go b/pkg/source/source_suite_test.go index 131099f0b9..774d978ca3 100644 --- a/pkg/source/source_suite_test.go +++ b/pkg/source/source_suite_test.go @@ -39,11 +39,12 @@ var testenv *envtest.Environment var config *rest.Config var clientset *kubernetes.Clientset var icache cache.Cache -var ctx context.Context var cancel context.CancelFunc var _ = BeforeSuite(func() { - ctx, cancel = context.WithCancel(context.Background()) + var ctx context.Context + // Has to be derived from context.Background, as it stays valid past the BeforeSuite + ctx, cancel = context.WithCancel(context.Background()) //nolint:forbidigo logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) testenv = &envtest.Environment{} diff --git a/pkg/source/source_test.go b/pkg/source/source_test.go index eec3179c7d..ad311d73b2 100644 --- a/pkg/source/source_test.go +++ b/pkg/source/source_test.go @@ -56,7 +56,7 @@ var _ = Describe("Source", func() { }) Context("for a Pod resource", func() { - It("should provide a Pod CreateEvent", func() { + It("should provide a Pod CreateEvent", func(ctx SpecContext) { c := make(chan struct{}) p := &corev1.Pod{ Spec: corev1.PodSpec{ @@ -93,7 +93,7 @@ var _ = Describe("Source", func() { }) err := instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) - Expect(instance.WaitForSync(context.Background())).NotTo(HaveOccurred()) + Expect(instance.WaitForSync(ctx)).NotTo(HaveOccurred()) i, err := ic.FakeInformerFor(ctx, &corev1.Pod{}) Expect(err).NotTo(HaveOccurred()) @@ -102,7 +102,7 @@ var _ = Describe("Source", func() { <-c }) - It("should provide a Pod UpdateEvent", func() { + It("should provide a Pod UpdateEvent", func(ctx SpecContext) { p2 := p.DeepCopy() p2.SetLabels(map[string]string{"biz": "baz"}) @@ -137,7 +137,7 @@ var _ = Describe("Source", func() { }) err := instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) - Expect(instance.WaitForSync(context.Background())).NotTo(HaveOccurred()) + Expect(instance.WaitForSync(ctx)).NotTo(HaveOccurred()) i, err := ic.FakeInformerFor(ctx, &corev1.Pod{}) Expect(err).NotTo(HaveOccurred()) @@ -146,7 +146,7 @@ var _ = Describe("Source", func() { <-c }) - It("should provide a Pod DeletedEvent", func() { + It("should provide a Pod DeletedEvent", func(ctx SpecContext) { c := make(chan struct{}) p := &corev1.Pod{ Spec: corev1.PodSpec{ @@ -183,7 +183,7 @@ var _ = Describe("Source", func() { }) err := instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) - Expect(instance.WaitForSync(context.Background())).NotTo(HaveOccurred()) + Expect(instance.WaitForSync(ctx)).NotTo(HaveOccurred()) i, err := ic.FakeInformerFor(ctx, &corev1.Pod{}) Expect(err).NotTo(HaveOccurred()) @@ -193,38 +193,38 @@ var _ = Describe("Source", func() { }) }) - It("should return an error from Start cache was not provided", func() { + It("should return an error from Start cache was not provided", func(ctx SpecContext) { instance := source.Kind(nil, &corev1.Pod{}, nil) err := instance.Start(ctx, nil) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("must create Kind with a non-nil cache")) }) - It("should return an error from Start if a type was not provided", func() { + It("should return an error from Start if a type was not provided", func(ctx SpecContext) { instance := source.Kind[client.Object](ic, nil, nil) err := instance.Start(ctx, nil) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("must create Kind with a non-nil object")) }) - It("should return an error from Start if a handler was not provided", func() { + It("should return an error from Start if a handler was not provided", func(ctx SpecContext) { instance := source.Kind(ic, &corev1.Pod{}, nil) err := instance.Start(ctx, nil) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("must create Kind with non-nil handler")) }) - It("should return an error if syncing fails", func() { + It("should return an error if syncing fails", func(ctx SpecContext) { f := false instance := source.Kind[client.Object](&informertest.FakeInformers{Synced: &f}, &corev1.Pod{}, &handler.EnqueueRequestForObject{}) - Expect(instance.Start(context.Background(), nil)).NotTo(HaveOccurred()) - err := instance.WaitForSync(context.Background()) + Expect(instance.Start(ctx, nil)).NotTo(HaveOccurred()) + err := instance.WaitForSync(ctx) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("cache did not sync")) }) Context("for a Kind not in the cache", func() { - It("should return an error when WaitForSync is called", func() { + It("should return an error when WaitForSync is called", func(specContext SpecContext) { ic.Error = fmt.Errorf("test error") q := workqueue.NewTypedRateLimitingQueueWithConfig( workqueue.DefaultTypedControllerRateLimiter[reconcile.Request](), @@ -232,21 +232,21 @@ var _ = Describe("Source", func() { Name: "test", }) - ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + ctx, cancel := context.WithTimeout(specContext, 2*time.Second) defer cancel() instance := source.Kind(ic, &corev1.Pod{}, handler.TypedFuncs[*corev1.Pod, reconcile.Request]{}) err := instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) - Eventually(instance.WaitForSync).WithArguments(context.Background()).Should(HaveOccurred()) + Eventually(instance.WaitForSync).WithArguments(ctx).Should(HaveOccurred()) }) }) - It("should return an error if syncing fails", func() { + It("should return an error if syncing fails", func(ctx SpecContext) { f := false instance := source.Kind[client.Object](&informertest.FakeInformers{Synced: &f}, &corev1.Pod{}, &handler.EnqueueRequestForObject{}) - Expect(instance.Start(context.Background(), nil)).NotTo(HaveOccurred()) - err := instance.WaitForSync(context.Background()) + Expect(instance.Start(ctx, nil)).NotTo(HaveOccurred()) + err := instance.WaitForSync(ctx) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("cache did not sync")) @@ -254,7 +254,7 @@ var _ = Describe("Source", func() { }) Describe("Func", func() { - It("should be called from Start", func() { + It("should be called from Start", func(ctx SpecContext) { run := false instance := source.Func(func( context.Context, @@ -276,22 +276,18 @@ var _ = Describe("Source", func() { }) Describe("Channel", func() { - var ctx context.Context - var cancel context.CancelFunc var ch chan event.GenericEvent BeforeEach(func() { - ctx, cancel = context.WithCancel(context.Background()) ch = make(chan event.GenericEvent) }) AfterEach(func() { - cancel() close(ch) }) Context("for a source", func() { - It("should provide a GenericEvent", func() { + It("should provide a GenericEvent", func(ctx SpecContext) { ch := make(chan event.GenericEvent) c := make(chan struct{}) p := &corev1.Pod{ @@ -348,7 +344,7 @@ var _ = Describe("Source", func() { ch <- evt <-c }) - It("should get pending events processed once channel unblocked", func() { + It("should get pending events processed once channel unblocked", func(ctx SpecContext) { ch := make(chan event.GenericEvent) unblock := make(chan struct{}) processed := make(chan struct{}) @@ -411,7 +407,7 @@ var _ = Describe("Source", func() { // Validate all of the events have been processed. Expect(eventCount).To(Equal(3)) }) - It("should be able to cope with events in the channel before the source is started", func() { + It("should be able to cope with events in the channel before the source is started", func(ctx SpecContext) { ch := make(chan event.GenericEvent, 1) processed := make(chan struct{}) evt := event.GenericEvent{} @@ -451,7 +447,7 @@ var _ = Describe("Source", func() { <-processed }) - It("should stop when the source channel is closed", func() { + It("should stop when the source channel is closed", func(ctx SpecContext) { q := workqueue.NewTypedRateLimitingQueueWithConfig( workqueue.DefaultTypedControllerRateLimiter[reconcile.Request](), workqueue.TypedRateLimitingQueueConfig[reconcile.Request]{ @@ -500,7 +496,7 @@ var _ = Describe("Source", func() { Eventually(processed).Should(Receive()) Consistently(processed).ShouldNot(Receive()) }) - It("should get error if no source specified", func() { + It("should get error if no source specified", func(ctx SpecContext) { q := workqueue.NewTypedRateLimitingQueueWithConfig( workqueue.DefaultTypedControllerRateLimiter[reconcile.Request](), workqueue.TypedRateLimitingQueueConfig[reconcile.Request]{ diff --git a/pkg/webhook/admission/defaulter_custom_test.go b/pkg/webhook/admission/defaulter_custom_test.go index 4ccff8f429..1bc26e59f4 100644 --- a/pkg/webhook/admission/defaulter_custom_test.go +++ b/pkg/webhook/admission/defaulter_custom_test.go @@ -30,11 +30,11 @@ import ( var _ = Describe("Defaulter Handler", func() { - It("should remove unknown fields when DefaulterRemoveUnknownFields is passed", func() { + It("should remove unknown fields when DefaulterRemoveUnknownFields is passed", func(ctx SpecContext) { obj := &TestDefaulter{} handler := WithCustomDefaulter(admissionScheme, obj, &TestCustomDefaulter{}, DefaulterRemoveUnknownOrOmitableFields) - resp := handler.Handle(context.TODO(), Request{ + resp := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Create, Object: runtime.RawExtension{ @@ -67,11 +67,11 @@ var _ = Describe("Defaulter Handler", func() { Expect(resp.Result.Code).Should(Equal(int32(http.StatusOK))) }) - It("should preserve unknown fields by default", func() { + It("should preserve unknown fields by default", func(ctx SpecContext) { obj := &TestDefaulter{} handler := WithCustomDefaulter(admissionScheme, obj, &TestCustomDefaulter{}) - resp := handler.Handle(context.TODO(), Request{ + resp := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Create, Object: runtime.RawExtension{ @@ -100,10 +100,10 @@ var _ = Describe("Defaulter Handler", func() { Expect(resp.Result.Code).Should(Equal(int32(http.StatusOK))) }) - It("should return ok if received delete verb in defaulter handler", func() { + It("should return ok if received delete verb in defaulter handler", func(ctx SpecContext) { obj := &TestDefaulter{} handler := WithCustomDefaulter(admissionScheme, obj, &TestCustomDefaulter{}) - resp := handler.Handle(context.TODO(), Request{ + resp := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Delete, OldObject: runtime.RawExtension{ diff --git a/pkg/webhook/admission/http_test.go b/pkg/webhook/admission/http_test.go index 86f35ac882..9cea9dd9e7 100644 --- a/pkg/webhook/admission/http_test.go +++ b/pkg/webhook/admission/http_test.go @@ -156,7 +156,7 @@ var _ = Describe("Admission Webhooks", func() { Expect(respRecorder.Body.String()).To(Equal(expected)) }) - It("should present the Context from the HTTP request, if any", func() { + It("should present the Context from the HTTP request, if any", func(specCtx SpecContext) { req := &http.Request{ Header: http.Header{"Content-Type": []string{"application/json"}}, Body: nopCloser{Reader: bytes.NewBufferString(`{"request":{}}`)}, @@ -176,13 +176,13 @@ var _ = Describe("Admission Webhooks", func() { expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"message":%q,"code":200}}} `, gvkJSONv1, value) - ctx, cancel := context.WithCancel(context.WithValue(context.Background(), key, value)) + ctx, cancel := context.WithCancel(context.WithValue(specCtx, key, value)) cancel() webhook.ServeHTTP(respRecorder, req.WithContext(ctx)) Expect(respRecorder.Body.String()).To(Equal(expected)) }) - It("should mutate the Context from the HTTP request, if func supplied", func() { + It("should mutate the Context from the HTTP request, if func supplied", func(specCtx SpecContext) { req := &http.Request{ Header: http.Header{"Content-Type": []string{"application/json"}}, Body: nopCloser{Reader: bytes.NewBufferString(`{"request":{}}`)}, @@ -203,7 +203,7 @@ var _ = Describe("Admission Webhooks", func() { expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"message":%q,"code":200}}} `, gvkJSONv1, "application/json") - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specCtx) cancel() webhook.ServeHTTP(respRecorder, req.WithContext(ctx)) Expect(respRecorder.Body.String()).To(Equal(expected)) diff --git a/pkg/webhook/admission/multi_test.go b/pkg/webhook/admission/multi_test.go index d41675ab30..888836ed67 100644 --- a/pkg/webhook/admission/multi_test.go +++ b/pkg/webhook/admission/multi_test.go @@ -58,32 +58,32 @@ var _ = Describe("Multi-Handler Admission Webhooks", func() { } Context("with validating handlers", func() { - It("should deny the request if any handler denies the request", func() { + It("should deny the request if any handler denies the request", func(ctx SpecContext) { By("setting up a handler with accept and deny") handler := MultiValidatingHandler(alwaysAllow, alwaysDeny) By("checking that the handler denies the request") - resp := handler.Handle(context.Background(), Request{}) + resp := handler.Handle(ctx, Request{}) Expect(resp.Allowed).To(BeFalse()) Expect(resp.Warnings).To(BeEmpty()) }) - It("should allow the request if all handlers allow the request", func() { + It("should allow the request if all handlers allow the request", func(ctx SpecContext) { By("setting up a handler with only accept") handler := MultiValidatingHandler(alwaysAllow, alwaysAllow) By("checking that the handler allows the request") - resp := handler.Handle(context.Background(), Request{}) + resp := handler.Handle(ctx, Request{}) Expect(resp.Allowed).To(BeTrue()) Expect(resp.Warnings).To(BeEmpty()) }) - It("should show the warnings if all handlers allow the request", func() { + It("should show the warnings if all handlers allow the request", func(ctx SpecContext) { By("setting up a handler with only accept") handler := MultiValidatingHandler(alwaysAllow, withWarnings) By("checking that the handler allows the request") - resp := handler.Handle(context.Background(), Request{}) + resp := handler.Handle(ctx, Request{}) Expect(resp.Allowed).To(BeTrue()) Expect(resp.Warnings).To(HaveLen(1)) }) @@ -149,34 +149,34 @@ var _ = Describe("Multi-Handler Admission Webhooks", func() { }, } - It("should not return any patches if the request is denied", func() { + It("should not return any patches if the request is denied", func(ctx SpecContext) { By("setting up a webhook with some patches and a deny") handler := MultiMutatingHandler(patcher1, patcher2, alwaysDeny) By("checking that the handler denies the request and produces no patches") - resp := handler.Handle(context.Background(), Request{}) + resp := handler.Handle(ctx, Request{}) Expect(resp.Allowed).To(BeFalse()) Expect(resp.Patches).To(BeEmpty()) }) - It("should produce all patches if the requests are all allowed", func() { + It("should produce all patches if the requests are all allowed", func(ctx SpecContext) { By("setting up a webhook with some patches") handler := MultiMutatingHandler(patcher1, patcher2, alwaysAllow) By("checking that the handler accepts the request and returns all patches") - resp := handler.Handle(context.Background(), Request{}) + resp := handler.Handle(ctx, Request{}) Expect(resp.Allowed).To(BeTrue()) Expect(resp.Patch).To(Equal([]byte( `[{"op":"add","path":"/metadata/annotation/new-key","value":"new-value"},` + `{"op":"replace","path":"/spec/replicas","value":"2"},{"op":"add","path":"/metadata/annotation/hello","value":"world"}]`))) }) - It("should produce all patches if the requests are all allowed and show warnings", func() { + It("should produce all patches if the requests are all allowed and show warnings", func(ctx SpecContext) { By("setting up a webhook with some patches") handler := MultiMutatingHandler(patcher1, patcher2, alwaysAllow, patcher3) By("checking that the handler accepts the request and returns all patches") - resp := handler.Handle(context.Background(), Request{}) + resp := handler.Handle(ctx, Request{}) Expect(resp.Allowed).To(BeTrue()) Expect(resp.Patch).To(Equal([]byte( `[{"op":"add","path":"/metadata/annotation/new-key","value":"new-value"},` + diff --git a/pkg/webhook/admission/validator_custom_test.go b/pkg/webhook/admission/validator_custom_test.go index 0e783560a1..7c9615df71 100644 --- a/pkg/webhook/admission/validator_custom_test.go +++ b/pkg/webhook/admission/validator_custom_test.go @@ -37,9 +37,9 @@ var _ = Describe("customValidatingHandler", func() { f := &fakeValidator{} handler := WithCustomValidator(admissionScheme, f, val) - It("should return 200 in response when create succeeds", func() { + It("should return 200 in response when create succeeds", func(ctx SpecContext) { - response := handler.Handle(context.TODO(), Request{ + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Create, Object: runtime.RawExtension{ @@ -53,9 +53,9 @@ var _ = Describe("customValidatingHandler", func() { Expect(response.Result.Code).Should(Equal(int32(http.StatusOK))) }) - It("should return 200 in response when update succeeds", func() { + It("should return 200 in response when update succeeds", func(ctx SpecContext) { - response := handler.Handle(context.TODO(), Request{ + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Update, Object: runtime.RawExtension{ @@ -72,9 +72,9 @@ var _ = Describe("customValidatingHandler", func() { Expect(response.Result.Code).Should(Equal(int32(http.StatusOK))) }) - It("should return 200 in response when delete succeeds", func() { + It("should return 200 in response when delete succeeds", func(ctx SpecContext) { - response := handler.Handle(context.TODO(), Request{ + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Delete, OldObject: runtime.RawExtension{ @@ -97,8 +97,8 @@ var _ = Describe("customValidatingHandler", func() { anotherWarningMessage, }} handler := WithCustomValidator(admissionScheme, f, val) - It("should return 200 in response when create succeeds, with warning messages", func() { - response := handler.Handle(context.TODO(), Request{ + It("should return 200 in response when create succeeds, with warning messages", func(ctx SpecContext) { + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Create, Object: runtime.RawExtension{ @@ -114,9 +114,9 @@ var _ = Describe("customValidatingHandler", func() { Expect(response.AdmissionResponse.Warnings).Should(ContainElement(anotherWarningMessage)) }) - It("should return 200 in response when update succeeds, with warning messages", func() { + It("should return 200 in response when update succeeds, with warning messages", func(ctx SpecContext) { - response := handler.Handle(context.TODO(), Request{ + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Update, Object: runtime.RawExtension{ @@ -135,9 +135,9 @@ var _ = Describe("customValidatingHandler", func() { Expect(response.AdmissionResponse.Warnings).Should(ContainElement(anotherWarningMessage)) }) - It("should return 200 in response when delete succeeds, with warning messages", func() { + It("should return 200 in response when delete succeeds, with warning messages", func(ctx SpecContext) { - response := handler.Handle(context.TODO(), Request{ + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Delete, OldObject: runtime.RawExtension{ @@ -166,9 +166,9 @@ var _ = Describe("customValidatingHandler", func() { val := &fakeCustomValidator{ErrorToReturn: expectedError, GVKToReturn: fakeValidatorVK, WarningsToReturn: []string{warningMessage, anotherWarningMessage}} handler := WithCustomValidator(admissionScheme, f, val) - It("should propagate the Status from ValidateCreate's return value to the HTTP response", func() { + It("should propagate the Status from ValidateCreate's return value to the HTTP response", func(ctx SpecContext) { - response := handler.Handle(context.TODO(), Request{ + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Create, Object: runtime.RawExtension{ @@ -186,9 +186,9 @@ var _ = Describe("customValidatingHandler", func() { }) - It("should propagate the Status from ValidateUpdate's return value to the HTTP response", func() { + It("should propagate the Status from ValidateUpdate's return value to the HTTP response", func(ctx SpecContext) { - response := handler.Handle(context.TODO(), Request{ + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Update, Object: runtime.RawExtension{ @@ -211,9 +211,9 @@ var _ = Describe("customValidatingHandler", func() { }) - It("should propagate the Status from ValidateDelete's return value to the HTTP response", func() { + It("should propagate the Status from ValidateDelete's return value to the HTTP response", func(ctx SpecContext) { - response := handler.Handle(context.TODO(), Request{ + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Delete, @@ -246,9 +246,9 @@ var _ = Describe("customValidatingHandler", func() { val := &fakeCustomValidator{ErrorToReturn: expectedError, GVKToReturn: fakeValidatorVK, WarningsToReturn: nil} handler := WithCustomValidator(admissionScheme, f, val) - It("should propagate the Status from ValidateCreate's return value to the HTTP response", func() { + It("should propagate the Status from ValidateCreate's return value to the HTTP response", func(ctx SpecContext) { - response := handler.Handle(context.TODO(), Request{ + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Create, @@ -265,9 +265,9 @@ var _ = Describe("customValidatingHandler", func() { }) - It("should propagate the Status from ValidateUpdate's return value to the HTTP response", func() { + It("should propagate the Status from ValidateUpdate's return value to the HTTP response", func(ctx SpecContext) { - response := handler.Handle(context.TODO(), Request{ + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Update, Object: runtime.RawExtension{ @@ -287,9 +287,9 @@ var _ = Describe("customValidatingHandler", func() { }) - It("should propagate the Status from ValidateDelete's return value to the HTTP response", func() { + It("should propagate the Status from ValidateDelete's return value to the HTTP response", func(ctx SpecContext) { - response := handler.Handle(context.TODO(), Request{ + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Delete, OldObject: runtime.RawExtension{ @@ -315,9 +315,9 @@ var _ = Describe("customValidatingHandler", func() { val := &fakeCustomValidator{ErrorToReturn: expectedError, GVKToReturn: fakeValidatorVK} handler := WithCustomValidator(admissionScheme, f, val) - It("should return 403 response when ValidateCreate with error message embedded", func() { + It("should return 403 response when ValidateCreate with error message embedded", func(ctx SpecContext) { - response := handler.Handle(context.TODO(), Request{ + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Create, @@ -334,9 +334,9 @@ var _ = Describe("customValidatingHandler", func() { }) - It("should return 403 response when ValidateUpdate returns non-APIStatus error", func() { + It("should return 403 response when ValidateUpdate returns non-APIStatus error", func(ctx SpecContext) { - response := handler.Handle(context.TODO(), Request{ + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Update, @@ -357,8 +357,8 @@ var _ = Describe("customValidatingHandler", func() { }) - It("should return 403 response when ValidateDelete returns non-APIStatus error", func() { - response := handler.Handle(context.TODO(), Request{ + It("should return 403 response when ValidateDelete returns non-APIStatus error", func(ctx SpecContext) { + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Delete, OldObject: runtime.RawExtension{ @@ -381,9 +381,9 @@ var _ = Describe("customValidatingHandler", func() { val := &fakeCustomValidator{ErrorToReturn: expectedError, GVKToReturn: fakeValidatorVK, WarningsToReturn: []string{warningMessage, anotherWarningMessage}} handler := WithCustomValidator(admissionScheme, f, val) - It("should return 403 response when ValidateCreate with error message embedded", func() { + It("should return 403 response when ValidateCreate with error message embedded", func(ctx SpecContext) { - response := handler.Handle(context.TODO(), Request{ + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Create, @@ -402,9 +402,9 @@ var _ = Describe("customValidatingHandler", func() { Expect(response.AdmissionResponse.Warnings).Should(ContainElement(anotherWarningMessage)) }) - It("should return 403 response when ValidateUpdate returns non-APIStatus error", func() { + It("should return 403 response when ValidateUpdate returns non-APIStatus error", func(ctx SpecContext) { - response := handler.Handle(context.TODO(), Request{ + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Update, Object: runtime.RawExtension{ @@ -428,8 +428,8 @@ var _ = Describe("customValidatingHandler", func() { }) - It("should return 403 response when ValidateDelete returns non-APIStatus error", func() { - response := handler.Handle(context.TODO(), Request{ + It("should return 403 response when ValidateDelete returns non-APIStatus error", func(ctx SpecContext) { + response := handler.Handle(ctx, Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Delete, OldObject: runtime.RawExtension{ diff --git a/pkg/webhook/admission/webhook_test.go b/pkg/webhook/admission/webhook_test.go index 102988bc6e..5176077368 100644 --- a/pkg/webhook/admission/webhook_test.go +++ b/pkg/webhook/admission/webhook_test.go @@ -64,40 +64,40 @@ var _ = Describe("Admission Webhooks", func() { return webhook } - It("should invoke the handler to get a response", func() { + It("should invoke the handler to get a response", func(ctx SpecContext) { By("setting up a webhook with an allow handler") webhook := allowHandler() By("invoking the webhook") - resp := webhook.Handle(context.Background(), Request{}) + resp := webhook.Handle(ctx, Request{}) By("checking that it allowed the request") Expect(resp.Allowed).To(BeTrue()) }) - It("should ensure that the response's UID is set to the request's UID", func() { + It("should ensure that the response's UID is set to the request's UID", func(ctx SpecContext) { By("setting up a webhook") webhook := allowHandler() By("invoking the webhook") - resp := webhook.Handle(context.Background(), Request{AdmissionRequest: admissionv1.AdmissionRequest{UID: "foobar"}}) + resp := webhook.Handle(ctx, Request{AdmissionRequest: admissionv1.AdmissionRequest{UID: "foobar"}}) By("checking that the response share's the request's UID") Expect(resp.UID).To(Equal(machinerytypes.UID("foobar"))) }) - It("should populate the status on a response if one is not provided", func() { + It("should populate the status on a response if one is not provided", func(ctx SpecContext) { By("setting up a webhook") webhook := allowHandler() By("invoking the webhook") - resp := webhook.Handle(context.Background(), Request{}) + resp := webhook.Handle(ctx, Request{}) By("checking that the response share's the request's UID") Expect(resp.Result).To(Equal(&metav1.Status{Code: http.StatusOK})) }) - It("shouldn't overwrite the status on a response", func() { + It("shouldn't overwrite the status on a response", func(ctx SpecContext) { By("setting up a webhook that sets a status") webhook := &Webhook{ Handler: HandlerFunc(func(ctx context.Context, req Request) Response { @@ -111,14 +111,14 @@ var _ = Describe("Admission Webhooks", func() { } By("invoking the webhook") - resp := webhook.Handle(context.Background(), Request{}) + resp := webhook.Handle(ctx, Request{}) By("checking that the message is intact") Expect(resp.Result).NotTo(BeNil()) Expect(resp.Result.Message).To(Equal("Ground Control to Major Tom")) }) - It("should serialize patch operations into a single jsonpatch blob", func() { + It("should serialize patch operations into a single jsonpatch blob", func(ctx SpecContext) { By("setting up a webhook with a patching handler") webhook := &Webhook{ Handler: HandlerFunc(func(ctx context.Context, req Request) Response { @@ -127,7 +127,7 @@ var _ = Describe("Admission Webhooks", func() { } By("invoking the webhook") - resp := webhook.Handle(context.Background(), Request{}) + resp := webhook.Handle(ctx, Request{}) By("checking that a JSON patch is populated on the response") patchType := admissionv1.PatchTypeJSONPatch @@ -135,7 +135,7 @@ var _ = Describe("Admission Webhooks", func() { Expect(resp.Patch).To(Equal([]byte(`[{"op":"add","path":"/a","value":2},{"op":"replace","path":"/b","value":4}]`))) }) - It("should pass a request logger via the context", func() { + It("should pass a request logger via the context", func(ctx SpecContext) { By("setting up a webhook that uses the request logger") webhook := &Webhook{ Handler: HandlerFunc(func(ctx context.Context, req Request) Response { @@ -151,7 +151,7 @@ var _ = Describe("Admission Webhooks", func() { } By("invoking the webhook") - resp := webhook.Handle(context.Background(), Request{AdmissionRequest: admissionv1.AdmissionRequest{ + resp := webhook.Handle(ctx, Request{AdmissionRequest: admissionv1.AdmissionRequest{ UID: "test123", Name: "foo", Namespace: "bar", @@ -170,7 +170,7 @@ var _ = Describe("Admission Webhooks", func() { Eventually(logBuffer).Should(gbytes.Say(`"msg":"Received request","object":{"name":"foo","namespace":"bar"},"namespace":"bar","name":"foo","resource":{"group":"apps","version":"v1","resource":"deployments"},"user":"tim","requestID":"test123"}`)) }) - It("should pass a request logger created by LogConstructor via the context", func() { + It("should pass a request logger created by LogConstructor via the context", func(ctx SpecContext) { By("setting up a webhook that uses the request logger") webhook := &Webhook{ Handler: HandlerFunc(func(ctx context.Context, req Request) Response { @@ -189,7 +189,7 @@ var _ = Describe("Admission Webhooks", func() { } By("invoking the webhook") - resp := webhook.Handle(context.Background(), Request{AdmissionRequest: admissionv1.AdmissionRequest{ + resp := webhook.Handle(ctx, Request{AdmissionRequest: admissionv1.AdmissionRequest{ UID: "test123", Operation: admissionv1.Create, }}) @@ -200,7 +200,7 @@ var _ = Describe("Admission Webhooks", func() { }) Describe("panic recovery", func() { - It("should recover panic if RecoverPanic is true by default", func() { + It("should recover panic if RecoverPanic is true by default", func(ctx SpecContext) { panicHandler := func() *Webhook { handler := &fakeHandler{ fn: func(ctx context.Context, req Request) Response { @@ -219,7 +219,7 @@ var _ = Describe("Admission Webhooks", func() { webhook := panicHandler() By("invoking the webhook") - resp := webhook.Handle(context.Background(), Request{}) + resp := webhook.Handle(ctx, Request{}) By("checking that it errored the request") Expect(resp.Allowed).To(BeFalse()) @@ -227,7 +227,7 @@ var _ = Describe("Admission Webhooks", func() { Expect(resp.Result.Message).To(Equal("panic: fake panic test [recovered]")) }) - It("should recover panic if RecoverPanic is true", func() { + It("should recover panic if RecoverPanic is true", func(ctx SpecContext) { panicHandler := func() *Webhook { handler := &fakeHandler{ fn: func(ctx context.Context, req Request) Response { @@ -246,7 +246,7 @@ var _ = Describe("Admission Webhooks", func() { webhook := panicHandler() By("invoking the webhook") - resp := webhook.Handle(context.Background(), Request{}) + resp := webhook.Handle(ctx, Request{}) By("checking that it errored the request") Expect(resp.Allowed).To(BeFalse()) @@ -254,7 +254,7 @@ var _ = Describe("Admission Webhooks", func() { Expect(resp.Result.Message).To(Equal("panic: fake panic test [recovered]")) }) - It("should not recover panic if RecoverPanic is false", func() { + It("should not recover panic if RecoverPanic is false", func(ctx SpecContext) { panicHandler := func() *Webhook { handler := &fakeHandler{ fn: func(ctx context.Context, req Request) Response { @@ -276,20 +276,19 @@ var _ = Describe("Admission Webhooks", func() { webhook := panicHandler() By("invoking the webhook") - webhook.Handle(context.Background(), Request{}) + webhook.Handle(ctx, Request{}) }) }) }) -var _ = Describe("Should be able to write/read admission.Request to/from context", func() { - ctx := context.Background() +var _ = It("Should be able to write/read admission.Request to/from context", func(specContext SpecContext) { testRequest := Request{ admissionv1.AdmissionRequest{ UID: "test-uid", }, } - ctx = NewContextWithRequest(ctx, testRequest) + ctx := NewContextWithRequest(specContext, testRequest) gotRequest, err := RequestFromContext(ctx) Expect(err).To(Not(HaveOccurred())) diff --git a/pkg/webhook/authentication/http_test.go b/pkg/webhook/authentication/http_test.go index 101b0c702d..e51b2af7e6 100644 --- a/pkg/webhook/authentication/http_test.go +++ b/pkg/webhook/authentication/http_test.go @@ -50,10 +50,10 @@ var _ = Describe("Authentication Webhooks", func() { It("should return bad-request when given an empty body", func() { req := &http.Request{Body: nil} - expected := `{"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"request body is empty"}} + expected := `{"metadata":{},"spec":{},"status":{"user":{},"error":"request body is empty"}} ` webhook.ServeHTTP(respRecorder, req) - Expect(respRecorder.Body.String()).To(Equal(expected)) + Expect(respRecorder.Body.String()).To(BeComparableTo(expected)) }) It("should return bad-request when given the wrong content-type", func() { @@ -63,7 +63,7 @@ var _ = Describe("Authentication Webhooks", func() { Body: nopCloser{Reader: bytes.NewBuffer(nil)}, } - expected := `{"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"contentType=application/foo, expected application/json"}} + expected := `{"metadata":{},"spec":{},"status":{"user":{},"error":"contentType=application/foo, expected application/json"}} ` webhook.ServeHTTP(respRecorder, req) Expect(respRecorder.Body.String()).To(Equal(expected)) @@ -76,7 +76,7 @@ var _ = Describe("Authentication Webhooks", func() { Body: nopCloser{Reader: bytes.NewBufferString("{")}, } - expected := `{"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"couldn't get version/kind; json parse error: unexpected end of JSON input"}} + expected := `{"metadata":{},"spec":{},"status":{"user":{},"error":"couldn't get version/kind; json parse error: unexpected end of JSON input"}} ` webhook.ServeHTTP(respRecorder, req) Expect(respRecorder.Body.String()).To(Equal(expected)) @@ -89,7 +89,7 @@ var _ = Describe("Authentication Webhooks", func() { Body: nopCloser{Reader: bytes.NewBufferString(`{"spec":{"token":""}}`)}, } - expected := `{"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"token is empty"}} + expected := `{"metadata":{},"spec":{},"status":{"user":{},"error":"token is empty"}} ` webhook.ServeHTTP(respRecorder, req) Expect(respRecorder.Body.String()).To(Equal(expected)) @@ -102,7 +102,7 @@ var _ = Describe("Authentication Webhooks", func() { Body: http.NoBody, } - expected := `{"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"request body is empty"}} + expected := `{"metadata":{},"spec":{},"status":{"user":{},"error":"request body is empty"}} ` webhook.ServeHTTP(respRecorder, req) Expect(respRecorder.Body.String()).To(Equal(expected)) @@ -115,7 +115,7 @@ var _ = Describe("Authentication Webhooks", func() { Body: nopCloser{Reader: rand.Reader}, } - expected := `{"metadata":{"creationTimestamp":null},"spec":{},"status":{"user":{},"error":"request entity is too large; limit is 1048576 bytes"}} + expected := `{"metadata":{},"spec":{},"status":{"user":{},"error":"request entity is too large; limit is 1048576 bytes"}} ` webhook.ServeHTTP(respRecorder, req) Expect(respRecorder.Body.String()).To(Equal(expected)) @@ -131,7 +131,7 @@ var _ = Describe("Authentication Webhooks", func() { Handler: &fakeHandler{}, } - expected := fmt.Sprintf(`{%s,"metadata":{"creationTimestamp":null},"spec":{},"status":{"authenticated":true,"user":{}}} + expected := fmt.Sprintf(`{%s,"metadata":{},"spec":{},"status":{"authenticated":true,"user":{}}} `, gvkJSONv1) webhook.ServeHTTP(respRecorder, req) @@ -148,13 +148,13 @@ var _ = Describe("Authentication Webhooks", func() { Handler: &fakeHandler{}, } - expected := fmt.Sprintf(`{%s,"metadata":{"creationTimestamp":null},"spec":{},"status":{"authenticated":true,"user":{}}} + expected := fmt.Sprintf(`{%s,"metadata":{},"spec":{},"status":{"authenticated":true,"user":{}}} `, gvkJSONv1) webhook.ServeHTTP(respRecorder, req) Expect(respRecorder.Body.String()).To(Equal(expected)) }) - It("should present the Context from the HTTP request, if any", func() { + It("should present the Context from the HTTP request, if any", func(specContext SpecContext) { req := &http.Request{ Header: http.Header{"Content-Type": []string{"application/json"}}, Method: http.MethodPost, @@ -172,16 +172,16 @@ var _ = Describe("Authentication Webhooks", func() { }, } - expected := fmt.Sprintf(`{%s,"metadata":{"creationTimestamp":null},"spec":{},"status":{"authenticated":true,"user":{},"error":%q}} + expected := fmt.Sprintf(`{%s,"metadata":{},"spec":{},"status":{"authenticated":true,"user":{},"error":%q}} `, gvkJSONv1, value) - ctx, cancel := context.WithCancel(context.WithValue(context.Background(), key, value)) + ctx, cancel := context.WithCancel(context.WithValue(specContext, key, value)) cancel() webhook.ServeHTTP(respRecorder, req.WithContext(ctx)) Expect(respRecorder.Body.String()).To(Equal(expected)) }) - It("should mutate the Context from the HTTP request, if func supplied", func() { + It("should mutate the Context from the HTTP request, if func supplied", func(specContext SpecContext) { req := &http.Request{ Header: http.Header{"Content-Type": []string{"application/json"}}, Method: http.MethodPost, @@ -200,10 +200,10 @@ var _ = Describe("Authentication Webhooks", func() { }, } - expected := fmt.Sprintf(`{%s,"metadata":{"creationTimestamp":null},"spec":{},"status":{"authenticated":true,"user":{},"error":%q}} + expected := fmt.Sprintf(`{%s,"metadata":{},"spec":{},"status":{"authenticated":true,"user":{},"error":%q}} `, gvkJSONv1, "application/json") - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(specContext) cancel() webhook.ServeHTTP(respRecorder, req.WithContext(ctx)) Expect(respRecorder.Body.String()).To(Equal(expected)) diff --git a/pkg/webhook/authentication/webhook_test.go b/pkg/webhook/authentication/webhook_test.go index 3df446d898..22c4e284cd 100644 --- a/pkg/webhook/authentication/webhook_test.go +++ b/pkg/webhook/authentication/webhook_test.go @@ -47,40 +47,40 @@ var _ = Describe("Authentication Webhooks", func() { return webhook } - It("should invoke the handler to get a response", func() { + It("should invoke the handler to get a response", func(ctx SpecContext) { By("setting up a webhook with an allow handler") webhook := allowHandler() By("invoking the webhook") - resp := webhook.Handle(context.Background(), Request{}) + resp := webhook.Handle(ctx, Request{}) By("checking that it allowed the request") Expect(resp.Status.Authenticated).To(BeTrue()) }) - It("should ensure that the response's UID is set to the request's UID", func() { + It("should ensure that the response's UID is set to the request's UID", func(ctx SpecContext) { By("setting up a webhook") webhook := allowHandler() By("invoking the webhook") - resp := webhook.Handle(context.Background(), Request{TokenReview: authenticationv1.TokenReview{ObjectMeta: metav1.ObjectMeta{UID: "foobar"}}}) + resp := webhook.Handle(ctx, Request{TokenReview: authenticationv1.TokenReview{ObjectMeta: metav1.ObjectMeta{UID: "foobar"}}}) By("checking that the response share's the request's UID") Expect(resp.UID).To(Equal(machinerytypes.UID("foobar"))) }) - It("should populate the status on a response if one is not provided", func() { + It("should populate the status on a response if one is not provided", func(ctx SpecContext) { By("setting up a webhook") webhook := allowHandler() By("invoking the webhook") - resp := webhook.Handle(context.Background(), Request{}) + resp := webhook.Handle(ctx, Request{}) By("checking that the response share's the request's UID") Expect(resp.Status).To(Equal(authenticationv1.TokenReviewStatus{Authenticated: true})) }) - It("shouldn't overwrite the status on a response", func() { + It("shouldn't overwrite the status on a response", func(ctx SpecContext) { By("setting up a webhook that sets a status") webhook := &Webhook{ Handler: HandlerFunc(func(ctx context.Context, req Request) Response { @@ -96,7 +96,7 @@ var _ = Describe("Authentication Webhooks", func() { } By("invoking the webhook") - resp := webhook.Handle(context.Background(), Request{}) + resp := webhook.Handle(ctx, Request{}) By("checking that the message is intact") Expect(resp.Status).NotTo(BeNil()) diff --git a/pkg/webhook/server_test.go b/pkg/webhook/server_test.go index 04d4ac7f86..6542222585 100644 --- a/pkg/webhook/server_test.go +++ b/pkg/webhook/server_test.go @@ -36,17 +36,19 @@ import ( var _ = Describe("Webhook Server", func() { var ( - ctx context.Context - ctxCancel context.CancelFunc - testHostPort string - client *http.Client - server webhook.Server - servingOpts envtest.WebhookInstallOptions + ctxCancel context.CancelFunc + testHostPort string + client *http.Client + server webhook.Server + servingOpts envtest.WebhookInstallOptions + genericStartServer func(f func(ctx context.Context)) (done <-chan struct{}) ) BeforeEach(func() { - ctx, ctxCancel = context.WithCancel(context.Background()) - // closed in individual tests differently + var ctx context.Context + // Has to be derived from context.Background() as it needs to be + // valid past the BeforeEach + ctx, ctxCancel = context.WithCancel(context.Background()) //nolint:forbidigo servingOpts = envtest.WebhookInstallOptions{} Expect(servingOpts.PrepWithoutInstalling()).To(Succeed()) @@ -67,27 +69,27 @@ var _ = Describe("Webhook Server", func() { Port: servingOpts.LocalServingPort, CertDir: servingOpts.LocalServingCertDir, }) + + genericStartServer = func(f func(ctx context.Context)) (done <-chan struct{}) { + doneCh := make(chan struct{}) + go func() { + defer GinkgoRecover() + defer close(doneCh) + f(ctx) + }() + // wait till we can ping the server to start the test + Eventually(func() error { + _, err := client.Get(fmt.Sprintf("https://%s/unservedpath", testHostPort)) + return err + }).Should(Succeed()) + + return doneCh + } }) AfterEach(func() { Expect(servingOpts.Cleanup()).To(Succeed()) }) - genericStartServer := func(f func(ctx context.Context)) (done <-chan struct{}) { - doneCh := make(chan struct{}) - go func() { - defer GinkgoRecover() - defer close(doneCh) - f(ctx) - }() - // wait till we can ping the server to start the test - Eventually(func() error { - _, err := client.Get(fmt.Sprintf("https://%s/unservedpath", testHostPort)) - return err - }).Should(Succeed()) - - return doneCh - } - startServer := func() (done <-chan struct{}) { return genericStartServer(func(ctx context.Context) { Expect(server.Start(ctx)).To(Succeed()) diff --git a/pkg/webhook/webhook_integration_test.go b/pkg/webhook/webhook_integration_test.go index 752a1fe6f5..98538cf822 100644 --- a/pkg/webhook/webhook_integration_test.go +++ b/pkg/webhook/webhook_integration_test.go @@ -73,7 +73,7 @@ var _ = Describe("Webhook", func() { } }) Context("when running a webhook server with a manager", func() { - It("should reject create request for webhook that rejects all requests", func() { + It("should reject create request for webhook that rejects all requests", func(ctx SpecContext) { m, err := manager.New(cfg, manager.Options{ WebhookServer: webhook.NewServer(webhook.Options{ Port: testenv.WebhookInstallOptions.LocalServingPort, @@ -86,20 +86,17 @@ var _ = Describe("Webhook", func() { server := m.GetWebhookServer() server.Register("/failing", &webhook.Admission{Handler: &rejectingValidator{d: admission.NewDecoder(testenv.Scheme)}}) - ctx, cancel := context.WithCancel(context.Background()) go func() { err := server.Start(ctx) Expect(err).NotTo(HaveOccurred()) }() Eventually(func() bool { - err := c.Create(context.TODO(), obj) + err := c.Create(ctx, obj) return err != nil && strings.HasSuffix(err.Error(), "Always denied") && apierrors.ReasonForError(err) == metav1.StatusReasonForbidden }, 1*time.Second).Should(BeTrue()) - - cancel() }) - It("should reject create request for multi-webhook that rejects all requests", func() { + It("should reject create request for multi-webhook that rejects all requests", func(ctx SpecContext) { m, err := manager.New(cfg, manager.Options{ Metrics: metricsserver.Options{BindAddress: "0"}, WebhookServer: webhook.NewServer(webhook.Options{ @@ -113,22 +110,19 @@ var _ = Describe("Webhook", func() { server := m.GetWebhookServer() server.Register("/failing", &webhook.Admission{Handler: admission.MultiValidatingHandler(&rejectingValidator{d: admission.NewDecoder(testenv.Scheme)})}) - ctx, cancel := context.WithCancel(context.Background()) go func() { err = server.Start(ctx) Expect(err).NotTo(HaveOccurred()) }() Eventually(func() bool { - err = c.Create(context.TODO(), obj) + err = c.Create(ctx, obj) return err != nil && strings.HasSuffix(err.Error(), "Always denied") && apierrors.ReasonForError(err) == metav1.StatusReasonForbidden }, 1*time.Second).Should(BeTrue()) - - cancel() }) }) Context("when running a webhook server without a manager", func() { - It("should reject create request for webhook that rejects all requests", func() { + It("should reject create request for webhook that rejects all requests", func(ctx SpecContext) { server := webhook.NewServer(webhook.Options{ Port: testenv.WebhookInstallOptions.LocalServingPort, Host: testenv.WebhookInstallOptions.LocalServingHost, @@ -136,18 +130,15 @@ var _ = Describe("Webhook", func() { }) server.Register("/failing", &webhook.Admission{Handler: &rejectingValidator{d: admission.NewDecoder(testenv.Scheme)}}) - ctx, cancel := context.WithCancel(context.Background()) go func() { err := server.Start(ctx) Expect(err).NotTo(HaveOccurred()) }() Eventually(func() bool { - err := c.Create(context.TODO(), obj) + err := c.Create(ctx, obj) return err != nil && strings.HasSuffix(err.Error(), "Always denied") && apierrors.ReasonForError(err) == metav1.StatusReasonForbidden }, 1*time.Second).Should(BeTrue()) - - cancel() }) }) }) diff --git a/tools/setup-envtest/go.mod b/tools/setup-envtest/go.mod index 62b88736ed..08698ed6c1 100644 --- a/tools/setup-envtest/go.mod +++ b/tools/setup-envtest/go.mod @@ -10,8 +10,8 @@ require ( github.com/spf13/afero v1.12.0 github.com/spf13/pflag v1.0.6 go.uber.org/zap v1.27.0 - k8s.io/apimachinery v0.33.0 - sigs.k8s.io/yaml v1.4.0 + k8s.io/apimachinery v0.34.0-beta.0 + sigs.k8s.io/yaml v1.5.0 ) require ( @@ -19,6 +19,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect go.uber.org/multierr v1.10.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect diff --git a/tools/setup-envtest/go.sum b/tools/setup-envtest/go.sum index ed5625aff7..e785ee0954 100644 --- a/tools/setup-envtest/go.sum +++ b/tools/setup-envtest/go.sum @@ -6,7 +6,6 @@ github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= @@ -29,6 +28,10 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= @@ -43,7 +46,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= -k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +k8s.io/apimachinery v0.34.0-beta.0 h1:C6teSJBCx6ArW7122MM9hQqeGW2w/QQ0lB4x4Z4Iftc= +k8s.io/apimachinery v0.34.0-beta.0/go.mod h1:TP8uyOuDEOnzGpLOdffo8hPnIjVDljZCxCM/fruV+5M= +sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= +sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= diff --git a/tools/setup-envtest/store/store_suite_test.go b/tools/setup-envtest/store/store_suite_test.go index c2795a3227..649c22d545 100644 --- a/tools/setup-envtest/store/store_suite_test.go +++ b/tools/setup-envtest/store/store_suite_test.go @@ -40,8 +40,8 @@ func zapLogger() logr.Logger { return zapr.NewLogger(zapLog) } -func logCtx() context.Context { - return logr.NewContext(context.Background(), testLog) +func logCtx(ctx context.Context) context.Context { + return logr.NewContext(ctx, testLog) } func TestStore(t *testing.T) { diff --git a/tools/setup-envtest/store/store_test.go b/tools/setup-envtest/store/store_test.go index b128be5933..575d49dd3b 100644 --- a/tools/setup-envtest/store/store_test.go +++ b/tools/setup-envtest/store/store_test.go @@ -47,23 +47,23 @@ var _ = Describe("Store", func() { } }) Describe("initialization", func() { - It("should ensure the repo root exists", func() { + It("should ensure the repo root exists", func(ctx SpecContext) { // remove the old dir Expect(st.Root.RemoveAll("")).To(Succeed(), "should be able to remove the store before trying to initialize") - Expect(st.Initialize(logCtx())).To(Succeed(), "initialization should succeed") + Expect(st.Initialize(logCtx(ctx))).To(Succeed(), "initialization should succeed") Expect(st.Root.Stat("k8s")).NotTo(BeNil(), "store's binary dir should exist") }) - It("should be fine if the repo root already exists", func() { - Expect(st.Initialize(logCtx())).To(Succeed()) + It("should be fine if the repo root already exists", func(ctx SpecContext) { + Expect(st.Initialize(logCtx(ctx))).To(Succeed()) }) }) Describe("listing items", func() { - It("should filter results by the given filter, sorted in version order (newest first)", func() { + It("should filter results by the given filter, sorted in version order (newest first)", func(ctx SpecContext) { sel, err := versions.FromExpr("<=1.16") Expect(err).NotTo(HaveOccurred(), "should be able to construct <=1.16 selector") - Expect(st.List(logCtx(), store.Filter{ + Expect(st.List(logCtx(ctx), store.Filter{ Version: sel, Platform: versions.Platform{OS: "*", Arch: "amd64"}, })).To(Equal([]store.Item{ @@ -73,16 +73,16 @@ var _ = Describe("Store", func() { {Version: ver(1, 14, 26), Platform: versions.Platform{OS: "linux", Arch: "amd64"}}, })) }) - It("should skip non-folders in the store", func() { + It("should skip non-folders in the store", func(ctx SpecContext) { Expect(afero.WriteFile(st.Root, "k8s/2.3.6-linux-amd128", []byte{0x01}, fs.ModePerm)).To(Succeed(), "should be able to create a non-store file in the store directory") - Expect(st.List(logCtx(), store.Filter{ + Expect(st.List(logCtx(ctx), store.Filter{ Version: versions.AnyVersion, Platform: versions.Platform{OS: "linux", Arch: "amd128"}, })).To(BeEmpty()) }) - It("should skip non-matching names in the store", func() { + It("should skip non-matching names in the store", func(ctx SpecContext) { Expect(st.Root.Mkdir("k8s/somedir-2.3.6-linux-amd128", fs.ModePerm)).To(Succeed(), "should be able to create a non-store file in the store directory") - Expect(st.List(logCtx(), store.Filter{ + Expect(st.List(logCtx(ctx), store.Filter{ Version: versions.AnyVersion, Platform: versions.Platform{OS: "linux", Arch: "amd128"}, })).To(BeEmpty()) }) @@ -90,10 +90,10 @@ var _ = Describe("Store", func() { Describe("removing items", func() { var res []store.Item - BeforeEach(func() { + BeforeEach(func(ctx SpecContext) { sel, err := versions.FromExpr("<=1.16") Expect(err).NotTo(HaveOccurred(), "should be able to construct <=1.16 selector") - res, err = st.Remove(logCtx(), store.Filter{ + res, err = st.Remove(logCtx(ctx), store.Filter{ Version: sel, Platform: versions.Platform{OS: "*", Arch: "amd64"}, }) @@ -128,27 +128,27 @@ var _ = Describe("Store", func() { Describe("adding items (controller-tools archives)", func() { archiveName := "envtest-v1.16.3-linux-amd64.tar.gz" - It("should support .tar.gz input", func() { - Expect(st.Add(logCtx(), newItem, makeFakeArchive(archiveName, "controller-tools/envtest/"))).To(Succeed()) + It("should support .tar.gz input", func(ctx SpecContext) { + Expect(st.Add(logCtx(ctx), newItem, makeFakeArchive(archiveName, "controller-tools/envtest/"))).To(Succeed()) Expect(st.Has(newItem)).To(BeTrue(), "should have the item after adding it") }) - It("should extract binaries from the given archive to a directly to the item's directory, regardless of path", func() { - Expect(st.Add(logCtx(), newItem, makeFakeArchive(archiveName, "controller-tools/envtest/"))).To(Succeed()) + It("should extract binaries from the given archive to a directly to the item's directory, regardless of path", func(ctx SpecContext) { + Expect(st.Add(logCtx(ctx), newItem, makeFakeArchive(archiveName, "controller-tools/envtest/"))).To(Succeed()) dirName := newItem.Platform.BaseName(newItem.Version) Expect(afero.ReadFile(st.Root, filepath.Join("k8s", dirName, "some-file"))).To(HavePrefix(archiveName + "some-file")) Expect(afero.ReadFile(st.Root, filepath.Join("k8s", dirName, "other-file"))).To(HavePrefix(archiveName + "other-file")) }) - It("should clean up any existing item directory before creating the new one", func() { + It("should clean up any existing item directory before creating the new one", func(ctx SpecContext) { item := localVersions[0] - Expect(st.Add(logCtx(), item, makeFakeArchive(archiveName, "controller-tools/envtest/"))).To(Succeed()) + Expect(st.Add(logCtx(ctx), item, makeFakeArchive(archiveName, "controller-tools/envtest/"))).To(Succeed()) Expect(st.Root.Stat(filepath.Join("k8s", item.Platform.BaseName(item.Version)))).NotTo(BeNil(), "new files should exist") }) - It("should clean up if it errors before finishing", func() { + It("should clean up if it errors before finishing", func(ctx SpecContext) { item := localVersions[0] - Expect(st.Add(logCtx(), item, new(bytes.Buffer))).NotTo(Succeed(), "should fail to extract") + Expect(st.Add(logCtx(ctx), item, new(bytes.Buffer))).NotTo(Succeed(), "should fail to extract") _, err := st.Root.Stat(filepath.Join("k8s", item.Platform.BaseName(item.Version))) Expect(err).To(HaveOccurred(), "the binaries dir for the item should be gone")