From dc34900cad1fba2f82e47fa37d08f0a5ad82b415 Mon Sep 17 00:00:00 2001 From: Ismail Alidzhikov Date: Fri, 8 Sep 2023 14:14:56 +0300 Subject: [PATCH] Extract the registry deployment into component. Add missing unit tests (#36) * Require at least one cache to be specified * Add unit tests for the shoot validator * Fix `addDefaultingFuncs` for the config API * Remove test suite for package that does not define any tests * Extract the registry deployment into component. Add unit tests for the new component A new component is introduced in `./pkg/component/registrycaches`. The component deploys registry caches. It implements the well-known `component.DeployWaiter` interface. This commit also moves the existing controller from `./pkg/controller` to dedicated pkg `./pkg/controller/extension`. The controller name is changed from `registry-cache` to `extension-controller`. This commit also introduces a new pkg `./pkg/constants`. It exports constants that are used from 2 or more packages. * Address review comments --- .../app/app.go | 4 +- .../app/app.go | 12 +- pkg/admission/validator/serialization.go | 35 -- pkg/admission/validator/shoot.go | 17 +- pkg/admission/validator/shoot_test.go | 165 +++++ pkg/admission/validator/webhook.go | 4 +- pkg/apis/config/v1alpha1/defaults.go | 5 +- pkg/apis/registry/v1alpha1/types.go | 3 - pkg/apis/registry/validation/validation.go | 18 +- .../registry/validation/validation_test.go | 35 +- pkg/cmd/options.go | 4 +- .../registrycaches/registrycaches.go | 255 ++++++++ .../registrycaches_suite_test.go} | 6 +- .../registrycaches/registrycaches_test.go | 366 +++++++++++ pkg/constants/constants.go | 27 + pkg/controller/{ => extension}/actuator.go | 122 +--- pkg/controller/{ => extension}/add.go | 4 +- pkg/controller/registrydeployer.go | 162 ----- .../pkg/mock/controller-runtime/client/doc.go | 17 +- .../mock/controller-runtime/client/mocks.go | 586 ++++++++++++++++++ .../gardener/pkg/utils/retry/fake/retry.go | 64 ++ .../gardener/pkg/utils/test/gomock.go | 49 ++ .../gardener/pkg/utils/test/manager.go | 52 ++ .../pkg/utils/test/matchers/conditions.go | 74 +++ .../gardener/pkg/utils/test/matchers/deep.go | 81 +++ .../pkg/utils/test/matchers/fields.go | 38 ++ .../utils/test/matchers/kubernetes_errors.go | 47 ++ .../pkg/utils/test/matchers/matchers.go | 142 +++++ .../pkg/utils/test/matchers/reference.go | 43 ++ .../gardener/pkg/utils/test/options.go | 142 +++++ .../gardener/gardener/pkg/utils/test/test.go | 298 +++++++++ .../gardener/pkg/utils/test/test_resources.go | 138 +++++ vendor/go.uber.org/mock/gomock/call.go | 471 ++++++++++++++ vendor/go.uber.org/mock/gomock/callset.go | 164 +++++ vendor/go.uber.org/mock/gomock/controller.go | 324 ++++++++++ vendor/go.uber.org/mock/gomock/doc.go | 60 ++ vendor/go.uber.org/mock/gomock/matchers.go | 346 +++++++++++ vendor/modules.txt | 5 + 38 files changed, 4042 insertions(+), 343 deletions(-) delete mode 100644 pkg/admission/validator/serialization.go create mode 100644 pkg/admission/validator/shoot_test.go create mode 100644 pkg/component/registrycaches/registrycaches.go rename pkg/{controller/controller_suite_test.go => component/registrycaches/registrycaches_suite_test.go} (88%) create mode 100644 pkg/component/registrycaches/registrycaches_test.go create mode 100644 pkg/constants/constants.go rename pkg/controller/{ => extension}/actuator.go (61%) rename pkg/controller/{ => extension}/add.go (97%) delete mode 100644 pkg/controller/registrydeployer.go rename pkg/apis/config/v1alpha1/v1alpha1_suite_test.go => vendor/github.com/gardener/gardener/pkg/mock/controller-runtime/client/doc.go (58%) create mode 100644 vendor/github.com/gardener/gardener/pkg/mock/controller-runtime/client/mocks.go create mode 100644 vendor/github.com/gardener/gardener/pkg/utils/retry/fake/retry.go create mode 100644 vendor/github.com/gardener/gardener/pkg/utils/test/gomock.go create mode 100644 vendor/github.com/gardener/gardener/pkg/utils/test/manager.go create mode 100644 vendor/github.com/gardener/gardener/pkg/utils/test/matchers/conditions.go create mode 100644 vendor/github.com/gardener/gardener/pkg/utils/test/matchers/deep.go create mode 100644 vendor/github.com/gardener/gardener/pkg/utils/test/matchers/fields.go create mode 100644 vendor/github.com/gardener/gardener/pkg/utils/test/matchers/kubernetes_errors.go create mode 100644 vendor/github.com/gardener/gardener/pkg/utils/test/matchers/matchers.go create mode 100644 vendor/github.com/gardener/gardener/pkg/utils/test/matchers/reference.go create mode 100644 vendor/github.com/gardener/gardener/pkg/utils/test/options.go create mode 100644 vendor/github.com/gardener/gardener/pkg/utils/test/test.go create mode 100644 vendor/github.com/gardener/gardener/pkg/utils/test/test_resources.go create mode 100644 vendor/go.uber.org/mock/gomock/call.go create mode 100644 vendor/go.uber.org/mock/gomock/callset.go create mode 100644 vendor/go.uber.org/mock/gomock/controller.go create mode 100644 vendor/go.uber.org/mock/gomock/doc.go create mode 100644 vendor/go.uber.org/mock/gomock/matchers.go diff --git a/cmd/gardener-extension-registry-cache-admission/app/app.go b/cmd/gardener-extension-registry-cache-admission/app/app.go index a8519e85..3c7010f3 100644 --- a/cmd/gardener-extension-registry-cache-admission/app/app.go +++ b/cmd/gardener-extension-registry-cache-admission/app/app.go @@ -30,7 +30,7 @@ import ( admissioncmd "github.com/gardener/gardener-extension-registry-cache/pkg/admission/cmd" registryinstall "github.com/gardener/gardener-extension-registry-cache/pkg/apis/registry/install" - "github.com/gardener/gardener-extension-registry-cache/pkg/controller" + "github.com/gardener/gardener-extension-registry-cache/pkg/constants" ) var log = logf.Log.WithName("gardener-extension-registry-cache-admission") @@ -56,7 +56,7 @@ func NewAdmissionCommand(ctx context.Context) *cobra.Command { ) cmd := &cobra.Command{ - Use: fmt.Sprintf("gardener-extension-%s-admission", controller.Type), + Use: fmt.Sprintf("gardener-extension-%s-admission", constants.ExtensionType), RunE: func(cmd *cobra.Command, args []string) error { verflag.PrintAndExitIfRequested() diff --git a/cmd/gardener-extension-registry-cache/app/app.go b/cmd/gardener-extension-registry-cache/app/app.go index ca9828a6..b664b605 100644 --- a/cmd/gardener-extension-registry-cache/app/app.go +++ b/cmd/gardener-extension-registry-cache/app/app.go @@ -19,7 +19,7 @@ import ( "fmt" extensionscontroller "github.com/gardener/gardener/extensions/pkg/controller" - "github.com/gardener/gardener/extensions/pkg/controller/heartbeat" + heartbeatcontroller "github.com/gardener/gardener/extensions/pkg/controller/heartbeat" "github.com/gardener/gardener/extensions/pkg/util" gardenerhealthz "github.com/gardener/gardener/pkg/healthz" "github.com/spf13/cobra" @@ -30,7 +30,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager" registryinstall "github.com/gardener/gardener-extension-registry-cache/pkg/apis/registry/install" - "github.com/gardener/gardener-extension-registry-cache/pkg/controller" + extensioncontroller "github.com/gardener/gardener-extension-registry-cache/pkg/controller/extension" ) // NewServiceControllerCommand creates a new command that is used to start the registry service controller. @@ -88,10 +88,10 @@ func (o *Options) run(ctx context.Context) error { } ctrlConfig := o.registryOptions.Completed() - ctrlConfig.Apply(&controller.DefaultAddOptions.Config) - o.controllerOptions.Completed().Apply(&controller.DefaultAddOptions.ControllerOptions) - o.reconcileOptions.Completed().Apply(&controller.DefaultAddOptions.IgnoreOperationAnnotation) - o.heartbeatOptions.Completed().Apply(&heartbeat.DefaultAddOptions) + ctrlConfig.Apply(&extensioncontroller.DefaultAddOptions.Config) + o.controllerOptions.Completed().Apply(&extensioncontroller.DefaultAddOptions.ControllerOptions) + o.reconcileOptions.Completed().Apply(&extensioncontroller.DefaultAddOptions.IgnoreOperationAnnotation) + o.heartbeatOptions.Completed().Apply(&heartbeatcontroller.DefaultAddOptions) if err := o.controllerSwitches.Completed().AddToManager(ctx, mgr); err != nil { return fmt.Errorf("could not add controllers to manager: %w", err) diff --git a/pkg/admission/validator/serialization.go b/pkg/admission/validator/serialization.go deleted file mode 100644 index 9260fa1e..00000000 --- a/pkg/admission/validator/serialization.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2022 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file -// -// 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 validator - -import ( - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/validation/field" - - api "github.com/gardener/gardener-extension-registry-cache/pkg/apis/registry" -) - -func decodeRegistryConfig(decoder runtime.Decoder, config *runtime.RawExtension, fldPath *field.Path) (*api.RegistryConfig, error) { - if config == nil { - return nil, field.Required(fldPath, "Registry configuration is required when using the gardener-extension-registry-cache") - } - - registryConfig := &api.RegistryConfig{} - if err := runtime.DecodeInto(decoder, config.Raw, registryConfig); err != nil { - return nil, err - } - - return registryConfig, nil -} diff --git a/pkg/admission/validator/shoot.go b/pkg/admission/validator/shoot.go index b47049ca..0a8ab8b7 100644 --- a/pkg/admission/validator/shoot.go +++ b/pkg/admission/validator/shoot.go @@ -24,8 +24,9 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" "sigs.k8s.io/controller-runtime/pkg/client" + api "github.com/gardener/gardener-extension-registry-cache/pkg/apis/registry" "github.com/gardener/gardener-extension-registry-cache/pkg/apis/registry/validation" - "github.com/gardener/gardener-extension-registry-cache/pkg/controller" + "github.com/gardener/gardener-extension-registry-cache/pkg/constants" ) // shoot validates shoots @@ -50,7 +51,7 @@ func (s *shoot) Validate(_ context.Context, new, _ client.Object) error { var ext *core.Extension var fldPath *field.Path for i, ex := range shoot.Spec.Extensions { - if ex.Type == controller.Type { + if ex.Type == constants.ExtensionType { ext = ex.DeepCopy() fldPath = field.NewPath("spec", "extensions").Index(i) break @@ -62,17 +63,19 @@ func (s *shoot) Validate(_ context.Context, new, _ client.Object) error { for _, worker := range shoot.Spec.Provider.Workers { if worker.CRI.Name != "containerd" { - return fmt.Errorf("containerruntime needs to be containerd when container registry cache is used") + return fmt.Errorf("container runtime needs to be containerd when the registry-cache extension is enabled") } } providerConfigPath := fldPath.Child("providerConfig") + if ext.ProviderConfig == nil { + return field.Required(providerConfigPath, "providerConfig is required for the registry-cache extension") + } - registryConfig, err := decodeRegistryConfig(s.decoder, ext.ProviderConfig, providerConfigPath) - if err != nil { - return err + registryConfig := &api.RegistryConfig{} + if err := runtime.DecodeInto(s.decoder, ext.ProviderConfig.Raw, registryConfig); err != nil { + return fmt.Errorf("failed to decode providerConfig: %w", err) } return validation.ValidateRegistryConfig(registryConfig, providerConfigPath).ToAggregate() - } diff --git a/pkg/admission/validator/shoot_test.go b/pkg/admission/validator/shoot_test.go new file mode 100644 index 00000000..a4351c2e --- /dev/null +++ b/pkg/admission/validator/shoot_test.go @@ -0,0 +1,165 @@ +// Copyright (c) 2023 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 validator_test + +import ( + "context" + "encoding/json" + + extensionswebhook "github.com/gardener/gardener/extensions/pkg/webhook" + "github.com/gardener/gardener/pkg/apis/core" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/util/validation/field" + + "github.com/gardener/gardener-extension-registry-cache/pkg/admission/validator" + api "github.com/gardener/gardener-extension-registry-cache/pkg/apis/registry" + "github.com/gardener/gardener-extension-registry-cache/pkg/apis/registry/v1alpha1" +) + +var _ = Describe("Shoot validator", func() { + + Describe("#Validate", func() { + var ( + ctx = context.Background() + size = resource.MustParse("20Gi") + + shootValidator extensionswebhook.Validator + + shoot *core.Shoot + ) + + BeforeEach(func() { + scheme := runtime.NewScheme() + Expect(api.AddToScheme(scheme)).To(Succeed()) + Expect(v1alpha1.AddToScheme(scheme)).To(Succeed()) + + decoder := serializer.NewCodecFactory(scheme, serializer.EnableStrict).UniversalDecoder() + shootValidator = validator.NewShootValidator(decoder) + + shoot = &core.Shoot{ + Spec: core.ShootSpec{ + Extensions: []core.Extension{ + { + Type: "registry-cache", + ProviderConfig: &runtime.RawExtension{ + Raw: encode(&v1alpha1.RegistryConfig{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: "RegistryConfig", + }, + Caches: []v1alpha1.RegistryCache{ + { + Upstream: "docker.io", + Size: &size, + }, + }, + }), + }, + }, + }, + Provider: core.Provider{ + Workers: []core.Worker{ + { + CRI: &core.CRI{Name: "containerd"}, + }, + }, + }, + }, + } + }) + + It("should return err when new is not a Shoot", func() { + err := shootValidator.Validate(ctx, &corev1.Pod{}, nil) + Expect(err).To(MatchError("wrong object type *v1.Pod")) + }) + + It("should do nothing when the Shoot does no specify a registry-cache extension", func() { + shoot.Spec.Extensions[0].Type = "foo" + + Expect(shootValidator.Validate(ctx, shoot, nil)).To(Succeed()) + }) + + It("should return err when there is contrainer runtime that is not containerd", func() { + worker := core.Worker{ + CRI: &core.CRI{ + Name: "docker", + }, + } + shoot.Spec.Provider.Workers = append(shoot.Spec.Provider.Workers, worker) + + err := shootValidator.Validate(ctx, shoot, nil) + Expect(err).To(MatchError("container runtime needs to be containerd when the registry-cache extension is enabled")) + }) + + It("should return err when registry-cache's providerConfig is nil", func() { + shoot.Spec.Extensions[0].ProviderConfig = nil + + err := shootValidator.Validate(ctx, shoot, nil) + Expect(err).To(PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeRequired), + "Field": Equal("spec.extensions[0].providerConfig"), + "Detail": Equal("providerConfig is required for the registry-cache extension"), + }))) + }) + + It("should return err when registry-cache's providerConfig cannot be decoded", func() { + shoot.Spec.Extensions[0].ProviderConfig = &runtime.RawExtension{ + Raw: []byte(`{"bar": "baz"}`), + } + + err := shootValidator.Validate(ctx, shoot, nil) + Expect(err).To(MatchError(ContainSubstring("failed to decode providerConfig"))) + }) + + It("should return err when registry-cache's providerConfig is invalid", func() { + shoot.Spec.Extensions[0].ProviderConfig = &runtime.RawExtension{ + Raw: encode(&v1alpha1.RegistryConfig{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: "RegistryConfig", + }, + Caches: []v1alpha1.RegistryCache{ + { + Upstream: "https://registry.example.com", + }, + }, + }), + } + + err := shootValidator.Validate(ctx, shoot, nil) + Expect(err).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("spec.extensions[0].providerConfig.caches[0].upstream"), + "Detail": ContainSubstring("upstream must not include a scheme"), + })))) + }) + + It("should succeed for valid Shoot", func() { + Expect(shootValidator.Validate(ctx, shoot, nil)).To(Succeed()) + }) + }) +}) + +func encode(obj runtime.Object) []byte { + data, _ := json.Marshal(obj) + return data +} diff --git a/pkg/admission/validator/webhook.go b/pkg/admission/validator/webhook.go index 47d2721d..d29f9348 100644 --- a/pkg/admission/validator/webhook.go +++ b/pkg/admission/validator/webhook.go @@ -21,7 +21,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" - "github.com/gardener/gardener-extension-registry-cache/pkg/controller" + "github.com/gardener/gardener-extension-registry-cache/pkg/constants" ) const ( @@ -38,7 +38,7 @@ func New(mgr manager.Manager) (*extensionswebhook.Webhook, error) { logger.Info("Setting up webhook", "name", Name) return extensionswebhook.New(mgr, extensionswebhook.Args{ - Provider: controller.Type, + Provider: constants.ExtensionType, Name: Name, Path: "/webhooks/validate", Validators: map[extensionswebhook.Validator][]extensionswebhook.Type{ diff --git a/pkg/apis/config/v1alpha1/defaults.go b/pkg/apis/config/v1alpha1/defaults.go index 5db9a539..58a98de7 100644 --- a/pkg/apis/config/v1alpha1/defaults.go +++ b/pkg/apis/config/v1alpha1/defaults.go @@ -18,7 +18,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) -func addDefaultingFuncs(_ *runtime.Scheme) error { - return nil - // return RegisterDefaults(scheme) +func addDefaultingFuncs(scheme *runtime.Scheme) error { + return RegisterDefaults(scheme) } diff --git a/pkg/apis/registry/v1alpha1/types.go b/pkg/apis/registry/v1alpha1/types.go index 47245404..5f749f0d 100644 --- a/pkg/apis/registry/v1alpha1/types.go +++ b/pkg/apis/registry/v1alpha1/types.go @@ -19,9 +19,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// RegistryResourceName is the name for registry resources in the shoot. -const RegistryResourceName = "extension-registry-cache" - // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // RegistryConfig contains information about registry caches to deploy. diff --git a/pkg/apis/registry/validation/validation.go b/pkg/apis/registry/validation/validation.go index 512a62ec..c9e8732a 100644 --- a/pkg/apis/registry/validation/validation.go +++ b/pkg/apis/registry/validation/validation.go @@ -17,6 +17,7 @@ package validation import ( "strings" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/validation/field" "github.com/gardener/gardener-extension-registry-cache/pkg/apis/registry" @@ -26,6 +27,10 @@ import ( func ValidateRegistryConfig(config *registry.RegistryConfig, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} + if len(config.Caches) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("caches"), "at least one cache must be provided")) + } + for i, cache := range config.Caches { allErrs = append(allErrs, validateRegistryCache(cache, fldPath.Child("caches").Index(i))...) } @@ -37,8 +42,8 @@ func validateRegistryCache(cache registry.RegistryCache, fldPath *field.Path) fi var allErrs field.ErrorList allErrs = append(allErrs, validateUpstream(fldPath.Child("upstream"), cache.Upstream)...) - if size := cache.Size; size != nil && size.Sign() != 1 { - allErrs = append(allErrs, field.Invalid(fldPath.Child("size"), size, "size must be a quantity greater than zero")) + if cache.Size != nil { + allErrs = append(allErrs, validatePositiveQuantity(*cache.Size, fldPath.Child("size"))...) } return allErrs @@ -58,3 +63,12 @@ func validateUpstream(fldPath *field.Path, upstream string) field.ErrorList { return allErrors } + +// validatePositiveQuantity validates that a Quantity is positive. +func validatePositiveQuantity(value resource.Quantity, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if value.Cmp(resource.Quantity{}) <= 0 { + allErrs = append(allErrs, field.Invalid(fldPath, value.String(), "must be greater than 0")) + } + return allErrs +} diff --git a/pkg/apis/registry/validation/validation_test.go b/pkg/apis/registry/validation/validation_test.go index e1b30287..b4db8323 100644 --- a/pkg/apis/registry/validation/validation_test.go +++ b/pkg/apis/registry/validation/validation_test.go @@ -49,14 +49,33 @@ var _ = Describe("Validation", func() { Expect(ValidateRegistryConfig(registryConfig, fldPath)).To(BeEmpty()) }) + It("should deny configuration without a cache", func() { + registryConfig = &api.RegistryConfig{Caches: nil} + Expect(ValidateRegistryConfig(registryConfig, fldPath)).To(ConsistOf( + PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeRequired), + "Field": Equal("providerConfig.caches"), + "Detail": ContainSubstring("at least one cache must be provided"), + })), + )) + + registryConfig = &api.RegistryConfig{Caches: []api.RegistryCache{}} + Expect(ValidateRegistryConfig(registryConfig, fldPath)).To(ConsistOf( + PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeRequired), + "Field": Equal("providerConfig.caches"), + "Detail": ContainSubstring("at least one cache must be provided"), + })), + )) + }) + It("should require upstream", func() { registryConfig.Caches[0].Upstream = "" - path := fldPath.Child("caches").Index(0).Child("upstream").String() Expect(ValidateRegistryConfig(registryConfig, fldPath)).To(ConsistOf( PointTo(MatchFields(IgnoreExtras, Fields{ "Type": Equal(field.ErrorTypeRequired), - "Field": Equal(path), + "Field": Equal("providerConfig.caches[0].upstream"), "Detail": ContainSubstring("upstream must be provided"), })), )) @@ -70,12 +89,12 @@ var _ = Describe("Validation", func() { Expect(ValidateRegistryConfig(registryConfig, fldPath)).To(ConsistOf( PointTo(MatchFields(IgnoreExtras, Fields{ "Type": Equal(field.ErrorTypeInvalid), - "Field": Equal(fldPath.Child("caches").Index(0).Child("upstream").String()), + "Field": Equal("providerConfig.caches[0].upstream"), "Detail": ContainSubstring("upstream must not include a scheme"), })), PointTo(MatchFields(IgnoreExtras, Fields{ "Type": Equal(field.ErrorTypeInvalid), - "Field": Equal(fldPath.Child("caches").Index(1).Child("upstream").String()), + "Field": Equal("providerConfig.caches[1].upstream"), "Detail": ContainSubstring("upstream must not include a scheme"), })), )) @@ -91,13 +110,13 @@ var _ = Describe("Validation", func() { Expect(ValidateRegistryConfig(registryConfig, fldPath)).To(ConsistOf( PointTo(MatchFields(IgnoreExtras, Fields{ "Type": Equal(field.ErrorTypeInvalid), - "Field": Equal(fldPath.Child("caches").Index(0).Child("size").String()), - "Detail": ContainSubstring("size must be a quantity greater than zero"), + "Field": Equal("providerConfig.caches[0].size"), + "Detail": ContainSubstring("must be greater than 0"), })), PointTo(MatchFields(IgnoreExtras, Fields{ "Type": Equal(field.ErrorTypeInvalid), - "Field": Equal(fldPath.Child("caches").Index(1).Child("size").String()), - "Detail": ContainSubstring("size must be a quantity greater than zero"), + "Field": Equal("providerConfig.caches[1].size"), + "Detail": ContainSubstring("must be greater than 0"), })), )) }) diff --git a/pkg/cmd/options.go b/pkg/cmd/options.go index 49df46fe..93e2d228 100644 --- a/pkg/cmd/options.go +++ b/pkg/cmd/options.go @@ -29,7 +29,7 @@ import ( configapi "github.com/gardener/gardener-extension-registry-cache/pkg/apis/config" "github.com/gardener/gardener-extension-registry-cache/pkg/apis/config/v1alpha1" "github.com/gardener/gardener-extension-registry-cache/pkg/apis/config/validation" - "github.com/gardener/gardener-extension-registry-cache/pkg/controller" + extensioncontroller "github.com/gardener/gardener-extension-registry-cache/pkg/controller/extension" oscwebhook "github.com/gardener/gardener-extension-registry-cache/pkg/webhook/operatingsystemconfig" ) @@ -102,7 +102,7 @@ func (c *RegistryServiceConfig) Apply(config *configapi.Configuration) { // ControllerSwitches are the cmd.SwitchOptions for the provider controllers. func ControllerSwitches() *cmd.SwitchOptions { return cmd.NewSwitchOptions( - cmd.Switch(controller.ControllerName, controller.AddToManager), + cmd.Switch(extensioncontroller.ControllerName, extensioncontroller.AddToManager), cmd.Switch(extensionsheartbeatcontroller.ControllerName, extensionsheartbeatcontroller.AddToManager), ) } diff --git a/pkg/component/registrycaches/registrycaches.go b/pkg/component/registrycaches/registrycaches.go new file mode 100644 index 00000000..e39b12b4 --- /dev/null +++ b/pkg/component/registrycaches/registrycaches.go @@ -0,0 +1,255 @@ +// Copyright (c) 2023 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 registrycaches + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + "github.com/gardener/gardener/pkg/client/kubernetes" + "github.com/gardener/gardener/pkg/component" + "github.com/gardener/gardener/pkg/utils/managedresources" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/gardener/gardener-extension-registry-cache/pkg/apis/registry/v1alpha1" + "github.com/gardener/gardener-extension-registry-cache/pkg/constants" + registryutils "github.com/gardener/gardener-extension-registry-cache/pkg/utils/registry" +) + +const ( + // ManagedResourceName is the ManagedResource name for the registry cache resources in the shoot. + ManagedResourceName = "extension-registry-cache" +) + +// Values is a set of configuration values for the registry caches. +type Values struct { + // Image is the container image used for the registry cache. + Image string + // Caches are the registry caches to deploy. + Caches []v1alpha1.RegistryCache +} + +// New creates a new instance of DeployWaiter for registry caches. +func New( + client client.Client, + namespace string, + values Values, +) component.DeployWaiter { + return ®istryCaches{ + client: client, + namespace: namespace, + values: values, + } +} + +type registryCaches struct { + client client.Client + namespace string + values Values +} + +// Deploy implements component.DeployWaiter. +func (r *registryCaches) Deploy(ctx context.Context) error { + data, err := r.computeResourcesData() + if err != nil { + return err + } + + var ( + origin = "registry-cache" + keepObjects = false + + secretName, secret = managedresources.NewSecret(r.client, r.namespace, ManagedResourceName, data, false) + managedResource = managedresources.NewForShoot(r.client, r.namespace, ManagedResourceName, origin, keepObjects). + WithSecretRef(secretName). + DeletePersistentVolumeClaims(true) + ) + + if err := secret.Reconcile(ctx); err != nil { + return fmt.Errorf("failed to create or update secret of managed resources: %w", err) + } + + if err := managedResource.Reconcile(ctx); err != nil { + return fmt.Errorf("failed to not create or update managed resource: %w", err) + } + + return nil +} + +// Destroy implements component.DeployWaiter. +func (r *registryCaches) Destroy(ctx context.Context) error { + return managedresources.Delete(ctx, r.client, r.namespace, ManagedResourceName, false) +} + +// TimeoutWaitForManagedResource is the timeout used while waiting for the ManagedResources to become healthy +// or deleted. +var TimeoutWaitForManagedResource = 2 * time.Minute + +// Wait implements component.DeployWaiter. +func (r *registryCaches) Wait(ctx context.Context) error { + timeoutCtx, cancel := context.WithTimeout(ctx, TimeoutWaitForManagedResource) + defer cancel() + + return managedresources.WaitUntilHealthy(timeoutCtx, r.client, r.namespace, ManagedResourceName) +} + +// WaitCleanup implements component.DeployWaiter. +func (r *registryCaches) WaitCleanup(ctx context.Context) error { + timeoutCtx, cancel := context.WithTimeout(ctx, TimeoutWaitForManagedResource) + defer cancel() + + return managedresources.WaitUntilDeleted(timeoutCtx, r.client, r.namespace, ManagedResourceName) +} + +func (r *registryCaches) computeResourcesData() (map[string][]byte, error) { + objects := []client.Object{ + &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.NamespaceRegistryCache, + }, + }, + } + + for _, cache := range r.values.Caches { + cacheObjects, err := computeResourcesDataForRegistryCache(&cache, r.values.Image) + if err != nil { + return nil, fmt.Errorf("failed to compute resources for upstream %s: %w", cache.Upstream, err) + } + + objects = append(objects, cacheObjects...) + } + + registry := managedresources.NewRegistry(kubernetes.ShootScheme, kubernetes.ShootCodec, kubernetes.ShootSerializer) + + return registry.AddAllAndSerialize(objects...) +} + +func computeResourcesDataForRegistryCache(cache *v1alpha1.RegistryCache, image string) ([]client.Object, error) { + if cache.Size == nil { + return nil, fmt.Errorf("registry cache size is required") + } + if cache.GarbageCollectionEnabled == nil { + return nil, fmt.Errorf("registry cache garbageCollectionEnabled is required") + } + + const registryCacheVolumeName = "cache-volume" + + var ( + name = strings.Replace(fmt.Sprintf("registry-%s", strings.Split(cache.Upstream, ":")[0]), ".", "-", -1) + labels = map[string]string{ + "app": name, + constants.UpstreamHostLabel: cache.Upstream, + } + + service = &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: constants.NamespaceRegistryCache, + Labels: labels, + }, + Spec: corev1.ServiceSpec{ + Selector: labels, + Ports: []corev1.ServicePort{{ + Name: "registry-cache", + Port: constants.RegistryCachePort, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromString("registry-cache"), + }}, + Type: corev1.ServiceTypeClusterIP, + }, + } + + statefulSet = &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: constants.NamespaceRegistryCache, + Labels: labels, + }, + Spec: appsv1.StatefulSetSpec{ + ServiceName: service.Name, + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Replicas: pointer.Int32(1), + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "registry-cache", + Image: image, + ImagePullPolicy: corev1.PullIfNotPresent, + Ports: []corev1.ContainerPort{ + { + ContainerPort: constants.RegistryCachePort, + Name: "registry-cache", + }, + }, + Env: []corev1.EnvVar{ + { + Name: "REGISTRY_PROXY_REMOTEURL", + Value: registryutils.GetUpstreamURL(cache.Upstream), + }, + { + Name: "REGISTRY_STORAGE_DELETE_ENABLED", + Value: strconv.FormatBool(*cache.GarbageCollectionEnabled), + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: registryCacheVolumeName, + ReadOnly: false, + MountPath: "/var/lib/registry", + }, + }, + }, + }, + }, + }, + VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: registryCacheVolumeName, + Labels: labels, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: *cache.Size, + }, + }, + }, + }, + }, + }, + } + ) + + return []client.Object{ + service, + statefulSet, + }, nil +} diff --git a/pkg/controller/controller_suite_test.go b/pkg/component/registrycaches/registrycaches_suite_test.go similarity index 88% rename from pkg/controller/controller_suite_test.go rename to pkg/component/registrycaches/registrycaches_suite_test.go index 8bdde243..a1286805 100644 --- a/pkg/controller/controller_suite_test.go +++ b/pkg/component/registrycaches/registrycaches_suite_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package controller_test +package registrycaches_test import ( "testing" @@ -21,7 +21,7 @@ import ( . "github.com/onsi/gomega" ) -func TestController(t *testing.T) { +func TestRegistryCaches(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Controller Suite") + RunSpecs(t, "Component RegistryCaches Suite") } diff --git a/pkg/component/registrycaches/registrycaches_test.go b/pkg/component/registrycaches/registrycaches_test.go new file mode 100644 index 00000000..d819e779 --- /dev/null +++ b/pkg/component/registrycaches/registrycaches_test.go @@ -0,0 +1,366 @@ +// Copyright (c) 2023 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 registrycaches_test + +import ( + "context" + "strconv" + + gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1" + resourcesv1alpha1 "github.com/gardener/gardener/pkg/apis/resources/v1alpha1" + "github.com/gardener/gardener/pkg/client/kubernetes" + "github.com/gardener/gardener/pkg/component" + "github.com/gardener/gardener/pkg/resourcemanager/controller/garbagecollector/references" + "github.com/gardener/gardener/pkg/utils/retry" + retryfake "github.com/gardener/gardener/pkg/utils/retry/fake" + "github.com/gardener/gardener/pkg/utils/test" + . "github.com/gardener/gardener/pkg/utils/test/matchers" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/gardener/gardener-extension-registry-cache/pkg/apis/registry/v1alpha1" + . "github.com/gardener/gardener-extension-registry-cache/pkg/component/registrycaches" +) + +var _ = Describe("RegistryCaches", func() { + const ( + managedResourceName = "extension-registry-cache" + + namespace = "some-namespace" + image = "some-image:some-tag" + ) + + var ( + ctx = context.TODO() + dockerSize = resource.MustParse("10Gi") + gcrSize = resource.MustParse("20Gi") + + c client.Client + + registryCaches component.DeployWaiter + + managedResource *resourcesv1alpha1.ManagedResource + managedResourceSecret *corev1.Secret + ) + + BeforeEach(func() { + c = fakeclient.NewClientBuilder().WithScheme(kubernetes.SeedScheme).Build() + values := Values{ + Image: image, + Caches: []v1alpha1.RegistryCache{ + { + Upstream: "docker.io", + Size: &dockerSize, + GarbageCollectionEnabled: pointer.Bool(true), + }, + { + Upstream: "eu.gcr.io", + Size: &gcrSize, + GarbageCollectionEnabled: pointer.Bool(false), + }, + }, + } + registryCaches = New(c, namespace, values) + + managedResource = &resourcesv1alpha1.ManagedResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: managedResourceName, + Namespace: namespace, + }, + } + managedResourceSecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: managedResource.Name, + Namespace: namespace, + }, + } + }) + + Describe("#Deploy", func() { + var ( + namespaceYAML = `apiVersion: v1 +kind: Namespace +metadata: + creationTimestamp: null + name: registry-cache +spec: {} +status: {} +` + serviceYAMLFor = func(name, upstream string) string { + return `apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + labels: + app: ` + name + ` + upstream-host: ` + upstream + ` + name: ` + name + ` + namespace: registry-cache +spec: + ports: + - name: registry-cache + port: 5000 + protocol: TCP + targetPort: registry-cache + selector: + app: ` + name + ` + upstream-host: ` + upstream + ` + type: ClusterIP +status: + loadBalancer: {} +` + } + + statefulSetYAMLFor = func(name, upstream, upstreamURL, size string, garbageCollectionEnabled bool) string { + return `apiVersion: apps/v1 +kind: StatefulSet +metadata: + creationTimestamp: null + labels: + app: ` + name + ` + upstream-host: ` + upstream + ` + name: ` + name + ` + namespace: registry-cache +spec: + replicas: 1 + selector: + matchLabels: + app: ` + name + ` + upstream-host: ` + upstream + ` + serviceName: ` + name + ` + template: + metadata: + creationTimestamp: null + labels: + app: ` + name + ` + upstream-host: ` + upstream + ` + spec: + containers: + - env: + - name: REGISTRY_PROXY_REMOTEURL + value: ` + upstreamURL + ` + - name: REGISTRY_STORAGE_DELETE_ENABLED + value: "` + strconv.FormatBool(garbageCollectionEnabled) + `" + image: ` + image + ` + imagePullPolicy: IfNotPresent + name: registry-cache + ports: + - containerPort: 5000 + name: registry-cache + resources: {} + volumeMounts: + - mountPath: /var/lib/registry + name: cache-volume + updateStrategy: {} + volumeClaimTemplates: + - metadata: + creationTimestamp: null + labels: + app: ` + name + ` + upstream-host: ` + upstream + ` + name: cache-volume + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: ` + size + ` + status: {} +status: + availableReplicas: 0 + replicas: 0 +` + } + ) + + It("should return err when cache size is nil", func() { + values := Values{ + Image: image, + Caches: []v1alpha1.RegistryCache{ + { + Upstream: "docker.io", + GarbageCollectionEnabled: pointer.Bool(true), + }, + }, + } + registryCaches = New(c, namespace, values) + + Expect(registryCaches.Deploy(ctx)).To(MatchError(ContainSubstring("registry cache size is required"))) + }) + + It("should return err when cache garbageCollectionEnabled is nil", func() { + values := Values{ + Image: image, + Caches: []v1alpha1.RegistryCache{ + { + Upstream: "docker.io", + Size: &dockerSize, + }, + }, + } + registryCaches = New(c, namespace, values) + + Expect(registryCaches.Deploy(ctx)).To(MatchError(ContainSubstring("registry cache garbageCollectionEnabled is required"))) + }) + + It("should successfully deploy the resources", func() { + Expect(c.Get(ctx, client.ObjectKeyFromObject(managedResource), managedResource)).To(MatchError(apierrors.NewNotFound(schema.GroupResource{Group: resourcesv1alpha1.SchemeGroupVersion.Group, Resource: "managedresources"}, managedResource.Name))) + + Expect(registryCaches.Deploy(ctx)).To(Succeed()) + + Expect(c.Get(ctx, client.ObjectKeyFromObject(managedResource), managedResource)).To(Succeed()) + expectedMr := &resourcesv1alpha1.ManagedResource{ + TypeMeta: metav1.TypeMeta{ + APIVersion: resourcesv1alpha1.SchemeGroupVersion.String(), + Kind: "ManagedResource", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: managedResource.Name, + Namespace: managedResource.Namespace, + ResourceVersion: "1", + Labels: map[string]string{"origin": "registry-cache"}, + }, + Spec: resourcesv1alpha1.ManagedResourceSpec{ + DeletePersistentVolumeClaims: pointer.Bool(true), + InjectLabels: map[string]string{"shoot.gardener.cloud/no-cleanup": "true"}, + SecretRefs: []corev1.LocalObjectReference{{ + Name: managedResource.Spec.SecretRefs[0].Name, + }}, + KeepObjects: pointer.Bool(false), + }, + } + utilruntime.Must(references.InjectAnnotations(expectedMr)) + Expect(managedResource).To(DeepEqual(expectedMr)) + + managedResourceSecret.Name = managedResource.Spec.SecretRefs[0].Name + Expect(c.Get(ctx, client.ObjectKeyFromObject(managedResourceSecret), managedResourceSecret)).To(Succeed()) + Expect(managedResourceSecret.Type).To(Equal(corev1.SecretTypeOpaque)) + Expect(managedResourceSecret.Immutable).To(Equal(pointer.Bool(true))) + Expect(managedResourceSecret.Labels["resources.gardener.cloud/garbage-collectable-reference"]).To(Equal("true")) + Expect(managedResourceSecret.Data).To(HaveLen(5)) + Expect(string(managedResourceSecret.Data["namespace____registry-cache.yaml"])).To(Equal(namespaceYAML)) + Expect(string(managedResourceSecret.Data["service__registry-cache__registry-docker-io.yaml"])).To(Equal(serviceYAMLFor("registry-docker-io", "docker.io"))) + Expect(string(managedResourceSecret.Data["statefulset__registry-cache__registry-docker-io.yaml"])).To(Equal(statefulSetYAMLFor("registry-docker-io", "docker.io", "https://registry-1.docker.io", "10Gi", true))) + Expect(string(managedResourceSecret.Data["service__registry-cache__registry-eu-gcr-io.yaml"])).To(Equal(serviceYAMLFor("registry-eu-gcr-io", "eu.gcr.io"))) + Expect(string(managedResourceSecret.Data["statefulset__registry-cache__registry-eu-gcr-io.yaml"])).To(Equal(statefulSetYAMLFor("registry-eu-gcr-io", "eu.gcr.io", "https://eu.gcr.io", "20Gi", false))) + }) + }) + + Describe("#Destroy", func() { + It("should successfully destroy all resources", func() { + Expect(c.Create(ctx, managedResource)).To(Succeed()) + Expect(c.Create(ctx, managedResourceSecret)).To(Succeed()) + + Expect(c.Get(ctx, client.ObjectKeyFromObject(managedResource), managedResource)).To(Succeed()) + Expect(c.Get(ctx, client.ObjectKeyFromObject(managedResourceSecret), managedResourceSecret)).To(Succeed()) + + Expect(registryCaches.Destroy(ctx)).To(Succeed()) + + Expect(c.Get(ctx, client.ObjectKeyFromObject(managedResource), managedResource)).To(MatchError(apierrors.NewNotFound(schema.GroupResource{Group: resourcesv1alpha1.SchemeGroupVersion.Group, Resource: "managedresources"}, managedResource.Name))) + Expect(c.Get(ctx, client.ObjectKeyFromObject(managedResourceSecret), managedResourceSecret)).To(MatchError(apierrors.NewNotFound(schema.GroupResource{Group: corev1.SchemeGroupVersion.Group, Resource: "secrets"}, managedResourceSecret.Name))) + }) + }) + + Context("waiting functions", func() { + var fakeOps *retryfake.Ops + + BeforeEach(func() { + fakeOps = &retryfake.Ops{MaxAttempts: 1} + DeferCleanup(test.WithVars( + &retry.Until, fakeOps.Until, + &retry.UntilTimeout, fakeOps.UntilTimeout, + )) + }) + + Describe("#Wait", func() { + It("should fail because the ManagedResource doesn't become healthy", func() { + fakeOps.MaxAttempts = 2 + + Expect(c.Create(ctx, &resourcesv1alpha1.ManagedResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: managedResourceName, + Namespace: namespace, + Generation: 1, + }, + Status: resourcesv1alpha1.ManagedResourceStatus{ + ObservedGeneration: 1, + Conditions: []gardencorev1beta1.Condition{ + { + Type: resourcesv1alpha1.ResourcesApplied, + Status: gardencorev1beta1.ConditionFalse, + }, + { + Type: resourcesv1alpha1.ResourcesHealthy, + Status: gardencorev1beta1.ConditionFalse, + }, + }, + }, + })).To(Succeed()) + + Expect(registryCaches.Wait(ctx)).To(MatchError(ContainSubstring("is not healthy"))) + }) + + It("should successfully wait for the managed resource to become healthy", func() { + fakeOps.MaxAttempts = 2 + + Expect(c.Create(ctx, &resourcesv1alpha1.ManagedResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: managedResourceName, + Namespace: namespace, + Generation: 1, + }, + Status: resourcesv1alpha1.ManagedResourceStatus{ + ObservedGeneration: 1, + Conditions: []gardencorev1beta1.Condition{ + { + Type: resourcesv1alpha1.ResourcesApplied, + Status: gardencorev1beta1.ConditionTrue, + }, + { + Type: resourcesv1alpha1.ResourcesHealthy, + Status: gardencorev1beta1.ConditionTrue, + }, + }, + }, + })).To(Succeed()) + + Expect(registryCaches.Wait(ctx)).To(Succeed()) + }) + }) + + Describe("#WaitCleanup", func() { + It("should fail when the wait for the managed resource deletion times out", func() { + fakeOps.MaxAttempts = 2 + + Expect(c.Create(ctx, managedResource)).To(Succeed()) + + Expect(registryCaches.WaitCleanup(ctx)).To(MatchError(ContainSubstring("still exists"))) + }) + + It("should not return an error when it's already removed", func() { + Expect(registryCaches.WaitCleanup(ctx)).To(Succeed()) + }) + }) + }) +}) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go new file mode 100644 index 00000000..df6e537d --- /dev/null +++ b/pkg/constants/constants.go @@ -0,0 +1,27 @@ +// Copyright (c) 2023 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 constants + +const ( + // ExtensionType is the name of the extension type. + ExtensionType = "registry-cache" + + // NamespaceRegistryCache is the namespace where the registry cache resources are deployed. + NamespaceRegistryCache = "registry-cache" + // UpstreamHostLabel is a label on registry cache resources (Service, StatefulSet) which denotes the upstream host. + UpstreamHostLabel = "upstream-host" + // RegistryCachePort is the port on which the pull through cache serves requests. + RegistryCachePort = 5000 +) diff --git a/pkg/controller/actuator.go b/pkg/controller/extension/actuator.go similarity index 61% rename from pkg/controller/actuator.go rename to pkg/controller/extension/actuator.go index dcfe3fdf..be3c3f49 100644 --- a/pkg/controller/actuator.go +++ b/pkg/controller/extension/actuator.go @@ -12,12 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package controller +package extension import ( "context" "fmt" - "time" extensionsconfig "github.com/gardener/gardener/extensions/pkg/apis/config" "github.com/gardener/gardener/extensions/pkg/controller" @@ -25,8 +24,7 @@ import ( "github.com/gardener/gardener/extensions/pkg/util" v1beta1helper "github.com/gardener/gardener/pkg/apis/core/v1beta1/helper" extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" - "github.com/gardener/gardener/pkg/client/kubernetes" - "github.com/gardener/gardener/pkg/utils/managedresources" + "github.com/gardener/gardener/pkg/component" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -37,6 +35,8 @@ import ( "github.com/gardener/gardener-extension-registry-cache/pkg/apis/config" "github.com/gardener/gardener-extension-registry-cache/pkg/apis/registry/v1alpha1" + "github.com/gardener/gardener-extension-registry-cache/pkg/component/registrycaches" + "github.com/gardener/gardener-extension-registry-cache/pkg/constants" "github.com/gardener/gardener-extension-registry-cache/pkg/imagevector" ) @@ -58,7 +58,7 @@ type actuator struct { // Reconcile the Extension resource. func (a *actuator) Reconcile(ctx context.Context, _ logr.Logger, ex *extensionsv1alpha1.Extension) error { if ex.Spec.ProviderConfig == nil { - return nil + return fmt.Errorf("providerConfig is required for the registry-cache extension") } registryConfig := &v1alpha1.RegistryConfig{} @@ -66,9 +66,19 @@ func (a *actuator) Reconcile(ctx context.Context, _ logr.Logger, ex *extensionsv return fmt.Errorf("failed to decode provider config: %w", err) } + image, err := imagevector.ImageVector().FindImage("registry") + if err != nil { + return fmt.Errorf("failed to find registry image: %w", err) + } + namespace := ex.GetNamespace() - if err := a.createResources(ctx, registryConfig, namespace); err != nil { - return fmt.Errorf("failed to create resources: %w", err) + registryCaches := registrycaches.New(a.client, namespace, registrycaches.Values{ + Image: image.String(), + Caches: registryConfig.Caches, + }) + + if err := registryCaches.Deploy(ctx); err != nil { + return fmt.Errorf("failed to deploy the registry caches component: %w", err) } cluster, err := controller.GetCluster(ctx, a.client, namespace) @@ -92,8 +102,15 @@ func (a *actuator) Reconcile(ctx context.Context, _ logr.Logger, ex *extensionsv } // Delete the Extension resource. -func (a *actuator) Delete(ctx context.Context, log logr.Logger, ex *extensionsv1alpha1.Extension) error { - return a.deleteResources(ctx, log, ex.GetNamespace()) +func (a *actuator) Delete(ctx context.Context, _ logr.Logger, ex *extensionsv1alpha1.Extension) error { + namespace := ex.GetNamespace() + registryCaches := registrycaches.New(a.client, namespace, registrycaches.Values{}) + + if err := component.OpDestroyAndWait(registryCaches).Destroy(ctx); err != nil { + return fmt.Errorf("failed to destroy the registry caches component: %w", err) + } + + return nil } // Restore the Extension resource. @@ -106,85 +123,6 @@ func (a *actuator) Migrate(_ context.Context, _ logr.Logger, _ *extensionsv1alph return nil } -func (a *actuator) createResources(ctx context.Context, registryConfig *v1alpha1.RegistryConfig, namespace string) error { - registryImage, err := imagevector.ImageVector().FindImage("registry") - if err != nil { - return fmt.Errorf("failed to find registry image: %w", err) - } - - objects := []client.Object{ - &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: registryCacheNamespaceName, - }, - }, - } - - for _, cache := range registryConfig.Caches { - c := registryCache{ - Namespace: registryCacheNamespaceName, - Upstream: cache.Upstream, - VolumeSize: *cache.Size, - GarbageCollectionEnabled: *cache.GarbageCollectionEnabled, - RegistryImage: registryImage.String(), - } - - os, err := c.Ensure() - if err != nil { - return err - } - - objects = append(objects, os...) - } - - resources, err := managedresources.NewRegistry(kubernetes.ShootScheme, kubernetes.ShootCodec, kubernetes.ShootSerializer).AddAllAndSerialize(objects...) - if err != nil { - return err - } - - // create ManagedResource for the registryCache - err = a.createManagedResources(ctx, v1alpha1.RegistryResourceName, namespace, resources) - if err != nil { - return err - } - - return nil -} - -func (a *actuator) deleteResources(ctx context.Context, log logr.Logger, namespace string) error { - log.Info("Deleting managed resource for registry cache") - - if err := managedresources.Delete(ctx, a.client, namespace, v1alpha1.RegistryResourceName, false); err != nil { - return err - } - - timeoutCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) - defer cancel() - return managedresources.WaitUntilDeleted(timeoutCtx, a.client, namespace, v1alpha1.RegistryResourceName) -} - -func (a *actuator) createManagedResources(ctx context.Context, name, namespace string, resources map[string][]byte) error { - var ( - origin = "registry-cache" - keepObjects = false - - secretName, secret = managedresources.NewSecret(a.client, namespace, name, resources, false) - managedResource = managedresources.NewForShoot(a.client, namespace, name, origin, keepObjects). - WithSecretRef(secretName). - DeletePersistentVolumeClaims(true) - ) - - if err := secret.Reconcile(ctx); err != nil { - return fmt.Errorf("failed to create or update secret of managed resources: %w", err) - } - - if err := managedResource.Reconcile(ctx); err != nil { - return fmt.Errorf("failed to not create or update managed resource: %w", err) - } - - return nil -} - func (a *actuator) computeProviderStatus(ctx context.Context, registryConfig *v1alpha1.RegistryConfig, namespace string) (*v1alpha1.RegistryStatus, error) { // get service IPs from shoot _, shootClient, err := util.NewClientForShoot(ctx, a.client, namespace, client.Options{}, extensionsconfig.RESTOptions{}) @@ -193,7 +131,7 @@ func (a *actuator) computeProviderStatus(ctx context.Context, registryConfig *v1 } selector := labels.NewSelector() - r, err := labels.NewRequirement(registryCacheServiceUpstreamLabel, selection.Exists, nil) + r, err := labels.NewRequirement(constants.UpstreamHostLabel, selection.Exists, nil) if err != nil { return nil, err } @@ -201,7 +139,7 @@ func (a *actuator) computeProviderStatus(ctx context.Context, registryConfig *v1 // get all registry cache services services := &corev1.ServiceList{} - if err := shootClient.List(ctx, services, client.InNamespace(registryCacheNamespaceName), client.MatchingLabelsSelector{Selector: selector}); err != nil { + if err := shootClient.List(ctx, services, client.InNamespace(constants.NamespaceRegistryCache), client.MatchingLabelsSelector{Selector: selector}); err != nil { return nil, fmt.Errorf("failed to read services from shoot: %w", err) } @@ -212,8 +150,8 @@ func (a *actuator) computeProviderStatus(ctx context.Context, registryConfig *v1 caches := []v1alpha1.RegistryCacheStatus{} for _, service := range services.Items { caches = append(caches, v1alpha1.RegistryCacheStatus{ - Upstream: service.Labels[registryCacheServiceUpstreamLabel], - Endpoint: fmt.Sprintf("http://%s:%d", service.Spec.ClusterIP, registryCachePort), + Upstream: service.Labels[constants.UpstreamHostLabel], + Endpoint: fmt.Sprintf("http://%s:%d", service.Spec.ClusterIP, constants.RegistryCachePort), }) } diff --git a/pkg/controller/add.go b/pkg/controller/extension/add.go similarity index 97% rename from pkg/controller/add.go rename to pkg/controller/extension/add.go index 0c358351..0eac4a1a 100644 --- a/pkg/controller/add.go +++ b/pkg/controller/extension/add.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package controller +package extension import ( "context" @@ -29,7 +29,7 @@ const ( // Type is the type of Extension resource. Type = "registry-cache" // ControllerName is the name of the registry cache service controller. - ControllerName = "registry-cache" + ControllerName = "extension-controller" // FinalizerSuffix is the finalizer suffix for the registry cache service controller. FinalizerSuffix = "registry-cache" ) diff --git a/pkg/controller/registrydeployer.go b/pkg/controller/registrydeployer.go deleted file mode 100644 index a770d053..00000000 --- a/pkg/controller/registrydeployer.go +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright (c) 2022 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file -// -// 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 controller - -import ( - "fmt" - "strconv" - "strings" - - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" - "k8s.io/utils/pointer" - "sigs.k8s.io/controller-runtime/pkg/client" - - registryutils "github.com/gardener/gardener-extension-registry-cache/pkg/utils/registry" -) - -type registryCache struct { - Name string - Namespace string - Labels map[string]string - - Upstream string - VolumeSize resource.Quantity - GarbageCollectionEnabled bool - - RegistryImage string -} - -const ( - registryCacheNamespaceName = "registry-cache" - registryCacheInternalName = "registry-cache" - registryCacheVolumeName = "cache-volume" - registryVolumeMountPath = "/var/lib/registry" - // registryCachePort is the port on which the pull through cache serves requests. - registryCachePort = 5000 - - environmentVarialbleNameRegistryURL = "REGISTRY_PROXY_REMOTEURL" - environmentVarialbleNameRegistryDelete = "REGISTRY_STORAGE_DELETE_ENABLED" - - registryCacheServiceUpstreamLabel = "upstream-host" -) - -func (c *registryCache) Ensure() ([]client.Object, error) { - c.Name = strings.Replace(fmt.Sprintf("registry-%s", strings.Split(c.Upstream, ":")[0]), ".", "-", -1) - - if c.Labels == nil { - c.Labels = map[string]string{ - "app": c.Name, - } - } - - c.Labels[registryCacheServiceUpstreamLabel] = c.Upstream - - var ( - service = &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: c.Name, - Namespace: registryCacheNamespaceName, - Labels: c.Labels, - }, - Spec: corev1.ServiceSpec{ - Selector: c.Labels, - Ports: []corev1.ServicePort{{ - Name: registryCacheInternalName, - Port: registryCachePort, - Protocol: corev1.ProtocolTCP, - TargetPort: intstr.FromString(registryCacheInternalName), - }}, - Type: corev1.ServiceTypeClusterIP, - }, - } - - statefulSet = &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: c.Name, - Namespace: registryCacheNamespaceName, - Labels: c.Labels, - }, - Spec: appsv1.StatefulSetSpec{ - ServiceName: service.Name, - Selector: &metav1.LabelSelector{ - MatchLabels: c.Labels, - }, - Replicas: pointer.Int32(1), - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: c.Labels, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: registryCacheInternalName, - Image: c.RegistryImage, - ImagePullPolicy: corev1.PullIfNotPresent, - Ports: []corev1.ContainerPort{ - { - ContainerPort: registryCachePort, - Name: registryCacheInternalName, - }, - }, - Env: []corev1.EnvVar{ - { - Name: environmentVarialbleNameRegistryURL, - Value: registryutils.GetUpstreamURL(c.Upstream), - }, - { - Name: environmentVarialbleNameRegistryDelete, - Value: strconv.FormatBool(c.GarbageCollectionEnabled), - }, - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: registryCacheVolumeName, - ReadOnly: false, - MountPath: registryVolumeMountPath, - }, - }, - }, - }, - }, - }, - VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: registryCacheVolumeName, - Labels: c.Labels, - }, - Spec: corev1.PersistentVolumeClaimSpec{ - AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: c.VolumeSize, - }, - }, - }, - }, - }, - }, - } - ) - - return []client.Object{ - service, - statefulSet, - }, nil -} diff --git a/pkg/apis/config/v1alpha1/v1alpha1_suite_test.go b/vendor/github.com/gardener/gardener/pkg/mock/controller-runtime/client/doc.go similarity index 58% rename from pkg/apis/config/v1alpha1/v1alpha1_suite_test.go rename to vendor/github.com/gardener/gardener/pkg/mock/controller-runtime/client/doc.go index 60921d28..dd93a846 100644 --- a/pkg/apis/config/v1alpha1/v1alpha1_suite_test.go +++ b/vendor/github.com/gardener/gardener/pkg/mock/controller-runtime/client/doc.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// Copyright 2018 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,17 +11,6 @@ // 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. +//go:generate mockgen -package client -destination=mocks.go sigs.k8s.io/controller-runtime/pkg/client Client,StatusWriter,Reader,Writer,SubResourceClient -package v1alpha1_test - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestV1alpha1(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "API Config V1alpha1 Suite") -} +package client diff --git a/vendor/github.com/gardener/gardener/pkg/mock/controller-runtime/client/mocks.go b/vendor/github.com/gardener/gardener/pkg/mock/controller-runtime/client/mocks.go new file mode 100644 index 00000000..80ecabfa --- /dev/null +++ b/vendor/github.com/gardener/gardener/pkg/mock/controller-runtime/client/mocks.go @@ -0,0 +1,586 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: sigs.k8s.io/controller-runtime/pkg/client (interfaces: Client,StatusWriter,Reader,Writer,SubResourceClient) + +// Package client is a generated GoMock package. +package client + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" + meta "k8s.io/apimachinery/pkg/api/meta" + runtime "k8s.io/apimachinery/pkg/runtime" + types "k8s.io/apimachinery/pkg/types" + client "sigs.k8s.io/controller-runtime/pkg/client" +) + +// MockClient is a mock of Client interface. +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockClient) Create(arg0 context.Context, arg1 client.Object, arg2 ...client.CreateOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Create", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockClientMockRecorder) Create(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockClient)(nil).Create), varargs...) +} + +// Delete mocks base method. +func (m *MockClient) Delete(arg0 context.Context, arg1 client.Object, arg2 ...client.DeleteOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Delete", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockClientMockRecorder) Delete(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockClient)(nil).Delete), varargs...) +} + +// DeleteAllOf mocks base method. +func (m *MockClient) DeleteAllOf(arg0 context.Context, arg1 client.Object, arg2 ...client.DeleteAllOfOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteAllOf", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteAllOf indicates an expected call of DeleteAllOf. +func (mr *MockClientMockRecorder) DeleteAllOf(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllOf", reflect.TypeOf((*MockClient)(nil).DeleteAllOf), varargs...) +} + +// Get mocks base method. +func (m *MockClient) Get(arg0 context.Context, arg1 types.NamespacedName, arg2 client.Object, arg3 ...client.GetOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Get", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Get indicates an expected call of Get. +func (mr *MockClientMockRecorder) Get(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClient)(nil).Get), varargs...) +} + +// List mocks base method. +func (m *MockClient) List(arg0 context.Context, arg1 client.ObjectList, arg2 ...client.ListOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "List", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// List indicates an expected call of List. +func (mr *MockClientMockRecorder) List(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockClient)(nil).List), varargs...) +} + +// Patch mocks base method. +func (m *MockClient) Patch(arg0 context.Context, arg1 client.Object, arg2 client.Patch, arg3 ...client.PatchOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Patch", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Patch indicates an expected call of Patch. +func (mr *MockClientMockRecorder) Patch(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockClient)(nil).Patch), varargs...) +} + +// RESTMapper mocks base method. +func (m *MockClient) RESTMapper() meta.RESTMapper { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RESTMapper") + ret0, _ := ret[0].(meta.RESTMapper) + return ret0 +} + +// RESTMapper indicates an expected call of RESTMapper. +func (mr *MockClientMockRecorder) RESTMapper() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RESTMapper", reflect.TypeOf((*MockClient)(nil).RESTMapper)) +} + +// Scheme mocks base method. +func (m *MockClient) Scheme() *runtime.Scheme { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Scheme") + ret0, _ := ret[0].(*runtime.Scheme) + return ret0 +} + +// Scheme indicates an expected call of Scheme. +func (mr *MockClientMockRecorder) Scheme() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Scheme", reflect.TypeOf((*MockClient)(nil).Scheme)) +} + +// Status mocks base method. +func (m *MockClient) Status() client.SubResourceWriter { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Status") + ret0, _ := ret[0].(client.SubResourceWriter) + return ret0 +} + +// Status indicates an expected call of Status. +func (mr *MockClientMockRecorder) Status() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockClient)(nil).Status)) +} + +// SubResource mocks base method. +func (m *MockClient) SubResource(arg0 string) client.SubResourceClient { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubResource", arg0) + ret0, _ := ret[0].(client.SubResourceClient) + return ret0 +} + +// SubResource indicates an expected call of SubResource. +func (mr *MockClientMockRecorder) SubResource(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubResource", reflect.TypeOf((*MockClient)(nil).SubResource), arg0) +} + +// Update mocks base method. +func (m *MockClient) Update(arg0 context.Context, arg1 client.Object, arg2 ...client.UpdateOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Update", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockClientMockRecorder) Update(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockClient)(nil).Update), varargs...) +} + +// MockStatusWriter is a mock of StatusWriter interface. +type MockStatusWriter struct { + ctrl *gomock.Controller + recorder *MockStatusWriterMockRecorder +} + +// MockStatusWriterMockRecorder is the mock recorder for MockStatusWriter. +type MockStatusWriterMockRecorder struct { + mock *MockStatusWriter +} + +// NewMockStatusWriter creates a new mock instance. +func NewMockStatusWriter(ctrl *gomock.Controller) *MockStatusWriter { + mock := &MockStatusWriter{ctrl: ctrl} + mock.recorder = &MockStatusWriterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStatusWriter) EXPECT() *MockStatusWriterMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockStatusWriter) Create(arg0 context.Context, arg1, arg2 client.Object, arg3 ...client.SubResourceCreateOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Create", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockStatusWriterMockRecorder) Create(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockStatusWriter)(nil).Create), varargs...) +} + +// Patch mocks base method. +func (m *MockStatusWriter) Patch(arg0 context.Context, arg1 client.Object, arg2 client.Patch, arg3 ...client.SubResourcePatchOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Patch", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Patch indicates an expected call of Patch. +func (mr *MockStatusWriterMockRecorder) Patch(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockStatusWriter)(nil).Patch), varargs...) +} + +// Update mocks base method. +func (m *MockStatusWriter) Update(arg0 context.Context, arg1 client.Object, arg2 ...client.SubResourceUpdateOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Update", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockStatusWriterMockRecorder) Update(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockStatusWriter)(nil).Update), varargs...) +} + +// MockReader is a mock of Reader interface. +type MockReader struct { + ctrl *gomock.Controller + recorder *MockReaderMockRecorder +} + +// MockReaderMockRecorder is the mock recorder for MockReader. +type MockReaderMockRecorder struct { + mock *MockReader +} + +// NewMockReader creates a new mock instance. +func NewMockReader(ctrl *gomock.Controller) *MockReader { + mock := &MockReader{ctrl: ctrl} + mock.recorder = &MockReaderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockReader) EXPECT() *MockReaderMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockReader) Get(arg0 context.Context, arg1 types.NamespacedName, arg2 client.Object, arg3 ...client.GetOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Get", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Get indicates an expected call of Get. +func (mr *MockReaderMockRecorder) Get(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockReader)(nil).Get), varargs...) +} + +// List mocks base method. +func (m *MockReader) List(arg0 context.Context, arg1 client.ObjectList, arg2 ...client.ListOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "List", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// List indicates an expected call of List. +func (mr *MockReaderMockRecorder) List(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockReader)(nil).List), varargs...) +} + +// MockWriter is a mock of Writer interface. +type MockWriter struct { + ctrl *gomock.Controller + recorder *MockWriterMockRecorder +} + +// MockWriterMockRecorder is the mock recorder for MockWriter. +type MockWriterMockRecorder struct { + mock *MockWriter +} + +// NewMockWriter creates a new mock instance. +func NewMockWriter(ctrl *gomock.Controller) *MockWriter { + mock := &MockWriter{ctrl: ctrl} + mock.recorder = &MockWriterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockWriter) EXPECT() *MockWriterMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockWriter) Create(arg0 context.Context, arg1 client.Object, arg2 ...client.CreateOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Create", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockWriterMockRecorder) Create(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockWriter)(nil).Create), varargs...) +} + +// Delete mocks base method. +func (m *MockWriter) Delete(arg0 context.Context, arg1 client.Object, arg2 ...client.DeleteOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Delete", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockWriterMockRecorder) Delete(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockWriter)(nil).Delete), varargs...) +} + +// DeleteAllOf mocks base method. +func (m *MockWriter) DeleteAllOf(arg0 context.Context, arg1 client.Object, arg2 ...client.DeleteAllOfOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteAllOf", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteAllOf indicates an expected call of DeleteAllOf. +func (mr *MockWriterMockRecorder) DeleteAllOf(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllOf", reflect.TypeOf((*MockWriter)(nil).DeleteAllOf), varargs...) +} + +// Patch mocks base method. +func (m *MockWriter) Patch(arg0 context.Context, arg1 client.Object, arg2 client.Patch, arg3 ...client.PatchOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Patch", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Patch indicates an expected call of Patch. +func (mr *MockWriterMockRecorder) Patch(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockWriter)(nil).Patch), varargs...) +} + +// Update mocks base method. +func (m *MockWriter) Update(arg0 context.Context, arg1 client.Object, arg2 ...client.UpdateOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Update", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockWriterMockRecorder) Update(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockWriter)(nil).Update), varargs...) +} + +// MockSubResourceClient is a mock of SubResourceClient interface. +type MockSubResourceClient struct { + ctrl *gomock.Controller + recorder *MockSubResourceClientMockRecorder +} + +// MockSubResourceClientMockRecorder is the mock recorder for MockSubResourceClient. +type MockSubResourceClientMockRecorder struct { + mock *MockSubResourceClient +} + +// NewMockSubResourceClient creates a new mock instance. +func NewMockSubResourceClient(ctrl *gomock.Controller) *MockSubResourceClient { + mock := &MockSubResourceClient{ctrl: ctrl} + mock.recorder = &MockSubResourceClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSubResourceClient) EXPECT() *MockSubResourceClientMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockSubResourceClient) Create(arg0 context.Context, arg1, arg2 client.Object, arg3 ...client.SubResourceCreateOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Create", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockSubResourceClientMockRecorder) Create(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockSubResourceClient)(nil).Create), varargs...) +} + +// Get mocks base method. +func (m *MockSubResourceClient) Get(arg0 context.Context, arg1, arg2 client.Object, arg3 ...client.SubResourceGetOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Get", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Get indicates an expected call of Get. +func (mr *MockSubResourceClientMockRecorder) Get(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSubResourceClient)(nil).Get), varargs...) +} + +// Patch mocks base method. +func (m *MockSubResourceClient) Patch(arg0 context.Context, arg1 client.Object, arg2 client.Patch, arg3 ...client.SubResourcePatchOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Patch", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Patch indicates an expected call of Patch. +func (mr *MockSubResourceClientMockRecorder) Patch(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockSubResourceClient)(nil).Patch), varargs...) +} + +// Update mocks base method. +func (m *MockSubResourceClient) Update(arg0 context.Context, arg1 client.Object, arg2 ...client.SubResourceUpdateOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Update", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockSubResourceClientMockRecorder) Update(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockSubResourceClient)(nil).Update), varargs...) +} diff --git a/vendor/github.com/gardener/gardener/pkg/utils/retry/fake/retry.go b/vendor/github.com/gardener/gardener/pkg/utils/retry/fake/retry.go new file mode 100644 index 00000000..ebfbf62b --- /dev/null +++ b/vendor/github.com/gardener/gardener/pkg/utils/retry/fake/retry.go @@ -0,0 +1,64 @@ +// Copyright 2020 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 ( + "context" + "fmt" + "time" + + "github.com/gardener/gardener/pkg/utils/retry" +) + +var _ retry.Ops = &Ops{} + +// Ops implements retry.Ops and can be used to mock calls to retry.Until and retry.UntilTimeout in unit tests. +// This implementation ignores the `interval` parameter and doesn't wait between retries, which makes it useful for +// writing quick and stable unit tests. +type Ops struct { + // MaxAttempts configures the maximum amount of attempts before returning a retryError. If it is set to 0, it + // fails immediately and f is never called. + MaxAttempts int +} + +// Until implements retry.Ops without waiting between retries. +func (o *Ops) Until(ctx context.Context, _ time.Duration, f retry.Func) error { + var minorErr error + attempts := 0 + + for { + attempts++ + if attempts > o.MaxAttempts { + return retry.NewError(fmt.Errorf("max attempts reached"), minorErr) + } + + done, err := f(ctx) + if err != nil { + if done { + return err + } + + minorErr = err + } else if done { + return nil + } + } +} + +// UntilTimeout implements retry.Ops without waiting between retries. UntilTimeout ignores the timeout +// parameter and instead uses Ops.MaxAttempts to configure, how often f is retried. +func (o *Ops) UntilTimeout(ctx context.Context, interval, _ time.Duration, f retry.Func) error { + return o.Until(ctx, interval, f) +} diff --git a/vendor/github.com/gardener/gardener/pkg/utils/test/gomock.go b/vendor/github.com/gardener/gardener/pkg/utils/test/gomock.go new file mode 100644 index 00000000..2f1b5420 --- /dev/null +++ b/vendor/github.com/gardener/gardener/pkg/utils/test/gomock.go @@ -0,0 +1,49 @@ +// Copyright 2021 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 test + +import ( + "fmt" + + "go.uber.org/mock/gomock" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// HasObjectKeyOf returns a gomock.Matcher that matches if actual is a client.Object that has the same +// ObjectKey as expected. +func HasObjectKeyOf(expected client.Object) gomock.Matcher { + return &objectKeyMatcher{key: client.ObjectKeyFromObject(expected)} +} + +type objectKeyMatcher struct { + key client.ObjectKey +} + +func (o *objectKeyMatcher) Matches(actual interface{}) bool { + if actual == nil { + return false + } + + obj, ok := actual.(client.Object) + if !ok { + return false + } + + return o.key == client.ObjectKeyFromObject(obj) +} + +func (o *objectKeyMatcher) String() string { + return fmt.Sprintf("has object key %q", o.key) +} diff --git a/vendor/github.com/gardener/gardener/pkg/utils/test/manager.go b/vendor/github.com/gardener/gardener/pkg/utils/test/manager.go new file mode 100644 index 00000000..4813c41f --- /dev/null +++ b/vendor/github.com/gardener/gardener/pkg/utils/test/manager.go @@ -0,0 +1,52 @@ +// Copyright 2022 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 test + +import ( + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +// FakeManager fakes a manager.Manager. +type FakeManager struct { + manager.Manager + + Client client.Client + Cache cache.Cache + EventRecorder record.EventRecorder + APIReader client.Reader +} + +// GetClient returns the given client. +func (f FakeManager) GetClient() client.Client { + return f.Client +} + +// GetCache returns the given cache. +func (f FakeManager) GetCache() cache.Cache { + return f.Cache +} + +// GetEventRecorderFor returns the given eventRecorder. +func (f FakeManager) GetEventRecorderFor(name string) record.EventRecorder { + return f.EventRecorder +} + +// GetAPIReader returns the given apiReader. +func (f FakeManager) GetAPIReader() client.Reader { + return f.APIReader +} diff --git a/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/conditions.go b/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/conditions.go new file mode 100644 index 00000000..a802c648 --- /dev/null +++ b/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/conditions.go @@ -0,0 +1,74 @@ +// Copyright 2022 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 matchers + +import ( + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gstruct" + gomegatypes "github.com/onsi/gomega/types" + + gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1" +) + +// ContainCondition returns a matchers for checking whether a condition is contained. +func ContainCondition(matchers ...gomegatypes.GomegaMatcher) gomegatypes.GomegaMatcher { + return ContainElement(And(matchers...)) +} + +// OfType returns a matcher for checking whether a condition has a certain type. +func OfType(conditionType gardencorev1beta1.ConditionType) gomegatypes.GomegaMatcher { + return gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "Type": Equal(conditionType), + }) +} + +// WithStatus returns a matcher for checking whether a condition has a certain status. +func WithStatus(status gardencorev1beta1.ConditionStatus) gomegatypes.GomegaMatcher { + return gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "Status": Equal(status), + }) +} + +// WithReason returns a matcher for checking whether a condition has a certain reason. +func WithReason(reason string) gomegatypes.GomegaMatcher { + return gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "Reason": Equal(reason), + }) +} + +// WithMessage returns a matcher for checking whether a condition has a certain message. +func WithMessage(message string) gomegatypes.GomegaMatcher { + return gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "Message": ContainSubstring(message), + }) +} + +// WithCodes returns a matcher for checking whether a condition contains certain error codes. +func WithCodes(codes ...gardencorev1beta1.ErrorCode) gomegatypes.GomegaMatcher { + return gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "Codes": ContainElements(codes), + }) +} + +// WithMessageSubstrings returns a matcher for checking whether a condition's message contains certain substrings. +func WithMessageSubstrings(messages ...string) gomegatypes.GomegaMatcher { + var substringMatchers = make([]gomegatypes.GomegaMatcher, 0, len(messages)) + for _, message := range messages { + substringMatchers = append(substringMatchers, ContainSubstring(message)) + } + return gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "Message": SatisfyAll(substringMatchers...), + }) +} diff --git a/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/deep.go b/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/deep.go new file mode 100644 index 00000000..f0c9c5e5 --- /dev/null +++ b/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/deep.go @@ -0,0 +1,81 @@ +// Copyright 2020 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" + gomegatypes "github.com/onsi/gomega/types" + "k8s.io/apimachinery/pkg/api/equality" + "sigs.k8s.io/yaml" +) + +const ( + deepMatcherNilError = `refusing to compare to . +Be explicit and use BeNil() instead. +This is to avoid mistakes where both sides of an assertion are erroneously uninitialized` +) + +type deepMatcher struct { + name string + expected interface{} + compareFn func(a1, a2 interface{}) bool +} + +func newDeepDerivativeMatcher(expected interface{}) gomegatypes.GomegaMatcher { + return &deepMatcher{ + name: "deep derivative equal", + expected: expected, + compareFn: equality.Semantic.DeepDerivative, + } +} + +func newDeepEqualMatcher(expected interface{}) gomegatypes.GomegaMatcher { + return &deepMatcher{ + name: "deep equal", + expected: expected, + compareFn: equality.Semantic.DeepEqual, + } +} + +func (m *deepMatcher) Match(actual interface{}) (success bool, err error) { + if actual == nil && m.expected == nil { + return false, fmt.Errorf(deepMatcherNilError) + } + + return m.compareFn(m.expected, actual), nil +} + +func (m *deepMatcher) FailureMessage(actual interface{}) (message string) { + return m.failureMessage(actual, "to") +} + +func (m *deepMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return m.failureMessage(actual, "not to") +} + +func (m *deepMatcher) failureMessage(actual interface{}, messagePrefix string) (message string) { + var ( + actualYAML, actualErr = yaml.Marshal(actual) + expectedYAML, expectedErr = yaml.Marshal(m.expected) + ) + + if actualErr == nil && expectedErr == nil { + return format.MessageWithDiff(string(actualYAML), messagePrefix+" "+m.name, string(expectedYAML)) + } + + return format.Message(actual, messagePrefix+" "+m.name, m.expected) +} diff --git a/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/fields.go b/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/fields.go new file mode 100644 index 00000000..4150cfdc --- /dev/null +++ b/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/fields.go @@ -0,0 +1,38 @@ +// Copyright 2018 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 matchers + +import ( + "github.com/onsi/gomega" + "github.com/onsi/gomega/gstruct" + "github.com/onsi/gomega/types" +) + +// HaveFields succeeds if actual is a pointer and has a specific fields. +// Ignores extra elements or fields. +func HaveFields(fields gstruct.Fields) types.GomegaMatcher { + return gstruct.PointTo(gstruct.MatchFields(gstruct.IgnoreExtras, fields)) +} + +// ConsistOfFields succeeds if actual matches all selected fields. +// Actual must be an array, slice or map. For maps, ConsistOfFields matches against the map's values. +// Actual's elements must be pointers. +func ConsistOfFields(fields ...gstruct.Fields) types.GomegaMatcher { + var m []interface{} + for _, f := range fields { + m = append(m, HaveFields(f)) + } + return gomega.ConsistOf(m...) +} diff --git a/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/kubernetes_errors.go b/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/kubernetes_errors.go new file mode 100644 index 00000000..79e04f23 --- /dev/null +++ b/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/kubernetes_errors.go @@ -0,0 +1,47 @@ +// Copyright 2020 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" +) + +type kubernetesErrors struct { + checkFunc func(error) bool + message string +} + +func (k *kubernetesErrors) Match(actual interface{}) (success bool, err error) { + // is purely nil? + if actual == nil { + return false, nil + } + + actualErr, actualOk := actual.(error) + if !actualOk { + return false, fmt.Errorf("expected an error-type. got:\n%s", format.Object(actual, 1)) + } + + return k.checkFunc(actualErr), nil +} + +func (k *kubernetesErrors) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("to be %s error", k.message)) +} +func (k *kubernetesErrors) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, fmt.Sprintf("to not be %s error", k.message)) +} diff --git a/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/matchers.go b/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/matchers.go new file mode 100644 index 00000000..7ef6be87 --- /dev/null +++ b/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/matchers.go @@ -0,0 +1,142 @@ +// Copyright 2020 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 matchers + +import ( + "errors" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + + kubernetescache "github.com/gardener/gardener/pkg/client/kubernetes/cache" +) + +func init() { + // if CharactersAroundMismatchToInclude is too small, then format.MessageWithDiff will be unable to output our + // mismatch message + // set the variable in init func, otherwise the race detector will complain when matchers are used concurrently in + // multiple goroutines + if format.CharactersAroundMismatchToInclude < 50 { + format.CharactersAroundMismatchToInclude = 50 + } +} + +// DeepEqual returns a Gomega matcher which checks whether the expected object is deeply equal with the object it is +// being compared against. +func DeepEqual(expected interface{}) types.GomegaMatcher { + return newDeepEqualMatcher(expected) +} + +// DeepDerivativeEqual is similar to DeepEqual except that unset fields in actual are +// ignored (not compared). This allows us to focus on the fields that matter to +// the semantic comparison. +func DeepDerivativeEqual(expected interface{}) types.GomegaMatcher { + return newDeepDerivativeMatcher(expected) +} + +// BeNotFoundError checks if error is NotFound. +func BeNotFoundError() types.GomegaMatcher { + return &kubernetesErrors{ + checkFunc: apierrors.IsNotFound, + message: "NotFound", + } +} + +// BeNotRegisteredError checks if error is NotRegistered. +func BeNotRegisteredError() types.GomegaMatcher { + return &kubernetesErrors{ + checkFunc: runtime.IsNotRegisteredError, + message: "NotRegistered", + } +} + +// BeAlreadyExistsError checks if error is AlreadyExists. +func BeAlreadyExistsError() types.GomegaMatcher { + return &kubernetesErrors{ + checkFunc: apierrors.IsAlreadyExists, + message: "AlreadyExists", + } +} + +// BeForbiddenError checks if error is Forbidden. +func BeForbiddenError() types.GomegaMatcher { + return &kubernetesErrors{ + checkFunc: apierrors.IsForbidden, + message: "Forbidden", + } +} + +// BeBadRequestError checks if error is BadRequest. +func BeBadRequestError() types.GomegaMatcher { + return &kubernetesErrors{ + checkFunc: apierrors.IsBadRequest, + message: "BadRequest", + } +} + +// BeNoMatchError checks if error is a NoMatchError. +func BeNoMatchError() types.GomegaMatcher { + return &kubernetesErrors{ + checkFunc: meta.IsNoMatchError, + message: "NoMatch", + } +} + +// BeMissingKindError checks if error is a MissingKindError. +func BeMissingKindError() types.GomegaMatcher { + return &kubernetesErrors{ + checkFunc: runtime.IsMissingKind, + message: "Object 'Kind' is missing", + } +} + +// BeInternalServerError checks if error is a InternalServerError. +func BeInternalServerError() types.GomegaMatcher { + return &kubernetesErrors{ + checkFunc: apierrors.IsInternalError, + message: "", + } +} + +// BeInvalidError checks if error is an InvalidError. +func BeInvalidError() types.GomegaMatcher { + return &kubernetesErrors{ + checkFunc: apierrors.IsInvalid, + message: "Invalid", + } +} + +// BeCacheError checks if error is a CacheError. +func BeCacheError() types.GomegaMatcher { + return &kubernetesErrors{ + checkFunc: func(err error) bool { + cacheErr := &kubernetescache.CacheError{} + return errors.As(err, &cacheErr) + }, + message: "", + } +} + +// ShareSameReferenceAs checks if objects shares the same underlying reference as the passed object. +// This can be used to check if maps or slices have the same underlying data store. +// Only objects that work for 'reflect.ValueOf(x).Pointer' can be compared. +func ShareSameReferenceAs(expected interface{}) types.GomegaMatcher { + return &referenceMatcher{ + expected: expected, + } +} diff --git a/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/reference.go b/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/reference.go new file mode 100644 index 00000000..65ccb721 --- /dev/null +++ b/vendor/github.com/gardener/gardener/pkg/utils/test/matchers/reference.go @@ -0,0 +1,43 @@ +// Copyright 2023 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 matchers + +import ( + "reflect" + + "github.com/onsi/gomega/format" +) + +type referenceMatcher struct { + expected interface{} +} + +func (r *referenceMatcher) Match(actual interface{}) (success bool, err error) { + return func(expected, actual interface{}) bool { + return reflect.ValueOf(expected).Pointer() == reflect.ValueOf(actual).Pointer() + }(r.expected, actual), nil +} + +func (r *referenceMatcher) FailureMessage(actual interface{}) (message string) { + return r.failureMessage(actual, "") +} + +func (r *referenceMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return r.failureMessage(actual, " not") +} + +func (r *referenceMatcher) failureMessage(actual interface{}, messagePrefix string) (message string) { + return format.Message(actual, "to"+messagePrefix+" share reference with the compared object") +} diff --git a/vendor/github.com/gardener/gardener/pkg/utils/test/options.go b/vendor/github.com/gardener/gardener/pkg/utils/test/options.go new file mode 100644 index 00000000..06d615c2 --- /dev/null +++ b/vendor/github.com/gardener/gardener/pkg/utils/test/options.go @@ -0,0 +1,142 @@ +// Copyright 2019 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 test + +import ( + "fmt" + "strings" +) + +// Flag is a flag that can be represented as a slice of strings. +type Flag interface { + // Slice returns a representation of this Flag as a slice of strings. + Slice() []string +} + +func keyToFlag(key string) string { + return fmt.Sprintf("--%s", key) +} + +type intFlag struct { + key string + value int +} + +func (f *intFlag) Slice() []string { + return []string{keyToFlag(f.key), fmt.Sprintf("%d", f.value)} +} + +type stringFlag struct { + key string + value string +} + +func (f *stringFlag) Slice() []string { + return []string{keyToFlag(f.key), f.value} +} + +type boolFlag struct { + key string + value bool +} + +func (f *boolFlag) Slice() []string { + var value string + if f.value { + value = "true" + } else { + value = "false" + } + + return []string{keyToFlag(f.key), value} +} + +type stringSliceFlag struct { + key string + value []string +} + +func (f *stringSliceFlag) Slice() []string { + return []string{keyToFlag(f.key), strings.Join(f.value, ",")} +} + +// IntFlag returns a Flag with the given key and integer value. +func IntFlag(key string, value int) Flag { + return &intFlag{key, value} +} + +// StringFlag returns a Flag with the given key and string value. +func StringFlag(key, value string) Flag { + return &stringFlag{key, value} +} + +// BoolFlag returns a Flag with the given key and boolean value. +func BoolFlag(key string, value bool) Flag { + return &boolFlag{key, value} +} + +// StringSliceFlag returns a flag with the given key and string slice value. +func StringSliceFlag(key string, value ...string) Flag { + return &stringSliceFlag{key, value} +} + +// Command is a command that has a name, a list of flags, and a list of arguments. +type Command struct { + Name string + Flags []Flag + Args []string +} + +// CommandBuilder is a builder for Command objects. +type CommandBuilder struct { + command Command +} + +// NewCommandBuilder creates and returns a new CommandBuilder with the given name. +func NewCommandBuilder(name string) *CommandBuilder { + return &CommandBuilder{Command{Name: name}} +} + +// Flags appends the given flags to this CommandBuilder. +func (c *CommandBuilder) Flags(flags ...Flag) *CommandBuilder { + c.command.Flags = append(c.command.Flags, flags...) + return c +} + +// Args appends the given arguments to this CommandBuilder. +func (c *CommandBuilder) Args(args ...string) *CommandBuilder { + c.command.Args = append(c.command.Args, args...) + return c +} + +// Command returns the Command that has been built by this CommandBuilder. +func (c *CommandBuilder) Command() *Command { + return &c.command +} + +// Slice returns a representation of this Command as a slice of strings. +func (c *Command) Slice() []string { + out := []string{c.Name} + for _, flag := range c.Flags { + out = append(out, flag.Slice()...) + } + out = append(out, c.Args...) + return out +} + +// String returns a representation of this Command as a string. +func (c *Command) String() string { + return strings.Join(c.Slice(), " ") +} diff --git a/vendor/github.com/gardener/gardener/pkg/utils/test/test.go b/vendor/github.com/gardener/gardener/pkg/utils/test/test.go new file mode 100644 index 00000000..a9588cb6 --- /dev/null +++ b/vendor/github.com/gardener/gardener/pkg/utils/test/test.go @@ -0,0 +1,298 @@ +// Copyright 2018 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 test + +import ( + "context" + "fmt" + "os" + "reflect" + "time" + + "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" + "k8s.io/apimachinery/pkg/types" + "k8s.io/component-base/featuregate" + "sigs.k8s.io/controller-runtime/pkg/client" + + mockclient "github.com/gardener/gardener/pkg/mock/controller-runtime/client" + . "github.com/gardener/gardener/pkg/utils/test/matchers" +) + +// WithVar sets the given var to the src value and returns a function to revert to the original state. +// The type of `dst` has to be a settable pointer. +// The value of `src` has to be assignable to the type of `dst`. +// +// Example usage: +// +// v := "foo" +// DeferCleanup(WithVar(&v, "bar")) +func WithVar(dst, src interface{}) func() { + dstValue := reflect.ValueOf(dst) + if dstValue.Type().Kind() != reflect.Ptr { + ginkgo.Fail(fmt.Sprintf("destination value %T is not a pointer", dst)) + } + + if dstValue.CanSet() { + ginkgo.Fail(fmt.Sprintf("value %T cannot be set", dst)) + } + + srcValue := reflect.ValueOf(src) + if srcValue.Type().AssignableTo(dstValue.Type()) { + ginkgo.Fail(fmt.Sprintf("cannot write %T into %T", src, dst)) + } + + tmp := dstValue.Elem().Interface() + dstValue.Elem().Set(srcValue) + return func() { + dstValue.Elem().Set(reflect.ValueOf(tmp)) + } +} + +// WithVars sets the given vars to the given values and returns a function to revert back. +// dstsAndSrcs have to appear in pairs of 2, otherwise there will be a runtime panic. +// +// Example usage: +// +// DeferCleanup(WithVars(&v, "foo", &x, "bar")) +func WithVars(dstsAndSrcs ...interface{}) func() { + if len(dstsAndSrcs)%2 != 0 { + ginkgo.Fail(fmt.Sprintf("dsts and srcs are not of equal length: %v", dstsAndSrcs)) + } + reverts := make([]func(), 0, len(dstsAndSrcs)/2) + + for i := 0; i < len(dstsAndSrcs); i += 2 { + dst := dstsAndSrcs[i] + src := dstsAndSrcs[i+1] + + reverts = append(reverts, WithVar(dst, src)) + } + + return func() { + for _, revert := range reverts { + revert() + } + } +} + +// WithEnvVar sets the env variable to the given environment variable and returns a function to revert. +// If the value is empty, the environment variable will be unset. +func WithEnvVar(key, value string) func() { + tmp := os.Getenv(key) + + var err error + if value == "" { + err = os.Unsetenv(key) + } else { + err = os.Setenv(key, value) + } + if err != nil { + ginkgo.Fail(fmt.Sprintf("Could not set the env variable %q to %q: %v", key, value, err)) + } + + return func() { + var err error + if tmp == "" { + err = os.Unsetenv(key) + } else { + err = os.Setenv(key, tmp) + } + if err != nil { + ginkgo.Fail(fmt.Sprintf("Could not revert the env variable %q to %q: %v", key, value, err)) + } + } +} + +// WithWd sets the working directory and returns a function to revert to the previous one. +func WithWd(path string) func() { + oldPath, err := os.Getwd() + if err != nil { + ginkgo.Fail(fmt.Sprintf("Could not obtain current working diretory: %v", err)) + } + + if err := os.Chdir(path); err != nil { + ginkgo.Fail(fmt.Sprintf("Could not change working diretory: %v", err)) + } + + return func() { + if err := os.Chdir(oldPath); err != nil { + ginkgo.Fail(fmt.Sprintf("Could not revert working diretory: %v", err)) + } + } +} + +// WithFeatureGate sets the specified gate to the specified value, and returns a function that restores the original value. +// Failures to set or restore cause the test to fail. +// Example use: +// +// DeferCleanup(WithFeatureGate(features.DefaultFeatureGate, features., true)) +func WithFeatureGate(gate featuregate.FeatureGate, f featuregate.Feature, value bool) func() { + originalValue := gate.Enabled(f) + + if err := gate.(featuregate.MutableFeatureGate).Set(fmt.Sprintf("%s=%v", f, value)); err != nil { + ginkgo.Fail(fmt.Sprintf("could not set feature gate %s=%v: %v", f, value, err)) + } + + return func() { + if err := gate.(featuregate.MutableFeatureGate).Set(fmt.Sprintf("%s=%v", f, originalValue)); err != nil { + ginkgo.Fail(fmt.Sprintf("could not restore feature gate %s=%v: %v", f, originalValue, err)) + } + } +} + +// WithTempFile creates a temporary file with the given dir and pattern, writes the given content to it, +// and returns a function to delete it. Failures to create, open, close, or delete the file case the test to fail. +// +// The filename is generated by taking pattern and adding a random string to the end. If pattern includes a "*", +// the random string replaces the last "*". If dir is the empty string, WriteTempFile uses the default directory for +// temporary files (see ioutil.TempFile). The caller can use the value of fileName to find the pathname of the file. +// +// Example usage: +// +// var fileName string +// DeferCleanup(WithTempFile("", "test", []byte("test file content"), &fileName)) +func WithTempFile(dir, pattern string, content []byte, fileName *string) func() { + file, err := os.CreateTemp(dir, pattern) + if err != nil { + ginkgo.Fail(fmt.Sprintf("could not create temp file in directory %s: %v", dir, err)) + } + + *fileName = file.Name() + + if _, err := file.Write(content); err != nil { + ginkgo.Fail(fmt.Sprintf("could not write to temp file %s: %v", file.Name(), err)) + } + if err := file.Close(); err != nil { + ginkgo.Fail(fmt.Sprintf("could not close temp file %s: %v", file.Name(), err)) + } + + return func() { + if err := os.Remove(file.Name()); err != nil { + ginkgo.Fail(fmt.Sprintf("could not delete temp file %s: %v", file.Name(), err)) + } + } +} + +// EXPECTPatch is a helper function for a GoMock call expecting a patch with the mock client. +func EXPECTPatch(ctx interface{}, c *mockclient.MockClient, expectedObj, mergeFrom client.Object, patchType types.PatchType, rets ...interface{}) *gomock.Call { + var expectedPatch client.Patch + + switch patchType { + case types.MergePatchType: + expectedPatch = client.MergeFrom(mergeFrom) + case types.StrategicMergePatchType: + expectedPatch = client.StrategicMergeFrom(mergeFrom.DeepCopyObject().(client.Object)) + } + + return expectPatch(ctx, c, expectedObj, expectedPatch, rets...) +} + +// EXPECTStatusPatch is a helper function for a GoMock call expecting a status patch with the mock client. +func EXPECTStatusPatch(ctx interface{}, c *mockclient.MockStatusWriter, expectedObj, mergeFrom client.Object, patchType types.PatchType, rets ...interface{}) *gomock.Call { + var expectedPatch client.Patch + + switch patchType { + case types.MergePatchType: + expectedPatch = client.MergeFrom(mergeFrom) + case types.StrategicMergePatchType: + expectedPatch = client.StrategicMergeFrom(mergeFrom.DeepCopyObject().(client.Object)) + } + + return expectStatusPatch(ctx, c, expectedObj, expectedPatch, rets...) +} + +// EXPECTPatchWithOptimisticLock is a helper function for a GoMock call with the mock client +// expecting a merge patch with optimistic lock. +func EXPECTPatchWithOptimisticLock(ctx interface{}, c *mockclient.MockClient, expectedObj, mergeFrom client.Object, patchType types.PatchType, rets ...interface{}) *gomock.Call { + var expectedPatch client.Patch + + switch patchType { + case types.MergePatchType: + expectedPatch = client.MergeFromWithOptions(mergeFrom, client.MergeFromWithOptimisticLock{}) + case types.StrategicMergePatchType: + expectedPatch = client.StrategicMergeFrom(mergeFrom.DeepCopyObject().(client.Object), client.MergeFromWithOptimisticLock{}) + } + + return expectPatch(ctx, c, expectedObj, expectedPatch, rets...) +} + +func expectPatch(ctx interface{}, c *mockclient.MockClient, expectedObj client.Object, expectedPatch client.Patch, rets ...interface{}) *gomock.Call { + expectedData, expectedErr := expectedPatch.Data(expectedObj) + Expect(expectedErr).To(BeNil()) + + if rets == nil { + rets = []interface{}{nil} + } + + // match object key here, but verify contents only inside DoAndReturn. + // This is to tell gomock, for which object we expect the given patch, but to enable rich yaml diff between + // actual and expected via `DeepEqual`. + return c. + EXPECT(). + Patch(ctx, HasObjectKeyOf(expectedObj), gomock.Any()). + DoAndReturn(func(_ context.Context, obj client.Object, patch client.Patch, _ ...client.PatchOption) error { + // if one of these Expects fails and Patch is called in some goroutine (e.g. via flow.Parallel) + // the failures will not be shown, as the ginkgo panic is not recovered, so the test is hard to fix + defer ginkgo.GinkgoRecover() + + Expect(obj).To(DeepEqual(expectedObj)) + data, err := patch.Data(obj) + Expect(err).To(BeNil()) + Expect(patch.Type()).To(Equal(expectedPatch.Type())) + Expect(string(data)).To(Equal(string(expectedData))) + return nil + }). + Return(rets...) +} + +func expectStatusPatch(ctx interface{}, c *mockclient.MockStatusWriter, expectedObj client.Object, expectedPatch client.Patch, rets ...interface{}) *gomock.Call { + expectedData, expectedErr := expectedPatch.Data(expectedObj) + Expect(expectedErr).To(BeNil()) + + if rets == nil { + rets = []interface{}{nil} + } + + // match object key here, but verify contents only inside DoAndReturn. + // This is to tell gomock, for which object we expect the given patch, but to enable rich yaml diff between + // actual and expected via `DeepEqual`. + return c. + EXPECT(). + Patch(ctx, HasObjectKeyOf(expectedObj), gomock.Any()). + DoAndReturn(func(_ context.Context, obj client.Object, patch client.Patch, _ ...client.PatchOption) error { + // if one of these Expects fails and Patch is called in some goroutine (e.g. via flow.Parallel) + // the failures will not be shown, as the ginkgo panic is not recovered, so the test is hard to fix + defer ginkgo.GinkgoRecover() + + Expect(obj).To(DeepEqual(expectedObj)) + data, err := patch.Data(obj) + Expect(err).To(BeNil()) + Expect(patch.Type()).To(Equal(expectedPatch.Type())) + Expect(string(data)).To(Equal(string(expectedData))) + return nil + }). + Return(rets...) +} + +// CEventually is like gomega.Eventually but with a context.Context. When it has a deadline then the gomega.Eventually +// call with be configured with a the respective timeout. +func CEventually(ctx context.Context, actual interface{}) AsyncAssertion { + deadline, ok := ctx.Deadline() + if !ok { + return Eventually(actual) + } + return Eventually(actual).WithTimeout(time.Until(deadline)) +} diff --git a/vendor/github.com/gardener/gardener/pkg/utils/test/test_resources.go b/vendor/github.com/gardener/gardener/pkg/utils/test/test_resources.go new file mode 100644 index 00000000..820bfb61 --- /dev/null +++ b/vendor/github.com/gardener/gardener/pkg/utils/test/test_resources.go @@ -0,0 +1,138 @@ +// Copyright 2021 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 test + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/util/sets" + utilyaml "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// EnsureTestResources reads test resources from path, applies them using the given client and returns the created +// objects. +func EnsureTestResources(ctx context.Context, c client.Client, namespaceName, path string) ([]client.Object, error) { + objects, err := ReadTestResources(c.Scheme(), namespaceName, path) + if err != nil { + return nil, fmt.Errorf("error decoding resources: %w", err) + } + + for _, obj := range objects { + current := obj.DeepCopyObject().(client.Object) + if err := c.Get(ctx, client.ObjectKeyFromObject(current), current); err != nil { + if !apierrors.IsNotFound(err) { + return nil, err + } + + // object doesn't exists, create it + if err := c.Create(ctx, obj); err != nil { + return nil, err + } + } else { + // object already exists, update it + if err := c.Patch(ctx, obj, client.MergeFromWithOptions(current, client.MergeFromWithOptimisticLock{})); err != nil { + return nil, err + } + } + } + return objects, nil +} + +// ReadTestResources reads test resources from path, decodes them using the given scheme and returns the parsed objects. +// Objects are values of the proper API types, if registered in the given scheme, and *unstructured.Unstructured +// otherwise. +func ReadTestResources(scheme *runtime.Scheme, namespaceName, path string) ([]client.Object, error) { + decoder := serializer.NewCodecFactory(scheme).UniversalDeserializer() + + files, err := os.ReadDir(path) + if err != nil { + return nil, err + } + + // file extensions that may contain Webhooks + resourceExtensions := sets.New(".json", ".yaml", ".yml") + + var objects []client.Object + for _, file := range files { + + if file.IsDir() { + continue + } + // Only parse allowlisted file types + if !resourceExtensions.Has(filepath.Ext(file.Name())) { + continue + } + + // Unmarshal Webhooks from file into structs + docs, err := readDocuments(filepath.Join(path, file.Name())) + if err != nil { + return nil, err + } + + for _, doc := range docs { + obj, err := runtime.Decode(decoder, doc) + if err != nil { + return nil, err + } + clientObj, ok := obj.(client.Object) + if !ok { + return nil, fmt.Errorf("%T does not implement client.Object", obj) + } + if namespaceName != "" { + clientObj.SetNamespace(namespaceName) + } + + objects = append(objects, clientObj) + } + } + return objects, nil + +} + +// readDocuments reads documents from file +func readDocuments(fp string) ([][]byte, error) { + b, err := os.ReadFile(fp) + if err != nil { + return nil, err + } + + var docs [][]byte + reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(b))) + for { + // Read document + doc, err := reader.Read() + if err != nil { + if err == io.EOF { + break + } + + return nil, err + } + + docs = append(docs, doc) + } + + return docs, nil +} diff --git a/vendor/go.uber.org/mock/gomock/call.go b/vendor/go.uber.org/mock/gomock/call.go new file mode 100644 index 00000000..98881596 --- /dev/null +++ b/vendor/go.uber.org/mock/gomock/call.go @@ -0,0 +1,471 @@ +// Copyright 2010 Google Inc. +// +// 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 gomock + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +// Call represents an expected call to a mock. +type Call struct { + t TestHelper // for triggering test failures on invalid call setup + + receiver interface{} // the receiver of the method call + method string // the name of the method + methodType reflect.Type // the type of the method + args []Matcher // the args + origin string // file and line number of call setup + + preReqs []*Call // prerequisite calls + + // Expectations + minCalls, maxCalls int + + numCalls int // actual number made + + // actions are called when this Call is called. Each action gets the args and + // can set the return values by returning a non-nil slice. Actions run in the + // order they are created. + actions []func([]interface{}) []interface{} +} + +// newCall creates a *Call. It requires the method type in order to support +// unexported methods. +func newCall(t TestHelper, receiver interface{}, method string, methodType reflect.Type, args ...interface{}) *Call { + t.Helper() + + // TODO: check arity, types. + mArgs := make([]Matcher, len(args)) + for i, arg := range args { + if m, ok := arg.(Matcher); ok { + mArgs[i] = m + } else if arg == nil { + // Handle nil specially so that passing a nil interface value + // will match the typed nils of concrete args. + mArgs[i] = Nil() + } else { + mArgs[i] = Eq(arg) + } + } + + // callerInfo's skip should be updated if the number of calls between the user's test + // and this line changes, i.e. this code is wrapped in another anonymous function. + // 0 is us, 1 is RecordCallWithMethodType(), 2 is the generated recorder, and 3 is the user's test. + origin := callerInfo(3) + actions := []func([]interface{}) []interface{}{func([]interface{}) []interface{} { + // Synthesize the zero value for each of the return args' types. + rets := make([]interface{}, methodType.NumOut()) + for i := 0; i < methodType.NumOut(); i++ { + rets[i] = reflect.Zero(methodType.Out(i)).Interface() + } + return rets + }} + return &Call{t: t, receiver: receiver, method: method, methodType: methodType, + args: mArgs, origin: origin, minCalls: 1, maxCalls: 1, actions: actions} +} + +// AnyTimes allows the expectation to be called 0 or more times +func (c *Call) AnyTimes() *Call { + c.minCalls, c.maxCalls = 0, 1e8 // close enough to infinity + return c +} + +// MinTimes requires the call to occur at least n times. If AnyTimes or MaxTimes have not been called or if MaxTimes +// was previously called with 1, MinTimes also sets the maximum number of calls to infinity. +func (c *Call) MinTimes(n int) *Call { + c.minCalls = n + if c.maxCalls == 1 { + c.maxCalls = 1e8 + } + return c +} + +// MaxTimes limits the number of calls to n times. If AnyTimes or MinTimes have not been called or if MinTimes was +// previously called with 1, MaxTimes also sets the minimum number of calls to 0. +func (c *Call) MaxTimes(n int) *Call { + c.maxCalls = n + if c.minCalls == 1 { + c.minCalls = 0 + } + return c +} + +// DoAndReturn declares the action to run when the call is matched. +// The return values from this function are returned by the mocked function. +// It takes an interface{} argument to support n-arity functions. +// The anonymous function must match the function signature mocked method. +func (c *Call) DoAndReturn(f interface{}) *Call { + // TODO: Check arity and types here, rather than dying badly elsewhere. + v := reflect.ValueOf(f) + + c.addAction(func(args []interface{}) []interface{} { + c.t.Helper() + ft := v.Type() + if c.methodType.NumIn() != ft.NumIn() { + if ft.IsVariadic() { + c.t.Fatalf("wrong number of arguments in DoAndReturn func for %T.%v The function signature must match the mocked method, a variadic function cannot be used.", + c.receiver, c.method) + } else { + c.t.Fatalf("wrong number of arguments in DoAndReturn func for %T.%v: got %d, want %d [%s]", + c.receiver, c.method, ft.NumIn(), c.methodType.NumIn(), c.origin) + } + return nil + } + vArgs := make([]reflect.Value, len(args)) + for i := 0; i < len(args); i++ { + if args[i] != nil { + vArgs[i] = reflect.ValueOf(args[i]) + } else { + // Use the zero value for the arg. + vArgs[i] = reflect.Zero(ft.In(i)) + } + } + vRets := v.Call(vArgs) + rets := make([]interface{}, len(vRets)) + for i, ret := range vRets { + rets[i] = ret.Interface() + } + return rets + }) + return c +} + +// Do declares the action to run when the call is matched. The function's +// return values are ignored to retain backward compatibility. To use the +// return values call DoAndReturn. +// It takes an interface{} argument to support n-arity functions. +// The anonymous function must match the function signature mocked method. +func (c *Call) Do(f interface{}) *Call { + // TODO: Check arity and types here, rather than dying badly elsewhere. + v := reflect.ValueOf(f) + + c.addAction(func(args []interface{}) []interface{} { + c.t.Helper() + ft := v.Type() + if c.methodType.NumIn() != ft.NumIn() { + if ft.IsVariadic() { + c.t.Fatalf("wrong number of arguments in Do func for %T.%v The function signature must match the mocked method, a variadic function cannot be used.", + c.receiver, c.method) + } else { + c.t.Fatalf("wrong number of arguments in Do func for %T.%v: got %d, want %d [%s]", + c.receiver, c.method, ft.NumIn(), c.methodType.NumIn(), c.origin) + } + return nil + } + vArgs := make([]reflect.Value, len(args)) + for i := 0; i < len(args); i++ { + if args[i] != nil { + vArgs[i] = reflect.ValueOf(args[i]) + } else { + // Use the zero value for the arg. + vArgs[i] = reflect.Zero(ft.In(i)) + } + } + v.Call(vArgs) + return nil + }) + return c +} + +// Return declares the values to be returned by the mocked function call. +func (c *Call) Return(rets ...interface{}) *Call { + c.t.Helper() + + mt := c.methodType + if len(rets) != mt.NumOut() { + c.t.Fatalf("wrong number of arguments to Return for %T.%v: got %d, want %d [%s]", + c.receiver, c.method, len(rets), mt.NumOut(), c.origin) + } + for i, ret := range rets { + if got, want := reflect.TypeOf(ret), mt.Out(i); got == want { + // Identical types; nothing to do. + } else if got == nil { + // Nil needs special handling. + switch want.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + // ok + default: + c.t.Fatalf("argument %d to Return for %T.%v is nil, but %v is not nillable [%s]", + i, c.receiver, c.method, want, c.origin) + } + } else if got.AssignableTo(want) { + // Assignable type relation. Make the assignment now so that the generated code + // can return the values with a type assertion. + v := reflect.New(want).Elem() + v.Set(reflect.ValueOf(ret)) + rets[i] = v.Interface() + } else { + c.t.Fatalf("wrong type of argument %d to Return for %T.%v: %v is not assignable to %v [%s]", + i, c.receiver, c.method, got, want, c.origin) + } + } + + c.addAction(func([]interface{}) []interface{} { + return rets + }) + + return c +} + +// Times declares the exact number of times a function call is expected to be executed. +func (c *Call) Times(n int) *Call { + c.minCalls, c.maxCalls = n, n + return c +} + +// SetArg declares an action that will set the nth argument's value, +// indirected through a pointer. Or, in the case of a slice and map, SetArg +// will copy value's elements/key-value pairs into the nth argument. +func (c *Call) SetArg(n int, value interface{}) *Call { + c.t.Helper() + + mt := c.methodType + // TODO: This will break on variadic methods. + // We will need to check those at invocation time. + if n < 0 || n >= mt.NumIn() { + c.t.Fatalf("SetArg(%d, ...) called for a method with %d args [%s]", + n, mt.NumIn(), c.origin) + } + // Permit setting argument through an interface. + // In the interface case, we don't (nay, can't) check the type here. + at := mt.In(n) + switch at.Kind() { + case reflect.Ptr: + dt := at.Elem() + if vt := reflect.TypeOf(value); !vt.AssignableTo(dt) { + c.t.Fatalf("SetArg(%d, ...) argument is a %v, not assignable to %v [%s]", + n, vt, dt, c.origin) + } + case reflect.Interface: + // nothing to do + case reflect.Slice: + // nothing to do + case reflect.Map: + // nothing to do + default: + c.t.Fatalf("SetArg(%d, ...) referring to argument of non-pointer non-interface non-slice non-map type %v [%s]", + n, at, c.origin) + } + + c.addAction(func(args []interface{}) []interface{} { + v := reflect.ValueOf(value) + switch reflect.TypeOf(args[n]).Kind() { + case reflect.Slice: + setSlice(args[n], v) + case reflect.Map: + setMap(args[n], v) + default: + reflect.ValueOf(args[n]).Elem().Set(v) + } + return nil + }) + return c +} + +// isPreReq returns true if other is a direct or indirect prerequisite to c. +func (c *Call) isPreReq(other *Call) bool { + for _, preReq := range c.preReqs { + if other == preReq || preReq.isPreReq(other) { + return true + } + } + return false +} + +// After declares that the call may only match after preReq has been exhausted. +func (c *Call) After(preReq *Call) *Call { + c.t.Helper() + + if c == preReq { + c.t.Fatalf("A call isn't allowed to be its own prerequisite") + } + if preReq.isPreReq(c) { + c.t.Fatalf("Loop in call order: %v is a prerequisite to %v (possibly indirectly).", c, preReq) + } + + c.preReqs = append(c.preReqs, preReq) + return c +} + +// Returns true if the minimum number of calls have been made. +func (c *Call) satisfied() bool { + return c.numCalls >= c.minCalls +} + +// Returns true if the maximum number of calls have been made. +func (c *Call) exhausted() bool { + return c.numCalls >= c.maxCalls +} + +func (c *Call) String() string { + args := make([]string, len(c.args)) + for i, arg := range c.args { + args[i] = arg.String() + } + arguments := strings.Join(args, ", ") + return fmt.Sprintf("%T.%v(%s) %s", c.receiver, c.method, arguments, c.origin) +} + +// Tests if the given call matches the expected call. +// If yes, returns nil. If no, returns error with message explaining why it does not match. +func (c *Call) matches(args []interface{}) error { + if !c.methodType.IsVariadic() { + if len(args) != len(c.args) { + return fmt.Errorf("expected call at %s has the wrong number of arguments. Got: %d, want: %d", + c.origin, len(args), len(c.args)) + } + + for i, m := range c.args { + if !m.Matches(args[i]) { + return fmt.Errorf( + "expected call at %s doesn't match the argument at index %d.\nGot: %v\nWant: %v", + c.origin, i, formatGottenArg(m, args[i]), m, + ) + } + } + } else { + if len(c.args) < c.methodType.NumIn()-1 { + return fmt.Errorf("expected call at %s has the wrong number of matchers. Got: %d, want: %d", + c.origin, len(c.args), c.methodType.NumIn()-1) + } + if len(c.args) != c.methodType.NumIn() && len(args) != len(c.args) { + return fmt.Errorf("expected call at %s has the wrong number of arguments. Got: %d, want: %d", + c.origin, len(args), len(c.args)) + } + if len(args) < len(c.args)-1 { + return fmt.Errorf("expected call at %s has the wrong number of arguments. Got: %d, want: greater than or equal to %d", + c.origin, len(args), len(c.args)-1) + } + + for i, m := range c.args { + if i < c.methodType.NumIn()-1 { + // Non-variadic args + if !m.Matches(args[i]) { + return fmt.Errorf("expected call at %s doesn't match the argument at index %s.\nGot: %v\nWant: %v", + c.origin, strconv.Itoa(i), formatGottenArg(m, args[i]), m) + } + continue + } + // The last arg has a possibility of a variadic argument, so let it branch + + // sample: Foo(a int, b int, c ...int) + if i < len(c.args) && i < len(args) { + if m.Matches(args[i]) { + // Got Foo(a, b, c) want Foo(matcherA, matcherB, gomock.Any()) + // Got Foo(a, b, c) want Foo(matcherA, matcherB, someSliceMatcher) + // Got Foo(a, b, c) want Foo(matcherA, matcherB, matcherC) + // Got Foo(a, b) want Foo(matcherA, matcherB) + // Got Foo(a, b, c, d) want Foo(matcherA, matcherB, matcherC, matcherD) + continue + } + } + + // The number of actual args don't match the number of matchers, + // or the last matcher is a slice and the last arg is not. + // If this function still matches it is because the last matcher + // matches all the remaining arguments or the lack of any. + // Convert the remaining arguments, if any, into a slice of the + // expected type. + vArgsType := c.methodType.In(c.methodType.NumIn() - 1) + vArgs := reflect.MakeSlice(vArgsType, 0, len(args)-i) + for _, arg := range args[i:] { + vArgs = reflect.Append(vArgs, reflect.ValueOf(arg)) + } + if m.Matches(vArgs.Interface()) { + // Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, gomock.Any()) + // Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, someSliceMatcher) + // Got Foo(a, b) want Foo(matcherA, matcherB, gomock.Any()) + // Got Foo(a, b) want Foo(matcherA, matcherB, someEmptySliceMatcher) + break + } + // Wrong number of matchers or not match. Fail. + // Got Foo(a, b) want Foo(matcherA, matcherB, matcherC, matcherD) + // Got Foo(a, b, c) want Foo(matcherA, matcherB, matcherC, matcherD) + // Got Foo(a, b, c, d) want Foo(matcherA, matcherB, matcherC, matcherD, matcherE) + // Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, matcherC, matcherD) + // Got Foo(a, b, c) want Foo(matcherA, matcherB) + + return fmt.Errorf("expected call at %s doesn't match the argument at index %s.\nGot: %v\nWant: %v", + c.origin, strconv.Itoa(i), formatGottenArg(m, args[i:]), c.args[i]) + } + } + + // Check that all prerequisite calls have been satisfied. + for _, preReqCall := range c.preReqs { + if !preReqCall.satisfied() { + return fmt.Errorf("expected call at %s doesn't have a prerequisite call satisfied:\n%v\nshould be called before:\n%v", + c.origin, preReqCall, c) + } + } + + // Check that the call is not exhausted. + if c.exhausted() { + return fmt.Errorf("expected call at %s has already been called the max number of times", c.origin) + } + + return nil +} + +// dropPrereqs tells the expected Call to not re-check prerequisite calls any +// longer, and to return its current set. +func (c *Call) dropPrereqs() (preReqs []*Call) { + preReqs = c.preReqs + c.preReqs = nil + return +} + +func (c *Call) call() []func([]interface{}) []interface{} { + c.numCalls++ + return c.actions +} + +// InOrder declares that the given calls should occur in order. +func InOrder(calls ...*Call) { + for i := 1; i < len(calls); i++ { + calls[i].After(calls[i-1]) + } +} + +func setSlice(arg interface{}, v reflect.Value) { + va := reflect.ValueOf(arg) + for i := 0; i < v.Len(); i++ { + va.Index(i).Set(v.Index(i)) + } +} + +func setMap(arg interface{}, v reflect.Value) { + va := reflect.ValueOf(arg) + for _, e := range va.MapKeys() { + va.SetMapIndex(e, reflect.Value{}) + } + for _, e := range v.MapKeys() { + va.SetMapIndex(e, v.MapIndex(e)) + } +} + +func (c *Call) addAction(action func([]interface{}) []interface{}) { + c.actions = append(c.actions, action) +} + +func formatGottenArg(m Matcher, arg interface{}) string { + got := fmt.Sprintf("%v (%T)", arg, arg) + if gs, ok := m.(GotFormatter); ok { + got = gs.Got(arg) + } + return got +} diff --git a/vendor/go.uber.org/mock/gomock/callset.go b/vendor/go.uber.org/mock/gomock/callset.go new file mode 100644 index 00000000..f2131a14 --- /dev/null +++ b/vendor/go.uber.org/mock/gomock/callset.go @@ -0,0 +1,164 @@ +// Copyright 2011 Google Inc. +// +// 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 gomock + +import ( + "bytes" + "errors" + "fmt" + "sync" +) + +// callSet represents a set of expected calls, indexed by receiver and method +// name. +type callSet struct { + // Calls that are still expected. + expected map[callSetKey][]*Call + expectedMu *sync.Mutex + // Calls that have been exhausted. + exhausted map[callSetKey][]*Call + // when set to true, existing call expectations are overridden when new call expectations are made + allowOverride bool +} + +// callSetKey is the key in the maps in callSet +type callSetKey struct { + receiver interface{} + fname string +} + +func newCallSet() *callSet { + return &callSet{ + expected: make(map[callSetKey][]*Call), + expectedMu: &sync.Mutex{}, + exhausted: make(map[callSetKey][]*Call), + } +} + +func newOverridableCallSet() *callSet { + return &callSet{ + expected: make(map[callSetKey][]*Call), + expectedMu: &sync.Mutex{}, + exhausted: make(map[callSetKey][]*Call), + allowOverride: true, + } +} + +// Add adds a new expected call. +func (cs callSet) Add(call *Call) { + key := callSetKey{call.receiver, call.method} + + cs.expectedMu.Lock() + defer cs.expectedMu.Unlock() + + m := cs.expected + if call.exhausted() { + m = cs.exhausted + } + if cs.allowOverride { + m[key] = make([]*Call, 0) + } + + m[key] = append(m[key], call) +} + +// Remove removes an expected call. +func (cs callSet) Remove(call *Call) { + key := callSetKey{call.receiver, call.method} + + cs.expectedMu.Lock() + defer cs.expectedMu.Unlock() + + calls := cs.expected[key] + for i, c := range calls { + if c == call { + // maintain order for remaining calls + cs.expected[key] = append(calls[:i], calls[i+1:]...) + cs.exhausted[key] = append(cs.exhausted[key], call) + break + } + } +} + +// FindMatch searches for a matching call. Returns error with explanation message if no call matched. +func (cs callSet) FindMatch(receiver interface{}, method string, args []interface{}) (*Call, error) { + key := callSetKey{receiver, method} + + cs.expectedMu.Lock() + defer cs.expectedMu.Unlock() + + // Search through the expected calls. + expected := cs.expected[key] + var callsErrors bytes.Buffer + for _, call := range expected { + err := call.matches(args) + if err != nil { + _, _ = fmt.Fprintf(&callsErrors, "\n%v", err) + } else { + return call, nil + } + } + + // If we haven't found a match then search through the exhausted calls so we + // get useful error messages. + exhausted := cs.exhausted[key] + for _, call := range exhausted { + if err := call.matches(args); err != nil { + _, _ = fmt.Fprintf(&callsErrors, "\n%v", err) + continue + } + _, _ = fmt.Fprintf( + &callsErrors, "all expected calls for method %q have been exhausted", method, + ) + } + + if len(expected)+len(exhausted) == 0 { + _, _ = fmt.Fprintf(&callsErrors, "there are no expected calls of the method %q for that receiver", method) + } + + return nil, errors.New(callsErrors.String()) +} + +// Failures returns the calls that are not satisfied. +func (cs callSet) Failures() []*Call { + cs.expectedMu.Lock() + defer cs.expectedMu.Unlock() + + failures := make([]*Call, 0, len(cs.expected)) + for _, calls := range cs.expected { + for _, call := range calls { + if !call.satisfied() { + failures = append(failures, call) + } + } + } + return failures +} + +// Satisfied returns true in case all expected calls in this callSet are satisfied. +func (cs callSet) Satisfied() bool { + cs.expectedMu.Lock() + defer cs.expectedMu.Unlock() + + for _, calls := range cs.expected { + for _, call := range calls { + if !call.satisfied() { + return false + } + } + } + + return true +} diff --git a/vendor/go.uber.org/mock/gomock/controller.go b/vendor/go.uber.org/mock/gomock/controller.go new file mode 100644 index 00000000..de904c8c --- /dev/null +++ b/vendor/go.uber.org/mock/gomock/controller.go @@ -0,0 +1,324 @@ +// Copyright 2010 Google Inc. +// +// 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 gomock + +import ( + "context" + "fmt" + "reflect" + "runtime" + "sync" +) + +// A TestReporter is something that can be used to report test failures. It +// is satisfied by the standard library's *testing.T. +type TestReporter interface { + Errorf(format string, args ...interface{}) + Fatalf(format string, args ...interface{}) +} + +// TestHelper is a TestReporter that has the Helper method. It is satisfied +// by the standard library's *testing.T. +type TestHelper interface { + TestReporter + Helper() +} + +// cleanuper is used to check if TestHelper also has the `Cleanup` method. A +// common pattern is to pass in a `*testing.T` to +// `NewController(t TestReporter)`. In Go 1.14+, `*testing.T` has a cleanup +// method. This can be utilized to call `Finish()` so the caller of this library +// does not have to. +type cleanuper interface { + Cleanup(func()) +} + +// A Controller represents the top-level control of a mock ecosystem. It +// defines the scope and lifetime of mock objects, as well as their +// expectations. It is safe to call Controller's methods from multiple +// goroutines. Each test should create a new Controller and invoke Finish via +// defer. +// +// func TestFoo(t *testing.T) { +// ctrl := gomock.NewController(t) +// defer ctrl.Finish() +// // .. +// } +// +// func TestBar(t *testing.T) { +// t.Run("Sub-Test-1", st) { +// ctrl := gomock.NewController(st) +// defer ctrl.Finish() +// // .. +// }) +// t.Run("Sub-Test-2", st) { +// ctrl := gomock.NewController(st) +// defer ctrl.Finish() +// // .. +// }) +// }) +type Controller struct { + // T should only be called within a generated mock. It is not intended to + // be used in user code and may be changed in future versions. T is the + // TestReporter passed in when creating the Controller via NewController. + // If the TestReporter does not implement a TestHelper it will be wrapped + // with a nopTestHelper. + T TestHelper + mu sync.Mutex + expectedCalls *callSet + finished bool +} + +// NewController returns a new Controller. It is the preferred way to create a +// Controller. +// +// New in go1.14+, if you are passing a *testing.T into this function you no +// longer need to call ctrl.Finish() in your test methods. +func NewController(t TestReporter, opts ...ControllerOption) *Controller { + h, ok := t.(TestHelper) + if !ok { + h = &nopTestHelper{t} + } + ctrl := &Controller{ + T: h, + expectedCalls: newCallSet(), + } + for _, opt := range opts { + opt.apply(ctrl) + } + if c, ok := isCleanuper(ctrl.T); ok { + c.Cleanup(func() { + ctrl.T.Helper() + ctrl.finish(true, nil) + }) + } + + return ctrl +} + +// ControllerOption configures how a Controller should behave. +type ControllerOption interface { + apply(*Controller) +} + +type overridableExpectationsOption struct{} + +// WithOverridableExpectations allows for overridable call expectations +// i.e., subsequent call expectations override existing call expectations +func WithOverridableExpectations() overridableExpectationsOption { + return overridableExpectationsOption{} +} + +func (o overridableExpectationsOption) apply(ctrl *Controller) { + ctrl.expectedCalls = newOverridableCallSet() +} + +type cancelReporter struct { + t TestHelper + cancel func() +} + +func (r *cancelReporter) Errorf(format string, args ...interface{}) { + r.t.Errorf(format, args...) +} +func (r *cancelReporter) Fatalf(format string, args ...interface{}) { + defer r.cancel() + r.t.Fatalf(format, args...) +} + +func (r *cancelReporter) Helper() { + r.t.Helper() +} + +// WithContext returns a new Controller and a Context, which is cancelled on any +// fatal failure. +func WithContext(ctx context.Context, t TestReporter) (*Controller, context.Context) { + h, ok := t.(TestHelper) + if !ok { + h = &nopTestHelper{t: t} + } + + ctx, cancel := context.WithCancel(ctx) + return NewController(&cancelReporter{t: h, cancel: cancel}), ctx +} + +type nopTestHelper struct { + t TestReporter +} + +func (h *nopTestHelper) Errorf(format string, args ...interface{}) { + h.t.Errorf(format, args...) +} +func (h *nopTestHelper) Fatalf(format string, args ...interface{}) { + h.t.Fatalf(format, args...) +} + +func (h nopTestHelper) Helper() {} + +// RecordCall is called by a mock. It should not be called by user code. +func (ctrl *Controller) RecordCall(receiver interface{}, method string, args ...interface{}) *Call { + ctrl.T.Helper() + + recv := reflect.ValueOf(receiver) + for i := 0; i < recv.Type().NumMethod(); i++ { + if recv.Type().Method(i).Name == method { + return ctrl.RecordCallWithMethodType(receiver, method, recv.Method(i).Type(), args...) + } + } + ctrl.T.Fatalf("gomock: failed finding method %s on %T", method, receiver) + panic("unreachable") +} + +// RecordCallWithMethodType is called by a mock. It should not be called by user code. +func (ctrl *Controller) RecordCallWithMethodType(receiver interface{}, method string, methodType reflect.Type, args ...interface{}) *Call { + ctrl.T.Helper() + + call := newCall(ctrl.T, receiver, method, methodType, args...) + + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + ctrl.expectedCalls.Add(call) + + return call +} + +// Call is called by a mock. It should not be called by user code. +func (ctrl *Controller) Call(receiver interface{}, method string, args ...interface{}) []interface{} { + ctrl.T.Helper() + + // Nest this code so we can use defer to make sure the lock is released. + actions := func() []func([]interface{}) []interface{} { + ctrl.T.Helper() + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + + expected, err := ctrl.expectedCalls.FindMatch(receiver, method, args) + if err != nil { + // callerInfo's skip should be updated if the number of calls between the user's test + // and this line changes, i.e. this code is wrapped in another anonymous function. + // 0 is us, 1 is controller.Call(), 2 is the generated mock, and 3 is the user's test. + origin := callerInfo(3) + ctrl.T.Fatalf("Unexpected call to %T.%v(%v) at %s because: %s", receiver, method, args, origin, err) + } + + // Two things happen here: + // * the matching call no longer needs to check prerequite calls, + // * and the prerequite calls are no longer expected, so remove them. + preReqCalls := expected.dropPrereqs() + for _, preReqCall := range preReqCalls { + ctrl.expectedCalls.Remove(preReqCall) + } + + actions := expected.call() + if expected.exhausted() { + ctrl.expectedCalls.Remove(expected) + } + return actions + }() + + var rets []interface{} + for _, action := range actions { + if r := action(args); r != nil { + rets = r + } + } + + return rets +} + +// Finish checks to see if all the methods that were expected to be called +// were called. It should be invoked for each Controller. It is not idempotent +// and therefore can only be invoked once. +// +// New in go1.14+, if you are passing a *testing.T into NewController function you no +// longer need to call ctrl.Finish() in your test methods. +func (ctrl *Controller) Finish() { + // If we're currently panicking, probably because this is a deferred call. + // This must be recovered in the deferred function. + err := recover() + ctrl.finish(false, err) +} + +// Satisfied returns whether all expected calls bound to this Controller have been satisfied. +// Calling Finish is then guaranteed to not fail due to missing calls. +func (ctrl *Controller) Satisfied() bool { + return ctrl.expectedCalls.Satisfied() +} + +func (ctrl *Controller) finish(cleanup bool, panicErr interface{}) { + ctrl.T.Helper() + + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + + if ctrl.finished { + if _, ok := isCleanuper(ctrl.T); !ok { + ctrl.T.Fatalf("Controller.Finish was called more than once. It has to be called exactly once.") + } + return + } + ctrl.finished = true + + // Short-circuit, pass through the panic. + if panicErr != nil { + panic(panicErr) + } + + // Check that all remaining expected calls are satisfied. + failures := ctrl.expectedCalls.Failures() + for _, call := range failures { + ctrl.T.Errorf("missing call(s) to %v", call) + } + if len(failures) != 0 { + if !cleanup { + ctrl.T.Fatalf("aborting test due to missing call(s)") + return + } + ctrl.T.Errorf("aborting test due to missing call(s)") + } +} + +// callerInfo returns the file:line of the call site. skip is the number +// of stack frames to skip when reporting. 0 is callerInfo's call site. +func callerInfo(skip int) string { + if _, file, line, ok := runtime.Caller(skip + 1); ok { + return fmt.Sprintf("%s:%d", file, line) + } + return "unknown file" +} + +// isCleanuper checks it if t's base TestReporter has a Cleanup method. +func isCleanuper(t TestReporter) (cleanuper, bool) { + tr := unwrapTestReporter(t) + c, ok := tr.(cleanuper) + return c, ok +} + +// unwrapTestReporter unwraps TestReporter to the base implementation. +func unwrapTestReporter(t TestReporter) TestReporter { + tr := t + switch nt := t.(type) { + case *cancelReporter: + tr = nt.t + if h, check := tr.(*nopTestHelper); check { + tr = h.t + } + case *nopTestHelper: + tr = nt.t + default: + // not wrapped + } + return tr +} diff --git a/vendor/go.uber.org/mock/gomock/doc.go b/vendor/go.uber.org/mock/gomock/doc.go new file mode 100644 index 00000000..f1a304fb --- /dev/null +++ b/vendor/go.uber.org/mock/gomock/doc.go @@ -0,0 +1,60 @@ +// Copyright 2022 Google LLC +// +// 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 gomock is a mock framework for Go. +// +// Standard usage: +// +// (1) Define an interface that you wish to mock. +// type MyInterface interface { +// SomeMethod(x int64, y string) +// } +// (2) Use mockgen to generate a mock from the interface. +// (3) Use the mock in a test: +// func TestMyThing(t *testing.T) { +// mockCtrl := gomock.NewController(t)// +// mockObj := something.NewMockMyInterface(mockCtrl) +// mockObj.EXPECT().SomeMethod(4, "blah") +// // pass mockObj to a real object and play with it. +// } +// +// By default, expected calls are not enforced to run in any particular order. +// Call order dependency can be enforced by use of InOrder and/or Call.After. +// Call.After can create more varied call order dependencies, but InOrder is +// often more convenient. +// +// The following examples create equivalent call order dependencies. +// +// Example of using Call.After to chain expected call order: +// +// firstCall := mockObj.EXPECT().SomeMethod(1, "first") +// secondCall := mockObj.EXPECT().SomeMethod(2, "second").After(firstCall) +// mockObj.EXPECT().SomeMethod(3, "third").After(secondCall) +// +// Example of using InOrder to declare expected call order: +// +// gomock.InOrder( +// mockObj.EXPECT().SomeMethod(1, "first"), +// mockObj.EXPECT().SomeMethod(2, "second"), +// mockObj.EXPECT().SomeMethod(3, "third"), +// ) +// +// The standard TestReporter most users will pass to `NewController` is a +// `*testing.T` from the context of the test. Note that this will use the +// standard `t.Error` and `t.Fatal` methods to report what happened in the test. +// In some cases this can leave your testing package in a weird state if global +// state is used since `t.Fatal` is like calling panic in the middle of a +// function. In these cases it is recommended that you pass in your own +// `TestReporter`. +package gomock diff --git a/vendor/go.uber.org/mock/gomock/matchers.go b/vendor/go.uber.org/mock/gomock/matchers.go new file mode 100644 index 00000000..6d5eff4f --- /dev/null +++ b/vendor/go.uber.org/mock/gomock/matchers.go @@ -0,0 +1,346 @@ +// Copyright 2010 Google Inc. +// +// 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 gomock + +import ( + "fmt" + "reflect" + "strings" +) + +// A Matcher is a representation of a class of values. +// It is used to represent the valid or expected arguments to a mocked method. +type Matcher interface { + // Matches returns whether x is a match. + Matches(x interface{}) bool + + // String describes what the matcher matches. + String() string +} + +// WantFormatter modifies the given Matcher's String() method to the given +// Stringer. This allows for control on how the "Want" is formatted when +// printing . +func WantFormatter(s fmt.Stringer, m Matcher) Matcher { + type matcher interface { + Matches(x interface{}) bool + } + + return struct { + matcher + fmt.Stringer + }{ + matcher: m, + Stringer: s, + } +} + +// StringerFunc type is an adapter to allow the use of ordinary functions as +// a Stringer. If f is a function with the appropriate signature, +// StringerFunc(f) is a Stringer that calls f. +type StringerFunc func() string + +// String implements fmt.Stringer. +func (f StringerFunc) String() string { + return f() +} + +// GotFormatter is used to better print failure messages. If a matcher +// implements GotFormatter, it will use the result from Got when printing +// the failure message. +type GotFormatter interface { + // Got is invoked with the received value. The result is used when + // printing the failure message. + Got(got interface{}) string +} + +// GotFormatterFunc type is an adapter to allow the use of ordinary +// functions as a GotFormatter. If f is a function with the appropriate +// signature, GotFormatterFunc(f) is a GotFormatter that calls f. +type GotFormatterFunc func(got interface{}) string + +// Got implements GotFormatter. +func (f GotFormatterFunc) Got(got interface{}) string { + return f(got) +} + +// GotFormatterAdapter attaches a GotFormatter to a Matcher. +func GotFormatterAdapter(s GotFormatter, m Matcher) Matcher { + return struct { + GotFormatter + Matcher + }{ + GotFormatter: s, + Matcher: m, + } +} + +type anyMatcher struct{} + +func (anyMatcher) Matches(interface{}) bool { + return true +} + +func (anyMatcher) String() string { + return "is anything" +} + +type eqMatcher struct { + x interface{} +} + +func (e eqMatcher) Matches(x interface{}) bool { + // In case, some value is nil + if e.x == nil || x == nil { + return reflect.DeepEqual(e.x, x) + } + + // Check if types assignable and convert them to common type + x1Val := reflect.ValueOf(e.x) + x2Val := reflect.ValueOf(x) + + if x1Val.Type().AssignableTo(x2Val.Type()) { + x1ValConverted := x1Val.Convert(x2Val.Type()) + return reflect.DeepEqual(x1ValConverted.Interface(), x2Val.Interface()) + } + + return false +} + +func (e eqMatcher) String() string { + return fmt.Sprintf("is equal to %v (%T)", e.x, e.x) +} + +type nilMatcher struct{} + +func (nilMatcher) Matches(x interface{}) bool { + if x == nil { + return true + } + + v := reflect.ValueOf(x) + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, + reflect.Ptr, reflect.Slice: + return v.IsNil() + } + + return false +} + +func (nilMatcher) String() string { + return "is nil" +} + +type notMatcher struct { + m Matcher +} + +func (n notMatcher) Matches(x interface{}) bool { + return !n.m.Matches(x) +} + +func (n notMatcher) String() string { + return "not(" + n.m.String() + ")" +} + +type assignableToTypeOfMatcher struct { + targetType reflect.Type +} + +func (m assignableToTypeOfMatcher) Matches(x interface{}) bool { + return reflect.TypeOf(x).AssignableTo(m.targetType) +} + +func (m assignableToTypeOfMatcher) String() string { + return "is assignable to " + m.targetType.Name() +} + +type allMatcher struct { + matchers []Matcher +} + +func (am allMatcher) Matches(x interface{}) bool { + for _, m := range am.matchers { + if !m.Matches(x) { + return false + } + } + return true +} + +func (am allMatcher) String() string { + ss := make([]string, 0, len(am.matchers)) + for _, matcher := range am.matchers { + ss = append(ss, matcher.String()) + } + return strings.Join(ss, "; ") +} + +type lenMatcher struct { + i int +} + +func (m lenMatcher) Matches(x interface{}) bool { + v := reflect.ValueOf(x) + switch v.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == m.i + default: + return false + } +} + +func (m lenMatcher) String() string { + return fmt.Sprintf("has length %d", m.i) +} + +type inAnyOrderMatcher struct { + x interface{} +} + +func (m inAnyOrderMatcher) Matches(x interface{}) bool { + given, ok := m.prepareValue(x) + if !ok { + return false + } + wanted, ok := m.prepareValue(m.x) + if !ok { + return false + } + + if given.Len() != wanted.Len() { + return false + } + + usedFromGiven := make([]bool, given.Len()) + foundFromWanted := make([]bool, wanted.Len()) + for i := 0; i < wanted.Len(); i++ { + wantedMatcher := Eq(wanted.Index(i).Interface()) + for j := 0; j < given.Len(); j++ { + if usedFromGiven[j] { + continue + } + if wantedMatcher.Matches(given.Index(j).Interface()) { + foundFromWanted[i] = true + usedFromGiven[j] = true + break + } + } + } + + missingFromWanted := 0 + for _, found := range foundFromWanted { + if !found { + missingFromWanted++ + } + } + extraInGiven := 0 + for _, used := range usedFromGiven { + if !used { + extraInGiven++ + } + } + + return extraInGiven == 0 && missingFromWanted == 0 +} + +func (m inAnyOrderMatcher) prepareValue(x interface{}) (reflect.Value, bool) { + xValue := reflect.ValueOf(x) + switch xValue.Kind() { + case reflect.Slice, reflect.Array: + return xValue, true + default: + return reflect.Value{}, false + } +} + +func (m inAnyOrderMatcher) String() string { + return fmt.Sprintf("has the same elements as %v", m.x) +} + +// Constructors + +// All returns a composite Matcher that returns true if and only all of the +// matchers return true. +func All(ms ...Matcher) Matcher { return allMatcher{ms} } + +// Any returns a matcher that always matches. +func Any() Matcher { return anyMatcher{} } + +// Eq returns a matcher that matches on equality. +// +// Example usage: +// +// Eq(5).Matches(5) // returns true +// Eq(5).Matches(4) // returns false +func Eq(x interface{}) Matcher { return eqMatcher{x} } + +// Len returns a matcher that matches on length. This matcher returns false if +// is compared to a type that is not an array, chan, map, slice, or string. +func Len(i int) Matcher { + return lenMatcher{i} +} + +// Nil returns a matcher that matches if the received value is nil. +// +// Example usage: +// +// var x *bytes.Buffer +// Nil().Matches(x) // returns true +// x = &bytes.Buffer{} +// Nil().Matches(x) // returns false +func Nil() Matcher { return nilMatcher{} } + +// Not reverses the results of its given child matcher. +// +// Example usage: +// +// Not(Eq(5)).Matches(4) // returns true +// Not(Eq(5)).Matches(5) // returns false +func Not(x interface{}) Matcher { + if m, ok := x.(Matcher); ok { + return notMatcher{m} + } + return notMatcher{Eq(x)} +} + +// AssignableToTypeOf is a Matcher that matches if the parameter to the mock +// function is assignable to the type of the parameter to this function. +// +// Example usage: +// +// var s fmt.Stringer = &bytes.Buffer{} +// AssignableToTypeOf(s).Matches(time.Second) // returns true +// AssignableToTypeOf(s).Matches(99) // returns false +// +// var ctx = reflect.TypeOf((*context.Context)(nil)).Elem() +// AssignableToTypeOf(ctx).Matches(context.Background()) // returns true +func AssignableToTypeOf(x interface{}) Matcher { + if xt, ok := x.(reflect.Type); ok { + return assignableToTypeOfMatcher{xt} + } + return assignableToTypeOfMatcher{reflect.TypeOf(x)} +} + +// InAnyOrder is a Matcher that returns true for collections of the same elements ignoring the order. +// +// Example usage: +// +// InAnyOrder([]int{1, 2, 3}).Matches([]int{1, 3, 2}) // returns true +// InAnyOrder([]int{1, 2, 3}).Matches([]int{1, 2}) // returns false +func InAnyOrder(x interface{}) Matcher { + return inAnyOrderMatcher{x} +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 5b57cb4d..41b97e4c 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -145,6 +145,7 @@ github.com/gardener/gardener/pkg/gardenlet/apis/config github.com/gardener/gardener/pkg/gardenlet/apis/config/v1alpha1 github.com/gardener/gardener/pkg/healthz github.com/gardener/gardener/pkg/logger +github.com/gardener/gardener/pkg/mock/controller-runtime/client github.com/gardener/gardener/pkg/resourcemanager/controller/garbagecollector/references github.com/gardener/gardener/pkg/utils github.com/gardener/gardener/pkg/utils/chart @@ -159,8 +160,11 @@ github.com/gardener/gardener/pkg/utils/kubernetes/unstructured github.com/gardener/gardener/pkg/utils/managedresources github.com/gardener/gardener/pkg/utils/managedresources/builder github.com/gardener/gardener/pkg/utils/retry +github.com/gardener/gardener/pkg/utils/retry/fake github.com/gardener/gardener/pkg/utils/secrets github.com/gardener/gardener/pkg/utils/secrets/manager +github.com/gardener/gardener/pkg/utils/test +github.com/gardener/gardener/pkg/utils/test/matchers github.com/gardener/gardener/pkg/utils/timewindow github.com/gardener/gardener/pkg/utils/validation/kubernetesversion github.com/gardener/gardener/pkg/utils/version @@ -439,6 +443,7 @@ github.com/xi2/xz go.uber.org/atomic # go.uber.org/mock v0.2.0 ## explicit; go 1.19 +go.uber.org/mock/gomock go.uber.org/mock/mockgen go.uber.org/mock/mockgen/model # go.uber.org/multierr v1.7.0