From 2b9afb2dd4777f63218a7f8becfa2cbffa903c8b Mon Sep 17 00:00:00 2001 From: John Starich Date: Tue, 1 Sep 2020 20:03:00 -0500 Subject: [PATCH] Add binding controller unit tests (#190) Closes #174 Signed-off-by: John Starich --- Makefile | 2 +- controllers/binding_controller.go | 49 +- controllers/binding_controller_test.go | 2162 ++++++++++++++++++++++++ controllers/mock_client_test.go | 124 ++ controllers/suite_config_test.go | 11 + controllers/suite_test.go | 10 +- controllers/token_controller_test.go | 11 +- go.mod | 2 +- go.sum | 4 + internal/config/config.go | 15 +- internal/ibmcloud/ibmcloud.go | 9 +- main.go | 10 +- 12 files changed, 2367 insertions(+), 42 deletions(-) create mode 100644 controllers/mock_client_test.go diff --git a/Makefile b/Makefile index 0f59bdeb..4a3de9b6 100644 --- a/Makefile +++ b/Makefile @@ -67,7 +67,7 @@ cache/bin/kustomize: cache/bin .PHONY: test test: generate manifests kubebuilder - go test ./... -coverprofile cover.out + go test -race -coverprofile cover.out ./... .PHONY: test-e2e test-e2e: diff --git a/controllers/binding_controller.go b/controllers/binding_controller.go index ace33e26..9cbe92ef 100644 --- a/controllers/binding_controller.go +++ b/controllers/binding_controller.go @@ -39,7 +39,6 @@ import ( "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) const ( @@ -47,6 +46,7 @@ const ( inProgress = "IN PROGRESS" notFound = "Not Found" idkey = "ibmcloud.ibm.com/keyId" + requeueFast = 10 * time.Second ) const ( @@ -68,13 +68,19 @@ type BindingReconciler struct { CreateCFServiceKey cfservice.KeyCreator DeleteResourceServiceKey resource.KeyDeleter DeleteCFServiceKey cfservice.KeyDeleter + GetIBMCloudInfo IBMCloudInfoGetter GetResourceServiceKey resource.KeyGetter GetServiceInstanceCRN resource.ServiceInstanceCRNGetter GetCFServiceKeyCredentials cfservice.KeyGetter GetServiceName resource.ServiceNameGetter GetServiceRoleCRN iam.ServiceRolesGetter + SetControllerReference ControllerReferenceSetter } +type ControllerReferenceSetter func(owner, controlled metav1.Object, scheme *runtime.Scheme) error + +type IBMCloudInfoGetter func(logt logr.Logger, r client.Client, instance *ibmcloudv1beta1.Service) (*ibmcloud.Info, error) + func (r *BindingReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&ibmcloudv1beta1.Binding{}). @@ -113,8 +119,9 @@ func (r *BindingReconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) if reflect.DeepEqual(instance.Status, ibmcloudv1beta1.BindingStatus{}) { instance.Status.State = bindingStatePending instance.Status.Message = "Processing Resource" - if err := r.Status().Update(context.Background(), instance); err != nil { + if err := r.Status().Update(ctx, instance); err != nil { logt.Info("Binding could not update Status", instance.Name, err.Error()) + // TODO(johnstarich): Shouldn't this be a failure so it can be requeued? return ctrl.Result{}, nil } } @@ -130,7 +137,7 @@ func (r *BindingReconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) // the credentials do not exist on the cloud, since the service cannot be found. // Also by removing the Binding instance, any correponding secret will also be deleted by Kubernetes. instance.ObjectMeta.Finalizers = deleteBindingFinalizer(instance) - if err := r.Update(context.Background(), instance); err != nil { + if err := r.Update(ctx, instance); err != nil { logt.Info("Error removing finalizers", "in deletion", err.Error()) // No further action required, object was modified, another reconcile will finish the job. } @@ -142,17 +149,17 @@ func (r *BindingReconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) return r.resetResource(instance) } - return ctrl.Result{Requeue: true, RequeueAfter: time.Second * 10}, nil //Requeue fast + return ctrl.Result{Requeue: true, RequeueAfter: requeueFast}, nil } // Set an owner reference if service and binding are in the same namespace if serviceInstance.Namespace == instance.Namespace { - if err := controllerutil.SetControllerReference(serviceInstance, instance, r.Scheme); err != nil { - logt.Info("Binding could not update constroller reference", instance.Name, err.Error()) + if err := r.SetControllerReference(serviceInstance, instance, r.Scheme); err != nil { + logt.Info("Binding could not update controller reference", instance.Name, err.Error()) return ctrl.Result{}, err } - if err := r.Update(context.Background(), instance); err != nil { + if err := r.Update(ctx, instance); err != nil { logt.Info("Error setting controller reference", instance.Name, err.Error()) return ctrl.Result{}, nil } @@ -162,20 +169,20 @@ func (r *BindingReconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) if serviceInstance.Status.InstanceID == "" || serviceInstance.Status.InstanceID == inProgress { // The parent service has not been initialized fully yet logt.Info("Parent service", "not yet initialized", instance.Name) - return ctrl.Result{Requeue: true, RequeueAfter: time.Second * 10}, nil //Requeue fast + return ctrl.Result{Requeue: true, RequeueAfter: requeueFast}, nil } var serviceClassType string var session *session.Session { - ibmCloudInfo, err := ibmcloud.GetInfo(logt, r.Client, serviceInstance) + ibmCloudInfo, err := r.GetIBMCloudInfo(logt, r.Client, serviceInstance) if err != nil { - logt.Info("Unable to get", "ibmcloudInfo", instance.Name) + logt.Info("Unable to get IBM Cloud info", "ibmcloudInfo", instance.Name) if errors.IsNotFound(err) && containsBindingFinalizer(instance) && !instance.ObjectMeta.DeletionTimestamp.IsZero() { logt.Info("Cannot get IBMCloud related secrets and configmaps, just remove finalizers", "in deletion", err.Error()) instance.ObjectMeta.Finalizers = deleteBindingFinalizer(instance) - if err := r.Update(context.Background(), instance); err != nil { + if err := r.Update(ctx, instance); err != nil { logt.Info("Error removing finalizers", "in deletion", err.Error()) } return ctrl.Result{}, nil @@ -192,7 +199,7 @@ func (r *BindingReconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) // Instance is not being deleted, add the finalizer if not present if !containsBindingFinalizer(instance) { instance.ObjectMeta.Finalizers = append(instance.ObjectMeta.Finalizers, bindingFinalizer) - if err := r.Update(context.Background(), instance); err != nil { + if err := r.Update(ctx, instance); err != nil { logt.Info("Error adding finalizer", instance.Name, err.Error()) return ctrl.Result{}, nil } @@ -204,12 +211,12 @@ func (r *BindingReconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) err := r.deleteCredentials(session, instance, serviceClassType) if err != nil { logt.Info("Error deleting credentials", "in deletion", err.Error()) - return ctrl.Result{Requeue: true, RequeueAfter: time.Second * 10}, nil + return ctrl.Result{Requeue: true, RequeueAfter: requeueFast}, nil } // remove our finalizer from the list and update it. instance.ObjectMeta.Finalizers = deleteBindingFinalizer(instance) - if err := r.Update(context.Background(), instance); err != nil { + if err := r.Update(ctx, instance); err != nil { logt.Info("Error removing finalizers", "in deletion", err.Error()) } return ctrl.Result{}, nil @@ -234,8 +241,9 @@ func (r *BindingReconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) // Now instance.Status.IntanceID has been set properly if instance.Status.KeyInstanceID == "" { // The KeyInstanceID has not been set, need to create the key instance.Status.KeyInstanceID = inProgress - if err := r.Status().Update(context.Background(), instance); err != nil { + if err := r.Status().Update(ctx, instance); err != nil { logt.Info("Error updating KeyInstanceID to be in progress", "Error", err.Error()) + // TODO(johnstarich): Shouldn't this be a failure so it can be requeued? return ctrl.Result{}, nil } @@ -291,6 +299,8 @@ func (r *BindingReconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) return r.updateStatusError(instance, bindingStateFailed, err) } instance.Status.KeyInstanceID = keyInstanceID + } else if err != nil { + logt.Error(err, "Failed to fetch credentials") // TODO(johnstarich): should this fail and requeue? } } secret, err := getSecret(r, instance) @@ -310,7 +320,9 @@ func (r *BindingReconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) logt.Info("Error checking if key contents have changed", instance.Name, err.Error()) return r.updateStatusError(instance, bindingStateFailed, err) } - if instance.Status.KeyInstanceID != secret.Annotations["service-key-id"] || changed { // Warning: the deep comparison may not be needed, the key is probably enough + instanceIDMismatch := instance.Status.KeyInstanceID != secret.Annotations["service-key-id"] + if instanceIDMismatch || changed { // Warning: the deep comparison may not be needed, the key is probably enough + logt.Info("Updating secret", "key contents changed", changed, "status key ID and annotation mismatch", instanceIDMismatch) err := r.deleteSecret(instance) if err != nil { logt.Info("Error deleting secret before recreating", instance.Name, err.Error()) @@ -355,6 +367,7 @@ func (r *BindingReconciler) resetResource(instance *ibmcloudv1beta1.Binding) (ct instance.Status.SecretName = "" if err := r.Status().Update(context.Background(), instance); err != nil { r.Log.Info("Binding could not reset Status", instance.Name, err.Error()) + // TODO(johnstarich): Shouldn't this be a failure so it can be requeued? return ctrl.Result{}, nil } return ctrl.Result{Requeue: true, RequeueAfter: config.Get().SyncPeriod}, nil @@ -421,7 +434,7 @@ func (r *BindingReconciler) getAliasCredentials(logt logr.Logger, session *sessi } if keyName != name { // alias name and keyid annotations are inconsistent - return "", nil, fmt.Errorf("Alias credential name and keyid do not match") + return "", nil, fmt.Errorf("alias credential name and keyid do not match. Key name: %q, Alias name: %q", keyName, name) } _, contentsContainRedacted := credentials["REDACTED"] @@ -483,7 +496,7 @@ func (r *BindingReconciler) createSecret(instance *ibmcloudv1beta1.Binding, keyC }, Data: datamap, } - if err := controllerutil.SetControllerReference(instance, secret, r.Scheme); err != nil { + if err := r.SetControllerReference(instance, secret, r.Scheme); err != nil { return err } if err := r.Create(context.Background(), secret); err != nil { diff --git a/controllers/binding_controller_test.go b/controllers/binding_controller_test.go index b29788e6..261214ff 100644 --- a/controllers/binding_controller_test.go +++ b/controllers/binding_controller_test.go @@ -2,17 +2,30 @@ package controllers import ( "context" + "encoding/json" + "fmt" "io/ioutil" "testing" + "time" + "github.com/IBM-Cloud/bluemix-go/crn" + "github.com/IBM-Cloud/bluemix-go/session" "github.com/ghodss/yaml" + "github.com/go-logr/logr" ibmcloudv1beta1 "github.com/ibm/cloud-operators/api/v1beta1" + "github.com/ibm/cloud-operators/internal/config" + "github.com/ibm/cloud-operators/internal/ibmcloud" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func mustLoadObject(t *testing.T, file string, obj runtime.Object, meta *metav1.ObjectMeta) { @@ -88,3 +101,2152 @@ func TestBinding(t *testing.T) { }, defaultWait, defaultTick) }) } + +func TestBindingFailedLookup(t *testing.T) { + t.Parallel() + scheme := schemas(t) + r := &BindingReconciler{ + Client: fake.NewFakeClientWithScheme(scheme), + Log: testLogger(t), + Scheme: scheme, + } + + t.Run("not found", func(t *testing.T) { + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: "mybinding"}, + }) + assert.NoError(t, err, "Don't retry (return err) if binding does not exist") + assert.Equal(t, ctrl.Result{}, result) + }) + + r.Client = fake.NewFakeClientWithScheme(runtime.NewScheme()) // fail to read the type Binding + t.Run("failed to read binding", func(t *testing.T) { + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: "mybinding"}, + }) + assert.Error(t, err) + assert.False(t, k8sErrors.IsNotFound(err)) + assert.Equal(t, ctrl.Result{}, result) + }) +} + +func TestBindingFailInitialStatus(t *testing.T) { + t.Parallel() + scheme := schemas(t) + objects := []runtime.Object{ + &ibmcloudv1beta1.Binding{ + ObjectMeta: metav1.ObjectMeta{Name: "mybinding"}, + Status: ibmcloudv1beta1.BindingStatus{}, // empty + }, + } + client := fake.NewFakeClientWithScheme(scheme, objects...) + client = newMockClient(client, MockConfig{ + StatusUpdateErr: fmt.Errorf("failed"), + }) + r := &BindingReconciler{ + Client: client, + Log: testLogger(t), + Scheme: scheme, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: "mybinding"}, + }) + assert.NoError(t, err, "Don't retry (return err) if binding does not exist") + assert.Equal(t, ctrl.Result{}, result) +} + +func TestBindingFailGetServiceInstance(t *testing.T) { + t.Parallel() + now := metav1Now(t) + for _, tc := range []struct { + description string + binding *ibmcloudv1beta1.Binding + fakeClient *MockConfig + expectUpdate *ibmcloudv1beta1.Binding + expectStatusUpdate *ibmcloudv1beta1.Binding + expectResult ctrl.Result + }{ + { + description: "no service instance", + binding: &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: "mybinding"}, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: "myservice", + }, + }, + expectResult: ctrl.Result{ + Requeue: true, + RequeueAfter: requeueFast, + }, + }, + { + description: "binding is deleting", + binding: &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "mybinding", + DeletionTimestamp: now, + Finalizers: []string{bindingFinalizer}, + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: "myservice", + }, + Status: ibmcloudv1beta1.BindingStatus{State: bindingStateOnline}, + }, + fakeClient: &MockConfig{}, + expectUpdate: &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "mybinding", + DeletionTimestamp: now, + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: "myservice", + }, + Status: ibmcloudv1beta1.BindingStatus{State: bindingStateOnline}, + }, + expectResult: ctrl.Result{}, + }, + { + description: "binding is deleting but update fails", + binding: &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "mybinding", + DeletionTimestamp: now, + Finalizers: []string{bindingFinalizer}, + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: "myservice", + }, + Status: ibmcloudv1beta1.BindingStatus{State: bindingStateOnline}, + }, + fakeClient: &MockConfig{UpdateErr: fmt.Errorf("failed")}, + expectUpdate: &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "mybinding", + DeletionTimestamp: now, + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: "myservice", + }, + Status: ibmcloudv1beta1.BindingStatus{State: bindingStateOnline}, + }, + expectResult: ctrl.Result{}, + }, + { + description: "binding is deleting and status service instance is set", + binding: &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "mybinding", + Finalizers: []string{bindingFinalizer}, + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: "myservice", + }, + Status: ibmcloudv1beta1.BindingStatus{ + State: bindingStateOnline, + KeyInstanceID: "myinstance", + }, + }, + fakeClient: &MockConfig{}, + expectStatusUpdate: &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "mybinding", + Finalizers: []string{bindingFinalizer}, + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: "myservice", + }, + Status: ibmcloudv1beta1.BindingStatus{ + State: bindingStatePending, + Message: "Processing Resource", + }, + }, + expectResult: ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, + }, + } { + t.Run(tc.description, func(t *testing.T) { + scheme := schemas(t) + r := &BindingReconciler{ + Client: fake.NewFakeClientWithScheme(scheme, tc.binding), + Log: testLogger(t), + Scheme: scheme, + } + if tc.fakeClient != nil { + r.Client = newMockClient(r.Client, *tc.fakeClient) + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: "mybinding"}, + }) + assert.NoError(t, err) + assert.Equal(t, tc.expectResult, result) + if tc.expectUpdate != nil { + assert.Equal(t, tc.expectUpdate, r.Client.(MockClient).LastUpdate(), "Binding update should be equal") + } + if tc.expectStatusUpdate != nil { + assert.Equal(t, tc.expectStatusUpdate, r.Client.(MockClient).LastStatusUpdate(), "Binding status update should be equal") + } + }) + } +} + +func TestBindingSetOwnerReferenceFailed(t *testing.T) { + t.Parallel() + t.Run("setting owner reference failed", func(t *testing.T) { + scheme := schemas(t) + const namespace = "mynamespace" + objects := []runtime.Object{ + &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: "mybinding", Namespace: namespace}, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: "myservice", + }, + }, + &ibmcloudv1beta1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: "myservice", Namespace: namespace}, + }, + } + r := &BindingReconciler{ + Client: fake.NewFakeClientWithScheme(scheme, objects...), + Log: testLogger(t), + Scheme: scheme, + + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return fmt.Errorf("failed") + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: "mybinding", Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{}, result) + assert.EqualError(t, err, "failed") + }) + + t.Run("binding update failed", func(t *testing.T) { + scheme := schemas(t) + const namespace = "mynamespace" + objects := []runtime.Object{ + &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: "mybinding", Namespace: namespace}, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: "myservice", + }, + Status: ibmcloudv1beta1.BindingStatus{ + State: bindingStateOnline, + }, + }, + &ibmcloudv1beta1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: "myservice", Namespace: namespace}, + }, + } + client := newMockClient( + fake.NewFakeClientWithScheme(scheme, objects...), + MockConfig{ + UpdateErr: fmt.Errorf("failed"), + }) + r := &BindingReconciler{ + Client: client, + Log: testLogger(t), + Scheme: scheme, + + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: "mybinding", Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{}, result) + assert.NoError(t, err) + assert.Equal(t, &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: "mybinding", Namespace: namespace}, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: "myservice", + }, + Status: ibmcloudv1beta1.BindingStatus{ + State: bindingStateOnline, + }, + }, client.LastUpdate()) + }) +} + +func TestBindingServiceIsNotReady(t *testing.T) { + t.Parallel() + t.Run("empty instance ID", func(t *testing.T) { + scheme := schemas(t) + const namespace = "mynamespace" + objects := []runtime.Object{ + &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: "mybinding", Namespace: namespace}, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: "myservice", + }, + }, + &ibmcloudv1beta1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: "myservice", Namespace: namespace}, + Status: ibmcloudv1beta1.ServiceStatus{ + InstanceID: "", + }, + }, + } + r := &BindingReconciler{ + Client: fake.NewFakeClientWithScheme(scheme, objects...), + Log: testLogger(t), + Scheme: scheme, + + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: "mybinding", Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{ + Requeue: true, + RequeueAfter: requeueFast, + }, result) + assert.NoError(t, err) + }) + + t.Run("status instance ID is in progress", func(t *testing.T) { + scheme := schemas(t) + const namespace = "mynamespace" + objects := []runtime.Object{ + &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: "mybinding", Namespace: namespace}, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: "myservice", + }, + }, + &ibmcloudv1beta1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: "myservice", Namespace: namespace}, + Status: ibmcloudv1beta1.ServiceStatus{ + InstanceID: inProgress, + }, + }, + } + r := &BindingReconciler{ + Client: fake.NewFakeClientWithScheme(scheme, objects...), + Log: testLogger(t), + Scheme: scheme, + + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: "mybinding", Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{ + Requeue: true, + RequeueAfter: requeueFast, + }, result) + assert.NoError(t, err) + }) +} + +func TestBindingGetIBMCloudInfoFailed(t *testing.T) { + t.Parallel() + now := metav1Now(t) + scheme := schemas(t) + const ( + namespace = "mynamespace" + bindingName = "mybinding" + serviceName = "myservice" + someInstanceID = "some-instance-id" + ) + objects := []runtime.Object{ + &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: namespace, + DeletionTimestamp: now, + Finalizers: []string{bindingFinalizer}, + }, + Spec: ibmcloudv1beta1.BindingSpec{ServiceName: serviceName}, + Status: ibmcloudv1beta1.BindingStatus{State: bindingStateFailed}, + }, + &ibmcloudv1beta1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: namespace}, + Status: ibmcloudv1beta1.ServiceStatus{ + InstanceID: someInstanceID, + }, + }, + } + + t.Run("not found error", func(t *testing.T) { + var r *BindingReconciler + r = &BindingReconciler{ + Client: newMockClient(fake.NewFakeClientWithScheme(scheme, objects...), MockConfig{}), + Log: testLogger(t), + Scheme: scheme, + + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + GetIBMCloudInfo: func(logt logr.Logger, _ client.Client, instance *ibmcloudv1beta1.Service) (*ibmcloud.Info, error) { + r.Client = newMockClient( // swap out client so next update fails + fake.NewFakeClientWithScheme(scheme, objects...), + MockConfig{UpdateErr: fmt.Errorf("failed")}, + ) + return nil, errors.NewNotFound(ctrl.GroupResource{Group: "ibmcloud.ibm.com", Resource: "secret"}, "secret-ibm-cloud-operator") + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: bindingName, Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{}, result) + assert.NoError(t, err) + assert.Equal(t, &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: namespace, + DeletionTimestamp: now, + Finalizers: nil, // attempt to remove finalizers + }, + Spec: ibmcloudv1beta1.BindingSpec{ServiceName: serviceName}, + Status: ibmcloudv1beta1.BindingStatus{State: bindingStateFailed}, + }, r.Client.(MockClient).LastUpdate()) + assert.Equal(t, nil, r.Client.(MockClient).LastStatusUpdate()) + }) + + t.Run("other error", func(t *testing.T) { + r := &BindingReconciler{ + Client: newMockClient(fake.NewFakeClientWithScheme(scheme, objects...), MockConfig{}), + Log: testLogger(t), + Scheme: scheme, + + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + GetIBMCloudInfo: func(logt logr.Logger, r client.Client, instance *ibmcloudv1beta1.Service) (*ibmcloud.Info, error) { + return nil, fmt.Errorf("failed") + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: bindingName, Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, result) + assert.NoError(t, err) + assert.Equal(t, &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: namespace, + DeletionTimestamp: now, + Finalizers: []string{bindingFinalizer}, + }, + Spec: ibmcloudv1beta1.BindingSpec{ServiceName: serviceName}, + Status: ibmcloudv1beta1.BindingStatus{ + State: bindingStatePending, + Message: "failed", + }, + }, r.Client.(MockClient).LastStatusUpdate()) + }) +} + +func TestBindingDeletesWithFinalizerFailed(t *testing.T) { + t.Parallel() + now := metav1Now(t) + + t.Run("deleting credentials failed", func(t *testing.T) { + scheme := schemas(t) + const ( + namespace = "mynamespace" + secretName = "mysecret" + bindingName = "mybinding" + serviceName = "myservice" + someInstanceID = "some-instance-id" + ) + objects := []runtime.Object{ + &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: namespace, + DeletionTimestamp: now, + Finalizers: []string{bindingFinalizer}, + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: serviceName, + Alias: "some-binding-alias", // use alias plan to mock fewer dependencies during delete creds + SecretName: secretName, + }, + }, + &ibmcloudv1beta1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: namespace}, + Status: ibmcloudv1beta1.ServiceStatus{ + InstanceID: someInstanceID, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: namespace}, + }, + } + fakeClient := newMockClient( + fake.NewFakeClientWithScheme(scheme, objects...), + MockConfig{DeleteErr: fmt.Errorf("failed")}, + ) + r := &BindingReconciler{ + Client: fakeClient, + Log: testLogger(t), + Scheme: scheme, + + GetIBMCloudInfo: func(logt logr.Logger, r client.Client, instance *ibmcloudv1beta1.Service) (*ibmcloud.Info, error) { + return &ibmcloud.Info{}, nil + }, + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: bindingName, Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{ + Requeue: true, + RequeueAfter: requeueFast, + }, result) + assert.NoError(t, err) + assert.Equal(t, &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: namespace}, + }, fakeClient.LastDelete()) + }) + + t.Run("removing finalizer failed", func(t *testing.T) { + scheme := schemas(t) + const ( + namespace = "mynamespace" + secretName = "mysecret" + bindingName = "mybinding" + serviceName = "myservice" + someInstanceID = "some-instance-id" + ) + objects := []runtime.Object{ + &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: namespace, + DeletionTimestamp: now, + Finalizers: []string{bindingFinalizer}, + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: serviceName, + Alias: "some-binding-alias", // use alias plan to mock fewer dependencies during delete creds + SecretName: secretName, + }, + Status: ibmcloudv1beta1.BindingStatus{State: bindingStatePending}, + }, + &ibmcloudv1beta1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: namespace}, + Status: ibmcloudv1beta1.ServiceStatus{ + InstanceID: someInstanceID, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: namespace}, + }, + } + var r *BindingReconciler + r = &BindingReconciler{ + Client: fake.NewFakeClientWithScheme(scheme, objects...), + Log: testLogger(t), + Scheme: scheme, + + GetIBMCloudInfo: func(logt logr.Logger, _ client.Client, instance *ibmcloudv1beta1.Service) (*ibmcloud.Info, error) { + r.Client = newMockClient( // swap out client so next update fails + fake.NewFakeClientWithScheme(scheme, objects...), + MockConfig{UpdateErr: fmt.Errorf("failed")}, + ) + return &ibmcloud.Info{}, nil + }, + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: bindingName, Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{}, result) + assert.NoError(t, err) + assert.Equal(t, &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: namespace, + DeletionTimestamp: now, + Finalizers: nil, // attempt to remove finalizers + ResourceVersion: "1", + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: serviceName, + Alias: "some-binding-alias", + SecretName: secretName, + }, + Status: ibmcloudv1beta1.BindingStatus{State: bindingStatePending}, + }, r.Client.(MockClient).LastUpdate()) + }) +} + +func TestBindingDeletesMissingFinalizerFailed(t *testing.T) { + t.Parallel() + scheme := schemas(t) + const ( + namespace = "mynamespace" + secretName = "mysecret" + bindingName = "mybinding" + serviceName = "myservice" + someInstanceID = "some-instance-id" + ) + objects := []runtime.Object{ + &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: namespace, + DeletionTimestamp: nil, // not deleting + Finalizers: nil, // AND missing finalizer + }, + Spec: ibmcloudv1beta1.BindingSpec{ServiceName: serviceName}, + Status: ibmcloudv1beta1.BindingStatus{State: bindingStatePending}, + }, + &ibmcloudv1beta1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: namespace}, + Status: ibmcloudv1beta1.ServiceStatus{ + InstanceID: someInstanceID, + }, + }, + } + var r *BindingReconciler + r = &BindingReconciler{ + Client: fake.NewFakeClientWithScheme(scheme, objects...), + Log: testLogger(t), + Scheme: scheme, + + GetIBMCloudInfo: func(logt logr.Logger, _ client.Client, instance *ibmcloudv1beta1.Service) (*ibmcloud.Info, error) { + r.Client = newMockClient( // swap out client so next update fails + fake.NewFakeClientWithScheme(scheme, objects...), + MockConfig{UpdateErr: fmt.Errorf("failed")}, + ) + return &ibmcloud.Info{}, nil + }, + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: bindingName, Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{}, result) + assert.NoError(t, err) + assert.Equal(t, &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: namespace, + Finalizers: []string{bindingFinalizer}, // added a finalizer + ResourceVersion: "1", + }, + Spec: ibmcloudv1beta1.BindingSpec{ServiceName: serviceName}, + Status: ibmcloudv1beta1.BindingStatus{State: bindingStatePending}, + }, r.Client.(MockClient).LastUpdate()) +} + +func TestBindingDeleteMismatchedServiceIDsSecretFailed(t *testing.T) { + t.Parallel() + scheme := schemas(t) + const ( + namespace = "mynamespace" + secretName = "mysecret" + bindingName = "mybinding" + serviceName = "myservice" + someInstanceID = "some-instance-id" + ) + objects := []runtime.Object{ + &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: namespace, + Finalizers: []string{bindingFinalizer}, + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: serviceName, + Alias: "some-binding-alias", // use alias plan to mock fewer dependencies during delete creds + SecretName: secretName, + }, + Status: ibmcloudv1beta1.BindingStatus{ + State: bindingStatePending, + InstanceID: "a-deleted-instance-id", + SecretName: secretName, + }, + }, + &ibmcloudv1beta1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: namespace}, + Status: ibmcloudv1beta1.ServiceStatus{ + InstanceID: someInstanceID, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: namespace}, + }, + } + var r *BindingReconciler + r = &BindingReconciler{ + Client: fake.NewFakeClientWithScheme(scheme, objects...), + Log: testLogger(t), + Scheme: scheme, + + GetIBMCloudInfo: func(logt logr.Logger, _ client.Client, instance *ibmcloudv1beta1.Service) (*ibmcloud.Info, error) { + r.Client = newMockClient( // swap out client so next delete fails + fake.NewFakeClientWithScheme(scheme, objects...), + MockConfig{DeleteErr: fmt.Errorf("failed")}, + ) + return &ibmcloud.Info{}, nil + }, + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: bindingName, Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, result) + assert.NoError(t, err) + assert.Equal(t, &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: namespace, + Finalizers: []string{bindingFinalizer}, + ResourceVersion: "1", + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: serviceName, + Alias: "some-binding-alias", + SecretName: secretName, + }, + Status: ibmcloudv1beta1.BindingStatus{ + State: bindingStateFailed, // should move to failed state + Message: "failed", + InstanceID: "a-deleted-instance-id", + SecretName: secretName, + }, + }, r.Client.(MockClient).LastStatusUpdate()) + assert.Equal(t, &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: namespace}, + }, r.Client.(MockClient).LastDelete()) +} + +func TestBindingSetKeyInstanceFailed(t *testing.T) { + t.Parallel() + + scheme := schemas(t) + const ( + namespace = "mynamespace" + aliasTargetName = "myBindingToAlias" + secretName = "mysecret" + bindingName = "mybinding" + serviceName = "myservice" + someInstanceID = "some-instance-id" + ) + objects := []runtime.Object{ + &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: namespace, + Finalizers: []string{bindingFinalizer}, + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: serviceName, + SecretName: secretName, + }, + Status: ibmcloudv1beta1.BindingStatus{ + State: bindingStatePending, + InstanceID: someInstanceID, + SecretName: secretName, + }, + }, + &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: aliasTargetName, + Namespace: namespace, + Finalizers: []string{bindingFinalizer}, + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: serviceName, + SecretName: secretName, + }, + Status: ibmcloudv1beta1.BindingStatus{ + State: bindingStatePending, + InstanceID: someInstanceID, + SecretName: secretName, + }, + }, + &ibmcloudv1beta1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: namespace}, + Status: ibmcloudv1beta1.ServiceStatus{ + InstanceID: someInstanceID, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: namespace}, + }, + } + + for _, tc := range []struct { + description string + fakeClient MockConfig + isAlias bool + instanceIDKey bool + createServiceKeyErr error + expectResult ctrl.Result + expectState string + expectMessage string + }{ + { + description: "update status online", + fakeClient: MockConfig{}, + expectResult: ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, + expectState: bindingStateOnline, + expectMessage: bindingStateOnline, + }, + { + description: "fail to update key instance ID to inProgress", + fakeClient: MockConfig{StatusUpdateErr: fmt.Errorf("failed")}, + expectResult: ctrl.Result{}, + expectState: bindingStatePending, + }, + { + description: "missing alias instanceID annotation", + isAlias: true, + fakeClient: MockConfig{}, + expectResult: ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, + expectState: bindingStatePending, + }, + { + description: "update alias online", + isAlias: true, + instanceIDKey: true, + fakeClient: MockConfig{}, + expectResult: ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, + expectState: bindingStateOnline, + expectMessage: bindingStateOnline, + }, + { + description: "fail to create credentials", + fakeClient: MockConfig{}, + createServiceKeyErr: fmt.Errorf("failed"), + expectResult: ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, + expectState: bindingStateFailed, + expectMessage: "failed", + }, + { + description: "fail to create credentials - still in progress", + fakeClient: MockConfig{}, + createServiceKeyErr: fmt.Errorf("still in progress"), + expectResult: ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, + expectState: bindingStatePending, + }, + { + description: "fail to create secret", + fakeClient: MockConfig{CreateErr: fmt.Errorf("failed")}, + expectResult: ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, + expectState: bindingStateFailed, + expectMessage: "failed", + }, + } { + t.Run(tc.description, func(t *testing.T) { + var testObjects []runtime.Object + for _, obj := range objects { + if binding, ok := obj.(*ibmcloudv1beta1.Binding); ok && binding.Name != aliasTargetName { + binding = binding.DeepCopy() + if tc.instanceIDKey { + binding.Annotations = map[string]string{idkey: someInstanceID} + } + if tc.isAlias { + binding.Spec.Alias = aliasTargetName + } + obj = binding + } + testObjects = append(testObjects, obj) + } + + r := &BindingReconciler{ + Client: newMockClient( + fake.NewFakeClientWithScheme(scheme, testObjects...), + tc.fakeClient, + ), + Log: testLogger(t), + Scheme: scheme, + + GetIBMCloudInfo: func(logt logr.Logger, _ client.Client, instance *ibmcloudv1beta1.Service) (*ibmcloud.Info, error) { + return &ibmcloud.Info{}, nil + }, + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + GetServiceInstanceCRN: func(session *session.Session, instanceID string) (crn.CRN, string, error) { + return crn.CRN{}, "", nil + }, + GetServiceName: func(session *session.Session, serviceID string) (string, error) { + return "", nil + }, + GetServiceRoleCRN: func(session *session.Session, serviceName, roleName string) (crn.CRN, error) { + return crn.CRN{}, nil + }, + CreateResourceServiceKey: func(session *session.Session, name string, crn crn.CRN, parameters map[string]interface{}) (string, map[string]interface{}, error) { + return "", nil, tc.createServiceKeyErr + }, + GetResourceServiceKey: func(session *session.Session, keyID string) (string, string, map[string]interface{}, error) { + return "", aliasTargetName, nil, nil + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: bindingName, Namespace: namespace}, + }) + assert.Equal(t, tc.expectResult, result) + assert.NoError(t, err) + + update := r.Client.(MockClient).LastStatusUpdate() + require.IsType(t, &ibmcloudv1beta1.Binding{}, update) + status := update.(*ibmcloudv1beta1.Binding).Status + assert.Equal(t, tc.expectState, status.State) + assert.Equal(t, tc.expectMessage, status.Message) + }) + } +} + +func TestBindingEnsureCredentialsFailed(t *testing.T) { + t.Parallel() + scheme := schemas(t) + const ( + namespace = "mynamespace" + secretName = "mysecret" + bindingName = "mybinding" + serviceName = "myservice" + someInstanceID = "some-instance-id" + someKeyInstanceID = "some-key-instance-id" + ) + objects := []runtime.Object{ + &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: namespace, + Finalizers: []string{bindingFinalizer}, + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: serviceName, + SecretName: secretName, + }, + Status: ibmcloudv1beta1.BindingStatus{ + State: bindingStatePending, + InstanceID: someInstanceID, + SecretName: secretName, + KeyInstanceID: someKeyInstanceID, + }, + }, + &ibmcloudv1beta1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: namespace}, + Status: ibmcloudv1beta1.ServiceStatus{ + InstanceID: someInstanceID, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: namespace}, + }, + } + + r := &BindingReconciler{ + Client: newMockClient( + fake.NewFakeClientWithScheme(scheme, objects...), + MockConfig{}, + ), + Log: testLogger(t), + Scheme: scheme, + + GetIBMCloudInfo: func(logt logr.Logger, _ client.Client, instance *ibmcloudv1beta1.Service) (*ibmcloud.Info, error) { + return &ibmcloud.Info{}, nil + }, + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + GetResourceServiceKey: func(session *session.Session, keyID string) (string, string, map[string]interface{}, error) { + return "", "", nil, fmt.Errorf(notFound) + }, + GetServiceInstanceCRN: func(session *session.Session, instanceID string) (instanceCRN crn.CRN, serviceID string, err error) { + return crn.CRN{}, "", nil + }, + GetServiceName: func(session *session.Session, serviceID string) (string, error) { + return "", nil + }, + GetServiceRoleCRN: func(session *session.Session, serviceName, roleName string) (crn.CRN, error) { + return crn.CRN{}, nil + }, + CreateResourceServiceKey: func(session *session.Session, name string, crn crn.CRN, parameters map[string]interface{}) (string, map[string]interface{}, error) { + return "", nil, fmt.Errorf("failed") + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: bindingName, Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, result) + assert.NoError(t, err) + update := r.Client.(MockClient).LastStatusUpdate() + require.IsType(t, &ibmcloudv1beta1.Binding{}, update) + status := update.(*ibmcloudv1beta1.Binding).Status + assert.Equal(t, bindingStateFailed, status.State) + assert.Equal(t, "failed", status.Message) +} + +func TestBindingEnsureAliasCredentialsFailed(t *testing.T) { + t.Parallel() + const ( + namespace = "mynamespace" + aliasTargetName = "myBindingToAlias" + secretName = "mysecret" + bindingName = "mybinding" + serviceName = "myservice" + someInstanceID = "some-instance-id" + someKeyInstanceID = "some-key-instance-id" + ) + objects := []runtime.Object{ + &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: namespace, + Finalizers: []string{bindingFinalizer}, + Annotations: map[string]string{idkey: someInstanceID}, + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: serviceName, + SecretName: secretName, + Alias: aliasTargetName, + }, + Status: ibmcloudv1beta1.BindingStatus{ + State: bindingStatePending, + InstanceID: someInstanceID, + SecretName: secretName, + KeyInstanceID: someKeyInstanceID, + }, + }, + &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: aliasTargetName, + Namespace: namespace, + Finalizers: []string{bindingFinalizer}, + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: serviceName, + SecretName: secretName, + }, + Status: ibmcloudv1beta1.BindingStatus{ + State: bindingStatePending, + InstanceID: someInstanceID, + SecretName: secretName, + KeyInstanceID: someKeyInstanceID, + }, + }, + &ibmcloudv1beta1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: namespace}, + Status: ibmcloudv1beta1.ServiceStatus{ + InstanceID: someInstanceID, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: namespace}, + }, + } + + t.Run("reset if aliased creds don't exist", func(t *testing.T) { + scheme := schemas(t) + r := &BindingReconciler{ + Client: newMockClient( + fake.NewFakeClientWithScheme(scheme, objects...), + MockConfig{}, + ), + Log: testLogger(t), + Scheme: scheme, + + GetIBMCloudInfo: func(logt logr.Logger, _ client.Client, instance *ibmcloudv1beta1.Service) (*ibmcloud.Info, error) { + return &ibmcloud.Info{}, nil + }, + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + GetResourceServiceKey: func(session *session.Session, keyID string) (string, string, map[string]interface{}, error) { + return "", "", nil, fmt.Errorf(notFound) + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: bindingName, Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, result) + assert.NoError(t, err) + update := r.Client.(MockClient).LastStatusUpdate() + require.IsType(t, &ibmcloudv1beta1.Binding{}, update) + status := update.(*ibmcloudv1beta1.Binding).Status + assert.Equal(t, bindingStatePending, status.State) + assert.Equal(t, "Processing Resource", status.Message) + }) + + t.Run("other error", func(t *testing.T) { + scheme := schemas(t) + r := &BindingReconciler{ + Client: newMockClient( + fake.NewFakeClientWithScheme(scheme, objects...), + MockConfig{}, + ), + Log: testLogger(t), + Scheme: scheme, + + GetIBMCloudInfo: func(logt logr.Logger, _ client.Client, instance *ibmcloudv1beta1.Service) (*ibmcloud.Info, error) { + return &ibmcloud.Info{}, nil + }, + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + GetResourceServiceKey: func(session *session.Session, keyID string) (string, string, map[string]interface{}, error) { + return "", "", nil, fmt.Errorf("failed") + }, + GetServiceInstanceCRN: func(session *session.Session, instanceID string) (instanceCRN crn.CRN, serviceID string, err error) { + return crn.CRN{}, "", nil + }, + GetServiceName: func(session *session.Session, serviceID string) (string, error) { + return "", nil + }, + GetServiceRoleCRN: func(session *session.Session, serviceName, roleName string) (crn.CRN, error) { + return crn.CRN{}, nil + }, + CreateResourceServiceKey: func(session *session.Session, name string, crn crn.CRN, parameters map[string]interface{}) (string, map[string]interface{}, error) { + return "", nil, fmt.Errorf("failed") + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: bindingName, Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, result) + assert.NoError(t, err) + update := r.Client.(MockClient).LastStatusUpdate() + require.IsType(t, &ibmcloudv1beta1.Binding{}, update) + status := update.(*ibmcloudv1beta1.Binding).Status + assert.Equal(t, bindingStateFailed, status.State) + assert.Equal(t, "failed", status.Message) + }) +} + +func TestBindingEnsureSecretFailed(t *testing.T) { + t.Parallel() + scheme := schemas(t) + const ( + namespace = "mynamespace" + secretName = "mysecret" + bindingName = "mybinding" + serviceName = "myservice" + someInstanceID = "some-instance-id" + someKeyInstanceID = "some-key-instance-id" + ) + objects := []runtime.Object{ + &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: namespace, + Finalizers: []string{bindingFinalizer}, + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: serviceName, + SecretName: secretName, + }, + Status: ibmcloudv1beta1.BindingStatus{ + State: bindingStatePending, + InstanceID: someInstanceID, + SecretName: secretName, + KeyInstanceID: someKeyInstanceID, + }, + }, + &ibmcloudv1beta1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: namespace}, + Status: ibmcloudv1beta1.ServiceStatus{ + InstanceID: someInstanceID, + }, + }, + } + + t.Run("recreate secret success", func(t *testing.T) { + r := &BindingReconciler{ + Client: newMockClient( + fake.NewFakeClientWithScheme(scheme, objects...), + MockConfig{}, + ), + Log: testLogger(t), + Scheme: scheme, + + GetIBMCloudInfo: func(logt logr.Logger, _ client.Client, instance *ibmcloudv1beta1.Service) (*ibmcloud.Info, error) { + return &ibmcloud.Info{}, nil + }, + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + GetResourceServiceKey: func(session *session.Session, keyID string) (string, string, map[string]interface{}, error) { + return "", "", nil, nil + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: bindingName, Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, result) + assert.NoError(t, err) + assert.Equal(t, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + Annotations: map[string]string{ + "service-instance-id": someInstanceID, + "service-key-id": someKeyInstanceID, + "bindingFromName": serviceName, + }, + }, + Data: map[string][]byte{}, // TODO(johnstarich): validate key contents + }, r.Client.(MockClient).LastCreate()) + + update := r.Client.(MockClient).LastStatusUpdate() + require.IsType(t, &ibmcloudv1beta1.Binding{}, update) + status := update.(*ibmcloudv1beta1.Binding).Status + assert.Equal(t, bindingStateOnline, status.State) + assert.Equal(t, bindingStateOnline, status.Message) + }) + + t.Run("recreate secret failure", func(t *testing.T) { + r := &BindingReconciler{ + Client: newMockClient( + fake.NewFakeClientWithScheme(scheme, objects...), + MockConfig{CreateErr: fmt.Errorf("failed")}, + ), + Log: testLogger(t), + Scheme: scheme, + + GetIBMCloudInfo: func(logt logr.Logger, _ client.Client, instance *ibmcloudv1beta1.Service) (*ibmcloud.Info, error) { + return &ibmcloud.Info{}, nil + }, + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + GetResourceServiceKey: func(session *session.Session, keyID string) (string, string, map[string]interface{}, error) { + return "", "", nil, nil + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: bindingName, Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, result) + assert.NoError(t, err) + assert.Equal(t, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + Annotations: map[string]string{ + "service-instance-id": someInstanceID, + "service-key-id": someKeyInstanceID, + "bindingFromName": serviceName, + }, + }, + Data: map[string][]byte{}, // TODO(johnstarich): validate key contents + }, r.Client.(MockClient).LastCreate()) + + update := r.Client.(MockClient).LastStatusUpdate() + require.IsType(t, &ibmcloudv1beta1.Binding{}, update) + status := update.(*ibmcloudv1beta1.Binding).Status + assert.Equal(t, bindingStateFailed, status.State) + assert.Equal(t, "failed", status.Message) + }) +} + +func TestBindingEnsureKeyContentsFailed(t *testing.T) { + t.Parallel() + scheme := schemas(t) + const ( + namespace = "mynamespace" + secretName = "mysecret" + bindingName = "mybinding" + serviceName = "myservice" + someInstanceID = "some-instance-id" + someKeyInstanceID = "some-key-instance-id" + ) + objects := []runtime.Object{ + &ibmcloudv1beta1.Binding{ + TypeMeta: metav1.TypeMeta{Kind: "Binding", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: namespace, + Finalizers: []string{bindingFinalizer}, + }, + Spec: ibmcloudv1beta1.BindingSpec{ + ServiceName: serviceName, + SecretName: secretName, + }, + Status: ibmcloudv1beta1.BindingStatus{ + State: bindingStatePending, + InstanceID: someInstanceID, + SecretName: secretName, + KeyInstanceID: someKeyInstanceID, + }, + }, + &ibmcloudv1beta1.Service{ + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: "ibmcloud.ibm.com/v1beta1"}, + ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: namespace}, + Status: ibmcloudv1beta1.ServiceStatus{ + InstanceID: someInstanceID, + }, + }, + } + + t.Run("update key contents success", func(t *testing.T) { + keyContents := map[string]interface{}{ + "hello": "world", + } + testObjects := append( + objects, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + Annotations: map[string]string{ + "service-key-id": "some-old-service-key-id", + }, + }, + Data: map[string][]byte{ + "hello": []byte("world"), + }, + }, + ) + r := &BindingReconciler{ + Client: newMockClient( + fake.NewFakeClientWithScheme(scheme, testObjects...), + MockConfig{}, + ), + Log: testLogger(t), + Scheme: scheme, + + GetIBMCloudInfo: func(logt logr.Logger, _ client.Client, instance *ibmcloudv1beta1.Service) (*ibmcloud.Info, error) { + return &ibmcloud.Info{}, nil + }, + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + GetResourceServiceKey: func(session *session.Session, keyID string) (string, string, map[string]interface{}, error) { + return "", "", keyContents, nil + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: bindingName, Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, result) + assert.NoError(t, err) + + assert.Equal(t, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + Annotations: map[string]string{ + "service-instance-id": someInstanceID, + "service-key-id": someKeyInstanceID, + "bindingFromName": serviceName, + }, + }, + Data: map[string][]byte{ + "hello": []byte("world"), + }, + }, r.Client.(MockClient).LastCreate()) + + update := r.Client.(MockClient).LastStatusUpdate() + require.IsType(t, &ibmcloudv1beta1.Binding{}, update) + status := update.(*ibmcloudv1beta1.Binding).Status + assert.Equal(t, bindingStateOnline, status.State) + assert.Equal(t, bindingStateOnline, status.Message) + }) + + t.Run("key is up to date", func(t *testing.T) { + keyContents := map[string]interface{}{ + "hello": "world", + } + testObjects := append( + objects, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + Annotations: map[string]string{ + "service-key-id": someKeyInstanceID, + }, + }, + Data: map[string][]byte{ + "hello": []byte("world"), + }, + }, + ) + r := &BindingReconciler{ + Client: newMockClient( + fake.NewFakeClientWithScheme(scheme, testObjects...), + MockConfig{}, + ), + Log: testLogger(t), + Scheme: scheme, + + GetIBMCloudInfo: func(logt logr.Logger, _ client.Client, instance *ibmcloudv1beta1.Service) (*ibmcloud.Info, error) { + return &ibmcloud.Info{}, nil + }, + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + GetResourceServiceKey: func(session *session.Session, keyID string) (string, string, map[string]interface{}, error) { + return "", "", keyContents, nil + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: bindingName, Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, result) + assert.NoError(t, err) + + assert.Nil(t, r.Client.(MockClient).LastCreate()) + + update := r.Client.(MockClient).LastStatusUpdate() + require.IsType(t, &ibmcloudv1beta1.Binding{}, update) + status := update.(*ibmcloudv1beta1.Binding).Status + assert.Equal(t, bindingStateOnline, status.State) + assert.Equal(t, bindingStateOnline, status.Message) + }) + + t.Run("update key contents delete failed", func(t *testing.T) { + keyContents := map[string]interface{}{ + "hello": "world", + } + testObjects := append( + objects, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: namespace}, + }, + ) + r := &BindingReconciler{ + Client: newMockClient( + fake.NewFakeClientWithScheme(scheme, testObjects...), + MockConfig{DeleteErr: fmt.Errorf("failed")}, + ), + Log: testLogger(t), + Scheme: scheme, + + GetIBMCloudInfo: func(logt logr.Logger, _ client.Client, instance *ibmcloudv1beta1.Service) (*ibmcloud.Info, error) { + return &ibmcloud.Info{}, nil + }, + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + GetResourceServiceKey: func(session *session.Session, keyID string) (string, string, map[string]interface{}, error) { + return "", "", keyContents, nil + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: bindingName, Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, result) + assert.NoError(t, err) + + assert.Equal(t, &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: namespace}, + }, r.Client.(MockClient).LastDelete()) + + update := r.Client.(MockClient).LastStatusUpdate() + require.IsType(t, &ibmcloudv1beta1.Binding{}, update) + status := update.(*ibmcloudv1beta1.Binding).Status + assert.Equal(t, bindingStateFailed, status.State) + assert.Equal(t, "failed", status.Message) + }) + + t.Run("update key contents create failed", func(t *testing.T) { + keyContents := map[string]interface{}{ + "hello": "world", + } + testObjects := append( + objects, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: namespace}, + }, + ) + r := &BindingReconciler{ + Client: newMockClient( + fake.NewFakeClientWithScheme(scheme, testObjects...), + MockConfig{CreateErr: fmt.Errorf("failed")}, + ), + Log: testLogger(t), + Scheme: scheme, + + GetIBMCloudInfo: func(logt logr.Logger, _ client.Client, instance *ibmcloudv1beta1.Service) (*ibmcloud.Info, error) { + return &ibmcloud.Info{}, nil + }, + SetControllerReference: func(owner, controlled metav1.Object, scheme *runtime.Scheme) error { + return nil + }, + GetResourceServiceKey: func(session *session.Session, keyID string) (string, string, map[string]interface{}, error) { + return "", "", keyContents, nil + }, + } + + result, err := r.Reconcile(ctrl.Request{ + NamespacedName: types.NamespacedName{Name: bindingName, Namespace: namespace}, + }) + assert.Equal(t, ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, result) + assert.NoError(t, err) + + assert.Equal(t, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + Annotations: map[string]string{ + "service-instance-id": someInstanceID, + "service-key-id": someKeyInstanceID, + "bindingFromName": serviceName, + }, + }, + Data: map[string][]byte{ + "hello": []byte("world"), + }, + }, r.Client.(MockClient).LastCreate()) + + update := r.Client.(MockClient).LastStatusUpdate() + require.IsType(t, &ibmcloudv1beta1.Binding{}, update) + status := update.(*ibmcloudv1beta1.Binding).Status + assert.Equal(t, bindingStateFailed, status.State) + assert.Equal(t, "failed", status.Message) + }) +} + +func TestBindingResetResource(t *testing.T) { + t.Parallel() + scheme := schemas(t) + const ( + secretName = "mysecret" + namespace = "mynamespace" + ) + binding := &ibmcloudv1beta1.Binding{ + ObjectMeta: metav1.ObjectMeta{Name: "mybinding", Namespace: namespace}, + Spec: ibmcloudv1beta1.BindingSpec{ + SecretName: secretName, + }, + } + secret := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: namespace}, + } + + t.Run("happy path", func(t *testing.T) { + client := newMockClient( + fake.NewFakeClientWithScheme(scheme, binding, secret), + MockConfig{}, + ) + r := &BindingReconciler{ + Client: client, + Log: testLogger(t), + Scheme: scheme, + } + + result, err := r.resetResource(binding) + assert.Equal(t, ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, result) + assert.NoError(t, err) + assert.Equal(t, secret, client.LastDelete()) + assert.Equal(t, binding, client.LastStatusUpdate()) + }) + + t.Run("fail delete secret", func(t *testing.T) { + client := newMockClient( + fake.NewFakeClientWithScheme(scheme, binding, secret), + MockConfig{DeleteErr: fmt.Errorf("failed")}, + ) + r := &BindingReconciler{ + Client: client, + Log: testLogger(t), + Scheme: scheme, + } + + result, err := r.resetResource(binding) + assert.Equal(t, ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, result) + assert.NoError(t, err) + assert.Equal(t, secret, client.LastDelete()) + }) + + t.Run("fail update status", func(t *testing.T) { + client := newMockClient( + fake.NewFakeClientWithScheme(scheme, binding, secret), + MockConfig{StatusUpdateErr: fmt.Errorf("failed")}, + ) + r := &BindingReconciler{ + Client: client, + Log: testLogger(t), + Scheme: scheme, + } + + result, err := r.resetResource(binding) + assert.Equal(t, ctrl.Result{}, result) + assert.NoError(t, err) + assert.Equal(t, binding, client.LastStatusUpdate()) + }) +} + +func TestBindingUpdateStatusError(t *testing.T) { + for _, tc := range []struct { + description string + initialState string + initialMessage string + state string + err error + updateStatusError error + expectState string + expectMessage string + expectResult ctrl.Result + }{ + { + description: "happy path", + initialState: bindingStatePending, + state: bindingStateFailed, + err: fmt.Errorf("failed"), + expectState: bindingStateFailed, + expectMessage: "failed", + expectResult: ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, + }, + { + description: "no such host error", + initialState: bindingStatePending, + state: bindingStateFailed, + err: fmt.Errorf("no such host"), + expectResult: ctrl.Result{ + Requeue: true, + RequeueAfter: 5 * time.Minute, + }, + }, + { + description: "happy path - same state", + initialState: bindingStatePending, + initialMessage: "old message", + state: bindingStatePending, + err: fmt.Errorf("failed"), + expectResult: ctrl.Result{ + Requeue: true, + RequeueAfter: config.Get().SyncPeriod, + }, + }, + { + description: "status updated failed", + initialState: bindingStatePending, + state: bindingStateFailed, + err: fmt.Errorf("failed"), + updateStatusError: fmt.Errorf("failed status"), + expectState: bindingStateFailed, + expectMessage: "failed", + expectResult: ctrl.Result{}, + }, + } { + t.Run(tc.description, func(t *testing.T) { + scheme := schemas(t) + binding := &ibmcloudv1beta1.Binding{ + Status: ibmcloudv1beta1.BindingStatus{ + State: tc.initialState, + Message: tc.initialMessage, + }, + } + client := newMockClient( + fake.NewFakeClientWithScheme(scheme), + MockConfig{StatusUpdateErr: tc.updateStatusError}, + ) + r := &BindingReconciler{ + Client: client, + Log: testLogger(t), + Scheme: scheme, + } + + result, err := r.updateStatusError(binding, tc.state, tc.err) + assert.Equal(t, tc.expectResult, result) + assert.NoError(t, err) + var expectBinding runtime.Object + if tc.expectState != "" { + expectBinding = &ibmcloudv1beta1.Binding{ + Status: ibmcloudv1beta1.BindingStatus{ + State: tc.expectState, + Message: tc.expectMessage, + }, + } + } + assert.Equal(t, expectBinding, client.LastStatusUpdate()) + }) + } +} + +func TestBindingParamToJSON(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + description string + param ibmcloudv1beta1.Param + expectJSON map[string]interface{} + expectErr string + }{ + { + description: "error: value and valueFrom both set", + param: ibmcloudv1beta1.Param{ + Name: "myvalue", + Value: &ibmcloudv1beta1.ParamValue{}, + ValueFrom: &ibmcloudv1beta1.ParamSource{}, + }, + expectErr: "Value and ValueFrom properties are mutually exclusive (for myvalue variable)", + }, + { + description: "empty valueFrom error", + param: ibmcloudv1beta1.Param{ + Name: "myvalue", + ValueFrom: &ibmcloudv1beta1.ParamSource{}, + }, + expectErr: "Missing secretKeyRef or configMapKeyRef", + }, + { + description: "empty value error", + param: ibmcloudv1beta1.Param{ + Name: "myvalue", + Value: &ibmcloudv1beta1.ParamValue{}, + }, + expectErr: "unexpected end of JSON input", + }, + { + description: "value happy path", + param: ibmcloudv1beta1.Param{ + Name: "myvalue", + Value: &ibmcloudv1beta1.ParamValue{ + RawMessage: json.RawMessage(`{"hello": true, "world": {"!": 1}}`), + }, + }, + expectJSON: map[string]interface{}{ + "hello": true, + "world": map[string]interface{}{ + "!": 1.0, + }, + }, + }, + { + description: "neither value nor valueFrom set", + param: ibmcloudv1beta1.Param{Name: "myvalue"}, + expectJSON: nil, + expectErr: "", + }, + } { + t.Run(tc.description, func(t *testing.T) { + r := &BindingReconciler{} + j, err := r.paramToJSON(context.TODO(), tc.param, "someNamespace") + if tc.expectErr != "" { + assert.EqualError(t, err, tc.expectErr) + return + } + require.NoError(t, err) + if tc.expectJSON == nil { + assert.Nil(t, j) + } else { + assert.Equal(t, tc.expectJSON, j) + } + }) + } +} + +func TestBindingParamValueToJSON(t *testing.T) { + t.Parallel() + const ( + secretName = "secretName" + secretKey = "mykey" + secretValue = "myvalue" + configMapName = "configMapName" + configMapKey = "mykey" + configMapValue = "myvalue" + namespace = "mynamespace" + ) + + objects := []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: namespace}, + Data: map[string][]byte{ + secretKey: []byte(secretValue), + }, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: configMapName, Namespace: namespace}, + Data: map[string]string{ + configMapKey: configMapValue, + }, + }, + } + + for _, tc := range []struct { + description string + valueFrom ibmcloudv1beta1.ParamSource + expectJSON interface{} + expectErr string + }{ + { + description: "no value error", + expectErr: "Missing secretKeyRef or configMapKeyRef", + }, + { + description: "secret ref success", + valueFrom: ibmcloudv1beta1.ParamSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secretName, + }, + Key: secretKey, + }, + }, + expectJSON: secretValue, + }, + { + description: "secret ref name failure", + valueFrom: ibmcloudv1beta1.ParamSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "wrong-secret-name", + }, + Key: secretKey, + }, + }, + expectErr: "Missing secret wrong-secret-name", + }, + { + description: "secret ref key failure", + valueFrom: ibmcloudv1beta1.ParamSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: secretName, + }, + Key: "wrong-key-name", + }, + }, + expectJSON: "", + }, + { + description: "configmap ref success", + valueFrom: ibmcloudv1beta1.ParamSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: configMapName, + }, + Key: configMapKey, + }, + }, + expectJSON: configMapValue, + }, + { + description: "configmap ref name failure", + valueFrom: ibmcloudv1beta1.ParamSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "wrong-configmap-name", + }, + Key: configMapKey, + }, + }, + expectErr: "Missing configmap wrong-configmap-name", + }, + { + description: "configmap ref key failure", + valueFrom: ibmcloudv1beta1.ParamSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: configMapName, + }, + Key: "wrong-key-name", + }, + }, + expectJSON: "", + }, + } { + t.Run(tc.description, func(t *testing.T) { + scheme := schemas(t) + r := &BindingReconciler{ + Client: fake.NewFakeClientWithScheme(scheme, objects...), + Log: testLogger(t), + Scheme: scheme, + } + + j, err := r.paramValueToJSON(context.TODO(), tc.valueFrom, namespace) + if tc.expectErr != "" { + assert.EqualError(t, err, tc.expectErr) + return + } + require.NoError(t, err) + assert.Equal(t, tc.expectJSON, j) + }) + } +} + +func TestParamToJSONFromString(t *testing.T) { + t.Parallel() + t.Run("unmarshal happy path", func(t *testing.T) { + j, err := paramToJSONFromString(`{ + "hello": 1, + "world": 1.234567890987654321e1000, + "!": false + }`) + assert.NoError(t, err) + assert.Equal(t, map[string]interface{}{ + "hello": json.Number("1"), + "world": json.Number("1.234567890987654321e1000"), // large precision is kept identically as a Number type + "!": false, + }, j) + }) + + t.Run("too many JSON items", func(t *testing.T) { + const contents = ` +{ "hello": "abc" } +{ "world": "123" } +` + j, err := paramToJSONFromString(contents) + assert.NoError(t, err) + assert.Equal(t, contents, j) + }) + + t.Run("invalid JSON is not parsed", func(t *testing.T) { + const contents = `this is not JSON` + j, err := paramToJSONFromString(contents) + assert.NoError(t, err) + assert.Equal(t, contents, j) + }) +} + +func TestDeleteBindingFinalizer(t *testing.T) { + t.Parallel() + t.Run("no finalizer found", func(t *testing.T) { + finalizers := []string(nil) + assert.Equal(t, finalizers, deleteBindingFinalizer(&ibmcloudv1beta1.Binding{ + ObjectMeta: metav1.ObjectMeta{Finalizers: finalizers}, + })) + }) + + t.Run("one other finalizer found", func(t *testing.T) { + finalizers := []string{"not-binding-finalizer"} + assert.Equal(t, finalizers, deleteBindingFinalizer(&ibmcloudv1beta1.Binding{ + ObjectMeta: metav1.ObjectMeta{Finalizers: finalizers}, + })) + }) + + t.Run("one finalizer found", func(t *testing.T) { + finalizers := []string{bindingFinalizer} + assert.Equal(t, []string(nil), deleteBindingFinalizer(&ibmcloudv1beta1.Binding{ + ObjectMeta: metav1.ObjectMeta{Finalizers: finalizers}, + })) + }) + + t.Run("multiple finalizers found", func(t *testing.T) { + finalizers := []string{bindingFinalizer, bindingFinalizer} + assert.Equal(t, []string(nil), deleteBindingFinalizer(&ibmcloudv1beta1.Binding{ + ObjectMeta: metav1.ObjectMeta{Finalizers: finalizers}, + })) + }) +} + +func TestBindingDeleteCredentials(t *testing.T) { + t.Parallel() + scheme := schemas(t) + binding := &ibmcloudv1beta1.Binding{ + ObjectMeta: metav1.ObjectMeta{Name: "myservice", Namespace: "mynamespace"}, + Spec: ibmcloudv1beta1.BindingSpec{ + Alias: "", // not an alias, so should delete IBM Cloud resources + }, + } + secret := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"}, + ObjectMeta: metav1.ObjectMeta{Name: "myservice", Namespace: "mynamespace"}, + } + objects := []runtime.Object{ + binding, + &ibmcloudv1beta1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "myservice", Namespace: "mynamespace"}, + Spec: ibmcloudv1beta1.ServiceSpec{}, + }, + secret, + } + + for _, tc := range []struct { + description string + serviceClassType string + cfErr error + resourceErr error + expectDelete runtime.Object + deleteErr error + expectErr string + }{ + { + description: "delete resource service", + resourceErr: nil, + expectDelete: secret, + }, + { + description: "delete CF service", + serviceClassType: "CF", + cfErr: nil, + expectDelete: secret, + }, + { + description: "fail delete resource service", + resourceErr: fmt.Errorf("failed"), + expectErr: "failed", + }, + { + description: "fail delete CF service", + serviceClassType: "CF", + cfErr: fmt.Errorf("failed"), + expectErr: "failed", + }, + { + description: "fail delete secret", + deleteErr: fmt.Errorf("failed"), + expectErr: "failed", + expectDelete: secret, + }, + } { + t.Run(tc.description, func(t *testing.T) { + client := newMockClient( + fake.NewFakeClientWithScheme(scheme, objects...), + MockConfig{DeleteErr: tc.deleteErr}, + ) + r := &BindingReconciler{ + Client: client, + Log: testLogger(t), + Scheme: scheme, + + DeleteCFServiceKey: func(session *session.Session, serviceKeyGUID string) error { + return tc.cfErr + }, + DeleteResourceServiceKey: func(session *session.Session, serviceKeyGUID string) error { + return tc.resourceErr + }, + } + err := r.deleteCredentials(nil, binding, tc.serviceClassType) + assert.Equal(t, tc.expectDelete, client.LastDelete()) + if tc.expectErr != "" { + assert.EqualError(t, err, tc.expectErr) + return + } + assert.NoError(t, err) + }) + } +} diff --git a/controllers/mock_client_test.go b/controllers/mock_client_test.go new file mode 100644 index 00000000..0dacee5e --- /dev/null +++ b/controllers/mock_client_test.go @@ -0,0 +1,124 @@ +package controllers + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type MockClient interface { + client.Client + + LastCreate() runtime.Object + LastDelete() runtime.Object + LastDeleteAllOf() runtime.Object + LastPatch() runtime.Object + LastStatusPatch() runtime.Object + LastStatusUpdate() runtime.Object + LastUpdate() runtime.Object +} + +type mockClient struct { + client.Client + statusWriter *mockStatusWriter + MockConfig + + lastCreate runtime.Object + lastDelete runtime.Object + lastUpdate runtime.Object + lastPatch runtime.Object + lastDeleteAllOf runtime.Object + lastStatusUpdate runtime.Object + lastStatusPatch runtime.Object +} + +type mockStatusWriter struct { + *mockClient // pointer to parent mockClient +} + +type MockConfig struct { + CreateErr error + DeleteAllOfErr error + DeleteErr error + PatchErr error + StatusPatchErr error + StatusUpdateErr error + UpdateErr error +} + +func newMockClient(client client.Client, config MockConfig) MockClient { + m := &mockClient{ + Client: client, + MockConfig: config, + } + m.statusWriter = &mockStatusWriter{m} + return m +} + +func (m *mockClient) Create(ctx context.Context, obj runtime.Object, opts ...client.CreateOption) error { + m.lastCreate = obj.DeepCopyObject() + return m.CreateErr +} + +func (m *mockClient) LastCreate() runtime.Object { + return m.lastCreate +} + +func (m *mockClient) Delete(ctx context.Context, obj runtime.Object, opts ...client.DeleteOption) error { + m.lastDelete = obj.DeepCopyObject() + return m.DeleteErr +} + +func (m *mockClient) LastDelete() runtime.Object { + return m.lastDelete +} + +func (m *mockClient) Update(ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error { + m.lastUpdate = obj.DeepCopyObject() + return m.UpdateErr +} + +func (m *mockClient) LastUpdate() runtime.Object { + return m.lastUpdate +} + +func (m *mockClient) Patch(ctx context.Context, obj runtime.Object, patch client.Patch, opts ...client.PatchOption) error { + m.lastPatch = obj.DeepCopyObject() + return m.PatchErr +} + +func (m *mockClient) LastPatch() runtime.Object { + return m.lastPatch +} + +func (m *mockClient) DeleteAllOf(ctx context.Context, obj runtime.Object, opts ...client.DeleteAllOfOption) error { + m.lastDeleteAllOf = obj.DeepCopyObject() + return m.DeleteAllOfErr +} + +func (m *mockClient) LastDeleteAllOf() runtime.Object { + return m.lastDeleteAllOf +} + +func (m *mockClient) Status() client.StatusWriter { + return m.statusWriter +} + +func (s *mockStatusWriter) Update(ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error { + s.lastStatusUpdate = obj.DeepCopyObject() + return s.StatusUpdateErr +} + +func (m *mockClient) LastStatusUpdate() runtime.Object { + return m.lastStatusUpdate +} + +func (s *mockStatusWriter) Patch(ctx context.Context, obj runtime.Object, patch client.Patch, opts ...client.PatchOption) error { + s.lastStatusPatch = obj.DeepCopyObject() + return s.StatusPatchErr +} + +func (m *mockClient) LastStatusPatch() runtime.Object { + return m.lastStatusPatch +} diff --git a/controllers/suite_config_test.go b/controllers/suite_config_test.go index 2d7d65c3..2180bfe9 100644 --- a/controllers/suite_config_test.go +++ b/controllers/suite_config_test.go @@ -12,6 +12,7 @@ import ( "github.com/IBM-Cloud/bluemix-go/models" "github.com/IBM-Cloud/bluemix-go/rest" "github.com/IBM-Cloud/bluemix-go/session" + "github.com/ghodss/yaml" "github.com/go-logr/logr" "github.com/go-logr/zapr" ibmcloudv1beta1 "github.com/ibm/cloud-operators/api/v1beta1" @@ -171,3 +172,13 @@ func testLogger(t *testing.T) logr.Logger { } return zapr.NewLogger(zaptest.NewLogger(t, zaptest.WrapOptions(opts...))) } + +// metav1Now returns time.Now() in a serializer-friendly format. +// Serializing and deserializing this value are guaranteed to be deeply equal for tests. +func metav1Now(t *testing.T) *metav1.Time { + var now metav1.Time + buf, err := yaml.Marshal(metav1.Now()) + require.NoError(t, err) + require.NoError(t, yaml.Unmarshal(buf, &now)) + return &now +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go index e8458d12..829a5958 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -27,6 +27,7 @@ import ( "testing" "time" + "github.com/ibm/cloud-operators/internal/ibmcloud" "github.com/ibm/cloud-operators/internal/ibmcloud/auth" "github.com/ibm/cloud-operators/internal/ibmcloud/cfservice" "github.com/ibm/cloud-operators/internal/ibmcloud/iam" @@ -41,6 +42,7 @@ import ( "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/envtest" runtimeZap "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -143,15 +145,17 @@ func mainSetup(ctx context.Context) error { Log: ctrl.Log.WithName("controllers").WithName("Binding"), Scheme: k8sManager.GetScheme(), - CreateResourceServiceKey: resource.CreateKey, CreateCFServiceKey: cfservice.CreateKey, - DeleteResourceServiceKey: resource.DeleteKey, + CreateResourceServiceKey: resource.CreateKey, DeleteCFServiceKey: cfservice.DeleteKey, + DeleteResourceServiceKey: resource.DeleteKey, + GetCFServiceKeyCredentials: cfservice.GetKey, + GetIBMCloudInfo: ibmcloud.GetInfo, GetResourceServiceKey: resource.GetKey, GetServiceInstanceCRN: resource.GetServiceInstanceCRN, - GetCFServiceKeyCredentials: cfservice.GetKey, GetServiceName: resource.GetServiceName, GetServiceRoleCRN: iam.GetServiceRoleCRN, + SetControllerReference: controllerutil.SetControllerReference, }).SetupWithManager(k8sManager); err != nil { return errors.Wrap(err, "Failed to set up binding controller") } diff --git a/controllers/token_controller_test.go b/controllers/token_controller_test.go index 0c9d0a2a..ba2e90b8 100644 --- a/controllers/token_controller_test.go +++ b/controllers/token_controller_test.go @@ -79,6 +79,7 @@ func TestToken(t *testing.T) { } func TestTokenFailedAuth(t *testing.T) { + t.Parallel() scheme := schemas(t) objects := []runtime.Object{ &corev1.Secret{ @@ -105,6 +106,7 @@ func TestTokenFailedAuth(t *testing.T) { } func TestTokenFailedSecretLookup(t *testing.T) { + t.Parallel() scheme := schemas(t) r := &TokenReconciler{ Client: fake.NewFakeClientWithScheme(scheme), @@ -133,13 +135,14 @@ func TestTokenFailedSecretLookup(t *testing.T) { } func TestTokenSecretIsDeleting(t *testing.T) { + t.Parallel() scheme := schemas(t) - now := metav1.Now() + now := metav1Now(t) objects := []runtime.Object{ &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "secret", - DeletionTimestamp: &now, + DeletionTimestamp: now, }, }, } @@ -158,6 +161,7 @@ func TestTokenSecretIsDeleting(t *testing.T) { } func TestTokenAPIKeyIsMissing(t *testing.T) { + t.Parallel() scheme := schemas(t) objects := []runtime.Object{ &corev1.Secret{ @@ -180,6 +184,7 @@ func TestTokenAPIKeyIsMissing(t *testing.T) { } func TestTokenAuthInvalidConfig(t *testing.T) { + t.Parallel() scheme := schemas(t) const ( apiKey = "some API key" @@ -213,6 +218,7 @@ func TestTokenAuthInvalidConfig(t *testing.T) { } func TestTokenDeleteFailed(t *testing.T) { + t.Parallel() scheme := schemas(t) const ( apiKey = "some API key" @@ -252,6 +258,7 @@ func TestTokenDeleteFailed(t *testing.T) { } func TestTokenRaceCreateFailed(t *testing.T) { + t.Parallel() scheme := schemas(t) const ( apiKey = "some API key" diff --git a/go.mod b/go.mod index 0cd61881..346ef6d9 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/johnstarich/go/regext v0.0.1 github.com/kelseyhightower/envconfig v1.4.0 github.com/pkg/errors v0.8.1 - github.com/stretchr/testify v1.4.0 + github.com/stretchr/testify v1.6.1 go.uber.org/zap v1.10.0 gopkg.in/yaml.v2 v2.2.4 k8s.io/api v0.17.2 diff --git a/go.sum b/go.sum index f4c2b481..7cf2516d 100644 --- a/go.sum +++ b/go.sum @@ -318,6 +318,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= @@ -486,6 +488,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/config/config.go b/internal/config/config.go index 5911a12b..ee5bce41 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,13 +13,14 @@ var ( ) type Config struct { - APIKey string `envconfig:"bluemix_api_key"` - AccountID string `envconfig:"bluemix_account_id"` - Org string `envconfig:"bluemix_org"` - Region string `envconfig:"bluemix_region"` - ResourceGroupName string `envconfig:"bluemix_resource_group"` - Space string `envconfig:"bluemix_space"` - SyncPeriod time.Duration `envconfig:"sync_period"` + APIKey string `envconfig:"bluemix_api_key"` + AccountID string `envconfig:"bluemix_account_id"` + ControllerNamespace string `envconfig:"controller_namespace"` + Org string `envconfig:"bluemix_org"` + Region string `envconfig:"bluemix_region"` + ResourceGroupName string `envconfig:"bluemix_resource_group"` + Space string `envconfig:"bluemix_space"` + SyncPeriod time.Duration `envconfig:"sync_period"` } func Get() Config { diff --git a/internal/ibmcloud/ibmcloud.go b/internal/ibmcloud/ibmcloud.go index 23269f69..86dc3e47 100644 --- a/internal/ibmcloud/ibmcloud.go +++ b/internal/ibmcloud/ibmcloud.go @@ -3,7 +3,6 @@ package ibmcloud import ( "context" "fmt" - "os" "reflect" "strings" @@ -18,6 +17,7 @@ import ( "github.com/IBM-Cloud/bluemix-go/session" "github.com/go-logr/logr" ibmcloudv1beta1 "github.com/ibm/cloud-operators/api/v1beta1" + "github.com/ibm/cloud-operators/internal/config" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -32,8 +32,6 @@ const ( seedTokens = "secret-ibm-cloud-operator-tokens" ) -var controllerNamespace string - // Info kept all the needed client API resource and instance Info type Info struct { Session *session.Session @@ -350,11 +348,8 @@ func getIBMCloudContext(instance *ibmcloudv1beta1.Service, cm *v1.ConfigMap) ibm } func getDefaultNamespace(r client.Client) (string, bool) { - if controllerNamespace == "" { - controllerNamespace = os.Getenv("CONTROLLER_NAMESPACE") - } cm := &v1.ConfigMap{} - err := r.Get(context.Background(), types.NamespacedName{Namespace: controllerNamespace, Name: seedInstall}, cm) + err := r.Get(context.Background(), types.NamespacedName{Namespace: config.Get().ControllerNamespace, Name: seedInstall}, cm) if err != nil { return "default", false } diff --git a/main.go b/main.go index a2bc97ae..9b3fab3a 100644 --- a/main.go +++ b/main.go @@ -22,6 +22,7 @@ import ( "os" "github.com/ibm/cloud-operators/controllers" + "github.com/ibm/cloud-operators/internal/ibmcloud" "github.com/ibm/cloud-operators/internal/ibmcloud/auth" "github.com/ibm/cloud-operators/internal/ibmcloud/cfservice" "github.com/ibm/cloud-operators/internal/ibmcloud/iam" @@ -30,6 +31,7 @@ import ( clientgoscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log/zap" ibmcloudv1alpha1 "github.com/ibm/cloud-operators/api/v1alpha1" @@ -78,14 +80,16 @@ func main() { Log: ctrl.Log.WithName("controllers").WithName("Binding"), Scheme: mgr.GetScheme(), - CreateResourceServiceKey: resource.CreateKey, CreateCFServiceKey: cfservice.CreateKey, - DeleteResourceServiceKey: resource.DeleteKey, + CreateResourceServiceKey: resource.CreateKey, DeleteCFServiceKey: cfservice.DeleteKey, - GetResourceServiceKey: resource.GetKey, + DeleteResourceServiceKey: resource.DeleteKey, GetCFServiceKeyCredentials: cfservice.GetKey, + GetIBMCloudInfo: ibmcloud.GetInfo, + GetResourceServiceKey: resource.GetKey, GetServiceName: resource.GetServiceName, GetServiceRoleCRN: iam.GetServiceRoleCRN, + SetControllerReference: controllerutil.SetControllerReference, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Binding") os.Exit(1)