diff --git a/cmd/cluster-cloud-controller-manager-operator/main.go b/cmd/cluster-cloud-controller-manager-operator/main.go index d7dcb700d..f65b0ffd0 100644 --- a/cmd/cluster-cloud-controller-manager-operator/main.go +++ b/cmd/cluster-cloud-controller-manager-operator/main.go @@ -23,6 +23,7 @@ import ( // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/klog/klogr" "k8s.io/klog/v2" @@ -49,8 +50,10 @@ var ( ) const ( - defaultManagedNamespace = "openshift-cloud-controller-manager" - defaultImagesLocation = "/etc/cloud-controller-manager-config/images.json" + defaultManagedNamespace = "openshift-cloud-controller-manager" + defaultImagesLocation = "/etc/cloud-controller-manager-config/images.json" + releaseVersionEnvVariableName = "RELEASE_VERSION" + unknownVersionValue = "unknown" ) func init() { @@ -133,6 +136,8 @@ func main() { if err = (&controllers.CloudOperatorReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("cloud-controller-manager-operator"), + ReleaseVersion: getReleaseVersion(), ManagedNamespace: *managedNamespace, ImagesFile: *imagesFile, }).SetupWithManager(mgr); err != nil { @@ -156,3 +161,12 @@ func main() { os.Exit(1) } } + +func getReleaseVersion() string { + releaseVersion := os.Getenv(releaseVersionEnvVariableName) + if len(releaseVersion) == 0 { + releaseVersion = unknownVersionValue + klog.Infof("%s environment variable is missing, defaulting to %q", releaseVersionEnvVariableName, unknownVersionValue) + } + return releaseVersion +} diff --git a/controllers/cache.go b/controllers/cache.go index b03405897..c675c0023 100644 --- a/controllers/cache.go +++ b/controllers/cache.go @@ -26,7 +26,7 @@ type ObjectWatcher interface { func NewObjectWatcher(opts WatcherOptions) (ObjectWatcher, error) { if opts.Cache == nil { - return nil, errors.New("Cache is required") + return nil, errors.New("cache is required") } // Use the default Kubernetes Scheme if unset @@ -128,6 +128,7 @@ func (e *eventToChannelHandler) queueEventForObject(oldObj, newObj interface{}) } if new.GetName() != e.name { // Not the right object, skip + return } if oldObj != nil { diff --git a/controllers/clusteroperator_controller.go b/controllers/clusteroperator_controller.go index b1a027514..9c62a19dc 100644 --- a/controllers/clusteroperator_controller.go +++ b/controllers/clusteroperator_controller.go @@ -19,15 +19,15 @@ package controllers import ( "context" "fmt" - "os" configv1 "github.com/openshift/api/config/v1" "github.com/openshift/cluster-cloud-controller-manager-operator/pkg/cloud" "github.com/openshift/cluster-cloud-controller-manager-operator/pkg/cloud/openstack" - "k8s.io/apimachinery/pkg/api/equality" + "github.com/openshift/library-go/pkg/cloudprovider" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -37,27 +37,20 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" - - "github.com/openshift/library-go/pkg/cloudprovider" - "github.com/openshift/library-go/pkg/config/clusteroperator/v1helpers" ) const ( - clusterOperatorName = "cloud-controller-manager" - reasonAsExpected = "AsExpected" - unknownVersionValue = "unknown" - operatorVersionKey = "operator" - releaseVersionEnvVariableName = "RELEASE_VERSION" - infrastructureName = "cluster" - externalFeatureGateName = "cluster" - defaultManagementNamespace = "openshift-cloud-controller-manager-operator" + infrastructureName = "cluster" + externalFeatureGateName = "cluster" ) // CloudOperatorReconciler reconciles a ClusterOperator object type CloudOperatorReconciler struct { client.Client Scheme *runtime.Scheme + Recorder record.EventRecorder watcher ObjectWatcher + ReleaseVersion string ManagedNamespace string ImagesFile string } @@ -71,14 +64,19 @@ func (r *CloudOperatorReconciler) Reconcile(ctx context.Context, _ ctrl.Request) featureGate := &configv1.FeatureGate{} if err := r.Get(ctx, client.ObjectKey{Name: externalFeatureGateName}, featureGate); errors.IsNotFound(err) { klog.Infof("FeatureGate cluster does not exist. Skipping...") - if err := r.statusAvailable(ctx); err != nil { + + if err := r.setStatusAvailable(ctx); err != nil { klog.Errorf("Unable to sync cluster operator status: %s", err) return ctrl.Result{}, err } - return ctrl.Result{}, nil } else if err != nil { klog.Errorf("Unable to retrive FeatureGate object: %v", err) + + if err := r.setStatusDegraded(ctx, err); err != nil { + klog.Errorf("Error syncing ClusterOperatorStatus: %v", err) + return ctrl.Result{}, fmt.Errorf("error syncing ClusterOperatorStatus: %v", err) + } return ctrl.Result{}, err } @@ -86,7 +84,7 @@ func (r *CloudOperatorReconciler) Reconcile(ctx context.Context, _ ctrl.Request) if err := r.Get(ctx, client.ObjectKey{Name: infrastructureName}, infra); errors.IsNotFound(err) { klog.Infof("Infrastructure cluster does not exist. Skipping...") - if err := r.statusAvailable(ctx); err != nil { + if err := r.setStatusAvailable(ctx); err != nil { klog.Errorf("Unable to sync cluster operator status: %s", err) return ctrl.Result{}, err } @@ -94,6 +92,11 @@ func (r *CloudOperatorReconciler) Reconcile(ctx context.Context, _ ctrl.Request) return ctrl.Result{}, nil } else if err != nil { klog.Errorf("Unable to retrive Infrastructure object: %v", err) + + if err := r.setStatusDegraded(ctx, err); err != nil { + klog.Errorf("Error syncing ClusterOperatorStatus: %v", err) + return ctrl.Result{}, fmt.Errorf("error syncing ClusterOperatorStatus: %v", err) + } return ctrl.Result{}, err } @@ -102,6 +105,10 @@ func (r *CloudOperatorReconciler) Reconcile(ctx context.Context, _ ctrl.Request) klog.Errorf("Unable to determine platform from infrastructure: %s", err) // Ignoring error here as infrastructure resource needs to be reconciled externally // to provide correct platform + if err := r.setStatusAvailable(ctx); err != nil { + klog.Errorf("Unable to sync cluster operator status: %s", err) + return ctrl.Result{}, err + } return ctrl.Result{}, nil } @@ -109,11 +116,16 @@ func (r *CloudOperatorReconciler) Reconcile(ctx context.Context, _ ctrl.Request) external, err := cloudprovider.IsCloudProviderExternal(platform, featureGate) if err != nil { klog.Errorf("Could not determine external cloud provider state: %v", err) + + if err := r.setStatusDegraded(ctx, err); err != nil { + klog.Errorf("Error syncing ClusterOperatorStatus: %v", err) + return ctrl.Result{}, fmt.Errorf("error syncing ClusterOperatorStatus: %v", err) + } return ctrl.Result{}, err } else if !external { klog.Infof("FeatureGate cluster is not specifying external cloud provider requirement. Skipping...") - if err := r.statusAvailable(ctx); err != nil { + if err := r.setStatusAvailable(ctx); err != nil { klog.Errorf("Unable to sync cluster operator status: %s", err) return ctrl.Result{}, err } @@ -124,19 +136,23 @@ func (r *CloudOperatorReconciler) Reconcile(ctx context.Context, _ ctrl.Request) config, err := r.composeConfig(platform) if err != nil { klog.Errorf("Unable to build operator config %s", err) + if err := r.setStatusDegraded(ctx, err); err != nil { + klog.Errorf("Error syncing ClusterOperatorStatus: %v", err) + return ctrl.Result{}, fmt.Errorf("error syncing ClusterOperatorStatus: %v", err) + } return ctrl.Result{}, err } - // Deploy resources for platform - templates := getResources(platform) - resources := fillConfigValues(config, templates) - - if err := r.sync(ctx, resources); err != nil { + if err := r.sync(ctx, config); err != nil { klog.Errorf("Unable to sync operands: %s", err) + if err := r.setStatusDegraded(ctx, err); err != nil { + klog.Errorf("Error syncing ClusterOperatorStatus: %v", err) + return ctrl.Result{}, fmt.Errorf("error syncing ClusterOperatorStatus: %v", err) + } return ctrl.Result{}, err } - if err := r.statusAvailable(ctx); err != nil { + if err := r.setStatusAvailable(ctx); err != nil { klog.Errorf("Unable to sync cluster operator status: %s", err) return ctrl.Result{}, err } @@ -144,38 +160,64 @@ func (r *CloudOperatorReconciler) Reconcile(ctx context.Context, _ ctrl.Request) return ctrl.Result{}, nil } -func (r *CloudOperatorReconciler) relatedObjects() []configv1.ObjectReference { - // TBD: Add an actual set of object references from getResources method - return []configv1.ObjectReference{ - {Resource: "namespaces", Name: defaultManagementNamespace}, - {Group: configv1.GroupName, Resource: "clusteroperators", Name: clusterOperatorName}, - {Resource: "namespaces", Name: r.ManagedNamespace}, +func (r *CloudOperatorReconciler) sync(ctx context.Context, config operatorConfig) error { + // Deploy resources for platform + templates := getResources(config.Platform) + resources := fillConfigValues(config, templates) + + updated, err := r.applyResources(ctx, resources) + if err != nil { + return err } + if updated { + return r.setStatusProgressing(ctx) + } + + return nil } -func (r *CloudOperatorReconciler) sync(ctx context.Context, resources []client.Object) error { +// applyResources will apply all resources as is to the cluster with +// server-side apply patch and will enforce all the conflicts +func (r *CloudOperatorReconciler) applyResources(ctx context.Context, resources []client.Object) (bool, error) { + updated := false + for _, resource := range resources { - if err := applyServerSide(ctx, r.Client, clusterOperatorName, resource); err != nil { - klog.Errorf("Unable to apply object %T '%s': %+v", resource, resource.GetName(), err) - return err + resourceExisting := resource.DeepCopyObject().(client.Object) + err := r.Get(ctx, client.ObjectKeyFromObject(resourceExisting), resourceExisting) + if errors.IsNotFound(err) { + klog.Infof("Resource %s %q needs to be created, operator progressing...", resource.GetObjectKind().GroupVersionKind(), client.ObjectKeyFromObject(resource)) + updated = true + } else if err != nil { + r.Recorder.Event(resource, corev1.EventTypeWarning, "Update failed", err.Error()) + return false, err + } + + resourceUpdated := resource.DeepCopyObject().(client.Object) + if err := r.Patch(ctx, resourceUpdated, client.Apply, client.ForceOwnership, client.FieldOwner(clusterOperatorName)); err != nil { + klog.Errorf("Unable to apply object %s '%s': %+v", resource.GetObjectKind().GroupVersionKind(), resource.GetName(), err) + r.Recorder.Event(resourceExisting, corev1.EventTypeWarning, "Update failed", err.Error()) + return false, err + } + klog.V(2).Infof("Applied %s %q successfully", resource.GetObjectKind().GroupVersionKind(), client.ObjectKeyFromObject(resource)) + + if resourceExisting.GetGeneration() != resourceUpdated.GetGeneration() { + klog.Infof("Resource %s %q generation increased, resource updated, operator progressing...", resource.GetObjectKind().GroupVersionKind(), client.ObjectKeyFromObject(resource)) + updated = true + r.Recorder.Event(resourceExisting, corev1.EventTypeNormal, "Updated successfully", "Resource was successfully updated") } if err := r.watcher.Watch(ctx, resource); err != nil { - klog.Errorf("Unable to establish watch on object %T '%s': %+v", resource, resource.GetName(), err) - return err + klog.Errorf("Unable to establish watch on object %s '%s': %+v", resource.GetObjectKind().GroupVersionKind(), resource.GetName(), err) + r.Recorder.Event(resourceExisting, corev1.EventTypeWarning, "Establish watch failed", err.Error()) + return false, err } } if len(resources) > 0 { - klog.Info("Resources applied successfully.") + klog.V(2).Info("Resources applied successfully.") } - return nil -} - -func applyServerSide(ctx context.Context, c client.Client, owner client.FieldOwner, obj client.Object, opts ...client.PatchOption) error { - opts = append([]client.PatchOption{client.ForceOwnership, owner}, opts...) - return c.Patch(ctx, obj, client.Apply, opts...) + return updated, nil } func getResources(platform configv1.PlatformType) []client.Object { @@ -191,101 +233,6 @@ func getResources(platform configv1.PlatformType) []client.Object { return nil } -func (r *CloudOperatorReconciler) composeConfig(platform configv1.PlatformType) (operatorConfig, error) { - config := operatorConfig{} - - images, err := getImagesFromJSONFile(r.ImagesFile) - if err != nil { - klog.Errorf("Unable to decode images file from location %s", r.ImagesFile, err) - return config, err - } - - config.ControllerImage = getProviderControllerFromImages(platform, images) - config.ManagedNamespace = r.ManagedNamespace - - return config, nil -} - -// statusAvailable sets the Available condition to True, with the given reason -// and message, and sets both the Progressing and Degraded conditions to False. -func (r *CloudOperatorReconciler) statusAvailable(ctx context.Context) error { - co, err := r.getOrCreateClusterOperator(ctx) - if err != nil { - return err - } - - conds := []configv1.ClusterOperatorStatusCondition{ - { - Type: configv1.OperatorAvailable, - Status: configv1.ConditionTrue, - LastTransitionTime: metav1.Now(), - Reason: reasonAsExpected, - Message: fmt.Sprintf("Cluster Cloud Controller Manager Operator is available at %s", getReleaseVersion()), - }, - { - Type: configv1.OperatorDegraded, - Status: configv1.ConditionFalse, - LastTransitionTime: metav1.Now(), - Reason: reasonAsExpected, - Message: "", - }, - { - Type: configv1.OperatorProgressing, - Status: configv1.ConditionFalse, - LastTransitionTime: metav1.Now(), - Reason: reasonAsExpected, - Message: "", - }, - { - Type: configv1.OperatorUpgradeable, - Status: configv1.ConditionTrue, - LastTransitionTime: metav1.Now(), - Reason: reasonAsExpected, - Message: "", - }, - } - - co.Status.Versions = []configv1.OperandVersion{{Name: operatorVersionKey, Version: getReleaseVersion()}} - return r.syncStatus(ctx, co, conds) -} - -func (r *CloudOperatorReconciler) getOrCreateClusterOperator(ctx context.Context) (*configv1.ClusterOperator, error) { - co := &configv1.ClusterOperator{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterOperatorName, - }, - Status: configv1.ClusterOperatorStatus{}, - } - err := r.Get(ctx, client.ObjectKey{Name: clusterOperatorName}, co) - if errors.IsNotFound(err) { - klog.Infof("ClusterOperator does not exist, creating a new one.") - - err = r.Create(ctx, co) - if err != nil { - return nil, fmt.Errorf("failed to create cluster operator: %v", err) - } - return co, nil - } - - if err != nil { - return nil, fmt.Errorf("failed to get clusterOperator %q: %v", clusterOperatorName, err) - } - return co, nil -} - -//syncStatus applies the new condition to the ClusterOperator object. -func (r *CloudOperatorReconciler) syncStatus(ctx context.Context, co *configv1.ClusterOperator, conds []configv1.ClusterOperatorStatusCondition) error { - for _, c := range conds { - v1helpers.SetStatusCondition(&co.Status.Conditions, c) - } - - if !equality.Semantic.DeepEqual(co.Status.RelatedObjects, r.relatedObjects()) { - co.Status.RelatedObjects = r.relatedObjects() - } - - return r.Status().Update(ctx, co) -} - // SetupWithManager sets up the controller with the Manager. func (r *CloudOperatorReconciler) SetupWithManager(mgr ctrl.Manager) error { watcher, err := NewObjectWatcher(WatcherOptions{ @@ -357,12 +304,3 @@ func featureGatePredicates() predicate.Funcs { DeleteFunc: func(e event.DeleteEvent) bool { return isFeatureGateCluster(e.Object) }, } } - -func getReleaseVersion() string { - releaseVersion := os.Getenv(releaseVersionEnvVariableName) - if len(releaseVersion) == 0 { - releaseVersion = unknownVersionValue - klog.Infof("%s environment variable is missing, defaulting to %q", releaseVersionEnvVariableName, unknownVersionValue) - } - return releaseVersion -} diff --git a/controllers/clusteroperator_controller_test.go b/controllers/clusteroperator_controller_test.go index 86a26a82d..5cb6bedb7 100644 --- a/controllers/clusteroperator_controller_test.go +++ b/controllers/clusteroperator_controller_test.go @@ -2,9 +2,6 @@ package controllers import ( "context" - "io/ioutil" - "os" - "testing" "time" . "github.com/onsi/ginkgo" @@ -15,11 +12,15 @@ import ( openstack "github.com/openshift/cluster-cloud-controller-manager-operator/pkg/cloud/openstack" "github.com/openshift/library-go/pkg/cloudprovider" "github.com/openshift/library-go/pkg/config/clusteroperator/v1helpers" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/record" + "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -40,13 +41,13 @@ var _ = Describe("Cluster Operator status controller", func() { Client: cl, Scheme: scheme.Scheme, ManagedNamespace: defaultManagementNamespace, + Recorder: record.NewFakeRecorder(32), } operator = &configv1.ClusterOperator{} operator.SetName(clusterOperatorName) }) AfterEach(func() { - os.Unsetenv(releaseVersionEnvVariableName) co := &configv1.ClusterOperator{} err := cl.Get(context.Background(), client.ObjectKey{Name: clusterOperatorName}, co) if err == nil || !apierrors.IsNotFound(err) { @@ -66,14 +67,12 @@ var _ = Describe("Cluster Operator status controller", func() { DescribeTable("should ensure Cluster Operator status is present", func(tc testCase) { - expectedVersion := unknownVersionValue + expectedVersion := "unknown" - By("Setting the release version", func() { - if tc.releaseVersionEnvVariableValue != "" { - expectedVersion = tc.releaseVersionEnvVariableValue - Expect(os.Setenv(releaseVersionEnvVariableName, tc.releaseVersionEnvVariableValue)).To(Succeed()) - } - }) + if tc.releaseVersionEnvVariableValue != "" { + expectedVersion = tc.releaseVersionEnvVariableValue + operatorController.ReleaseVersion = tc.releaseVersionEnvVariableValue + } if tc.namespace != "" { operatorController.ManagedNamespace = tc.namespace @@ -104,19 +103,19 @@ var _ = Describe("Cluster Operator status controller", func() { // check conditions. Expect(v1helpers.IsStatusConditionTrue(getOp.Status.Conditions, configv1.OperatorAvailable)).To(BeTrue()) - Expect(v1helpers.FindStatusCondition(getOp.Status.Conditions, configv1.OperatorAvailable).Reason).To(Equal(reasonAsExpected)) + Expect(v1helpers.FindStatusCondition(getOp.Status.Conditions, configv1.OperatorAvailable).Reason).To(Equal(ReasonAsExpected)) Expect(v1helpers.IsStatusConditionTrue(getOp.Status.Conditions, configv1.OperatorUpgradeable)).To(BeTrue()) - Expect(v1helpers.FindStatusCondition(getOp.Status.Conditions, configv1.OperatorUpgradeable).Reason).To(Equal(reasonAsExpected)) + Expect(v1helpers.FindStatusCondition(getOp.Status.Conditions, configv1.OperatorUpgradeable).Reason).To(Equal(ReasonAsExpected)) Expect(v1helpers.IsStatusConditionFalse(getOp.Status.Conditions, configv1.OperatorDegraded)).To(BeTrue()) - Expect(v1helpers.FindStatusCondition(getOp.Status.Conditions, configv1.OperatorDegraded).Reason).To(Equal(reasonAsExpected)) + Expect(v1helpers.FindStatusCondition(getOp.Status.Conditions, configv1.OperatorDegraded).Reason).To(Equal(ReasonAsExpected)) Expect(v1helpers.IsStatusConditionFalse(getOp.Status.Conditions, configv1.OperatorProgressing)).To(BeTrue()) - Expect(v1helpers.FindStatusCondition(getOp.Status.Conditions, configv1.OperatorProgressing).Reason).To(Equal(reasonAsExpected)) + Expect(v1helpers.FindStatusCondition(getOp.Status.Conditions, configv1.OperatorProgressing).Reason).To(Equal(ReasonAsExpected)) // check related objects. Expect(getOp.Status.RelatedObjects).To(Equal(operatorController.relatedObjects())) }, Entry("when there's no existing cluster operator nor release version", testCase{ - releaseVersionEnvVariableValue: "", + releaseVersionEnvVariableValue: "unknown", existingCO: nil, }), Entry("when there's no existing cluster operator but there's release version", testCase{ @@ -240,6 +239,7 @@ var _ = Describe("Component sync controller", func() { watcher: w, ManagedNamespace: testManagementNamespace, ImagesFile: testImagesFilePath, + Recorder: record.NewFakeRecorder(32), } originalWatcher, _ := w.(*objectWatcher) watcher = mockedWatcher{watcher: originalWatcher} @@ -365,84 +365,104 @@ var _ = Describe("Component sync controller", func() { ) }) -func TestComposeConfig(t *testing.T) { - tc := []struct { - name string - namespace string - platform configv1.PlatformType - imagesContent string - expectConfig operatorConfig - expectError bool - }{{ - name: "Unmarshal images from file for AWS", - namespace: defaultManagementNamespace, - platform: configv1.AWSPlatformType, - imagesContent: `{ - "cloudControllerManagerAWS": "registry.ci.openshift.org/openshift:aws-cloud-controller-manager", - "cloudControllerManagerOpenStack": "registry.ci.openshift.org/openshift:openstack-cloud-controller-manager" -}`, - expectConfig: operatorConfig{ - ControllerImage: "registry.ci.openshift.org/openshift:aws-cloud-controller-manager", - ManagedNamespace: defaultManagementNamespace, - }, - }, { - name: "Unmarshal images from file for OpenStack", - namespace: defaultManagementNamespace, - platform: configv1.OpenStackPlatformType, - imagesContent: `{ - "cloudControllerManagerAWS": "registry.ci.openshift.org/openshift:aws-cloud-controller-manager", - "cloudControllerManagerOpenStack": "registry.ci.openshift.org/openshift:openstack-cloud-controller-manager" -}`, - expectConfig: operatorConfig{ - ControllerImage: "registry.ci.openshift.org/openshift:openstack-cloud-controller-manager", - ManagedNamespace: defaultManagementNamespace, - }, - }, { - name: "Unmarshal images from file for unknown platform returns nothing", - namespace: "otherNamespace", - platform: configv1.NonePlatformType, - imagesContent: `{ - "cloudControllerManagerAWS": "registry.ci.openshift.org/openshift:aws-cloud-controller-manager", - "cloudControllerManagerOpenStack": "registry.ci.openshift.org/openshift:openstack-cloud-controller-manager" -}`, - expectConfig: operatorConfig{ - ControllerImage: "", - ManagedNamespace: "otherNamespace", - }, - }, { - name: "Broken JSON is rejected", - imagesContent: `{ - "cloudControllerManagerAWS": BAD, -}`, - expectError: true, - }} - - for _, tc := range tc { - t.Run(tc.name, func(t *testing.T) { - file, err := ioutil.TempFile(os.TempDir(), "images") - path := file.Name() - if err != nil { - t.Fatal(err) - } - defer file.Close() +var _ = Describe("Apply resources should", func() { + var resources []client.Object + var reconciler *CloudOperatorReconciler + var recorder *record.FakeRecorder - _, err = file.WriteString(tc.imagesContent) - if err != nil { - t.Fatal(err) - } + BeforeEach(func() { + c, err := cache.New(cfg, cache.Options{}) + Expect(err).To(Succeed()) + w, err := NewObjectWatcher(WatcherOptions{Cache: c}) + Expect(err).To(Succeed()) - r := &CloudOperatorReconciler{ - ImagesFile: path, - ManagedNamespace: tc.namespace, - } - config, err := r.composeConfig(tc.platform) - if isErr := err != nil; isErr != tc.expectError { - t.Fatalf("Unexpected error result: %v", err) - } + ns := &corev1.Namespace{} + ns.SetName("cluster-cloud-controller-manager") + + resources = []client.Object{} + if !apierrors.IsNotFound(cl.Get(context.TODO(), client.ObjectKeyFromObject(ns), ns.DeepCopy())) { + Expect(cl.Create(context.TODO(), ns.DeepCopy())).ShouldNot(HaveOccurred()) + } + + recorder = record.NewFakeRecorder(32) + reconciler = &CloudOperatorReconciler{ + Client: cl, + Scheme: scheme.Scheme, + Recorder: recorder, + watcher: w, + } - if !equality.Semantic.DeepEqual(config, tc.expectConfig) { - t.Errorf("Config is not equal:\n%v\nexpected\n%v", config, tc.expectConfig) + }) + + It("Expect update when resources are not found", func() { + resources = append(resources, cloud.GetAWSResources()...) + + updated, err := reconciler.applyResources(context.TODO(), resources) + Expect(err).ShouldNot(HaveOccurred()) + Expect(updated).To(BeTrue()) + Eventually(recorder.Events).Should(Receive(ContainSubstring("Resource was successfully updated"))) + }) + + It("Expect update when deployment generation have changed", func() { + var dep *appsv1.Deployment + for _, res := range cloud.GetAWSResources() { + if deployment, ok := res.(*appsv1.Deployment); ok { + dep = deployment + break } - }) - } -} + } + resources = append(resources, dep) + + updated, err := reconciler.applyResources(context.TODO(), resources) + Expect(err).ShouldNot(HaveOccurred()) + Expect(updated).To(BeTrue()) + Eventually(recorder.Events).Should(Receive(ContainSubstring("Resource was successfully updated"))) + + dep.Spec.Replicas = pointer.Int32Ptr(20) + + updated, err = reconciler.applyResources(context.TODO(), resources) + Expect(err).ShouldNot(HaveOccurred()) + Expect(updated).To(BeTrue()) + Eventually(recorder.Events).Should(Receive(ContainSubstring("Resource was successfully updated"))) + + // No update as resource didn't change + updated, err = reconciler.applyResources(context.TODO(), resources) + Expect(err).ShouldNot(HaveOccurred()) + Expect(updated).To(BeFalse()) + }) + + It("Expect error when object requsted is incorrect", func() { + objects := cloud.GetAWSResources() + objects[0].SetNamespace("non-existent") + + updated, err := reconciler.applyResources(context.TODO(), objects) + Expect(err).Should(HaveOccurred()) + Expect(updated).To(BeFalse()) + Eventually(recorder.Events).Should(Receive(ContainSubstring("Update failed"))) + }) + + It("Expect no update when resources are applied twice", func() { + resources = append(resources, openstack.GetResources()...) + + updated, err := reconciler.applyResources(context.TODO(), resources) + Expect(err).ShouldNot(HaveOccurred()) + Expect(updated).To(BeTrue()) + Eventually(recorder.Events).Should(Receive(ContainSubstring("Resource was successfully updated"))) + + updated, err = reconciler.applyResources(context.TODO(), resources) + Expect(err).ShouldNot(HaveOccurred()) + Expect(updated).To(BeFalse()) + }) + + AfterEach(func() { + for _, operand := range resources { + Expect(cl.Delete(context.Background(), operand)).To(Succeed()) + + Eventually(func() bool { + return apierrors.IsNotFound(cl.Get(context.Background(), client.ObjectKeyFromObject(operand), operand)) + }, timeout).Should(BeTrue()) + } + Consistently(recorder.Events).ShouldNot(Receive()) + }) + +}) diff --git a/controllers/config.go b/controllers/config.go index c20b2d2bf..daad3a399 100644 --- a/controllers/config.go +++ b/controllers/config.go @@ -7,6 +7,7 @@ import ( "path/filepath" configv1 "github.com/openshift/api/config/v1" + "k8s.io/klog/v2" ) // imagesReference allows build systems to inject imagesReference for CCCMO components @@ -19,6 +20,7 @@ type imagesReference struct { type operatorConfig struct { ManagedNamespace string ControllerImage string + Platform configv1.PlatformType } func getProviderFromInfrastructure(infra *configv1.Infrastructure) (configv1.PlatformType, error) { @@ -55,3 +57,20 @@ func getProviderControllerFromImages(platform configv1.PlatformType, images imag return "" } } + +func (r *CloudOperatorReconciler) composeConfig(platform configv1.PlatformType) (operatorConfig, error) { + config := operatorConfig{ + Platform: platform, + ManagedNamespace: r.ManagedNamespace, + } + + images, err := getImagesFromJSONFile(r.ImagesFile) + if err != nil { + klog.Errorf("Unable to decode images file from location %s", r.ImagesFile, err) + return config, err + } + + config.ControllerImage = getProviderControllerFromImages(platform, images) + + return config, nil +} diff --git a/controllers/config_test.go b/controllers/config_test.go index 70ca6fbb3..e33d25c20 100644 --- a/controllers/config_test.go +++ b/controllers/config_test.go @@ -7,6 +7,7 @@ import ( configv1 "github.com/openshift/api/config/v1" "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/client-go/tools/record" ) func TestGetImagesFromJSONFile(t *testing.T) { @@ -181,3 +182,89 @@ func TestGetProviderControllerFromImages(t *testing.T) { }) } } + +func TestComposeConfig(t *testing.T) { + tc := []struct { + name string + namespace string + platform configv1.PlatformType + imagesContent string + expectConfig operatorConfig + expectError bool + }{{ + name: "Unmarshal images from file for AWS", + namespace: defaultManagementNamespace, + platform: configv1.AWSPlatformType, + imagesContent: `{ + "cloudControllerManagerAWS": "registry.ci.openshift.org/openshift:aws-cloud-controller-manager", + "cloudControllerManagerOpenStack": "registry.ci.openshift.org/openshift:openstack-cloud-controller-manager" +}`, + expectConfig: operatorConfig{ + ControllerImage: "registry.ci.openshift.org/openshift:aws-cloud-controller-manager", + ManagedNamespace: defaultManagementNamespace, + Platform: configv1.AWSPlatformType, + }, + }, { + name: "Unmarshal images from file for OpenStack", + namespace: defaultManagementNamespace, + platform: configv1.OpenStackPlatformType, + imagesContent: `{ + "cloudControllerManagerAWS": "registry.ci.openshift.org/openshift:aws-cloud-controller-manager", + "cloudControllerManagerOpenStack": "registry.ci.openshift.org/openshift:openstack-cloud-controller-manager" +}`, + expectConfig: operatorConfig{ + ControllerImage: "registry.ci.openshift.org/openshift:openstack-cloud-controller-manager", + ManagedNamespace: defaultManagementNamespace, + Platform: configv1.OpenStackPlatformType, + }, + }, { + name: "Unmarshal images from file for unknown platform returns nothing", + namespace: "otherNamespace", + platform: configv1.NonePlatformType, + imagesContent: `{ + "cloudControllerManagerAWS": "registry.ci.openshift.org/openshift:aws-cloud-controller-manager", + "cloudControllerManagerOpenStack": "registry.ci.openshift.org/openshift:openstack-cloud-controller-manager" +}`, + expectConfig: operatorConfig{ + ControllerImage: "", + ManagedNamespace: "otherNamespace", + Platform: configv1.NonePlatformType, + }, + }, { + name: "Broken JSON is rejected", + imagesContent: `{ + "cloudControllerManagerAWS": BAD, +}`, + expectError: true, + }} + + for _, tc := range tc { + t.Run(tc.name, func(t *testing.T) { + file, err := ioutil.TempFile(os.TempDir(), "images") + path := file.Name() + if err != nil { + t.Fatal(err) + } + defer file.Close() + + _, err = file.WriteString(tc.imagesContent) + if err != nil { + t.Fatal(err) + } + + r := &CloudOperatorReconciler{ + ImagesFile: path, + ManagedNamespace: tc.namespace, + Recorder: record.NewFakeRecorder(32), + } + config, err := r.composeConfig(tc.platform) + if isErr := err != nil; isErr != tc.expectError { + t.Fatalf("Unexpected error result: %v", err) + } + + if !equality.Semantic.DeepEqual(config, tc.expectConfig) { + t.Errorf("Config is not equal:\n%v\nexpected\n%v", config, tc.expectConfig) + } + }) + } +} diff --git a/controllers/status.go b/controllers/status.go new file mode 100644 index 000000000..46f4aab23 --- /dev/null +++ b/controllers/status.go @@ -0,0 +1,181 @@ +package controllers + +import ( + "context" + "fmt" + "reflect" + "strings" + + configv1 "github.com/openshift/api/config/v1" + "github.com/openshift/library-go/pkg/config/clusteroperator/v1helpers" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// The default set of status change reasons. +const ( + ReasonAsExpected = "AsExpected" + ReasonInitializing = "Initializing" + ReasonSyncing = "SyncingResources" + ReasonSyncFailed = "SyncingFailed" +) + +const ( + clusterOperatorName = "cloud-controller-manager" + operatorVersionKey = "operator" + defaultManagementNamespace = "openshift-cloud-controller-manager-operator" +) + +// setStatusDegraded sets the Degraded condition to True, with the given reason and +// message, and sets the upgradeable condition. It does not modify any existing +// Available or Progressing conditions. +func (r *CloudOperatorReconciler) setStatusDegraded(ctx context.Context, reconcileErr error) error { + co, err := r.getOrCreateClusterOperator(ctx) + if err != nil { + klog.Errorf("Failed to get or create Cluster Operator: %v", err) + return err + } + + desiredVersions := []configv1.OperandVersion{{Name: operatorVersionKey, Version: r.ReleaseVersion}} + currentVersions := co.Status.Versions + + var message string + if !reflect.DeepEqual(desiredVersions, currentVersions) { + message = fmt.Sprintf("Failed when progressing towards %s because %e", printOperandVersions(desiredVersions), reconcileErr) + } else { + message = fmt.Sprintf("Failed to resync for %s because %e", printOperandVersions(desiredVersions), reconcileErr) + } + + conds := []configv1.ClusterOperatorStatusCondition{ + newClusterOperatorStatusCondition(configv1.OperatorDegraded, configv1.ConditionTrue, + ReasonSyncFailed, message), + newClusterOperatorStatusCondition(configv1.OperatorUpgradeable, configv1.ConditionFalse, ReasonAsExpected, ""), + } + + r.Recorder.Eventf(co, corev1.EventTypeWarning, "Status degraded", reconcileErr.Error()) + klog.V(2).Infof("Syncing status: degraded: %s", message) + return r.syncStatus(ctx, co, conds) +} + +// setStatusProgressing sets the Progressing condition to True, with the given +// reason and message, and sets the upgradeable condition to True. It does not +// modify any existing Available or Degraded conditions. +func (r *CloudOperatorReconciler) setStatusProgressing(ctx context.Context) error { + co, err := r.getOrCreateClusterOperator(ctx) + if err != nil { + klog.Errorf("Failed to get or create Cluster Operator: %v", err) + return err + } + + desiredVersions := []configv1.OperandVersion{{Name: operatorVersionKey, Version: r.ReleaseVersion}} + currentVersions := co.Status.Versions + + var message, reason string + if !reflect.DeepEqual(desiredVersions, currentVersions) { + message = fmt.Sprintf("Progressing towards %s", printOperandVersions(desiredVersions)) + klog.V(2).Infof("Syncing status: %s", message) + r.Recorder.Eventf(co, corev1.EventTypeNormal, "Status upgrade", message) + reason = ReasonSyncing + } else { + klog.V(2).Info("Syncing status: re-syncing") + reason = ReasonAsExpected + } + + conds := []configv1.ClusterOperatorStatusCondition{ + newClusterOperatorStatusCondition(configv1.OperatorProgressing, configv1.ConditionTrue, reason, message), + newClusterOperatorStatusCondition(configv1.OperatorUpgradeable, configv1.ConditionTrue, ReasonAsExpected, ""), + } + + return r.syncStatus(ctx, co, conds) +} + +// setStatusAvailable sets the Available condition to True, with the given reason +// and message, and sets both the Progressing and Degraded conditions to False. +func (r *CloudOperatorReconciler) setStatusAvailable(ctx context.Context) error { + co, err := r.getOrCreateClusterOperator(ctx) + if err != nil { + return err + } + + conds := []configv1.ClusterOperatorStatusCondition{ + newClusterOperatorStatusCondition(configv1.OperatorAvailable, configv1.ConditionTrue, ReasonAsExpected, + fmt.Sprintf("Cluster Cloud Controller Manager Operator is available at %s", r.ReleaseVersion)), + newClusterOperatorStatusCondition(configv1.OperatorProgressing, configv1.ConditionFalse, ReasonAsExpected, ""), + newClusterOperatorStatusCondition(configv1.OperatorDegraded, configv1.ConditionFalse, ReasonAsExpected, ""), + newClusterOperatorStatusCondition(configv1.OperatorUpgradeable, configv1.ConditionTrue, ReasonAsExpected, ""), + } + + co.Status.Versions = []configv1.OperandVersion{{Name: operatorVersionKey, Version: r.ReleaseVersion}} + klog.V(2).Info("Syncing status: available") + return r.syncStatus(ctx, co, conds) +} + +func printOperandVersions(versions []configv1.OperandVersion) string { + versionsOutput := []string{} + for _, operand := range versions { + versionsOutput = append(versionsOutput, fmt.Sprintf("%s: %s", operand.Name, operand.Version)) + } + return strings.Join(versionsOutput, ", ") +} + +func newClusterOperatorStatusCondition(conditionType configv1.ClusterStatusConditionType, + conditionStatus configv1.ConditionStatus, reason string, + message string) configv1.ClusterOperatorStatusCondition { + return configv1.ClusterOperatorStatusCondition{ + Type: conditionType, + Status: conditionStatus, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + } +} + +func (r *CloudOperatorReconciler) getOrCreateClusterOperator(ctx context.Context) (*configv1.ClusterOperator, error) { + co := &configv1.ClusterOperator{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterOperatorName, + }, + Status: configv1.ClusterOperatorStatus{}, + } + err := r.Get(ctx, client.ObjectKey{Name: clusterOperatorName}, co) + if errors.IsNotFound(err) { + klog.Infof("ClusterOperator does not exist, creating a new one.") + + err = r.Create(ctx, co) + if err != nil { + return nil, fmt.Errorf("failed to create cluster operator: %v", err) + } + return co, nil + } + + if err != nil { + return nil, fmt.Errorf("failed to get clusterOperator %q: %v", clusterOperatorName, err) + } + return co, nil +} + +func (r *CloudOperatorReconciler) relatedObjects() []configv1.ObjectReference { + // TBD: Add an actual set of object references from getResources method + return []configv1.ObjectReference{ + {Resource: "namespaces", Name: defaultManagementNamespace}, + {Group: configv1.GroupName, Resource: "clusteroperators", Name: clusterOperatorName}, + {Resource: "namespaces", Name: r.ManagedNamespace}, + } +} + +//syncStatus applies the new condition to the ClusterOperator object. +func (r *CloudOperatorReconciler) syncStatus(ctx context.Context, co *configv1.ClusterOperator, conds []configv1.ClusterOperatorStatusCondition) error { + for _, c := range conds { + v1helpers.SetStatusCondition(&co.Status.Conditions, c) + } + + if !equality.Semantic.DeepEqual(co.Status.RelatedObjects, r.relatedObjects()) { + co.Status.RelatedObjects = r.relatedObjects() + } + + return r.Status().Update(ctx, co) +} diff --git a/controllers/status_test.go b/controllers/status_test.go new file mode 100644 index 000000000..bd2dbd657 --- /dev/null +++ b/controllers/status_test.go @@ -0,0 +1,411 @@ +package controllers + +import ( + "context" + "fmt" + "testing" + "time" + + configv1 "github.com/openshift/api/config/v1" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/openshift/library-go/pkg/config/clusteroperator/v1helpers" +) + +func TestPrintOperandVersions(t *testing.T) { + expectedOutput := "operator: 1.0, controller-manager: 2.0" + got := printOperandVersions([]configv1.OperandVersion{ + { + Name: "operator", + Version: "1.0", + }, + { + Name: "controller-manager", + Version: "2.0", + }, + }) + assert.Equal(t, got, expectedOutput) +} + +func TestOperatorSetStatusProgressing(t *testing.T) { + type tCase struct { + currentVersion []configv1.OperandVersion + desiredVersion string + expectedConditions []configv1.ClusterOperatorStatusCondition + } + tCases := []tCase{ + { + desiredVersion: "1.0", + expectedConditions: []configv1.ClusterOperatorStatusCondition{ + newClusterOperatorStatusCondition(configv1.OperatorProgressing, configv1.ConditionTrue, ReasonSyncing, ""), + newClusterOperatorStatusCondition(configv1.OperatorUpgradeable, configv1.ConditionTrue, ReasonAsExpected, ""), + }, + }, + { + currentVersion: []configv1.OperandVersion{ + { + Name: "operator", + Version: "1.0", + }, + }, + desiredVersion: "1.0", + expectedConditions: []configv1.ClusterOperatorStatusCondition{ + newClusterOperatorStatusCondition(configv1.OperatorProgressing, configv1.ConditionTrue, ReasonSyncing, ""), + newClusterOperatorStatusCondition(configv1.OperatorUpgradeable, configv1.ConditionTrue, ReasonAsExpected, ""), + }, + }, + { + currentVersion: []configv1.OperandVersion{ + { + Name: "operator", + Version: "1.0", + }, + }, + desiredVersion: "2.0", + expectedConditions: []configv1.ClusterOperatorStatusCondition{ + newClusterOperatorStatusCondition(configv1.OperatorProgressing, configv1.ConditionTrue, ReasonSyncing, ""), + newClusterOperatorStatusCondition(configv1.OperatorUpgradeable, configv1.ConditionTrue, ReasonAsExpected, ""), + }, + }, + } + + for i, tc := range tCases { + startTime := metav1.NewTime(time.Now().Add(-time.Second)) + + optr := CloudOperatorReconciler{ + Scheme: scheme.Scheme, + Recorder: record.NewFakeRecorder(32), + ReleaseVersion: tc.desiredVersion, + } + + builder := fake.NewClientBuilder() + if tc.currentVersion != nil { + operator := &configv1.ClusterOperator{} + operator.SetName(clusterOperatorName) + operator.Status.Versions = tc.currentVersion + builder = builder.WithObjects(operator) + } + optr.Client = builder.Build() + + err := optr.setStatusProgressing(context.TODO()) + assert.NoErrorf(t, err, "Failed to set Progressing status on ClusterOperator") + + gotCO, err := optr.getOrCreateClusterOperator(context.TODO()) + assert.NoErrorf(t, err, "Failed to fetch ClusterOperator") + + var condition configv1.ClusterOperatorStatusCondition + for _, coCondition := range gotCO.Status.Conditions { + assert.True(t, + startTime.Before(&coCondition.LastTransitionTime) || startTime.Equal(&coCondition.LastTransitionTime), + "test-case %v expected LastTransitionTime for the status condition %v to be after %v", i, coCondition, startTime) + if coCondition.Type == configv1.OperatorProgressing { + condition = coCondition + break + } + } + + for _, expectedCondition := range tc.expectedConditions { + ok := v1helpers.IsStatusConditionPresentAndEqual( + gotCO.Status.Conditions, expectedCondition.Type, expectedCondition.Status, + ) + if !ok { + t.Errorf("wrong status for condition. Expected: %v, got: %v", + expectedCondition, + v1helpers.FindStatusCondition(gotCO.Status.Conditions, expectedCondition.Type)) + } + } + + assert.True(t, + len(tc.expectedConditions) == len(gotCO.Status.Conditions), + "test-case %v expected equal number of conditions to %v, got %v", i, len(tc.expectedConditions), len(gotCO.Status.Conditions)) + + err = optr.setStatusProgressing(context.TODO()) + assert.NoErrorf(t, err, "Failed to set Progressing status on ClusterOperator") + + err = optr.Client.Get(context.TODO(), client.ObjectKey{Name: clusterOperatorName}, gotCO) + assert.NoErrorf(t, err, "Failed to fetch ClusterOperator") + var conditionAfterAnotherSync configv1.ClusterOperatorStatusCondition + for _, coCondition := range gotCO.Status.Conditions { + if coCondition.Type == configv1.OperatorProgressing { + conditionAfterAnotherSync = coCondition + break + } + } + assert.True(t, condition.LastTransitionTime.Equal(&conditionAfterAnotherSync.LastTransitionTime), "test-case %v expected LastTransitionTime not to be updated if condition state is same", i) + + for _, expectedCondition := range tc.expectedConditions { + ok := v1helpers.IsStatusConditionPresentAndEqual( + gotCO.Status.Conditions, expectedCondition.Type, expectedCondition.Status, + ) + if !ok { + t.Errorf("wrong status for condition. Expected: %v, got: %v", + expectedCondition, + v1helpers.FindStatusCondition(gotCO.Status.Conditions, expectedCondition.Type)) + } + } + + assert.True(t, + len(tc.expectedConditions) == len(gotCO.Status.Conditions), + "test-case %v expected equal number of conditions to %v, got %v", i, len(tc.expectedConditions), len(gotCO.Status.Conditions)) + } +} + +func TestOperatorSetStatusDegraded(t *testing.T) { + type tCase struct { + currentVersion []configv1.OperandVersion + desiredVersion string + expectedConditions []configv1.ClusterOperatorStatusCondition + passErr error + expectErrMessage string + } + tCases := []tCase{ + { + currentVersion: []configv1.OperandVersion{ + { + Name: "operator", + Version: "1.0", + }, + }, + desiredVersion: "1.0", + expectedConditions: []configv1.ClusterOperatorStatusCondition{ + newClusterOperatorStatusCondition(configv1.OperatorDegraded, configv1.ConditionTrue, ReasonSyncFailed, ""), + newClusterOperatorStatusCondition(configv1.OperatorUpgradeable, configv1.ConditionFalse, ReasonAsExpected, ""), + }, + passErr: fmt.Errorf("some failure"), + expectErrMessage: "Failed to resync for operator: 1.0 because &{%!e(string=some failure)}", + }, + { + currentVersion: []configv1.OperandVersion{ + { + Name: "operator", + Version: "1.0", + }, + }, + desiredVersion: "2.0", + expectedConditions: []configv1.ClusterOperatorStatusCondition{ + newClusterOperatorStatusCondition(configv1.OperatorDegraded, configv1.ConditionTrue, ReasonSyncFailed, ""), + newClusterOperatorStatusCondition(configv1.OperatorUpgradeable, configv1.ConditionFalse, ReasonAsExpected, ""), + }, + passErr: fmt.Errorf("some failure"), + expectErrMessage: "Failed when progressing towards operator: 2.0 because &{%!e(string=some failure)}", + }, + } + + for i, tc := range tCases { + startTime := metav1.NewTime(time.Now().Add(-time.Second)) + + optr := CloudOperatorReconciler{ + Scheme: scheme.Scheme, + Recorder: record.NewFakeRecorder(32), + ReleaseVersion: tc.desiredVersion, + } + + builder := fake.NewClientBuilder() + if tc.currentVersion != nil { + operator := &configv1.ClusterOperator{} + operator.SetName(clusterOperatorName) + operator.Status.Versions = tc.currentVersion + builder = builder.WithObjects(operator) + } + optr.Client = builder.Build() + + err := optr.setStatusDegraded(context.TODO(), tc.passErr) + assert.NoErrorf(t, err, "Failed to set Degraded status on ClusterOperator") + + gotCO, err := optr.getOrCreateClusterOperator(context.TODO()) + assert.NoErrorf(t, err, "Failed to fetch ClusterOperator") + + var condition configv1.ClusterOperatorStatusCondition + for _, coCondition := range gotCO.Status.Conditions { + assert.True(t, + startTime.Before(&coCondition.LastTransitionTime) || startTime.Equal(&coCondition.LastTransitionTime), + "test-case %v expected LastTransitionTime for the status condition %v to be after %v", i, coCondition, startTime) + if coCondition.Type == configv1.OperatorDegraded { + condition = coCondition + break + } + } + + assert.True(t, + tc.expectErrMessage == condition.Message, + "test-case %v expected error message to be equal %q, got %q", i, tc.expectErrMessage, condition.Message) + + for _, expectedCondition := range tc.expectedConditions { + ok := v1helpers.IsStatusConditionPresentAndEqual( + gotCO.Status.Conditions, expectedCondition.Type, expectedCondition.Status, + ) + if !ok { + t.Errorf("wrong status for condition. Expected: %v, got: %v", + expectedCondition, + v1helpers.FindStatusCondition(gotCO.Status.Conditions, expectedCondition.Type)) + } + } + + assert.True(t, + len(tc.expectedConditions) == len(gotCO.Status.Conditions), + "test-case %v expected equal number of conditions to %v, got %v", i, len(tc.expectedConditions), len(gotCO.Status.Conditions)) + + err = optr.setStatusDegraded(context.TODO(), tc.passErr) + assert.NoErrorf(t, err, "Failed to set Degraded status on ClusterOperator") + + err = optr.Client.Get(context.TODO(), client.ObjectKey{Name: clusterOperatorName}, gotCO) + assert.NoErrorf(t, err, "Failed to fetch ClusterOperator") + + var conditionAfterAnotherSync configv1.ClusterOperatorStatusCondition + for _, coCondition := range gotCO.Status.Conditions { + if coCondition.Type == configv1.OperatorDegraded { + conditionAfterAnotherSync = coCondition + break + } + } + assert.True(t, condition.LastTransitionTime.Equal(&conditionAfterAnotherSync.LastTransitionTime), "test-case %v expected LastTransitionTime not to be updated if condition state is same", i) + + for _, expectedCondition := range tc.expectedConditions { + ok := v1helpers.IsStatusConditionPresentAndEqual( + gotCO.Status.Conditions, expectedCondition.Type, expectedCondition.Status, + ) + if !ok { + t.Errorf("wrong status for condition. Expected: %v, got: %v", + expectedCondition, + v1helpers.FindStatusCondition(gotCO.Status.Conditions, expectedCondition.Type)) + } + } + + assert.True(t, + len(tc.expectedConditions) == len(gotCO.Status.Conditions), + "test-case %v expected equal number of conditions to %v, got %v", i, len(tc.expectedConditions), len(gotCO.Status.Conditions)) + } +} + +func TestOperatorSetStatusAvailable(t *testing.T) { + type tCase struct { + currentVersion []configv1.OperandVersion + desiredVersion string + expectedConditions []configv1.ClusterOperatorStatusCondition + } + tCases := []tCase{ + { + desiredVersion: "1.0", + expectedConditions: []configv1.ClusterOperatorStatusCondition{ + newClusterOperatorStatusCondition(configv1.OperatorAvailable, configv1.ConditionTrue, ReasonAsExpected, ""), + newClusterOperatorStatusCondition(configv1.OperatorProgressing, configv1.ConditionFalse, ReasonAsExpected, ""), + newClusterOperatorStatusCondition(configv1.OperatorUpgradeable, configv1.ConditionTrue, ReasonAsExpected, ""), + newClusterOperatorStatusCondition(configv1.OperatorDegraded, configv1.ConditionFalse, ReasonAsExpected, ""), + }, + }, + { + currentVersion: []configv1.OperandVersion{ + { + Name: "operator", + Version: "1.0", + }, + }, + desiredVersion: "2.0", + expectedConditions: []configv1.ClusterOperatorStatusCondition{ + newClusterOperatorStatusCondition(configv1.OperatorAvailable, configv1.ConditionTrue, ReasonAsExpected, ""), + newClusterOperatorStatusCondition(configv1.OperatorProgressing, configv1.ConditionFalse, ReasonAsExpected, ""), + newClusterOperatorStatusCondition(configv1.OperatorUpgradeable, configv1.ConditionTrue, ReasonAsExpected, ""), + newClusterOperatorStatusCondition(configv1.OperatorDegraded, configv1.ConditionFalse, ReasonAsExpected, ""), + }, + }, + } + + for i, tc := range tCases { + startTime := metav1.NewTime(time.Now().Add(-time.Second)) + + optr := CloudOperatorReconciler{ + Scheme: scheme.Scheme, + Recorder: record.NewFakeRecorder(32), + ReleaseVersion: tc.desiredVersion, + } + + builder := fake.NewClientBuilder() + if tc.currentVersion != nil { + operator := &configv1.ClusterOperator{} + operator.SetName(clusterOperatorName) + operator.Status.Versions = tc.currentVersion + builder = builder.WithObjects(operator) + } + optr.Client = builder.Build() + + err := optr.setStatusAvailable(context.TODO()) + assert.NoErrorf(t, err, "Failed to set Available status on ClusterOperator") + + gotCO, err := optr.getOrCreateClusterOperator(context.TODO()) + assert.NoErrorf(t, err, "Failed to fetch ClusterOperator") + + var condition configv1.ClusterOperatorStatusCondition + for _, coCondition := range gotCO.Status.Conditions { + assert.True(t, + startTime.Before(&coCondition.LastTransitionTime) || startTime.Equal(&coCondition.LastTransitionTime), + "test-case %v expected LastTransitionTime for the status condition %v to be after %v", i, coCondition, startTime) + if coCondition.Type == configv1.OperatorAvailable { + condition = coCondition + break + } + } + + for _, expectedCondition := range tc.expectedConditions { + ok := v1helpers.IsStatusConditionPresentAndEqual( + gotCO.Status.Conditions, expectedCondition.Type, expectedCondition.Status, + ) + if !ok { + t.Errorf("wrong status for condition. Expected: %v, got: %v", + expectedCondition, + v1helpers.FindStatusCondition(gotCO.Status.Conditions, expectedCondition.Type)) + } + } + + assert.True(t, + len(tc.expectedConditions) == len(gotCO.Status.Conditions), + "test-case %v expected equal number of conditions to %v, got %v", i, len(tc.expectedConditions), len(gotCO.Status.Conditions)) + + desiredVersion := []configv1.OperandVersion{ + { + Name: "operator", + Version: tc.desiredVersion, + }, + } + assert.True(t, equality.Semantic.DeepEqual(gotCO.Status.Versions, desiredVersion), + "test-case %v expected equal version for ClusterOperator to %v, got %v", i, desiredVersion, gotCO.Status.Versions) + + err = optr.setStatusAvailable(context.TODO()) + assert.NoErrorf(t, err, "Failed to set Available status on ClusterOperator") + + err = optr.Client.Get(context.TODO(), client.ObjectKey{Name: clusterOperatorName}, gotCO) + assert.NoErrorf(t, err, "Failed to fetch ClusterOperator") + + var conditionAfterAnotherSync configv1.ClusterOperatorStatusCondition + for _, coCondition := range gotCO.Status.Conditions { + if coCondition.Type == configv1.OperatorAvailable { + conditionAfterAnotherSync = coCondition + break + } + } + assert.True(t, condition.LastTransitionTime.Equal(&conditionAfterAnotherSync.LastTransitionTime), "test-case %v expected LastTransitionTime not to be updated if condition state is same", i) + + for _, expectedCondition := range tc.expectedConditions { + ok := v1helpers.IsStatusConditionPresentAndEqual( + gotCO.Status.Conditions, expectedCondition.Type, expectedCondition.Status, + ) + if !ok { + t.Errorf("wrong status for condition. Expected: %v, got: %v", + expectedCondition, + v1helpers.FindStatusCondition(gotCO.Status.Conditions, expectedCondition.Type)) + } + } + + assert.True(t, + len(tc.expectedConditions) == len(gotCO.Status.Conditions), + "test-case %v expected equal number of conditions to %v, got %v", i, len(tc.expectedConditions), len(gotCO.Status.Conditions)) + + assert.True(t, equality.Semantic.DeepEqual(gotCO.Status.Versions, desiredVersion), + "test-case %v expected equal version for ClusterOperator to %v, got %v", i, desiredVersion, gotCO.Status.Versions) + } +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 40bd86350..0f0f2ad56 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -40,6 +40,11 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. +func init() { + configv1.Install(scheme.Scheme) + v1.AddToScheme(scheme.Scheme) +} + var cfg *rest.Config var cl client.Client var testEnv *envtest.Environment diff --git a/go.mod b/go.mod index ccb2a7b17..2de38f515 100644 --- a/go.mod +++ b/go.mod @@ -15,5 +15,6 @@ require ( k8s.io/client-go v0.20.0 k8s.io/klog v1.0.0 k8s.io/klog/v2 v2.4.0 + k8s.io/utils v0.0.0-20201110183641-67b214c5f920 sigs.k8s.io/controller-runtime v0.7.0 ) diff --git a/vendor/k8s.io/apimachinery/pkg/util/rand/rand.go b/vendor/k8s.io/apimachinery/pkg/util/rand/rand.go new file mode 100644 index 000000000..82a473bb1 --- /dev/null +++ b/vendor/k8s.io/apimachinery/pkg/util/rand/rand.go @@ -0,0 +1,127 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package rand provides utilities related to randomization. +package rand + +import ( + "math/rand" + "sync" + "time" +) + +var rng = struct { + sync.Mutex + rand *rand.Rand +}{ + rand: rand.New(rand.NewSource(time.Now().UnixNano())), +} + +// Int returns a non-negative pseudo-random int. +func Int() int { + rng.Lock() + defer rng.Unlock() + return rng.rand.Int() +} + +// Intn generates an integer in range [0,max). +// By design this should panic if input is invalid, <= 0. +func Intn(max int) int { + rng.Lock() + defer rng.Unlock() + return rng.rand.Intn(max) +} + +// IntnRange generates an integer in range [min,max). +// By design this should panic if input is invalid, <= 0. +func IntnRange(min, max int) int { + rng.Lock() + defer rng.Unlock() + return rng.rand.Intn(max-min) + min +} + +// IntnRange generates an int64 integer in range [min,max). +// By design this should panic if input is invalid, <= 0. +func Int63nRange(min, max int64) int64 { + rng.Lock() + defer rng.Unlock() + return rng.rand.Int63n(max-min) + min +} + +// Seed seeds the rng with the provided seed. +func Seed(seed int64) { + rng.Lock() + defer rng.Unlock() + + rng.rand = rand.New(rand.NewSource(seed)) +} + +// Perm returns, as a slice of n ints, a pseudo-random permutation of the integers [0,n) +// from the default Source. +func Perm(n int) []int { + rng.Lock() + defer rng.Unlock() + return rng.rand.Perm(n) +} + +const ( + // We omit vowels from the set of available characters to reduce the chances + // of "bad words" being formed. + alphanums = "bcdfghjklmnpqrstvwxz2456789" + // No. of bits required to index into alphanums string. + alphanumsIdxBits = 5 + // Mask used to extract last alphanumsIdxBits of an int. + alphanumsIdxMask = 1<>= alphanumsIdxBits + remaining-- + } + return string(b) +} + +// SafeEncodeString encodes s using the same characters as rand.String. This reduces the chances of bad words and +// ensures that strings generated from hash functions appear consistent throughout the API. +func SafeEncodeString(s string) string { + r := make([]byte, len(s)) + for i, b := range []rune(s) { + r[i] = alphanums[(int(b) % len(alphanums))] + } + return string(r) +} diff --git a/vendor/k8s.io/client-go/testing/actions.go b/vendor/k8s.io/client-go/testing/actions.go new file mode 100644 index 000000000..b6b2c1f22 --- /dev/null +++ b/vendor/k8s.io/client-go/testing/actions.go @@ -0,0 +1,681 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testing + +import ( + "fmt" + "path" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" +) + +func NewRootGetAction(resource schema.GroupVersionResource, name string) GetActionImpl { + action := GetActionImpl{} + action.Verb = "get" + action.Resource = resource + action.Name = name + + return action +} + +func NewGetAction(resource schema.GroupVersionResource, namespace, name string) GetActionImpl { + action := GetActionImpl{} + action.Verb = "get" + action.Resource = resource + action.Namespace = namespace + action.Name = name + + return action +} + +func NewGetSubresourceAction(resource schema.GroupVersionResource, namespace, subresource, name string) GetActionImpl { + action := GetActionImpl{} + action.Verb = "get" + action.Resource = resource + action.Subresource = subresource + action.Namespace = namespace + action.Name = name + + return action +} + +func NewRootGetSubresourceAction(resource schema.GroupVersionResource, subresource, name string) GetActionImpl { + action := GetActionImpl{} + action.Verb = "get" + action.Resource = resource + action.Subresource = subresource + action.Name = name + + return action +} + +func NewRootListAction(resource schema.GroupVersionResource, kind schema.GroupVersionKind, opts interface{}) ListActionImpl { + action := ListActionImpl{} + action.Verb = "list" + action.Resource = resource + action.Kind = kind + labelSelector, fieldSelector, _ := ExtractFromListOptions(opts) + action.ListRestrictions = ListRestrictions{labelSelector, fieldSelector} + + return action +} + +func NewListAction(resource schema.GroupVersionResource, kind schema.GroupVersionKind, namespace string, opts interface{}) ListActionImpl { + action := ListActionImpl{} + action.Verb = "list" + action.Resource = resource + action.Kind = kind + action.Namespace = namespace + labelSelector, fieldSelector, _ := ExtractFromListOptions(opts) + action.ListRestrictions = ListRestrictions{labelSelector, fieldSelector} + + return action +} + +func NewRootCreateAction(resource schema.GroupVersionResource, object runtime.Object) CreateActionImpl { + action := CreateActionImpl{} + action.Verb = "create" + action.Resource = resource + action.Object = object + + return action +} + +func NewCreateAction(resource schema.GroupVersionResource, namespace string, object runtime.Object) CreateActionImpl { + action := CreateActionImpl{} + action.Verb = "create" + action.Resource = resource + action.Namespace = namespace + action.Object = object + + return action +} + +func NewRootCreateSubresourceAction(resource schema.GroupVersionResource, name, subresource string, object runtime.Object) CreateActionImpl { + action := CreateActionImpl{} + action.Verb = "create" + action.Resource = resource + action.Subresource = subresource + action.Name = name + action.Object = object + + return action +} + +func NewCreateSubresourceAction(resource schema.GroupVersionResource, name, subresource, namespace string, object runtime.Object) CreateActionImpl { + action := CreateActionImpl{} + action.Verb = "create" + action.Resource = resource + action.Namespace = namespace + action.Subresource = subresource + action.Name = name + action.Object = object + + return action +} + +func NewRootUpdateAction(resource schema.GroupVersionResource, object runtime.Object) UpdateActionImpl { + action := UpdateActionImpl{} + action.Verb = "update" + action.Resource = resource + action.Object = object + + return action +} + +func NewUpdateAction(resource schema.GroupVersionResource, namespace string, object runtime.Object) UpdateActionImpl { + action := UpdateActionImpl{} + action.Verb = "update" + action.Resource = resource + action.Namespace = namespace + action.Object = object + + return action +} + +func NewRootPatchAction(resource schema.GroupVersionResource, name string, pt types.PatchType, patch []byte) PatchActionImpl { + action := PatchActionImpl{} + action.Verb = "patch" + action.Resource = resource + action.Name = name + action.PatchType = pt + action.Patch = patch + + return action +} + +func NewPatchAction(resource schema.GroupVersionResource, namespace string, name string, pt types.PatchType, patch []byte) PatchActionImpl { + action := PatchActionImpl{} + action.Verb = "patch" + action.Resource = resource + action.Namespace = namespace + action.Name = name + action.PatchType = pt + action.Patch = patch + + return action +} + +func NewRootPatchSubresourceAction(resource schema.GroupVersionResource, name string, pt types.PatchType, patch []byte, subresources ...string) PatchActionImpl { + action := PatchActionImpl{} + action.Verb = "patch" + action.Resource = resource + action.Subresource = path.Join(subresources...) + action.Name = name + action.PatchType = pt + action.Patch = patch + + return action +} + +func NewPatchSubresourceAction(resource schema.GroupVersionResource, namespace, name string, pt types.PatchType, patch []byte, subresources ...string) PatchActionImpl { + action := PatchActionImpl{} + action.Verb = "patch" + action.Resource = resource + action.Subresource = path.Join(subresources...) + action.Namespace = namespace + action.Name = name + action.PatchType = pt + action.Patch = patch + + return action +} + +func NewRootUpdateSubresourceAction(resource schema.GroupVersionResource, subresource string, object runtime.Object) UpdateActionImpl { + action := UpdateActionImpl{} + action.Verb = "update" + action.Resource = resource + action.Subresource = subresource + action.Object = object + + return action +} +func NewUpdateSubresourceAction(resource schema.GroupVersionResource, subresource string, namespace string, object runtime.Object) UpdateActionImpl { + action := UpdateActionImpl{} + action.Verb = "update" + action.Resource = resource + action.Subresource = subresource + action.Namespace = namespace + action.Object = object + + return action +} + +func NewRootDeleteAction(resource schema.GroupVersionResource, name string) DeleteActionImpl { + action := DeleteActionImpl{} + action.Verb = "delete" + action.Resource = resource + action.Name = name + + return action +} + +func NewRootDeleteSubresourceAction(resource schema.GroupVersionResource, subresource string, name string) DeleteActionImpl { + action := DeleteActionImpl{} + action.Verb = "delete" + action.Resource = resource + action.Subresource = subresource + action.Name = name + + return action +} + +func NewDeleteAction(resource schema.GroupVersionResource, namespace, name string) DeleteActionImpl { + action := DeleteActionImpl{} + action.Verb = "delete" + action.Resource = resource + action.Namespace = namespace + action.Name = name + + return action +} + +func NewDeleteSubresourceAction(resource schema.GroupVersionResource, subresource, namespace, name string) DeleteActionImpl { + action := DeleteActionImpl{} + action.Verb = "delete" + action.Resource = resource + action.Subresource = subresource + action.Namespace = namespace + action.Name = name + + return action +} + +func NewRootDeleteCollectionAction(resource schema.GroupVersionResource, opts interface{}) DeleteCollectionActionImpl { + action := DeleteCollectionActionImpl{} + action.Verb = "delete-collection" + action.Resource = resource + labelSelector, fieldSelector, _ := ExtractFromListOptions(opts) + action.ListRestrictions = ListRestrictions{labelSelector, fieldSelector} + + return action +} + +func NewDeleteCollectionAction(resource schema.GroupVersionResource, namespace string, opts interface{}) DeleteCollectionActionImpl { + action := DeleteCollectionActionImpl{} + action.Verb = "delete-collection" + action.Resource = resource + action.Namespace = namespace + labelSelector, fieldSelector, _ := ExtractFromListOptions(opts) + action.ListRestrictions = ListRestrictions{labelSelector, fieldSelector} + + return action +} + +func NewRootWatchAction(resource schema.GroupVersionResource, opts interface{}) WatchActionImpl { + action := WatchActionImpl{} + action.Verb = "watch" + action.Resource = resource + labelSelector, fieldSelector, resourceVersion := ExtractFromListOptions(opts) + action.WatchRestrictions = WatchRestrictions{labelSelector, fieldSelector, resourceVersion} + + return action +} + +func ExtractFromListOptions(opts interface{}) (labelSelector labels.Selector, fieldSelector fields.Selector, resourceVersion string) { + var err error + switch t := opts.(type) { + case metav1.ListOptions: + labelSelector, err = labels.Parse(t.LabelSelector) + if err != nil { + panic(fmt.Errorf("invalid selector %q: %v", t.LabelSelector, err)) + } + fieldSelector, err = fields.ParseSelector(t.FieldSelector) + if err != nil { + panic(fmt.Errorf("invalid selector %q: %v", t.FieldSelector, err)) + } + resourceVersion = t.ResourceVersion + default: + panic(fmt.Errorf("expect a ListOptions %T", opts)) + } + if labelSelector == nil { + labelSelector = labels.Everything() + } + if fieldSelector == nil { + fieldSelector = fields.Everything() + } + return labelSelector, fieldSelector, resourceVersion +} + +func NewWatchAction(resource schema.GroupVersionResource, namespace string, opts interface{}) WatchActionImpl { + action := WatchActionImpl{} + action.Verb = "watch" + action.Resource = resource + action.Namespace = namespace + labelSelector, fieldSelector, resourceVersion := ExtractFromListOptions(opts) + action.WatchRestrictions = WatchRestrictions{labelSelector, fieldSelector, resourceVersion} + + return action +} + +func NewProxyGetAction(resource schema.GroupVersionResource, namespace, scheme, name, port, path string, params map[string]string) ProxyGetActionImpl { + action := ProxyGetActionImpl{} + action.Verb = "get" + action.Resource = resource + action.Namespace = namespace + action.Scheme = scheme + action.Name = name + action.Port = port + action.Path = path + action.Params = params + return action +} + +type ListRestrictions struct { + Labels labels.Selector + Fields fields.Selector +} +type WatchRestrictions struct { + Labels labels.Selector + Fields fields.Selector + ResourceVersion string +} + +type Action interface { + GetNamespace() string + GetVerb() string + GetResource() schema.GroupVersionResource + GetSubresource() string + Matches(verb, resource string) bool + + // DeepCopy is used to copy an action to avoid any risk of accidental mutation. Most people never need to call this + // because the invocation logic deep copies before calls to storage and reactors. + DeepCopy() Action +} + +type GenericAction interface { + Action + GetValue() interface{} +} + +type GetAction interface { + Action + GetName() string +} + +type ListAction interface { + Action + GetListRestrictions() ListRestrictions +} + +type CreateAction interface { + Action + GetObject() runtime.Object +} + +type UpdateAction interface { + Action + GetObject() runtime.Object +} + +type DeleteAction interface { + Action + GetName() string +} + +type DeleteCollectionAction interface { + Action + GetListRestrictions() ListRestrictions +} + +type PatchAction interface { + Action + GetName() string + GetPatchType() types.PatchType + GetPatch() []byte +} + +type WatchAction interface { + Action + GetWatchRestrictions() WatchRestrictions +} + +type ProxyGetAction interface { + Action + GetScheme() string + GetName() string + GetPort() string + GetPath() string + GetParams() map[string]string +} + +type ActionImpl struct { + Namespace string + Verb string + Resource schema.GroupVersionResource + Subresource string +} + +func (a ActionImpl) GetNamespace() string { + return a.Namespace +} +func (a ActionImpl) GetVerb() string { + return a.Verb +} +func (a ActionImpl) GetResource() schema.GroupVersionResource { + return a.Resource +} +func (a ActionImpl) GetSubresource() string { + return a.Subresource +} +func (a ActionImpl) Matches(verb, resource string) bool { + // Stay backwards compatible. + if !strings.Contains(resource, "/") { + return strings.EqualFold(verb, a.Verb) && + strings.EqualFold(resource, a.Resource.Resource) + } + + parts := strings.SplitN(resource, "/", 2) + topresource, subresource := parts[0], parts[1] + + return strings.EqualFold(verb, a.Verb) && + strings.EqualFold(topresource, a.Resource.Resource) && + strings.EqualFold(subresource, a.Subresource) +} +func (a ActionImpl) DeepCopy() Action { + ret := a + return ret +} + +type GenericActionImpl struct { + ActionImpl + Value interface{} +} + +func (a GenericActionImpl) GetValue() interface{} { + return a.Value +} + +func (a GenericActionImpl) DeepCopy() Action { + return GenericActionImpl{ + ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), + // TODO this is wrong, but no worse than before + Value: a.Value, + } +} + +type GetActionImpl struct { + ActionImpl + Name string +} + +func (a GetActionImpl) GetName() string { + return a.Name +} + +func (a GetActionImpl) DeepCopy() Action { + return GetActionImpl{ + ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), + Name: a.Name, + } +} + +type ListActionImpl struct { + ActionImpl + Kind schema.GroupVersionKind + Name string + ListRestrictions ListRestrictions +} + +func (a ListActionImpl) GetKind() schema.GroupVersionKind { + return a.Kind +} + +func (a ListActionImpl) GetListRestrictions() ListRestrictions { + return a.ListRestrictions +} + +func (a ListActionImpl) DeepCopy() Action { + return ListActionImpl{ + ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), + Kind: a.Kind, + Name: a.Name, + ListRestrictions: ListRestrictions{ + Labels: a.ListRestrictions.Labels.DeepCopySelector(), + Fields: a.ListRestrictions.Fields.DeepCopySelector(), + }, + } +} + +type CreateActionImpl struct { + ActionImpl + Name string + Object runtime.Object +} + +func (a CreateActionImpl) GetObject() runtime.Object { + return a.Object +} + +func (a CreateActionImpl) DeepCopy() Action { + return CreateActionImpl{ + ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), + Name: a.Name, + Object: a.Object.DeepCopyObject(), + } +} + +type UpdateActionImpl struct { + ActionImpl + Object runtime.Object +} + +func (a UpdateActionImpl) GetObject() runtime.Object { + return a.Object +} + +func (a UpdateActionImpl) DeepCopy() Action { + return UpdateActionImpl{ + ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), + Object: a.Object.DeepCopyObject(), + } +} + +type PatchActionImpl struct { + ActionImpl + Name string + PatchType types.PatchType + Patch []byte +} + +func (a PatchActionImpl) GetName() string { + return a.Name +} + +func (a PatchActionImpl) GetPatch() []byte { + return a.Patch +} + +func (a PatchActionImpl) GetPatchType() types.PatchType { + return a.PatchType +} + +func (a PatchActionImpl) DeepCopy() Action { + patch := make([]byte, len(a.Patch)) + copy(patch, a.Patch) + return PatchActionImpl{ + ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), + Name: a.Name, + PatchType: a.PatchType, + Patch: patch, + } +} + +type DeleteActionImpl struct { + ActionImpl + Name string +} + +func (a DeleteActionImpl) GetName() string { + return a.Name +} + +func (a DeleteActionImpl) DeepCopy() Action { + return DeleteActionImpl{ + ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), + Name: a.Name, + } +} + +type DeleteCollectionActionImpl struct { + ActionImpl + ListRestrictions ListRestrictions +} + +func (a DeleteCollectionActionImpl) GetListRestrictions() ListRestrictions { + return a.ListRestrictions +} + +func (a DeleteCollectionActionImpl) DeepCopy() Action { + return DeleteCollectionActionImpl{ + ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), + ListRestrictions: ListRestrictions{ + Labels: a.ListRestrictions.Labels.DeepCopySelector(), + Fields: a.ListRestrictions.Fields.DeepCopySelector(), + }, + } +} + +type WatchActionImpl struct { + ActionImpl + WatchRestrictions WatchRestrictions +} + +func (a WatchActionImpl) GetWatchRestrictions() WatchRestrictions { + return a.WatchRestrictions +} + +func (a WatchActionImpl) DeepCopy() Action { + return WatchActionImpl{ + ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), + WatchRestrictions: WatchRestrictions{ + Labels: a.WatchRestrictions.Labels.DeepCopySelector(), + Fields: a.WatchRestrictions.Fields.DeepCopySelector(), + ResourceVersion: a.WatchRestrictions.ResourceVersion, + }, + } +} + +type ProxyGetActionImpl struct { + ActionImpl + Scheme string + Name string + Port string + Path string + Params map[string]string +} + +func (a ProxyGetActionImpl) GetScheme() string { + return a.Scheme +} + +func (a ProxyGetActionImpl) GetName() string { + return a.Name +} + +func (a ProxyGetActionImpl) GetPort() string { + return a.Port +} + +func (a ProxyGetActionImpl) GetPath() string { + return a.Path +} + +func (a ProxyGetActionImpl) GetParams() map[string]string { + return a.Params +} + +func (a ProxyGetActionImpl) DeepCopy() Action { + params := map[string]string{} + for k, v := range a.Params { + params[k] = v + } + return ProxyGetActionImpl{ + ActionImpl: a.ActionImpl.DeepCopy().(ActionImpl), + Scheme: a.Scheme, + Name: a.Name, + Port: a.Port, + Path: a.Path, + Params: params, + } +} diff --git a/vendor/k8s.io/client-go/testing/fake.go b/vendor/k8s.io/client-go/testing/fake.go new file mode 100644 index 000000000..8b9ee149c --- /dev/null +++ b/vendor/k8s.io/client-go/testing/fake.go @@ -0,0 +1,216 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testing + +import ( + "fmt" + "sync" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + restclient "k8s.io/client-go/rest" +) + +// Fake implements client.Interface. Meant to be embedded into a struct to get +// a default implementation. This makes faking out just the method you want to +// test easier. +type Fake struct { + sync.RWMutex + actions []Action // these may be castable to other types, but "Action" is the minimum + + // ReactionChain is the list of reactors that will be attempted for every + // request in the order they are tried. + ReactionChain []Reactor + // WatchReactionChain is the list of watch reactors that will be attempted + // for every request in the order they are tried. + WatchReactionChain []WatchReactor + // ProxyReactionChain is the list of proxy reactors that will be attempted + // for every request in the order they are tried. + ProxyReactionChain []ProxyReactor + + Resources []*metav1.APIResourceList +} + +// Reactor is an interface to allow the composition of reaction functions. +type Reactor interface { + // Handles indicates whether or not this Reactor deals with a given + // action. + Handles(action Action) bool + // React handles the action and returns results. It may choose to + // delegate by indicated handled=false. + React(action Action) (handled bool, ret runtime.Object, err error) +} + +// WatchReactor is an interface to allow the composition of watch functions. +type WatchReactor interface { + // Handles indicates whether or not this Reactor deals with a given + // action. + Handles(action Action) bool + // React handles a watch action and returns results. It may choose to + // delegate by indicating handled=false. + React(action Action) (handled bool, ret watch.Interface, err error) +} + +// ProxyReactor is an interface to allow the composition of proxy get +// functions. +type ProxyReactor interface { + // Handles indicates whether or not this Reactor deals with a given + // action. + Handles(action Action) bool + // React handles a watch action and returns results. It may choose to + // delegate by indicating handled=false. + React(action Action) (handled bool, ret restclient.ResponseWrapper, err error) +} + +// ReactionFunc is a function that returns an object or error for a given +// Action. If "handled" is false, then the test client will ignore the +// results and continue to the next ReactionFunc. A ReactionFunc can describe +// reactions on subresources by testing the result of the action's +// GetSubresource() method. +type ReactionFunc func(action Action) (handled bool, ret runtime.Object, err error) + +// WatchReactionFunc is a function that returns a watch interface. If +// "handled" is false, then the test client will ignore the results and +// continue to the next ReactionFunc. +type WatchReactionFunc func(action Action) (handled bool, ret watch.Interface, err error) + +// ProxyReactionFunc is a function that returns a ResponseWrapper interface +// for a given Action. If "handled" is false, then the test client will +// ignore the results and continue to the next ProxyReactionFunc. +type ProxyReactionFunc func(action Action) (handled bool, ret restclient.ResponseWrapper, err error) + +// AddReactor appends a reactor to the end of the chain. +func (c *Fake) AddReactor(verb, resource string, reaction ReactionFunc) { + c.ReactionChain = append(c.ReactionChain, &SimpleReactor{verb, resource, reaction}) +} + +// PrependReactor adds a reactor to the beginning of the chain. +func (c *Fake) PrependReactor(verb, resource string, reaction ReactionFunc) { + c.ReactionChain = append([]Reactor{&SimpleReactor{verb, resource, reaction}}, c.ReactionChain...) +} + +// AddWatchReactor appends a reactor to the end of the chain. +func (c *Fake) AddWatchReactor(resource string, reaction WatchReactionFunc) { + c.WatchReactionChain = append(c.WatchReactionChain, &SimpleWatchReactor{resource, reaction}) +} + +// PrependWatchReactor adds a reactor to the beginning of the chain. +func (c *Fake) PrependWatchReactor(resource string, reaction WatchReactionFunc) { + c.WatchReactionChain = append([]WatchReactor{&SimpleWatchReactor{resource, reaction}}, c.WatchReactionChain...) +} + +// AddProxyReactor appends a reactor to the end of the chain. +func (c *Fake) AddProxyReactor(resource string, reaction ProxyReactionFunc) { + c.ProxyReactionChain = append(c.ProxyReactionChain, &SimpleProxyReactor{resource, reaction}) +} + +// PrependProxyReactor adds a reactor to the beginning of the chain. +func (c *Fake) PrependProxyReactor(resource string, reaction ProxyReactionFunc) { + c.ProxyReactionChain = append([]ProxyReactor{&SimpleProxyReactor{resource, reaction}}, c.ProxyReactionChain...) +} + +// Invokes records the provided Action and then invokes the ReactionFunc that +// handles the action if one exists. defaultReturnObj is expected to be of the +// same type a normal call would return. +func (c *Fake) Invokes(action Action, defaultReturnObj runtime.Object) (runtime.Object, error) { + c.Lock() + defer c.Unlock() + + actionCopy := action.DeepCopy() + c.actions = append(c.actions, action.DeepCopy()) + for _, reactor := range c.ReactionChain { + if !reactor.Handles(actionCopy) { + continue + } + + handled, ret, err := reactor.React(actionCopy) + if !handled { + continue + } + + return ret, err + } + + return defaultReturnObj, nil +} + +// InvokesWatch records the provided Action and then invokes the ReactionFunc +// that handles the action if one exists. +func (c *Fake) InvokesWatch(action Action) (watch.Interface, error) { + c.Lock() + defer c.Unlock() + + actionCopy := action.DeepCopy() + c.actions = append(c.actions, action.DeepCopy()) + for _, reactor := range c.WatchReactionChain { + if !reactor.Handles(actionCopy) { + continue + } + + handled, ret, err := reactor.React(actionCopy) + if !handled { + continue + } + + return ret, err + } + + return nil, fmt.Errorf("unhandled watch: %#v", action) +} + +// InvokesProxy records the provided Action and then invokes the ReactionFunc +// that handles the action if one exists. +func (c *Fake) InvokesProxy(action Action) restclient.ResponseWrapper { + c.Lock() + defer c.Unlock() + + actionCopy := action.DeepCopy() + c.actions = append(c.actions, action.DeepCopy()) + for _, reactor := range c.ProxyReactionChain { + if !reactor.Handles(actionCopy) { + continue + } + + handled, ret, err := reactor.React(actionCopy) + if !handled || err != nil { + continue + } + + return ret + } + + return nil +} + +// ClearActions clears the history of actions called on the fake client. +func (c *Fake) ClearActions() { + c.Lock() + defer c.Unlock() + + c.actions = make([]Action, 0) +} + +// Actions returns a chronologically ordered slice fake actions called on the +// fake client. +func (c *Fake) Actions() []Action { + c.RLock() + defer c.RUnlock() + fa := make([]Action, len(c.actions)) + copy(fa, c.actions) + return fa +} diff --git a/vendor/k8s.io/client-go/testing/fixture.go b/vendor/k8s.io/client-go/testing/fixture.go new file mode 100644 index 000000000..d3b937247 --- /dev/null +++ b/vendor/k8s.io/client-go/testing/fixture.go @@ -0,0 +1,564 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testing + +import ( + "fmt" + "reflect" + "sort" + "sync" + + jsonpatch "github.com/evanphx/json-patch" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/json" + "k8s.io/apimachinery/pkg/util/strategicpatch" + "k8s.io/apimachinery/pkg/watch" + restclient "k8s.io/client-go/rest" +) + +// ObjectTracker keeps track of objects. It is intended to be used to +// fake calls to a server by returning objects based on their kind, +// namespace and name. +type ObjectTracker interface { + // Add adds an object to the tracker. If object being added + // is a list, its items are added separately. + Add(obj runtime.Object) error + + // Get retrieves the object by its kind, namespace and name. + Get(gvr schema.GroupVersionResource, ns, name string) (runtime.Object, error) + + // Create adds an object to the tracker in the specified namespace. + Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error + + // Update updates an existing object in the tracker in the specified namespace. + Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error + + // List retrieves all objects of a given kind in the given + // namespace. Only non-List kinds are accepted. + List(gvr schema.GroupVersionResource, gvk schema.GroupVersionKind, ns string) (runtime.Object, error) + + // Delete deletes an existing object from the tracker. If object + // didn't exist in the tracker prior to deletion, Delete returns + // no error. + Delete(gvr schema.GroupVersionResource, ns, name string) error + + // Watch watches objects from the tracker. Watch returns a channel + // which will push added / modified / deleted object. + Watch(gvr schema.GroupVersionResource, ns string) (watch.Interface, error) +} + +// ObjectScheme abstracts the implementation of common operations on objects. +type ObjectScheme interface { + runtime.ObjectCreater + runtime.ObjectTyper +} + +// ObjectReaction returns a ReactionFunc that applies core.Action to +// the given tracker. +func ObjectReaction(tracker ObjectTracker) ReactionFunc { + return func(action Action) (bool, runtime.Object, error) { + ns := action.GetNamespace() + gvr := action.GetResource() + // Here and below we need to switch on implementation types, + // not on interfaces, as some interfaces are identical + // (e.g. UpdateAction and CreateAction), so if we use them, + // updates and creates end up matching the same case branch. + switch action := action.(type) { + + case ListActionImpl: + obj, err := tracker.List(gvr, action.GetKind(), ns) + return true, obj, err + + case GetActionImpl: + obj, err := tracker.Get(gvr, ns, action.GetName()) + return true, obj, err + + case CreateActionImpl: + objMeta, err := meta.Accessor(action.GetObject()) + if err != nil { + return true, nil, err + } + if action.GetSubresource() == "" { + err = tracker.Create(gvr, action.GetObject(), ns) + } else { + // TODO: Currently we're handling subresource creation as an update + // on the enclosing resource. This works for some subresources but + // might not be generic enough. + err = tracker.Update(gvr, action.GetObject(), ns) + } + if err != nil { + return true, nil, err + } + obj, err := tracker.Get(gvr, ns, objMeta.GetName()) + return true, obj, err + + case UpdateActionImpl: + objMeta, err := meta.Accessor(action.GetObject()) + if err != nil { + return true, nil, err + } + err = tracker.Update(gvr, action.GetObject(), ns) + if err != nil { + return true, nil, err + } + obj, err := tracker.Get(gvr, ns, objMeta.GetName()) + return true, obj, err + + case DeleteActionImpl: + err := tracker.Delete(gvr, ns, action.GetName()) + if err != nil { + return true, nil, err + } + return true, nil, nil + + case PatchActionImpl: + obj, err := tracker.Get(gvr, ns, action.GetName()) + if err != nil { + return true, nil, err + } + + old, err := json.Marshal(obj) + if err != nil { + return true, nil, err + } + + // reset the object in preparation to unmarshal, since unmarshal does not guarantee that fields + // in obj that are removed by patch are cleared + value := reflect.ValueOf(obj) + value.Elem().Set(reflect.New(value.Type().Elem()).Elem()) + + switch action.GetPatchType() { + case types.JSONPatchType: + patch, err := jsonpatch.DecodePatch(action.GetPatch()) + if err != nil { + return true, nil, err + } + modified, err := patch.Apply(old) + if err != nil { + return true, nil, err + } + + if err = json.Unmarshal(modified, obj); err != nil { + return true, nil, err + } + case types.MergePatchType: + modified, err := jsonpatch.MergePatch(old, action.GetPatch()) + if err != nil { + return true, nil, err + } + + if err := json.Unmarshal(modified, obj); err != nil { + return true, nil, err + } + case types.StrategicMergePatchType: + mergedByte, err := strategicpatch.StrategicMergePatch(old, action.GetPatch(), obj) + if err != nil { + return true, nil, err + } + if err = json.Unmarshal(mergedByte, obj); err != nil { + return true, nil, err + } + default: + return true, nil, fmt.Errorf("PatchType is not supported") + } + + if err = tracker.Update(gvr, obj, ns); err != nil { + return true, nil, err + } + + return true, obj, nil + + default: + return false, nil, fmt.Errorf("no reaction implemented for %s", action) + } + } +} + +type tracker struct { + scheme ObjectScheme + decoder runtime.Decoder + lock sync.RWMutex + objects map[schema.GroupVersionResource]map[types.NamespacedName]runtime.Object + // The value type of watchers is a map of which the key is either a namespace or + // all/non namespace aka "" and its value is list of fake watchers. + // Manipulations on resources will broadcast the notification events into the + // watchers' channel. Note that too many unhandled events (currently 100, + // see apimachinery/pkg/watch.DefaultChanSize) will cause a panic. + watchers map[schema.GroupVersionResource]map[string][]*watch.RaceFreeFakeWatcher +} + +var _ ObjectTracker = &tracker{} + +// NewObjectTracker returns an ObjectTracker that can be used to keep track +// of objects for the fake clientset. Mostly useful for unit tests. +func NewObjectTracker(scheme ObjectScheme, decoder runtime.Decoder) ObjectTracker { + return &tracker{ + scheme: scheme, + decoder: decoder, + objects: make(map[schema.GroupVersionResource]map[types.NamespacedName]runtime.Object), + watchers: make(map[schema.GroupVersionResource]map[string][]*watch.RaceFreeFakeWatcher), + } +} + +func (t *tracker) List(gvr schema.GroupVersionResource, gvk schema.GroupVersionKind, ns string) (runtime.Object, error) { + // Heuristic for list kind: original kind + List suffix. Might + // not always be true but this tracker has a pretty limited + // understanding of the actual API model. + listGVK := gvk + listGVK.Kind = listGVK.Kind + "List" + // GVK does have the concept of "internal version". The scheme recognizes + // the runtime.APIVersionInternal, but not the empty string. + if listGVK.Version == "" { + listGVK.Version = runtime.APIVersionInternal + } + + list, err := t.scheme.New(listGVK) + if err != nil { + return nil, err + } + + if !meta.IsListType(list) { + return nil, fmt.Errorf("%q is not a list type", listGVK.Kind) + } + + t.lock.RLock() + defer t.lock.RUnlock() + + objs, ok := t.objects[gvr] + if !ok { + return list, nil + } + + matchingObjs, err := filterByNamespace(objs, ns) + if err != nil { + return nil, err + } + if err := meta.SetList(list, matchingObjs); err != nil { + return nil, err + } + return list.DeepCopyObject(), nil +} + +func (t *tracker) Watch(gvr schema.GroupVersionResource, ns string) (watch.Interface, error) { + t.lock.Lock() + defer t.lock.Unlock() + + fakewatcher := watch.NewRaceFreeFake() + + if _, exists := t.watchers[gvr]; !exists { + t.watchers[gvr] = make(map[string][]*watch.RaceFreeFakeWatcher) + } + t.watchers[gvr][ns] = append(t.watchers[gvr][ns], fakewatcher) + return fakewatcher, nil +} + +func (t *tracker) Get(gvr schema.GroupVersionResource, ns, name string) (runtime.Object, error) { + errNotFound := errors.NewNotFound(gvr.GroupResource(), name) + + t.lock.RLock() + defer t.lock.RUnlock() + + objs, ok := t.objects[gvr] + if !ok { + return nil, errNotFound + } + + matchingObj, ok := objs[types.NamespacedName{Namespace: ns, Name: name}] + if !ok { + return nil, errNotFound + } + + // Only one object should match in the tracker if it works + // correctly, as Add/Update methods enforce kind/namespace/name + // uniqueness. + obj := matchingObj.DeepCopyObject() + if status, ok := obj.(*metav1.Status); ok { + if status.Status != metav1.StatusSuccess { + return nil, &errors.StatusError{ErrStatus: *status} + } + } + + return obj, nil +} + +func (t *tracker) Add(obj runtime.Object) error { + if meta.IsListType(obj) { + return t.addList(obj, false) + } + objMeta, err := meta.Accessor(obj) + if err != nil { + return err + } + gvks, _, err := t.scheme.ObjectKinds(obj) + if err != nil { + return err + } + + if partial, ok := obj.(*metav1.PartialObjectMetadata); ok && len(partial.TypeMeta.APIVersion) > 0 { + gvks = []schema.GroupVersionKind{partial.TypeMeta.GroupVersionKind()} + } + + if len(gvks) == 0 { + return fmt.Errorf("no registered kinds for %v", obj) + } + for _, gvk := range gvks { + // NOTE: UnsafeGuessKindToResource is a heuristic and default match. The + // actual registration in apiserver can specify arbitrary route for a + // gvk. If a test uses such objects, it cannot preset the tracker with + // objects via Add(). Instead, it should trigger the Create() function + // of the tracker, where an arbitrary gvr can be specified. + gvr, _ := meta.UnsafeGuessKindToResource(gvk) + // Resource doesn't have the concept of "__internal" version, just set it to "". + if gvr.Version == runtime.APIVersionInternal { + gvr.Version = "" + } + + err := t.add(gvr, obj, objMeta.GetNamespace(), false) + if err != nil { + return err + } + } + return nil +} + +func (t *tracker) Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error { + return t.add(gvr, obj, ns, false) +} + +func (t *tracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error { + return t.add(gvr, obj, ns, true) +} + +func (t *tracker) getWatches(gvr schema.GroupVersionResource, ns string) []*watch.RaceFreeFakeWatcher { + watches := []*watch.RaceFreeFakeWatcher{} + if t.watchers[gvr] != nil { + if w := t.watchers[gvr][ns]; w != nil { + watches = append(watches, w...) + } + if ns != metav1.NamespaceAll { + if w := t.watchers[gvr][metav1.NamespaceAll]; w != nil { + watches = append(watches, w...) + } + } + } + return watches +} + +func (t *tracker) add(gvr schema.GroupVersionResource, obj runtime.Object, ns string, replaceExisting bool) error { + t.lock.Lock() + defer t.lock.Unlock() + + gr := gvr.GroupResource() + + // To avoid the object from being accidentally modified by caller + // after it's been added to the tracker, we always store the deep + // copy. + obj = obj.DeepCopyObject() + + newMeta, err := meta.Accessor(obj) + if err != nil { + return err + } + + // Propagate namespace to the new object if hasn't already been set. + if len(newMeta.GetNamespace()) == 0 { + newMeta.SetNamespace(ns) + } + + if ns != newMeta.GetNamespace() { + msg := fmt.Sprintf("request namespace does not match object namespace, request: %q object: %q", ns, newMeta.GetNamespace()) + return errors.NewBadRequest(msg) + } + + _, ok := t.objects[gvr] + if !ok { + t.objects[gvr] = make(map[types.NamespacedName]runtime.Object) + } + + namespacedName := types.NamespacedName{Namespace: newMeta.GetNamespace(), Name: newMeta.GetName()} + if _, ok = t.objects[gvr][namespacedName]; ok { + if replaceExisting { + for _, w := range t.getWatches(gvr, ns) { + w.Modify(obj) + } + t.objects[gvr][namespacedName] = obj + return nil + } + return errors.NewAlreadyExists(gr, newMeta.GetName()) + } + + if replaceExisting { + // Tried to update but no matching object was found. + return errors.NewNotFound(gr, newMeta.GetName()) + } + + t.objects[gvr][namespacedName] = obj + + for _, w := range t.getWatches(gvr, ns) { + w.Add(obj) + } + + return nil +} + +func (t *tracker) addList(obj runtime.Object, replaceExisting bool) error { + list, err := meta.ExtractList(obj) + if err != nil { + return err + } + errs := runtime.DecodeList(list, t.decoder) + if len(errs) > 0 { + return errs[0] + } + for _, obj := range list { + if err := t.Add(obj); err != nil { + return err + } + } + return nil +} + +func (t *tracker) Delete(gvr schema.GroupVersionResource, ns, name string) error { + t.lock.Lock() + defer t.lock.Unlock() + + objs, ok := t.objects[gvr] + if !ok { + return errors.NewNotFound(gvr.GroupResource(), name) + } + + namespacedName := types.NamespacedName{Namespace: ns, Name: name} + obj, ok := objs[namespacedName] + if !ok { + return errors.NewNotFound(gvr.GroupResource(), name) + } + + delete(objs, namespacedName) + for _, w := range t.getWatches(gvr, ns) { + w.Delete(obj) + } + return nil +} + +// filterByNamespace returns all objects in the collection that +// match provided namespace. Empty namespace matches +// non-namespaced objects. +func filterByNamespace(objs map[types.NamespacedName]runtime.Object, ns string) ([]runtime.Object, error) { + var res []runtime.Object + + for _, obj := range objs { + acc, err := meta.Accessor(obj) + if err != nil { + return nil, err + } + if ns != "" && acc.GetNamespace() != ns { + continue + } + res = append(res, obj) + } + + // Sort res to get deterministic order. + sort.Slice(res, func(i, j int) bool { + acc1, _ := meta.Accessor(res[i]) + acc2, _ := meta.Accessor(res[j]) + if acc1.GetNamespace() != acc2.GetNamespace() { + return acc1.GetNamespace() < acc2.GetNamespace() + } + return acc1.GetName() < acc2.GetName() + }) + return res, nil +} + +func DefaultWatchReactor(watchInterface watch.Interface, err error) WatchReactionFunc { + return func(action Action) (bool, watch.Interface, error) { + return true, watchInterface, err + } +} + +// SimpleReactor is a Reactor. Each reaction function is attached to a given verb,resource tuple. "*" in either field matches everything for that value. +// For instance, *,pods matches all verbs on pods. This allows for easier composition of reaction functions +type SimpleReactor struct { + Verb string + Resource string + + Reaction ReactionFunc +} + +func (r *SimpleReactor) Handles(action Action) bool { + verbCovers := r.Verb == "*" || r.Verb == action.GetVerb() + if !verbCovers { + return false + } + resourceCovers := r.Resource == "*" || r.Resource == action.GetResource().Resource + if !resourceCovers { + return false + } + + return true +} + +func (r *SimpleReactor) React(action Action) (bool, runtime.Object, error) { + return r.Reaction(action) +} + +// SimpleWatchReactor is a WatchReactor. Each reaction function is attached to a given resource. "*" matches everything for that value. +// For instance, *,pods matches all verbs on pods. This allows for easier composition of reaction functions +type SimpleWatchReactor struct { + Resource string + + Reaction WatchReactionFunc +} + +func (r *SimpleWatchReactor) Handles(action Action) bool { + resourceCovers := r.Resource == "*" || r.Resource == action.GetResource().Resource + if !resourceCovers { + return false + } + + return true +} + +func (r *SimpleWatchReactor) React(action Action) (bool, watch.Interface, error) { + return r.Reaction(action) +} + +// SimpleProxyReactor is a ProxyReactor. Each reaction function is attached to a given resource. "*" matches everything for that value. +// For instance, *,pods matches all verbs on pods. This allows for easier composition of reaction functions. +type SimpleProxyReactor struct { + Resource string + + Reaction ProxyReactionFunc +} + +func (r *SimpleProxyReactor) Handles(action Action) bool { + resourceCovers := r.Resource == "*" || r.Resource == action.GetResource().Resource + if !resourceCovers { + return false + } + + return true +} + +func (r *SimpleProxyReactor) React(action Action) (bool, restclient.ResponseWrapper, error) { + return r.Reaction(action) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 9abfdf55e..6c8e22445 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -333,6 +333,7 @@ k8s.io/apimachinery/pkg/util/json k8s.io/apimachinery/pkg/util/mergepatch k8s.io/apimachinery/pkg/util/naming k8s.io/apimachinery/pkg/util/net +k8s.io/apimachinery/pkg/util/rand k8s.io/apimachinery/pkg/util/runtime k8s.io/apimachinery/pkg/util/sets k8s.io/apimachinery/pkg/util/strategicpatch @@ -408,6 +409,7 @@ k8s.io/client-go/plugin/pkg/client/auth/openstack k8s.io/client-go/rest k8s.io/client-go/rest/watch k8s.io/client-go/restmapper +k8s.io/client-go/testing k8s.io/client-go/third_party/forked/golang/template k8s.io/client-go/tools/auth k8s.io/client-go/tools/cache @@ -443,6 +445,7 @@ k8s.io/klog/v2 # k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd k8s.io/kube-openapi/pkg/util/proto # k8s.io/utils v0.0.0-20201110183641-67b214c5f920 +## explicit k8s.io/utils/buffer k8s.io/utils/integer k8s.io/utils/pointer @@ -456,6 +459,7 @@ sigs.k8s.io/controller-runtime/pkg/cache/internal sigs.k8s.io/controller-runtime/pkg/client sigs.k8s.io/controller-runtime/pkg/client/apiutil sigs.k8s.io/controller-runtime/pkg/client/config +sigs.k8s.io/controller-runtime/pkg/client/fake sigs.k8s.io/controller-runtime/pkg/config sigs.k8s.io/controller-runtime/pkg/config/v1alpha1 sigs.k8s.io/controller-runtime/pkg/controller @@ -469,6 +473,7 @@ sigs.k8s.io/controller-runtime/pkg/healthz sigs.k8s.io/controller-runtime/pkg/internal/controller sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics sigs.k8s.io/controller-runtime/pkg/internal/log +sigs.k8s.io/controller-runtime/pkg/internal/objectutil sigs.k8s.io/controller-runtime/pkg/internal/recorder sigs.k8s.io/controller-runtime/pkg/internal/testing/integration sigs.k8s.io/controller-runtime/pkg/internal/testing/integration/addr diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go new file mode 100644 index 000000000..f5aec0a05 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go @@ -0,0 +1,596 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fake + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + utilrand "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/testing" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/controller-runtime/pkg/internal/objectutil" +) + +type versionedTracker struct { + testing.ObjectTracker + scheme *runtime.Scheme +} + +type fakeClient struct { + tracker versionedTracker + scheme *runtime.Scheme +} + +var _ client.Client = &fakeClient{} + +const ( + maxNameLength = 63 + randomLength = 5 + maxGeneratedNameLength = maxNameLength - randomLength +) + +// NewFakeClient creates a new fake client for testing. +// You can choose to initialize it with a slice of runtime.Object. +// +// Deprecated: Please use NewClientBuilder instead. +func NewFakeClient(initObjs ...runtime.Object) client.Client { + return NewClientBuilder().WithRuntimeObjects(initObjs...).Build() +} + +// NewFakeClientWithScheme creates a new fake client with the given scheme +// for testing. +// You can choose to initialize it with a slice of runtime.Object. +// +// Deprecated: Please use NewClientBuilder instead. +func NewFakeClientWithScheme(clientScheme *runtime.Scheme, initObjs ...runtime.Object) client.Client { + return NewClientBuilder().WithScheme(clientScheme).WithRuntimeObjects(initObjs...).Build() +} + +// NewClientBuilder returns a new builder to create a fake client. +func NewClientBuilder() *ClientBuilder { + return &ClientBuilder{} +} + +// ClientBuilder builds a fake client. +type ClientBuilder struct { + scheme *runtime.Scheme + initObject []client.Object + initLists []client.ObjectList + initRuntimeObjects []runtime.Object +} + +// WithScheme sets this builder's internal scheme. +// If not set, defaults to client-go's global scheme.Scheme. +func (f *ClientBuilder) WithScheme(scheme *runtime.Scheme) *ClientBuilder { + f.scheme = scheme + return f +} + +// WithObjects can be optionally used to initialize this fake client with client.Object(s). +func (f *ClientBuilder) WithObjects(initObjs ...client.Object) *ClientBuilder { + f.initObject = append(f.initObject, initObjs...) + return f +} + +// WithLists can be optionally used to initialize this fake client with client.ObjectList(s). +func (f *ClientBuilder) WithLists(initLists ...client.ObjectList) *ClientBuilder { + f.initLists = append(f.initLists, initLists...) + return f +} + +// WithRuntimeObjects can be optionally used to initialize this fake client with runtime.Object(s). +func (f *ClientBuilder) WithRuntimeObjects(initRuntimeObjs ...runtime.Object) *ClientBuilder { + f.initRuntimeObjects = append(f.initRuntimeObjects, initRuntimeObjs...) + return f +} + +// Build builds and returns a new fake client. +func (f *ClientBuilder) Build() client.Client { + if f.scheme == nil { + f.scheme = scheme.Scheme + } + + tracker := testing.NewObjectTracker(f.scheme, scheme.Codecs.UniversalDecoder()) + for _, obj := range f.initObject { + if err := tracker.Add(obj); err != nil { + panic(fmt.Errorf("failed to add object %v to fake client: %w", obj, err)) + } + } + for _, obj := range f.initLists { + if err := tracker.Add(obj); err != nil { + panic(fmt.Errorf("failed to add list %v to fake client: %w", obj, err)) + } + } + for _, obj := range f.initRuntimeObjects { + if err := tracker.Add(obj); err != nil { + panic(fmt.Errorf("failed to add runtime object %v to fake client: %w", obj, err)) + } + } + return &fakeClient{ + tracker: versionedTracker{ObjectTracker: tracker, scheme: f.scheme}, + scheme: f.scheme, + } +} + +func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error { + accessor, err := meta.Accessor(obj) + if err != nil { + return fmt.Errorf("failed to get accessor for object: %v", err) + } + if accessor.GetName() == "" { + return apierrors.NewInvalid( + obj.GetObjectKind().GroupVersionKind().GroupKind(), + accessor.GetName(), + field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")}) + } + if accessor.GetResourceVersion() != "" { + return apierrors.NewBadRequest("resourceVersion can not be set for Create requests") + } + accessor.SetResourceVersion("1") + if err := t.ObjectTracker.Create(gvr, obj, ns); err != nil { + accessor.SetResourceVersion("") + return err + } + return nil +} + +func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error { + accessor, err := meta.Accessor(obj) + if err != nil { + return fmt.Errorf("failed to get accessor for object: %v", err) + } + + if accessor.GetName() == "" { + return apierrors.NewInvalid( + obj.GetObjectKind().GroupVersionKind().GroupKind(), + accessor.GetName(), + field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")}) + } + + gvk := obj.GetObjectKind().GroupVersionKind() + if gvk.Empty() { + gvk, err = apiutil.GVKForObject(obj, t.scheme) + if err != nil { + return err + } + } + + oldObject, err := t.ObjectTracker.Get(gvr, ns, accessor.GetName()) + if err != nil { + // If the resource is not found and the resource allows create on update, issue a + // create instead. + if apierrors.IsNotFound(err) && allowsCreateOnUpdate(gvk) { + return t.Create(gvr, obj, ns) + } + return err + } + + oldAccessor, err := meta.Accessor(oldObject) + if err != nil { + return err + } + + // If the new object does not have the resource version set and it allows unconditional update, + // default it to the resource version of the existing resource + if accessor.GetResourceVersion() == "" && allowsUnconditionalUpdate(gvk) { + accessor.SetResourceVersion(oldAccessor.GetResourceVersion()) + } + if accessor.GetResourceVersion() != oldAccessor.GetResourceVersion() { + return apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), errors.New("object was modified")) + } + if oldAccessor.GetResourceVersion() == "" { + oldAccessor.SetResourceVersion("0") + } + intResourceVersion, err := strconv.ParseUint(oldAccessor.GetResourceVersion(), 10, 64) + if err != nil { + return fmt.Errorf("can not convert resourceVersion %q to int: %v", oldAccessor.GetResourceVersion(), err) + } + intResourceVersion++ + accessor.SetResourceVersion(strconv.FormatUint(intResourceVersion, 10)) + return t.ObjectTracker.Update(gvr, obj, ns) +} + +func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error { + gvr, err := getGVRFromObject(obj, c.scheme) + if err != nil { + return err + } + o, err := c.tracker.Get(gvr, key.Namespace, key.Name) + if err != nil { + return err + } + + gvk, err := apiutil.GVKForObject(obj, c.scheme) + if err != nil { + return err + } + ta, err := meta.TypeAccessor(o) + if err != nil { + return err + } + ta.SetKind(gvk.Kind) + ta.SetAPIVersion(gvk.GroupVersion().String()) + + j, err := json.Marshal(o) + if err != nil { + return err + } + decoder := scheme.Codecs.UniversalDecoder() + _, _, err = decoder.Decode(j, nil, obj) + return err +} + +func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...client.ListOption) error { + gvk, err := apiutil.GVKForObject(obj, c.scheme) + if err != nil { + return err + } + + OriginalKind := gvk.Kind + + if !strings.HasSuffix(gvk.Kind, "List") { + return fmt.Errorf("non-list type %T (kind %q) passed as output", obj, gvk) + } + // we need the non-list GVK, so chop off the "List" from the end of the kind + gvk.Kind = gvk.Kind[:len(gvk.Kind)-4] + + listOpts := client.ListOptions{} + listOpts.ApplyOptions(opts) + + gvr, _ := meta.UnsafeGuessKindToResource(gvk) + o, err := c.tracker.List(gvr, gvk, listOpts.Namespace) + if err != nil { + return err + } + + ta, err := meta.TypeAccessor(o) + if err != nil { + return err + } + ta.SetKind(OriginalKind) + ta.SetAPIVersion(gvk.GroupVersion().String()) + + j, err := json.Marshal(o) + if err != nil { + return err + } + decoder := scheme.Codecs.UniversalDecoder() + _, _, err = decoder.Decode(j, nil, obj) + if err != nil { + return err + } + + if listOpts.LabelSelector != nil { + objs, err := meta.ExtractList(obj) + if err != nil { + return err + } + filteredObjs, err := objectutil.FilterWithLabels(objs, listOpts.LabelSelector) + if err != nil { + return err + } + err = meta.SetList(obj, filteredObjs) + if err != nil { + return err + } + } + return nil +} + +func (c *fakeClient) Scheme() *runtime.Scheme { + return c.scheme +} + +func (c *fakeClient) RESTMapper() meta.RESTMapper { + // TODO: Implement a fake RESTMapper. + return nil +} + +func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { + createOptions := &client.CreateOptions{} + createOptions.ApplyOptions(opts) + + for _, dryRunOpt := range createOptions.DryRun { + if dryRunOpt == metav1.DryRunAll { + return nil + } + } + + gvr, err := getGVRFromObject(obj, c.scheme) + if err != nil { + return err + } + accessor, err := meta.Accessor(obj) + if err != nil { + return err + } + + if accessor.GetName() == "" && accessor.GetGenerateName() != "" { + base := accessor.GetGenerateName() + if len(base) > maxGeneratedNameLength { + base = base[:maxGeneratedNameLength] + } + accessor.SetName(fmt.Sprintf("%s%s", base, utilrand.String(randomLength))) + } + + return c.tracker.Create(gvr, obj, accessor.GetNamespace()) +} + +func (c *fakeClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { + gvr, err := getGVRFromObject(obj, c.scheme) + if err != nil { + return err + } + accessor, err := meta.Accessor(obj) + if err != nil { + return err + } + delOptions := client.DeleteOptions{} + delOptions.ApplyOptions(opts) + + //TODO: implement propagation + return c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName()) +} + +func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { + gvk, err := apiutil.GVKForObject(obj, c.scheme) + if err != nil { + return err + } + + dcOptions := client.DeleteAllOfOptions{} + dcOptions.ApplyOptions(opts) + + gvr, _ := meta.UnsafeGuessKindToResource(gvk) + o, err := c.tracker.List(gvr, gvk, dcOptions.Namespace) + if err != nil { + return err + } + + objs, err := meta.ExtractList(o) + if err != nil { + return err + } + filteredObjs, err := objectutil.FilterWithLabels(objs, dcOptions.LabelSelector) + if err != nil { + return err + } + for _, o := range filteredObjs { + accessor, err := meta.Accessor(o) + if err != nil { + return err + } + err = c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName()) + if err != nil { + return err + } + } + return nil +} + +func (c *fakeClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + updateOptions := &client.UpdateOptions{} + updateOptions.ApplyOptions(opts) + + for _, dryRunOpt := range updateOptions.DryRun { + if dryRunOpt == metav1.DryRunAll { + return nil + } + } + + gvr, err := getGVRFromObject(obj, c.scheme) + if err != nil { + return err + } + accessor, err := meta.Accessor(obj) + if err != nil { + return err + } + return c.tracker.Update(gvr, obj, accessor.GetNamespace()) +} + +func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { + patchOptions := &client.PatchOptions{} + patchOptions.ApplyOptions(opts) + + for _, dryRunOpt := range patchOptions.DryRun { + if dryRunOpt == metav1.DryRunAll { + return nil + } + } + + gvr, err := getGVRFromObject(obj, c.scheme) + if err != nil { + return err + } + accessor, err := meta.Accessor(obj) + if err != nil { + return err + } + data, err := patch.Data(obj) + if err != nil { + return err + } + + reaction := testing.ObjectReaction(c.tracker) + handled, o, err := reaction(testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), data)) + if err != nil { + return err + } + if !handled { + panic("tracker could not handle patch method") + } + + gvk, err := apiutil.GVKForObject(obj, c.scheme) + if err != nil { + return err + } + ta, err := meta.TypeAccessor(o) + if err != nil { + return err + } + ta.SetKind(gvk.Kind) + ta.SetAPIVersion(gvk.GroupVersion().String()) + + j, err := json.Marshal(o) + if err != nil { + return err + } + decoder := scheme.Codecs.UniversalDecoder() + _, _, err = decoder.Decode(j, nil, obj) + return err +} + +func (c *fakeClient) Status() client.StatusWriter { + return &fakeStatusWriter{client: c} +} + +func getGVRFromObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersionResource, error) { + gvk, err := apiutil.GVKForObject(obj, scheme) + if err != nil { + return schema.GroupVersionResource{}, err + } + gvr, _ := meta.UnsafeGuessKindToResource(gvk) + return gvr, nil +} + +type fakeStatusWriter struct { + client *fakeClient +} + +func (sw *fakeStatusWriter) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + // TODO(droot): This results in full update of the obj (spec + status). Need + // a way to update status field only. + return sw.client.Update(ctx, obj, opts...) +} + +func (sw *fakeStatusWriter) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { + // TODO(droot): This results in full update of the obj (spec + status). Need + // a way to update status field only. + return sw.client.Patch(ctx, obj, patch, opts...) +} + +func allowsUnconditionalUpdate(gvk schema.GroupVersionKind) bool { + switch gvk.Group { + case "apps": + switch gvk.Kind { + case "ControllerRevision", "DaemonSet", "Deployment", "ReplicaSet", "StatefulSet": + return true + } + case "autoscaling": + switch gvk.Kind { + case "HorizontalPodAutoscaler": + return true + } + case "batch": + switch gvk.Kind { + case "CronJob", "Job": + return true + } + case "certificates": + switch gvk.Kind { + case "Certificates": + return true + } + case "flowcontrol": + switch gvk.Kind { + case "FlowSchema", "PriorityLevelConfiguration": + return true + } + case "networking": + switch gvk.Kind { + case "Ingress", "IngressClass", "NetworkPolicy": + return true + } + case "policy": + switch gvk.Kind { + case "PodSecurityPolicy": + return true + } + case "rbac": + switch gvk.Kind { + case "ClusterRole", "ClusterRoleBinding", "Role", "RoleBinding": + return true + } + case "scheduling": + switch gvk.Kind { + case "PriorityClass": + return true + } + case "settings": + switch gvk.Kind { + case "PodPreset": + return true + } + case "storage": + switch gvk.Kind { + case "StorageClass": + return true + } + case "": + switch gvk.Kind { + case "ConfigMap", "Endpoint", "Event", "LimitRange", "Namespace", "Node", + "PersistentVolume", "PersistentVolumeClaim", "Pod", "PodTemplate", + "ReplicationController", "ResourceQuota", "Secret", "Service", + "ServiceAccount", "EndpointSlice": + return true + } + } + + return false +} + +func allowsCreateOnUpdate(gvk schema.GroupVersionKind) bool { + switch gvk.Group { + case "coordination": + switch gvk.Kind { + case "Lease": + return true + } + case "node": + switch gvk.Kind { + case "RuntimeClass": + return true + } + case "rbac": + switch gvk.Kind { + case "ClusterRole", "ClusterRoleBinding", "Role", "RoleBinding": + return true + } + case "": + switch gvk.Kind { + case "Endpoint", "Event", "LimitRange", "Service": + return true + } + } + + return false +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/doc.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/doc.go new file mode 100644 index 000000000..7d680690d --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/doc.go @@ -0,0 +1,39 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Package fake provides a fake client for testing. + +A fake client is backed by its simple object store indexed by GroupVersionResource. +You can create a fake client with optional objects. + + client := NewFakeClientWithScheme(scheme, initObjs...) // initObjs is a slice of runtime.Object + +You can invoke the methods defined in the Client interface. + +When in doubt, it's almost always better not to use this package and instead use +envtest.Environment with a real client and API server. + +WARNING: ⚠️ Current Limitations / Known Issues with the fake Client ⚠️ +- This client does not have a way to inject specific errors to test handled vs. unhandled errors. +- There is some support for sub resources which can cause issues with tests if you're trying to update + e.g. metadata and status in the same reconcile. +- No OpeanAPI validation is performed when creating or updating objects. +- ObjectMeta's `Generation` and `ResourceVersion` don't behave properly, Patch or Update +operations that rely on these fields will fail, or give false positives. + +*/ +package fake diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/objectutil/filter.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/objectutil/filter.go new file mode 100644 index 000000000..8513846e2 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/objectutil/filter.go @@ -0,0 +1,42 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package objectutil + +import ( + apimeta "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +// FilterWithLabels returns a copy of the items in objs matching labelSel +func FilterWithLabels(objs []runtime.Object, labelSel labels.Selector) ([]runtime.Object, error) { + outItems := make([]runtime.Object, 0, len(objs)) + for _, obj := range objs { + meta, err := apimeta.Accessor(obj) + if err != nil { + return nil, err + } + if labelSel != nil { + lbls := labels.Set(meta.GetLabels()) + if !labelSel.Matches(lbls) { + continue + } + } + outItems = append(outItems, obj.DeepCopyObject()) + } + return outItems, nil +}