From 6c9e07da3665f542448ff46fc91d7fa1bfd83853 Mon Sep 17 00:00:00 2001 From: Francisc Munteanu Date: Wed, 23 Aug 2023 13:43:09 +0200 Subject: [PATCH] feat: Implement SpaceBindingRequest controller main logic (#848) * implement SBR main logic Co-authored-by: Matous Jobanek --- .../spacebindingrequest_controller.go | 351 +++++++++- .../spacebindingrequest_controller_test.go | 614 +++++++++++++++++- .../spacerequest_controller_test.go | 33 +- go.mod | 4 +- go.sum | 8 +- test/spacebinding/spacebinding.go | 27 +- .../spacebindingrequest.go | 15 + test/spacerequest/spacerequest.go | 20 + 8 files changed, 1025 insertions(+), 47 deletions(-) diff --git a/controllers/spacebindingrequest/spacebindingrequest_controller.go b/controllers/spacebindingrequest/spacebindingrequest_controller.go index a9ede3d00..fd71d45ab 100644 --- a/controllers/spacebindingrequest/spacebindingrequest_controller.go +++ b/controllers/spacebindingrequest/spacebindingrequest_controller.go @@ -2,10 +2,17 @@ package spacebindingrequest import ( "context" + "fmt" + "time" toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" "github.com/codeready-toolchain/host-operator/pkg/cluster" + "github.com/codeready-toolchain/toolchain-common/pkg/condition" + "github.com/codeready-toolchain/toolchain-common/pkg/spacebinding" + "github.com/go-logr/logr" errs "github.com/pkg/errors" + "github.com/redhat-cop/operator-utils/pkg/util" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -92,7 +99,349 @@ func (r *Reconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl. } logger.Info("spacebindingrequest found", "member cluster", memberClusterWithSpaceBindingRequest.Name) - // todo implement logic + if util.IsBeingDeleted(spaceBindingRequest) { + logger.Info("spaceBindingRequest is being deleted") + return reconcile.Result{}, r.ensureSpaceBindingDeletion(logger, memberClusterWithSpaceBindingRequest, spaceBindingRequest) + } + // Add the finalizer if it is not present + if err := r.addFinalizer(logger, memberClusterWithSpaceBindingRequest, spaceBindingRequest); err != nil { + return reconcile.Result{}, err + } + + // create spacebinding if not found for given spaceBindingRequest + spaceBinding, err := r.getSpaceBinding(spaceBindingRequest) + if err != nil { + return reconcile.Result{}, err + } + err = r.ensureSpaceBinding(logger, memberClusterWithSpaceBindingRequest, spaceBindingRequest, spaceBinding) + if err != nil { + if errStatus := r.setStatusFailedToCreateSpaceBinding(logger, memberClusterWithSpaceBindingRequest, spaceBindingRequest, err); errStatus != nil { + logger.Error(errStatus, "error updating SpaceBindingRequest status") + } + return reconcile.Result{}, err + } + + // set ready condition on spaceBindingRequest + err = r.updateStatus(spaceBindingRequest, memberClusterWithSpaceBindingRequest, toolchainv1alpha1.Condition{ + Type: toolchainv1alpha1.ConditionReady, + Status: corev1.ConditionTrue, + Reason: toolchainv1alpha1.SpaceBindingRequestProvisionedReason, + }) return ctrl.Result{}, err } + +func (r *Reconciler) ensureSpaceBindingDeletion(logger logr.Logger, memberClusterWithSpaceBindingRequest cluster.Cluster, spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest) error { + logger.Info("ensure spacebinding deletion") + if !util.HasFinalizer(spaceBindingRequest, toolchainv1alpha1.FinalizerName) { + // finalizer was already removed, nothing to delete anymore... + return nil + } + spaceBinding, err := r.getSpaceBinding(spaceBindingRequest) + if err != nil { + return err + } + if isBeingDeleted, err := r.deleteSpaceBinding(logger, spaceBinding); err != nil { + return r.setStatusTerminatingFailed(logger, memberClusterWithSpaceBindingRequest, spaceBindingRequest, err) + } else if isBeingDeleted { + return r.setStatusTerminating(memberClusterWithSpaceBindingRequest, spaceBindingRequest) + } + + // Remove finalizer from SpaceBindingRequest + util.RemoveFinalizer(spaceBindingRequest, toolchainv1alpha1.FinalizerName) + if err := memberClusterWithSpaceBindingRequest.Client.Update(context.TODO(), spaceBindingRequest); err != nil { + return r.setStatusTerminatingFailed(logger, memberClusterWithSpaceBindingRequest, spaceBindingRequest, errs.Wrap(err, "failed to remove finalizer")) + } + logger.Info("removed finalizer") + return nil +} + +// getSpaceBinding retrieves the spacebinding created by the spacebindingrequest +func (r *Reconciler) getSpaceBinding(spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest) (*toolchainv1alpha1.SpaceBinding, error) { + spaceBindings := &toolchainv1alpha1.SpaceBindingList{} + spaceBindingLabels := runtimeclient.MatchingLabels{ + toolchainv1alpha1.SpaceBindingRequestLabelKey: spaceBindingRequest.GetName(), + toolchainv1alpha1.SpaceBindingRequestNamespaceLabelKey: spaceBindingRequest.GetNamespace(), + } + err := r.Client.List(context.TODO(), spaceBindings, spaceBindingLabels, runtimeclient.InNamespace(r.Namespace)) + if err != nil { + return nil, errs.Wrap(err, "unable to list spacebindings") + } + + // spacebinding not found + if len(spaceBindings.Items) == 0 { + return nil, nil + } + + return &spaceBindings.Items[0], nil // all good +} + +// deleteSpaceBinding deletes a given spacebinding object in case deletion was not issued already. +// returns true/nil if the deletion of the spacebinding was triggered +// returns false/nil if the spacebinding was already deleted +// return false/err if something went wrong +func (r *Reconciler) deleteSpaceBinding(logger logr.Logger, spaceBinding *toolchainv1alpha1.SpaceBinding) (bool, error) { + // spacebinding not found, was already deleted + if spaceBinding == nil { + return false, nil + } + if util.IsBeingDeleted(spaceBinding) { + logger.Info("the spacebinding resource is already being deleted") + deletionTimestamp := spaceBinding.GetDeletionTimestamp() + if time.Since(deletionTimestamp.Time) > 120*time.Second { + return false, fmt.Errorf("spacebinding deletion has not completed in over 2 minutes") + } + return true, nil // spacebinding is still being deleted + } + + logger.Info("deleting the spacebinding resource", "spacebinding name", spaceBinding.Name) + if err := r.Client.Delete(context.TODO(), spaceBinding); err != nil { + if errors.IsNotFound(err) { + return false, nil // was already deleted + } + return false, errs.Wrap(err, "unable to delete spacebinding") // something wrong happened + } + logger.Info("deleted the spacebinding resource", "spacebinding name", spaceBinding.Name) + return true, nil +} + +// setFinalizers sets the finalizers for the SpaceBindingRequest +func (r *Reconciler) addFinalizer(logger logr.Logger, memberCluster cluster.Cluster, spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest) error { + // Add the finalizer if it is not present + if !util.HasFinalizer(spaceBindingRequest, toolchainv1alpha1.FinalizerName) { + logger.Info("adding finalizer on SpaceBindingRequest") + util.AddFinalizer(spaceBindingRequest, toolchainv1alpha1.FinalizerName) + if err := memberCluster.Client.Update(context.TODO(), spaceBindingRequest); err != nil { + return errs.Wrap(err, "error while adding finalizer") + } + } + return nil +} + +func (r *Reconciler) ensureSpaceBinding(logger logr.Logger, memberCluster cluster.Cluster, spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest, spaceBinding *toolchainv1alpha1.SpaceBinding) error { + logger.Info("ensuring spacebinding") + + // find space from namespace labels + space, err := r.getSpace(memberCluster, spaceBindingRequest) + if err != nil { + return err + } + // space is being deleted + if util.IsBeingDeleted(space) { + return errs.New("space is being deleted") + } + + // validate MUR + mur, err := r.getMUR(spaceBindingRequest) + if err != nil { + return err + } + // mur is being deleted + if util.IsBeingDeleted(mur) { + return errs.New("mur is being deleted") + } + + // validate Role + if err := r.validateRole(spaceBindingRequest, space); err != nil { + return err + } + + // spacebinding not found, creating it + if spaceBinding == nil { + return r.createNewSpaceBinding(logger, memberCluster, spaceBindingRequest, mur, space) + } + + logger.Info("SpaceBinding already exists") + return r.updateExistingSpaceBinding(logger, spaceBindingRequest, spaceBinding) +} + +// updateExistingSpaceBinding updates the spacebinding with the config from the spaceBindingRequest. +// returns true/nil if the spacebinding was updated +// returns false/nil if the spacebinding was already up-to-date +// returns false/err if something went wrong or the spacebinding is being deleted +func (r *Reconciler) updateExistingSpaceBinding(logger logr.Logger, spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest, spaceBinding *toolchainv1alpha1.SpaceBinding) error { + // check if spacebinding is being deleted + if util.IsBeingDeleted(spaceBinding) { + return errs.New("cannot update SpaceBinding because it is currently being deleted") + } + return r.updateSpaceBinding(logger, spaceBinding, spaceBindingRequest) +} + +// updateSpaceBinding updates the MUR and Role from the spaceBindingRequest to the spacebinding object +// if they are not up-to-date. +// returns false/nil if everything is up-to-date +// returns true/nil if spacebinding was updated +// returns false/err if something went wrong +func (r *Reconciler) updateSpaceBinding(logger logr.Logger, spaceBinding *toolchainv1alpha1.SpaceBinding, spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest) error { + logger.Info("update spaceBinding") + if spaceBindingRequest.Spec.MasterUserRecord == spaceBinding.Spec.MasterUserRecord && + spaceBindingRequest.Spec.SpaceRole == spaceBinding.Spec.SpaceRole { + // everything is up-to-date let's return + return nil + } + + // update SpaceRole and MUR + logger.Info("updating spaceBinding", "spaceBinding.Name", spaceBinding.Name) + spaceBinding.Spec.SpaceRole = spaceBindingRequest.Spec.SpaceRole + spaceBinding.Spec.MasterUserRecord = spaceBindingRequest.Spec.MasterUserRecord + // update also the MUR label + spaceBinding.Labels[toolchainv1alpha1.SpaceBindingMasterUserRecordLabelKey] = spaceBindingRequest.Spec.MasterUserRecord + err := r.Client.Update(context.TODO(), spaceBinding) + if err != nil { + return errs.Wrap(err, "unable to update SpaceRole and MasterUserRecord fields") + } + + logger.Info("spaceBinding updated", "spaceBinding.name", spaceBinding.Name, "spaceBinding.Spec.Space", spaceBinding.Spec.Space, "spaceBinding.Spec.SpaceRole", spaceBinding.Spec.SpaceRole, "spaceBinding.Spec.MasterUserRecord", spaceBinding.Spec.MasterUserRecord) + return nil +} + +func (r *Reconciler) createNewSpaceBinding(logger logr.Logger, memberCluster cluster.Cluster, spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest, mur *toolchainv1alpha1.MasterUserRecord, space *toolchainv1alpha1.Space) error { + spaceBinding := spacebinding.NewSpaceBinding(mur, space, spaceBindingRequest.Name) + // set SBR labels on spacebinding + spaceBinding.Labels[toolchainv1alpha1.SpaceBindingRequestLabelKey] = spaceBindingRequest.GetName() + spaceBinding.Labels[toolchainv1alpha1.SpaceBindingRequestNamespaceLabelKey] = spaceBindingRequest.GetNamespace() + logger.Info("creating spacebinding", "spaceBinding.Name", spaceBinding.Name) + if err := r.setStatusProvisioning(memberCluster, spaceBindingRequest); err != nil { + return err + } + if err := r.Client.Create(context.TODO(), spaceBinding); err != nil { + return errs.Wrap(err, "unable to create SpaceBinding") + } + logger.Info("Created SpaceBinding", "MUR", mur.Name, "Space", space.Name) + return nil +} + +// getSpace retrieves the name of the space that provisioned the namespace in which the spacebindingrequest was issued. +func (r *Reconciler) getSpace(memberCluster cluster.Cluster, spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest) (*toolchainv1alpha1.Space, error) { + space := &toolchainv1alpha1.Space{} + namespace := &corev1.Namespace{} + err := memberCluster.Client.Get(context.TODO(), types.NamespacedName{ + Namespace: "", + Name: spaceBindingRequest.Namespace, + }, namespace) + if err != nil { + return nil, errs.Wrap(err, "unable to get namespace") + } + // get the Space name from the namespace resource + spaceName, found := namespace.Labels[toolchainv1alpha1.SpaceLabelKey] + if !found || spaceName == "" { + return nil, errs.Errorf("unable to find space label %s on namespace %s", toolchainv1alpha1.SpaceLabelKey, namespace.GetName()) + } + + // get space object + err = r.Client.Get(context.TODO(), types.NamespacedName{ + Namespace: r.Namespace, + Name: spaceName, + }, space) + if err != nil { + return nil, errs.Wrap(err, "unable to get space") + } + + return space, nil // all good +} + +// getMUR retrieves the MUR specified in the spaceBindingRequest. +func (r *Reconciler) getMUR(spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest) (*toolchainv1alpha1.MasterUserRecord, error) { + if spaceBindingRequest.Spec.MasterUserRecord == "" { + return nil, fmt.Errorf("MasterUserRecord cannot be blank") + } + mur := &toolchainv1alpha1.MasterUserRecord{} + // check that MUR object exists + err := r.Client.Get(context.TODO(), types.NamespacedName{ + Namespace: r.Namespace, + Name: spaceBindingRequest.Spec.MasterUserRecord, + }, mur) + if err != nil { + return nil, errs.Wrap(err, "unable to get MUR") + } + + return mur, nil // all good +} + +// validateRole checks if the role is within the allowed spaceroles from the NSTemplateTier +func (r *Reconciler) validateRole(spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest, space *toolchainv1alpha1.Space) error { + if spaceBindingRequest.Spec.SpaceRole == "" { + return fmt.Errorf("SpaceRole cannot be blank") + } + // get the tier + nsTemplTier := &toolchainv1alpha1.NSTemplateTier{} + if err := r.Client.Get(context.TODO(), types.NamespacedName{ + Namespace: r.Namespace, + Name: space.Spec.TierName, + }, nsTemplTier); err != nil { + // Error reading the object - requeue the request. + return errs.Wrap(err, "unable to get the current NSTemplateTier") + } + + // search for the role + for actual := range nsTemplTier.Spec.SpaceRoles { + if spaceBindingRequest.Spec.SpaceRole == actual { + return nil + } + } + return fmt.Errorf("invalid role '%s' for space '%s'", spaceBindingRequest.Spec.SpaceRole, space.Name) +} + +func (r *Reconciler) setStatusTerminating(memberCluster cluster.Cluster, spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest) error { + return r.updateStatus( + spaceBindingRequest, + memberCluster, + toolchainv1alpha1.Condition{ + Type: toolchainv1alpha1.ConditionReady, + Status: corev1.ConditionFalse, + Reason: toolchainv1alpha1.SpaceBindingRequestTerminatingReason, + }) +} + +func (r *Reconciler) setStatusTerminatingFailed(logger logr.Logger, memberCluster cluster.Cluster, spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest, cause error) error { + if err := r.updateStatus( + spaceBindingRequest, + memberCluster, + toolchainv1alpha1.Condition{ + Type: toolchainv1alpha1.ConditionReady, + Status: corev1.ConditionFalse, + Reason: toolchainv1alpha1.SpaceBindingRequestTerminatingFailedReason, + Message: cause.Error(), + }); err != nil { + logger.Error(cause, "unable to terminate SpaceBinding") + return err + } + return cause +} + +func (r *Reconciler) setStatusFailedToCreateSpaceBinding(logger logr.Logger, memberCluster cluster.Cluster, spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest, cause error) error { + if err := r.updateStatus( + spaceBindingRequest, + memberCluster, + toolchainv1alpha1.Condition{ + Type: toolchainv1alpha1.ConditionReady, + Status: corev1.ConditionFalse, + Reason: toolchainv1alpha1.SpaceBindingRequestUnableToCreateSpaceBindingReason, + Message: cause.Error(), + }); err != nil { + logger.Error(err, "unable to create SpaceBinding") + return err + } + return cause +} + +func (r *Reconciler) setStatusProvisioning(memberCluster cluster.Cluster, spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest) error { + return r.updateStatus( + spaceBindingRequest, + memberCluster, + toolchainv1alpha1.Condition{ + Type: toolchainv1alpha1.ConditionReady, + Status: corev1.ConditionFalse, + Reason: toolchainv1alpha1.SpaceBindingRequestProvisioningReason, + }) +} + +func (r *Reconciler) updateStatus(spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest, memberCluster cluster.Cluster, conditions ...toolchainv1alpha1.Condition) error { + var updated bool + spaceBindingRequest.Status.Conditions, updated = condition.AddOrUpdateStatusConditions(spaceBindingRequest.Status.Conditions, conditions...) + if !updated { + // Nothing changed + return nil + } + return memberCluster.Client.Status().Update(context.TODO(), spaceBindingRequest) +} diff --git a/controllers/spacebindingrequest/spacebindingrequest_controller_test.go b/controllers/spacebindingrequest/spacebindingrequest_controller_test.go index 41afe98ea..219fd0266 100644 --- a/controllers/spacebindingrequest/spacebindingrequest_controller_test.go +++ b/controllers/spacebindingrequest/spacebindingrequest_controller_test.go @@ -4,17 +4,25 @@ import ( "context" "fmt" "testing" + "time" toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" "github.com/codeready-toolchain/host-operator/controllers/spacebindingrequest" "github.com/codeready-toolchain/host-operator/pkg/apis" "github.com/codeready-toolchain/host-operator/pkg/cluster" . "github.com/codeready-toolchain/host-operator/test" + tiertest "github.com/codeready-toolchain/host-operator/test/nstemplatetier" + spacebindingtest "github.com/codeready-toolchain/host-operator/test/spacebinding" spacebindingrequesttest "github.com/codeready-toolchain/host-operator/test/spacebindingrequest" + spacerequesttest "github.com/codeready-toolchain/host-operator/test/spacerequest" commoncluster "github.com/codeready-toolchain/toolchain-common/pkg/cluster" "github.com/codeready-toolchain/toolchain-common/pkg/test" + "github.com/codeready-toolchain/toolchain-common/pkg/test/masteruserrecord" + spacetest "github.com/codeready-toolchain/toolchain-common/pkg/test/space" + spacebindingrequesttestcommon "github.com/codeready-toolchain/toolchain-common/pkg/test/spacebindingrequest" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/scheme" runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" @@ -28,25 +36,62 @@ func TestCreateSpaceBindingRequest(t *testing.T) { logf.SetLogger(zap.New(zap.UseDevMode(true))) err := apis.AddToScheme(scheme.Scheme) require.NoError(t, err) + base1nsTier := tiertest.Base1nsTier(t, tiertest.CurrentBase1nsTemplates) + janeSpace := spacetest.NewSpace(test.HostOperatorNs, "jane") + janeMur := masteruserrecord.NewMasterUserRecord(t, "jane") + sbrNamespace := spacerequesttest.NewNamespace("jane") sbr := spacebindingrequesttest.NewSpaceBindingRequest("jane", "jane-tenant", spacebindingrequesttest.WithMUR("jane"), spacebindingrequesttest.WithSpaceRole("admin")) t.Run("success", func(t *testing.T) { - // given - member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbr), "member-1", corev1.ConditionTrue) - hostClient := test.NewFakeClient(t) - ctrl := newReconciler(t, hostClient, member1) + t.Run("spaceBinding doesn't exists it should be created", func(t *testing.T) { + // given + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbr, sbrNamespace), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, janeSpace, janeMur, base1nsTier) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err = ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + require.NoError(t, err) + // spaceBindingRequest exists with config and finalizer + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client). + HasSpecSpaceRole("admin"). + HasSpecMasterUserRecord(janeMur.Name). + HasConditions(spacebindingrequesttestcommon.Ready()). + HasFinalizer() + // there should be 1 spacebinding that was created from the SpaceBindingRequest + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, janeMur.Name, janeSpace.Name, hostClient). + Exists(). + HasLabelWithValue(toolchainv1alpha1.SpaceBindingRequestLabelKey, sbr.GetName()). // check expected labels are there + HasLabelWithValue(toolchainv1alpha1.SpaceBindingRequestNamespaceLabelKey, sbr.GetNamespace()) + }) - // when - _, err = ctrl.Reconcile(context.TODO(), requestFor(sbr)) + t.Run("spaceBinding exists and is up-to-date", func(t *testing.T) { + // given + spaceBinding := spacebindingtest.NewSpaceBinding(janeMur.Name, janeSpace.Name, "admin", sbr.GetName(), spacebindingtest.WithSpaceBindingRequest(sbr)) + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbr, sbrNamespace), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, janeSpace, janeMur, base1nsTier, spaceBinding) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err = ctrl.Reconcile(context.TODO(), requestFor(sbr)) - // then - require.NoError(t, err) - // spacebindingrequest exists with expected cluster roles and finalizer - spacebindingrequesttest.AssertThatSpaceBindingRequest(t, "jane-tenant", sbr.GetName(), member1.Client). - HasSpecSpaceRole("admin"). - HasSpecMasterUserRecord("jane") + // then + require.NoError(t, err) + // spaceBindingRequest exists with config and finalizer + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client). + HasSpecSpaceRole("admin"). + HasSpecMasterUserRecord(janeMur.Name). + HasConditions(spacebindingrequesttestcommon.Ready()). + HasFinalizer() + // there should be 1 spacebinding that was created from the SpaceBindingRequest + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, janeMur.Name, janeSpace.Name, hostClient).Exists(). + HasLabelWithValue(toolchainv1alpha1.SpaceBindingRequestLabelKey, sbr.GetName()). // check expected labels are there + HasLabelWithValue(toolchainv1alpha1.SpaceBindingRequestNamespaceLabelKey, sbr.GetNamespace()) + }) }) t.Run("failure", func(t *testing.T) { @@ -79,6 +124,542 @@ func TestCreateSpaceBindingRequest(t *testing.T) { require.EqualError(t, err, "unable to get the current SpaceBindingRequest: mock error") }) + t.Run("MasterUserRecord cannot be blank", func(t *testing.T) { + // given + badSBR := spacebindingrequesttest.NewSpaceBindingRequest("jane", "jane-tenant", + spacebindingrequesttest.WithMUR(""), // empty MUR + spacebindingrequesttest.WithSpaceRole("admin")) + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, badSBR, sbrNamespace), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, janeSpace, janeMur, base1nsTier) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err = ctrl.Reconcile(context.TODO(), requestFor(badSBR)) + + // then + // an error should be returned + cause := "MasterUserRecord cannot be blank" + require.EqualError(t, err, cause) + // spaceBindingRequest exists with config and finalizer + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, badSBR.GetNamespace(), badSBR.GetName(), member1.Client). + HasSpecSpaceRole("admin"). + HasSpecMasterUserRecord(""). // empty + HasConditions(spacebindingrequesttestcommon.UnableToCreateSpaceBinding(cause)). + HasFinalizer() + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, janeMur.Name, janeSpace.Name, hostClient).DoesNotExist() // there is no spacebinding created + }) + + t.Run("SpaceRole cannot be blank", func(t *testing.T) { + // given + badSBR := spacebindingrequesttest.NewSpaceBindingRequest("jane", "jane-tenant", + spacebindingrequesttest.WithMUR(janeMur.Name), + spacebindingrequesttest.WithSpaceRole("")) // empty spacerole + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, badSBR, sbrNamespace), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, janeSpace, janeMur, base1nsTier) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err = ctrl.Reconcile(context.TODO(), requestFor(badSBR)) + + // then + // an error should be returned + cause := "SpaceRole cannot be blank" + require.EqualError(t, err, cause) + // spaceBindingRequest exists with config and finalizer + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, badSBR.GetNamespace(), badSBR.GetName(), member1.Client). + HasSpecSpaceRole(""). // empty + HasSpecMasterUserRecord(janeMur.Name). + HasConditions(spacebindingrequesttestcommon.UnableToCreateSpaceBinding(cause)). + HasFinalizer() + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, janeMur.Name, janeSpace.Name, hostClient).DoesNotExist() // there is no spacebinding created + }) + + t.Run("error creating spaceBinding", func(t *testing.T) { + // given + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbr, sbrNamespace), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, janeSpace, janeMur, base1nsTier) + hostClient.MockCreate = func(ctx context.Context, obj runtimeclient.Object, opts ...runtimeclient.CreateOption) error { + if _, ok := obj.(*toolchainv1alpha1.SpaceBinding); ok { + return fmt.Errorf("mock error") + } + return hostClient.Client.Create(ctx, obj, opts...) + } + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + cause := "unable to create SpaceBinding: mock error" + require.EqualError(t, err, cause) + // spaceBindingRequest exists with ready condition set to false + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client). + HasSpecSpaceRole(sbr.Spec.SpaceRole). + HasSpecMasterUserRecord(sbr.Spec.MasterUserRecord). + HasConditions(spacebindingrequesttestcommon.UnableToCreateSpaceBinding(cause)). + HasFinalizer() + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, janeMur.Name, janeSpace.Name, hostClient).DoesNotExist() // there is no spacebinding created + }) + + t.Run("error while adding finalizer", func(t *testing.T) { + member1Client := test.NewFakeClient(t, sbr, sbrNamespace) + member1Client.MockUpdate = mockUpdateSpaceBindingRequestFail(member1Client) + member1 := NewMemberClusterWithClient(member1Client, "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, janeSpace, janeMur, base1nsTier) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + require.EqualError(t, err, "error while adding finalizer: mock error") + }) + + t.Run("spaceBindingRequest namespace not found", func(t *testing.T) { + member1Client := test.NewFakeClient(t, sbr) + member1 := NewMemberClusterWithClient(member1Client, "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, base1nsTier) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + cause := "unable to get namespace: namespaces \"jane-tenant\" not found" + require.EqualError(t, err, cause) + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client). + HasSpecSpaceRole(sbr.Spec.SpaceRole). + HasSpecMasterUserRecord(sbr.Spec.MasterUserRecord). + HasConditions(spacebindingrequesttestcommon.UnableToCreateSpaceBinding(cause)). + HasFinalizer() + }) + + t.Run("unable to find space label in spaceBindingRequest namespace", func(t *testing.T) { + // given + sbrNamespace := spacerequesttest.NewNamespace("nospace") + sbr := spacebindingrequesttest.NewSpaceBindingRequest("jane", sbrNamespace.GetName()) + member1Client := test.NewFakeClient(t, sbr, sbrNamespace) + member1 := NewMemberClusterWithClient(member1Client, "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, base1nsTier) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + cause := "unable to find space label toolchain.dev.openshift.com/space on namespace nospace-tenant" + require.EqualError(t, err, cause) + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client). + HasSpecSpaceRole(sbr.Spec.SpaceRole). + HasSpecMasterUserRecord(sbr.Spec.MasterUserRecord). + HasConditions(spacebindingrequesttestcommon.UnableToCreateSpaceBinding(cause)). + HasFinalizer() + }) + + t.Run("unable to get space", func(t *testing.T) { + // given + member1Client := test.NewFakeClient(t, sbr, sbrNamespace) + member1 := NewMemberClusterWithClient(member1Client, "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, base1nsTier) + hostClient.MockGet = func(ctx context.Context, key runtimeclient.ObjectKey, obj runtimeclient.Object, opts ...runtimeclient.GetOption) error { + if _, ok := obj.(*toolchainv1alpha1.Space); ok { + return fmt.Errorf("mock error") + } + return hostClient.Client.Get(ctx, key, obj, opts...) + } + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + cause := "unable to get space: mock error" + require.EqualError(t, err, cause) + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client). + HasSpecSpaceRole(sbr.Spec.SpaceRole). + HasSpecMasterUserRecord(sbr.Spec.MasterUserRecord). + HasConditions(spacebindingrequesttestcommon.UnableToCreateSpaceBinding(cause)). + HasFinalizer() + }) + + t.Run("space is being deleted", func(t *testing.T) { + // given + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbr, sbrNamespace), "member-1", corev1.ConditionTrue) + janeSpace := spacetest.NewSpace(test.HostOperatorNs, "jane", + spacetest.WithDeletionTimestamp()) // space is being deleted ... + hostClient := test.NewFakeClient(t, base1nsTier, janeSpace) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + cause := "space is being deleted" + require.EqualError(t, err, cause) + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client). + HasSpecSpaceRole(sbr.Spec.SpaceRole). + HasSpecMasterUserRecord(sbr.Spec.MasterUserRecord). + HasConditions(spacebindingrequesttestcommon.UnableToCreateSpaceBinding(cause)). + HasFinalizer() + }) + + t.Run("unable to get MUR", func(t *testing.T) { + // given + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbr, sbrNamespace), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, base1nsTier, janeSpace) + hostClient.MockGet = func(ctx context.Context, key runtimeclient.ObjectKey, obj runtimeclient.Object, opts ...runtimeclient.GetOption) error { + if _, ok := obj.(*toolchainv1alpha1.MasterUserRecord); ok { + return fmt.Errorf("mock error") + } + return hostClient.Client.Get(ctx, key, obj, opts...) + } + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + cause := "unable to get MUR: mock error" + require.EqualError(t, err, cause) + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client). + HasSpecSpaceRole(sbr.Spec.SpaceRole). + HasSpecMasterUserRecord(sbr.Spec.MasterUserRecord). + HasConditions(spacebindingrequesttestcommon.UnableToCreateSpaceBinding(cause)). + HasFinalizer() + }) + + t.Run("mur is being deleted", func(t *testing.T) { + // given + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbr, sbrNamespace), "member-1", corev1.ConditionTrue) + janeMur := masteruserrecord.NewMasterUserRecord(t, "jane", masteruserrecord.ToBeDeleted()) + hostClient := test.NewFakeClient(t, base1nsTier, janeSpace, janeMur) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + cause := "mur is being deleted" + require.EqualError(t, err, cause) + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client). + HasSpecSpaceRole(sbr.Spec.SpaceRole). + HasSpecMasterUserRecord(sbr.Spec.MasterUserRecord). + HasConditions(spacebindingrequesttestcommon.UnableToCreateSpaceBinding(cause)). + HasFinalizer() + }) + + t.Run("unable to get NSTemplateTier", func(t *testing.T) { + // given + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbr, sbrNamespace), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, janeSpace, janeMur) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + cause := "unable to get the current NSTemplateTier: nstemplatetiers.toolchain.dev.openshift.com \"base1ns\" not found" + require.EqualError(t, err, cause) + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client). + HasSpecSpaceRole(sbr.Spec.SpaceRole). + HasSpecMasterUserRecord(sbr.Spec.MasterUserRecord). + HasConditions(spacebindingrequesttestcommon.UnableToCreateSpaceBinding(cause)). + HasFinalizer() + }) + + t.Run("invalid SpaceRole", func(t *testing.T) { + // given + invalidSBR := spacebindingrequesttest.NewSpaceBindingRequest("jane", sbrNamespace.GetName(), spacebindingrequesttest.WithSpaceRole("maintainer"), spacebindingrequesttest.WithMUR(janeMur.Name)) // invalid role + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, invalidSBR, sbrNamespace), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, base1nsTier, janeSpace, janeMur) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(invalidSBR)) + + // then + cause := "invalid role 'maintainer' for space 'jane'" + require.EqualError(t, err, cause) + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, invalidSBR.GetNamespace(), invalidSBR.GetName(), member1.Client). + HasConditions(spacebindingrequesttestcommon.UnableToCreateSpaceBinding(cause)). + HasFinalizer() + }) + + t.Run("error listing SpaceBindings", func(t *testing.T) { + // given + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbr, sbrNamespace), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, base1nsTier, janeSpace, janeMur) + hostClient.MockList = func(ctx context.Context, list runtimeclient.ObjectList, opts ...runtimeclient.ListOption) error { + if _, ok := list.(*toolchainv1alpha1.SpaceBindingList); ok { + return fmt.Errorf("mock error") + } + return hostClient.Client.List(ctx, list, opts...) + } + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + require.EqualError(t, err, "unable to list spacebindings: mock error") + }) + + t.Run("SpaceBinding is being deleted", func(t *testing.T) { + // given + spaceBinding := spacebindingtest.NewSpaceBinding(janeMur.Name, janeSpace.Name, "admin", sbr.GetName(), spacebindingtest.WithSpaceBindingRequest(sbr), spacebindingtest.WithDeletionTimestamp()) + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbr, sbrNamespace), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, base1nsTier, janeSpace, janeMur, spaceBinding) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + cause := "cannot update SpaceBinding because it is currently being deleted" + require.EqualError(t, err, cause) + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client). + HasConditions(spacebindingrequesttestcommon.UnableToCreateSpaceBinding(cause)). + HasFinalizer() + }) + + }) +} + +func TestUpdateSpaceBindingRequest(t *testing.T) { + // given + logf.SetLogger(zap.New(zap.UseDevMode(true))) + err := apis.AddToScheme(scheme.Scheme) + require.NoError(t, err) + base1nsTier := tiertest.Base1nsTier(t, tiertest.CurrentBase1nsTemplates) + janeSpace := spacetest.NewSpace(test.HostOperatorNs, "jane") + janeMur := masteruserrecord.NewMasterUserRecord(t, "jane") + sbrNamespace := spacerequesttest.NewNamespace("jane") + t.Run("success", func(t *testing.T) { + + t.Run("update SpaceRole", func(t *testing.T) { + // given + sbr := spacebindingrequesttest.NewSpaceBindingRequest("jane", "jane-tenant", + spacebindingrequesttest.WithMUR("jane"), + spacebindingrequesttest.WithSpaceRole("admin")) + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbr, sbrNamespace), "member-1", corev1.ConditionTrue) + spaceBinding := spacebindingtest.NewSpaceBinding(janeMur.Name, janeSpace.Name, "maintainer", sbr.Name, spacebindingtest.WithSpaceBindingRequest(sbr)) // jane has maintainer, but SBR has admin + hostClient := test.NewFakeClient(t, base1nsTier, spaceBinding, janeSpace, janeMur) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + require.NoError(t, err) + // spacebindingrequest exists with expected config and finalizer + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client). + HasSpecSpaceRole("admin"). + HasSpecMasterUserRecord(janeMur.Name). + HasFinalizer() + // spacebinding was updated + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, janeMur.Name, janeSpace.Name, hostClient). + Exists(). + HasSpec(janeMur.Name, janeSpace.Name, "admin") // check that admin was set + }) + + t.Run("update MasterUserRecord", func(t *testing.T) { + // given + sbr := spacebindingrequesttest.NewSpaceBindingRequest("jane", "jane-tenant", + spacebindingrequesttest.WithMUR("jane"), + spacebindingrequesttest.WithSpaceRole("admin")) + spaceBinding := spacebindingtest.NewSpaceBinding(janeMur.Name, janeSpace.Name, "admin", sbr.Name, spacebindingtest.WithSpaceBindingRequest(sbr)) // jane is still the MUR on the spacebinding + lanaMur := masteruserrecord.NewMasterUserRecord(t, "lana") // we have a new MUR we want to replace the previous one + sbr.Spec.MasterUserRecord = lanaMur.GetName() // update MUR on SBR + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbr, sbrNamespace), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, base1nsTier, spaceBinding, janeSpace, lanaMur) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + require.NoError(t, err) + // spacebindingrequest exists with expected config and finalizer + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client). + HasSpecSpaceRole("admin"). + HasSpecMasterUserRecord(lanaMur.Name). + HasFinalizer() + // spacebinding was updated + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, lanaMur.Name, janeSpace.Name, hostClient). + Exists(). + HasSpec(lanaMur.Name, janeSpace.Name, "admin").HasLabelWithValue(toolchainv1alpha1.SpaceBindingMasterUserRecordLabelKey, lanaMur.GetName()) // check lana has now admin role in jane space + }) + }) + + t.Run("failure", func(t *testing.T) { + t.Run("unable to update SpaceBinding", func(t *testing.T) { + sbr := spacebindingrequesttest.NewSpaceBindingRequest("jane", "jane-tenant", + spacebindingrequesttest.WithMUR("jane"), + spacebindingrequesttest.WithSpaceRole("admin")) + // given + spaceBinding := spacebindingtest.NewSpaceBinding(janeMur.Name, janeSpace.Name, "oldrole", sbr.GetName(), spacebindingtest.WithSpaceBindingRequest(sbr)) // spacebinding role needs to be updated + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbr, sbrNamespace), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, base1nsTier, janeSpace, janeMur, spaceBinding) + hostClient.MockUpdate = func(ctx context.Context, obj runtimeclient.Object, opts ...runtimeclient.UpdateOption) error { + if _, ok := obj.(*toolchainv1alpha1.SpaceBinding); ok { + return fmt.Errorf("mock error") + } + return hostClient.Client.Update(ctx, obj, opts...) + } + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + cause := "unable to update SpaceRole and MasterUserRecord fields: mock error" + require.EqualError(t, err, cause) + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client). + HasConditions(spacebindingrequesttestcommon.UnableToCreateSpaceBinding(cause)). + HasFinalizer() + }) + }) +} + +func TestDeleteSpaceBindingRequest(t *testing.T) { + // given + logf.SetLogger(zap.New(zap.UseDevMode(true))) + err := apis.AddToScheme(scheme.Scheme) + require.NoError(t, err) + base1nsTier := tiertest.Base1nsTier(t, tiertest.CurrentBase1nsTemplates) + janeSpace := spacetest.NewSpace(test.HostOperatorNs, "jane") + janeMur := masteruserrecord.NewMasterUserRecord(t, "jane") + sbrNamespace := spacerequesttest.NewNamespace("jane") + sbr := spacebindingrequesttest.NewSpaceBindingRequest("jane", "jane-tenant", + spacebindingrequesttest.WithMUR("jane"), + spacebindingrequesttest.WithSpaceRole("admin"), + spacebindingrequesttest.WithDeletionTimestamp(), // spaceBindingRequest was deleted + spacebindingrequesttest.WithFinalizer()) // has finalizer still + t.Run("success", func(t *testing.T) { + t.Run("spaceBindingRequest should be in terminating while spacebinding is deleted", func(t *testing.T) { + // given + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbr, sbrNamespace), "member-1", corev1.ConditionTrue) + spaceBinding := spacebindingtest.NewSpaceBinding(janeMur.Name, janeSpace.Name, "maintainer", sbr.Name, spacebindingtest.WithSpaceBindingRequest(sbr)) // jane has maintainer, but SBR has admin + hostClient := test.NewFakeClient(t, base1nsTier, spaceBinding, janeSpace, janeMur) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + require.NoError(t, err) + // spacebindingrequest exists with expected config and finalizer + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client). + HasSpecSpaceRole(sbr.Spec.SpaceRole). + HasSpecMasterUserRecord(janeMur.Name). + HasConditions(spacebindingrequesttestcommon.Terminating()). + HasFinalizer() + // spacebinding was deleted + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, janeMur.Name, janeSpace.Name, hostClient). + DoesNotExist() + + t.Run("spaceBindingRequest is deleted when spacebinding is gone", func(t *testing.T) { + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + // spaceBindingRequest was deleted + require.NoError(t, err) + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client).DoesNotExist() // spaceBindingRequest is gone + }) + }) + + t.Run("finalizer was already removed", func(t *testing.T) { + // given + // spaceBindingRequest has no finalizer + sbrNoFinalizer := spacebindingrequesttest.NewSpaceBindingRequest("lana", "lana-tenant", + spacebindingrequesttest.WithMUR("lana"), + spacebindingrequesttest.WithSpaceRole("admin"), + spacebindingrequesttest.WithDeletionTimestamp()) // spaceBindingRequest was deleted + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbrNoFinalizer, sbrNamespace), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, base1nsTier, janeSpace, janeMur) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbrNoFinalizer)) + + // then + require.NoError(t, err) + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbrNoFinalizer.GetNamespace(), sbrNoFinalizer.GetName(), member1.Client).HasNoFinalizers() // spaceBindingRequest is gone + }) + }) + + t.Run("failure", func(t *testing.T) { + t.Run("SpaceBinding resource is already being deleted for more than 2 minutes", func(t *testing.T) { + // given + sbr := spacebindingrequesttest.NewSpaceBindingRequest("jane", sbrNamespace.GetName(), + spacebindingrequesttest.WithDeletionTimestamp(), + spacebindingrequesttest.WithFinalizer(), + ) // sbr is being deleted + spaceBinding := spacebindingtest.NewSpaceBinding(janeMur.Name, janeSpace.Name, "admin", sbr.Name, spacebindingtest.WithSpaceBindingRequest(sbr)) + spaceBinding.DeletionTimestamp = &metav1.Time{Time: time.Now().Add(-121 * time.Second)} // is being deleted since more than 2 minutes + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbr, sbrNamespace), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, base1nsTier, janeSpace, janeMur, spaceBinding) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + cause := "spacebinding deletion has not completed in over 2 minutes" + require.EqualError(t, err, cause) + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client). + HasConditions(spacebindingrequesttestcommon.TerminatingFailed(cause)). + HasFinalizer() + }) + + t.Run("unable to delete SpaceBinding", func(t *testing.T) { + // given + sbr := spacebindingrequesttest.NewSpaceBindingRequest("jane", sbrNamespace.GetName(), + spacebindingrequesttest.WithDeletionTimestamp(), + spacebindingrequesttest.WithFinalizer(), + ) // sbr is being deleted + spaceBinding := spacebindingtest.NewSpaceBinding(janeMur.Name, janeSpace.Name, "admin", sbr.Name, spacebindingtest.WithSpaceBindingRequest(sbr)) + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbr, sbrNamespace), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, base1nsTier, janeSpace, janeMur, spaceBinding) + hostClient.MockDelete = func(ctx context.Context, obj runtimeclient.Object, opts ...runtimeclient.DeleteOption) error { + if _, ok := obj.(*toolchainv1alpha1.SpaceBinding); ok { + return fmt.Errorf("mock error") + } + return hostClient.Client.Delete(ctx, obj, opts...) + } + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + cause := "unable to delete spacebinding: mock error" + require.EqualError(t, err, cause) + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, sbr.GetNamespace(), sbr.GetName(), member1.Client). + HasConditions(spacebindingrequesttestcommon.TerminatingFailed(cause)). + HasFinalizer() + }) + + t.Run("failed to remove finalizer", func(t *testing.T) { + // given + member1Client := test.NewFakeClient(t, sbr, sbrNamespace) + member1Client.MockUpdate = func(ctx context.Context, obj runtimeclient.Object, opts ...runtimeclient.UpdateOption) error { + if _, ok := obj.(*toolchainv1alpha1.SpaceBindingRequest); ok { + return fmt.Errorf("mock error") + } + return member1Client.Client.Update(ctx, obj, opts...) + } + member1 := NewMemberClusterWithClient(member1Client, "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbr)) + + // then + require.EqualError(t, err, "failed to remove finalizer: mock error") + }) }) } @@ -131,3 +712,12 @@ func mockGetSpaceBindingRequestFail(cl runtimeclient.Client) func(ctx context.Co return cl.Get(ctx, key, obj, opts...) } } + +func mockUpdateSpaceBindingRequestFail(cl runtimeclient.Client) func(ctx context.Context, obj runtimeclient.Object, opts ...runtimeclient.UpdateOption) error { + return func(ctx context.Context, obj runtimeclient.Object, opts ...runtimeclient.UpdateOption) error { + if _, ok := obj.(*toolchainv1alpha1.SpaceBindingRequest); ok { + return fmt.Errorf("mock error") + } + return cl.Update(ctx, obj, opts...) + } +} diff --git a/controllers/spacerequest/spacerequest_controller_test.go b/controllers/spacerequest/spacerequest_controller_test.go index 56dbf8a02..db9b86489 100644 --- a/controllers/spacerequest/spacerequest_controller_test.go +++ b/controllers/spacerequest/spacerequest_controller_test.go @@ -21,7 +21,6 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/h2non/gock.v1" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" @@ -37,7 +36,7 @@ func TestCreateSpaceRequest(t *testing.T) { err := apis.AddToScheme(scheme.Scheme) require.NoError(t, err) appstudioTier := tiertest.AppStudioTier(t, tiertest.AppStudioTemplates) - srNamespace := newNamespace("jane") + srNamespace := spacerequesttest.NewNamespace("jane") srClusterRoles := []string{commoncluster.RoleLabel(commoncluster.Tenant)} parentSpace := spacetest.NewSpace(test.HostOperatorNs, "jane") t.Run("success", func(t *testing.T) { @@ -317,7 +316,7 @@ func TestCreateSpaceRequest(t *testing.T) { spaceRequest := spacerequesttest.NewSpaceRequest("jane", "jane-tenant", spacerequesttest.WithTierName("appstudio"), spacerequesttest.WithTargetClusterRoles([]string{"member-2"})) // the provisioned namespace is targeted for member-2 - spaceRequestNamespace := newNamespace("jane") + spaceRequestNamespace := spacerequesttest.NewNamespace("jane") member1 := NewMemberClusterWithClient(test.NewFakeClient(t, spaceRequest, spaceRequestNamespace), "member-1", corev1.ConditionTrue) // spacerequest is created on member-1 but has target cluster member-2 // the provisioned namespace is on different cluster then the spacerequest resource member2 := NewMemberClusterWithClient(test.NewFakeClient(t), "member-2", corev1.ConditionTrue, @@ -431,7 +430,7 @@ func TestCreateSpaceRequest(t *testing.T) { t.Run("unable to find space label in spaceRequest namespace", func(t *testing.T) { // given - srNamespace := newNamespace("nospace") + srNamespace := spacerequesttest.NewNamespace("nospace") sr := spacerequesttest.NewSpaceRequest("jane", srNamespace.GetName(), spacerequesttest.WithTierName("appstudio"), spacerequesttest.WithTargetClusterRoles(srClusterRoles)) @@ -661,7 +660,7 @@ func TestUpdateSpaceRequest(t *testing.T) { err := apis.AddToScheme(scheme.Scheme) require.NoError(t, err) appstudioTier := tiertest.AppStudioTier(t, tiertest.AppStudioTemplates) - srNamespace := newNamespace("jane") + srNamespace := spacerequesttest.NewNamespace("jane") parentSpace := spacetest.NewSpace(test.HostOperatorNs, "jane") srClusterRoles := []string{commoncluster.RoleLabel(commoncluster.Tenant)} @@ -853,7 +852,7 @@ func TestDeleteSpaceRequest(t *testing.T) { err := apis.AddToScheme(scheme.Scheme) require.NoError(t, err) appstudioTier := tiertest.AppStudioTier(t, tiertest.AppStudioTemplates) - srNamespace := newNamespace("jane") + srNamespace := spacerequesttest.NewNamespace("jane") srClusterRoles := []string{commoncluster.RoleLabel(commoncluster.Tenant)} parentSpace := spacetest.NewSpace(test.HostOperatorNs, "jane") sr := spacerequesttest.NewSpaceRequest("jane", @@ -921,8 +920,8 @@ func TestDeleteSpaceRequest(t *testing.T) { // then // space request is gone require.NoError(t, err) - spacerequesttest.AssertThatSpaceRequest(t, srNamespace.Name, "jane-tenant", member1.Client). - DoesNotExist() + spacerequesttest.AssertThatSpaceRequest(t, srNamespace.Name, sr.GetName(), member1.Client). + HasNoFinalizers() }) }) @@ -1050,24 +1049,6 @@ func requestFor(s *toolchainv1alpha1.SpaceRequest) reconcile.Request { } } -func newNamespace(spacename string) *corev1.Namespace { - labels := map[string]string{ - toolchainv1alpha1.TypeLabelKey: "tenant", - toolchainv1alpha1.ProviderLabelKey: toolchainv1alpha1.ProviderLabelValue, - } - if spacename != "nospace" { - labels[toolchainv1alpha1.SpaceLabelKey] = spacename - } - ns := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-tenant", spacename), - Labels: labels, - }, - Status: corev1.NamespaceStatus{Phase: corev1.NamespaceActive}, - } - return ns -} - func mockGetSpaceRequestFail(cl runtimeclient.Client) func(ctx context.Context, key runtimeclient.ObjectKey, obj runtimeclient.Object, opts ...runtimeclient.GetOption) error { return func(ctx context.Context, key runtimeclient.ObjectKey, obj runtimeclient.Object, opts ...runtimeclient.GetOption) error { if _, ok := obj.(*toolchainv1alpha1.SpaceRequest); ok { diff --git a/go.mod b/go.mod index 1f50da3ad..bf3864892 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/codeready-toolchain/host-operator require ( - github.com/codeready-toolchain/api v0.0.0-20230809072818-b2867db6f98e - github.com/codeready-toolchain/toolchain-common v0.0.0-20230710095440-719b09376de3 + github.com/codeready-toolchain/api v0.0.0-20230823083409-fe9ca973d9a9 + github.com/codeready-toolchain/toolchain-common v0.0.0-20230823084119-693476668406 github.com/davecgh/go-spew v1.1.1 // indirect github.com/ghodss/yaml v1.0.0 github.com/go-bindata/go-bindata v3.1.2+incompatible diff --git a/go.sum b/go.sum index e5ebe9e16..258c1b112 100644 --- a/go.sum +++ b/go.sum @@ -136,10 +136,10 @@ github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWH github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= -github.com/codeready-toolchain/api v0.0.0-20230809072818-b2867db6f98e h1:7USP/1To/io70sHjfwVu2o1ZeYh6ihbS8CDotKNNvSA= -github.com/codeready-toolchain/api v0.0.0-20230809072818-b2867db6f98e/go.mod h1:nn3W6eKb9PFIVwSwZW7wDeLACMBOwAV+4kddGuN+ARM= -github.com/codeready-toolchain/toolchain-common v0.0.0-20230710095440-719b09376de3 h1:zPxv/JJRZsXS+OVgsrF1egFlTi45DXJ8MTPi50meujI= -github.com/codeready-toolchain/toolchain-common v0.0.0-20230710095440-719b09376de3/go.mod h1:vtUfWOJBDxQP1DtcIoxfjI5heBGcT8D+C8ux+PLouyg= +github.com/codeready-toolchain/api v0.0.0-20230823083409-fe9ca973d9a9 h1:ytFqNSSEvgevqvwMilmmqlrrDH1O/qUwzg8bO3CxCiY= +github.com/codeready-toolchain/api v0.0.0-20230823083409-fe9ca973d9a9/go.mod h1:nn3W6eKb9PFIVwSwZW7wDeLACMBOwAV+4kddGuN+ARM= +github.com/codeready-toolchain/toolchain-common v0.0.0-20230823084119-693476668406 h1:AkBhFV2tJhQaejTjntIhFC7r1FeLlQEiAL2gcn5/oW0= +github.com/codeready-toolchain/toolchain-common v0.0.0-20230823084119-693476668406/go.mod h1:wvbndymhFMqyc8syiaanid91GsywkDfuVyiTjdyiqNM= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= diff --git a/test/spacebinding/spacebinding.go b/test/spacebinding/spacebinding.go index 217a7c311..c891725d8 100644 --- a/test/spacebinding/spacebinding.go +++ b/test/spacebinding/spacebinding.go @@ -2,14 +2,18 @@ package spacebinding import ( "fmt" + "time" "github.com/codeready-toolchain/api/api/v1alpha1" + toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" "github.com/codeready-toolchain/toolchain-common/pkg/test" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func NewSpaceBinding(mur, space, spaceRole, creator string) *v1alpha1.SpaceBinding { - return &v1alpha1.SpaceBinding{ +type Option func(spaceRequest *toolchainv1alpha1.SpaceBinding) + +func NewSpaceBinding(mur, space, spaceRole, creator string, options ...Option) *v1alpha1.SpaceBinding { + sb := &v1alpha1.SpaceBinding{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-%s", mur, space), Namespace: test.HostOperatorNs, @@ -25,4 +29,23 @@ func NewSpaceBinding(mur, space, spaceRole, creator string) *v1alpha1.SpaceBindi SpaceRole: spaceRole, }, } + + for _, apply := range options { + apply(sb) + } + return sb +} + +func WithSpaceBindingRequest(sbr *toolchainv1alpha1.SpaceBindingRequest) Option { + return func(spaceBinding *toolchainv1alpha1.SpaceBinding) { + spaceBinding.Labels[toolchainv1alpha1.SpaceBindingRequestLabelKey] = sbr.Name + spaceBinding.Labels[toolchainv1alpha1.SpaceBindingRequestNamespaceLabelKey] = sbr.Namespace + } +} + +func WithDeletionTimestamp() Option { + return func(spaceBinding *toolchainv1alpha1.SpaceBinding) { + now := metav1.NewTime(time.Now()) + spaceBinding.DeletionTimestamp = &now + } } diff --git a/test/spacebindingrequest/spacebindingrequest.go b/test/spacebindingrequest/spacebindingrequest.go index cd99afbac..301d2ccba 100644 --- a/test/spacebindingrequest/spacebindingrequest.go +++ b/test/spacebindingrequest/spacebindingrequest.go @@ -1,6 +1,8 @@ package spacebindingrequest import ( + "time" + toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" "github.com/gofrs/uuid" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -34,3 +36,16 @@ func WithSpaceRole(spaceRole string) Option { spaceBindingRequest.Spec.SpaceRole = spaceRole } } + +func WithDeletionTimestamp() Option { + return func(spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest) { + now := metav1.NewTime(time.Now()) + spaceBindingRequest.DeletionTimestamp = &now + } +} + +func WithFinalizer() Option { + return func(spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest) { + spaceBindingRequest.Finalizers = append(spaceBindingRequest.Finalizers, toolchainv1alpha1.FinalizerName) + } +} diff --git a/test/spacerequest/spacerequest.go b/test/spacerequest/spacerequest.go index 236d0726d..7aad24cec 100644 --- a/test/spacerequest/spacerequest.go +++ b/test/spacerequest/spacerequest.go @@ -1,10 +1,12 @@ package spacerequest import ( + "fmt" "time" toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" "github.com/gofrs/uuid" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ) @@ -61,3 +63,21 @@ func WithStatusNamespaceAccess(namespaceAccess toolchainv1alpha1.NamespaceAccess spaceRequest.Status.NamespaceAccess = []toolchainv1alpha1.NamespaceAccess{namespaceAccess} } } + +func NewNamespace(spacename string) *corev1.Namespace { + labels := map[string]string{ + toolchainv1alpha1.TypeLabelKey: "tenant", + toolchainv1alpha1.ProviderLabelKey: toolchainv1alpha1.ProviderLabelValue, + } + if spacename != "nospace" { + labels[toolchainv1alpha1.SpaceLabelKey] = spacename + } + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-tenant", spacename), + Labels: labels, + }, + Status: corev1.NamespaceStatus{Phase: corev1.NamespaceActive}, + } + return ns +}