diff --git a/controllers/spacebindingrequestmigration/spacebindingrequest_migration_controller.go b/controllers/spacebindingrequestmigration/spacebindingrequest_migration_controller.go new file mode 100644 index 000000000..4e5036da0 --- /dev/null +++ b/controllers/spacebindingrequestmigration/spacebindingrequest_migration_controller.go @@ -0,0 +1,176 @@ +package spacebindingrequestmigration + +import ( + "context" + "fmt" + + toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" + "github.com/codeready-toolchain/host-operator/pkg/cluster" + errs "github.com/pkg/errors" + "github.com/redhat-cop/operator-utils/pkg/util" + "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" + runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +// Reconciler reconciles a SpaceBindingRequestMigration object +type Reconciler struct { + Client runtimeclient.Client + Scheme *runtime.Scheme + Namespace string + MemberClusters map[string]cluster.Cluster +} + +// SetupWithManager sets up the controller with the Manager. +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, memberClusters map[string]cluster.Cluster) error { + // Watch SpaceBindings from host cluster. + b := ctrl.NewControllerManagedBy(mgr). + For(&toolchainv1alpha1.SpaceBinding{}) + + // Watch SpaceBindingRequests in all member clusters and all namespaces and map those to their respective SpaceBinding resources. + for _, memberCluster := range memberClusters { + b = b.Watches( + source.NewKindWithCache(&toolchainv1alpha1.SpaceBindingRequest{}, memberCluster.Cache), + handler.EnqueueRequestsFromMapFunc(MapSpaceBindingRequestToSpaceBinding(r.Client, r.Namespace))) + } + return b.Complete(r) +} + +//+kubebuilder:rbac:groups=toolchain.dev.openshift.com,resources=spacebindingrequests,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=toolchain.dev.openshift.com,resources=spacebindingrequests/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=toolchain.dev.openshift.com,resources=spacebindingrequests/finalizers,verbs=update + +// Reconcile converts all the SpaceBindings created using the sandbox-cli to SpaceBindingRequests +func (r *Reconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + logger.Info("reconciling SpaceBindingRequestMigration") + + // Fetch the SpaceBinding instance + spaceBinding := &toolchainv1alpha1.SpaceBinding{} + err := r.Client.Get(ctx, request.NamespacedName, spaceBinding) + if err != nil { + if errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Return and don't requeue + return reconcile.Result{}, nil + } + // Error reading the object - requeue the request. + return reconcile.Result{}, errs.Wrapf(err, "unable to get spacebinding") + } + if util.IsBeingDeleted(spaceBinding) { + logger.Info("the SpaceBinding is already being deleted") + return reconcile.Result{}, nil + } + // check if spaceBinding was created from SpaceBindingRequest, + // in that case we can skip it + if hasSpaceBindingRequest(spaceBinding) { + return reconcile.Result{}, nil + } + + spaceName := types.NamespacedName{Namespace: spaceBinding.Namespace, Name: spaceBinding.Spec.Space} + space := &toolchainv1alpha1.Space{} + if err := r.Client.Get(ctx, spaceName, space); err != nil { + if errors.IsNotFound(err) { + // space was deleted + return reconcile.Result{}, nil + } + // error while reading space + return ctrl.Result{}, errs.Wrapf(err, "unable to get the bound Space") + } + + murName := types.NamespacedName{Namespace: spaceBinding.Namespace, Name: spaceBinding.Spec.MasterUserRecord} + mur := &toolchainv1alpha1.MasterUserRecord{} + if err := r.Client.Get(ctx, murName, mur); err != nil { + if errors.IsNotFound(err) { + // mur was deleted + return reconcile.Result{}, nil + } + // error while reading MUR + return ctrl.Result{}, errs.Wrapf(err, "unable to get the bound MUR") + } + + // error when mur has no owner label (should not happen in prod) + if _, ok := mur.Labels[toolchainv1alpha1.MasterUserRecordOwnerLabelKey]; !ok { + return ctrl.Result{}, errs.New("mur has no MasterUserRecordOwnerLabelKey set") + } + // error when space has no creator label (should not happen in prod) + if _, ok := space.Labels[toolchainv1alpha1.SpaceCreatorLabelKey]; !ok { + return ctrl.Result{}, errs.New("space has no SpaceCreatorLabelKey set") + } + + // skip workspace creator spacebinding + // the controller will convert only spacebindings created by system admins using the sandbox-cli. + // If the creator label on the space matches the owner label on the MUR then this is the owner of the space + // and the spacebinding should not be migrated. + if space.Labels[toolchainv1alpha1.SpaceCreatorLabelKey] == mur.Labels[toolchainv1alpha1.MasterUserRecordOwnerLabelKey] { + return reconcile.Result{}, nil + } + + // get the spaceRole + spaceRole := spaceBinding.Spec.SpaceRole + + // get member cluster name where the space was provisioned + targetCluster := space.Spec.TargetCluster + memberCluster, memberClusterFound := r.MemberClusters[targetCluster] + if !memberClusterFound { + return ctrl.Result{}, errs.New(fmt.Sprintf("unable to find member cluster: %s", targetCluster)) + } + + // get the home namespace from space + defaultNamespace := getDefaultNamespace(space.Status.ProvisionedNamespaces) + + // construct a SpaceBindingRequest object + sbrName := mur.GetName() + "-" + spaceRole + sbr := &toolchainv1alpha1.SpaceBindingRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: sbrName, + Namespace: defaultNamespace, + }, + } + + result, err := controllerutil.CreateOrUpdate(ctx, memberCluster.Client, sbr, func() error { + sbr.Spec = toolchainv1alpha1.SpaceBindingRequestSpec{ + MasterUserRecord: mur.GetName(), + SpaceRole: spaceRole, + } + return nil + }) + + if err != nil { + // something happened when we tried to read or write the sbr + return ctrl.Result{}, errs.Wrapf(err, "Failed to create or update space binding request %v", sbrName) + } + + if result == controllerutil.OperationResultCreated { + // let's requeue after we created the SBR, so that in next loop the migrated SpaceBinding object will be deleted + return ctrl.Result{Requeue: true}, nil + } + // if the SBR was found (was created from the previous reconcile loop), we can now delete the SpaceBinding object + if err := r.Client.Delete(ctx, spaceBinding); err != nil && !errors.IsNotFound(err) { + return ctrl.Result{}, errs.Wrapf(err, "unable to delete the SpaceBinding") + } + + return ctrl.Result{}, nil +} + +func getDefaultNamespace(provisionedNamespaces []toolchainv1alpha1.SpaceNamespace) string { + for _, namespaceObj := range provisionedNamespaces { + if namespaceObj.Type == "default" { + return namespaceObj.Name + } + } + return "" +} + +func hasSpaceBindingRequest(spaceBinding *toolchainv1alpha1.SpaceBinding) bool { + _, sbrNameFound := spaceBinding.Labels[toolchainv1alpha1.SpaceBindingRequestLabelKey] + return sbrNameFound +} diff --git a/controllers/spacebindingrequestmigration/spacebindingrequest_migration_controller_test.go b/controllers/spacebindingrequestmigration/spacebindingrequest_migration_controller_test.go new file mode 100644 index 000000000..0fe58206a --- /dev/null +++ b/controllers/spacebindingrequestmigration/spacebindingrequest_migration_controller_test.go @@ -0,0 +1,335 @@ +package spacebindingrequestmigration_test + +import ( + "context" + "fmt" + "testing" + + toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" + "github.com/codeready-toolchain/host-operator/controllers/spacebindingrequestmigration" + "github.com/codeready-toolchain/host-operator/pkg/apis" + "github.com/codeready-toolchain/host-operator/pkg/cluster" + . "github.com/codeready-toolchain/host-operator/test" + spacebindingtest "github.com/codeready-toolchain/host-operator/test/spacebinding" + spacebindingrequesttest "github.com/codeready-toolchain/host-operator/test/spacebindingrequest" + 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" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func TestMigrateSpaceBindingToSBR(t *testing.T) { + // given + logf.SetLogger(zap.New(zap.UseDevMode(true))) + err := apis.AddToScheme(scheme.Scheme) + require.NoError(t, err) + janeSpace := spacetest.NewSpace(test.HostOperatorNs, "jane", + spacetest.WithSpecTargetCluster("member-1"), + spacetest.WithStatusProvisionedNamespaces([]toolchainv1alpha1.SpaceNamespace{{ + Name: "jane-tenant", + Type: "default", + }}), + spacetest.WithLabel(toolchainv1alpha1.SpaceCreatorLabelKey, "jane"), + ) + + janeMur := masteruserrecord.NewMasterUserRecord(t, "jane", masteruserrecord.WithLabel(toolchainv1alpha1.MasterUserRecordOwnerLabelKey, "jane")) + sbForCreator := spacebindingtest.NewSpaceBinding(janeMur.Name, janeSpace.Name, "admin", janeMur.Name) + // we have a user which was added to the space via sandbox-cli + johnMur := masteruserrecord.NewMasterUserRecord(t, "john", masteruserrecord.WithLabel(toolchainv1alpha1.MasterUserRecordOwnerLabelKey, "john")) + sbForJohn := spacebindingtest.NewSpaceBinding(johnMur.Name, janeSpace.Name, "admin", janeMur.GetName()) + t.Run("success", func(t *testing.T) { + + t.Run("create sbr for sb added via sandbox-cli", func(t *testing.T) { + // given + member1 := NewMemberClusterWithClient(test.NewFakeClient(t), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, janeSpace, janeMur, johnMur, sbForCreator, sbForJohn) + ctrl := newReconciler(t, hostClient, member1) + + // when + res, err := ctrl.Reconcile(context.TODO(), requestFor(sbForJohn)) + + // then + require.NoError(t, err) + require.True(t, res.Requeue) // requeue should be triggered once SBR is created + // spaceBindingRequest with expected name, namespace and spec should be created + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, "jane-tenant", johnMur.Name+"-admin", member1.Client). + HasSpecSpaceRole("admin"). + HasSpecMasterUserRecord(johnMur.Name) + // the migrated spacebinding is still there, it will be deleted at the next reconcile loop + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, johnMur.Name, janeSpace.Name, hostClient). + Exists() + // the spacebinding for the space creator is still there + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, janeMur.Name, janeSpace.Name, hostClient). + Exists() + + t.Run("the next reconcile deletes the migrated spacebinding", func(t *testing.T) { + // when + res, err := ctrl.Reconcile(context.TODO(), requestFor(sbForJohn)) + + // then + require.NoError(t, err) + require.False(t, res.Requeue) // no requeue this time + // the migrated spacebinding was deleted + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, johnMur.Name, janeSpace.Name, hostClient). + DoesNotExist() + // spaceBindingRequest with expected name, namespace and spec is still there + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, "jane-tenant", johnMur.Name+"-admin", member1.Client). + HasSpecSpaceRole("admin"). + HasSpecMasterUserRecord(johnMur.Name) + // the spacebinding for the space creator is still there + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, janeMur.Name, janeSpace.Name, hostClient). + Exists() + }) + }) + + t.Run("skip space creator spacebinding ", func(t *testing.T) { + // given + // we have the workspace creator spacebinding, it should not be migrated to SpaceBindingRequest + member1 := NewMemberClusterWithClient(test.NewFakeClient(t), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, janeSpace, janeMur, sbForCreator) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err = ctrl.Reconcile(context.TODO(), requestFor(sbForCreator)) + + // then + require.NoError(t, err) + // the spacebinding for the space creator is still there + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, janeMur.Name, janeSpace.Name, hostClient). + Exists() + // the spaceBindingRequest wasn't created + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, "jane-tenant", janeMur.Name+"-admin", member1.Client). + DoesNotExist() + }) + + t.Run("space creator name is different than mur name", func(t *testing.T) { + // given + batmanSpace := spacetest.NewSpace(test.HostOperatorNs, "batman", + spacetest.WithStatusTargetCluster("member-1"), + spacetest.WithStatusProvisionedNamespaces([]toolchainv1alpha1.SpaceNamespace{{ + Name: "batman-tenant", + Type: "default", + }}), + spacetest.WithLabel(toolchainv1alpha1.SpaceCreatorLabelKey, "batman"), + ) + // mur name differs from the space creator label + // but the usersignup matches the space creator name + batmanMur := masteruserrecord.NewMasterUserRecord(t, "batman123", masteruserrecord.WithLabel(toolchainv1alpha1.MasterUserRecordOwnerLabelKey, "batman")) + sbForBatman := spacebindingtest.NewSpaceBinding(batmanMur.GetName(), batmanSpace.GetName(), "admin", "batman") + member1 := NewMemberClusterWithClient(test.NewFakeClient(t), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, batmanSpace, batmanMur, sbForBatman) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err = ctrl.Reconcile(context.TODO(), requestFor(sbForBatman)) + + // then + require.NoError(t, err) + // the spacebinding for the space creator is still there + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, batmanMur.Name, batmanSpace.Name, hostClient). + Exists() + // the spaceBindingRequest wasn't created + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, "batman-tenant", batmanMur.Name+"-admin", member1.Client). + DoesNotExist() + }) + + t.Run("space not found", func(t *testing.T) { + // given + member1 := NewMemberClusterWithClient(test.NewFakeClient(t), "member-1", corev1.ConditionTrue) + // let's not load the space object + hostClient := test.NewFakeClient(t, janeMur, johnMur, sbForCreator, sbForJohn) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err = ctrl.Reconcile(context.TODO(), requestFor(sbForJohn)) + + // then + require.NoError(t, err) + // the spacebinding for the space creator is still there + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, janeMur.Name, janeSpace.Name, hostClient). + Exists() + // the spacebinding for john user is still there + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, johnMur.Name, janeSpace.Name, hostClient). + Exists() + // the spaceBindingRequest wasn't created + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, "jane-tenant", johnMur.Name+"-admin", member1.Client). + DoesNotExist() + }) + + t.Run("mur not found", func(t *testing.T) { + // given + member1 := NewMemberClusterWithClient(test.NewFakeClient(t), "member-1", corev1.ConditionTrue) + // let's not load the mur object + hostClient := test.NewFakeClient(t, janeMur, janeSpace, sbForCreator, sbForJohn) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err = ctrl.Reconcile(context.TODO(), requestFor(sbForJohn)) + + // then + require.NoError(t, err) + // the spacebinding for the space creator is still there + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, janeMur.Name, janeSpace.Name, hostClient). + Exists() + // the spacebinding for john user is still there + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, johnMur.Name, janeSpace.Name, hostClient). + Exists() + // the spaceBindingRequest wasn't created + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, "jane-tenant", johnMur.Name+"-admin", member1.Client). + DoesNotExist() + }) + + t.Run("spacebinding has spacebindingrequest", func(t *testing.T) { + // given + // the spacebinding has a spacebindingrequest + sbrForJohn := spacebindingrequesttest.NewSpaceBindingRequest("john-admin", "jane-tenant", spacebindingrequesttest.WithLabel(toolchainv1alpha1.SpaceCreatorLabelKey, "somevalue")) + sbForJohnWithSBR := spacebindingtest.NewSpaceBinding(johnMur.Name, janeSpace.Name, "admin", janeMur.GetName(), spacebindingtest.WithSpaceBindingRequest(sbrForJohn)) + member1 := NewMemberClusterWithClient(test.NewFakeClient(t, sbrForJohn), "member-1", corev1.ConditionTrue) + hostClient := test.NewFakeClient(t, janeMur, janeSpace, johnMur, sbForJohnWithSBR) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err = ctrl.Reconcile(context.TODO(), requestFor(sbForJohnWithSBR)) + + // then + require.NoError(t, err) + // the spacebinding for john user is still there + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, johnMur.Name, janeSpace.Name, hostClient). + Exists() + // the spaceBindingRequest is unchanged + // no migration label as creator + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, "jane-tenant", johnMur.Name+"-admin", member1.Client). + HasLabelWithValue(toolchainv1alpha1.SpaceCreatorLabelKey, "somevalue"). + Exists() + }) + + t.Run("spacebinding is being deleted", func(t *testing.T) { + // given + member1 := NewMemberClusterWithClient(test.NewFakeClient(t), "member-1", corev1.ConditionTrue) + // the spacebinding is being deleted + sbForJohn := spacebindingtest.NewSpaceBinding(johnMur.Name, janeSpace.Name, "admin", janeMur.GetName(), spacebindingtest.WithDeletionTimestamp()) + hostClient := test.NewFakeClient(t, janeMur, janeSpace, sbForCreator, johnMur, sbForJohn) + ctrl := newReconciler(t, hostClient, member1) + + // when + _, err = ctrl.Reconcile(context.TODO(), requestFor(sbForJohn)) + + // then + require.NoError(t, err) + // the spacebinding for the space creator is still there + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, janeMur.Name, janeSpace.Name, hostClient). + Exists() + // the spacebinding for john user is still there + spacebindingtest.AssertThatSpaceBinding(t, test.HostOperatorNs, johnMur.Name, janeSpace.Name, hostClient). + Exists() + // the spaceBindingRequest wasn't created + spacebindingrequesttest.AssertThatSpaceBindingRequest(t, "jane-tenant", johnMur.Name+"-admin", member1.Client). + DoesNotExist() + }) + }) + + t.Run("error", func(t *testing.T) { + t.Run("unable to get spacebinding", func(t *testing.T) { + hostClient := test.NewFakeClient(t, sbForCreator) + hostClient.MockGet = mockGetSpaceBindingFail(hostClient) + ctrl := newReconciler(t, hostClient) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sbForCreator)) + + // then + // space binding request should not be there + require.EqualError(t, err, "unable to get spacebinding: mock error") + }) + + t.Run("member cluster not found", func(t *testing.T) { + spaceWithInvalidTargetCluster := spacetest.NewSpace(test.HostOperatorNs, "jane", + spacetest.WithSpecTargetCluster("invalid"), + spacetest.WithLabel(toolchainv1alpha1.SpaceCreatorLabelKey, "jane"), + ) + sb := spacebindingtest.NewSpaceBinding(johnMur.Name, spaceWithInvalidTargetCluster.Name, "admin", janeMur.Name) + hostClient := test.NewFakeClient(t, sb, spaceWithInvalidTargetCluster, johnMur) + ctrl := newReconciler(t, hostClient) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sb)) + + // then + // space binding request should not be there + require.EqualError(t, err, "unable to find member cluster: invalid") + }) + + t.Run("mur has no owner label", func(t *testing.T) { + murWithNoOwnership := masteruserrecord.NewMasterUserRecord(t, "jane") + sb := spacebindingtest.NewSpaceBinding(murWithNoOwnership.Name, janeSpace.Name, "admin", janeMur.Name) + hostClient := test.NewFakeClient(t, sb, janeSpace, murWithNoOwnership) + ctrl := newReconciler(t, hostClient) + + // when + _, err := ctrl.Reconcile(context.TODO(), requestFor(sb)) + + // then + // space binding request should not be there + require.EqualError(t, err, "mur has no MasterUserRecordOwnerLabelKey set") + }) + + }) +} + +func newReconciler(t *testing.T, hostCl runtimeclient.Client, memberClusters ...*commoncluster.CachedToolchainCluster) *spacebindingrequestmigration.Reconciler { + s := scheme.Scheme + err := apis.AddToScheme(s) + require.NoError(t, err) + + clusters := map[string]cluster.Cluster{} + for _, c := range memberClusters { + clusters[c.Name] = cluster.Cluster{ + Config: &commoncluster.Config{ + Type: commoncluster.Member, + OperatorNamespace: c.OperatorNamespace, + OwnerClusterName: test.MemberClusterName, + }, + Client: c.Client, + } + } + return &spacebindingrequestmigration.Reconciler{ + Client: hostCl, + Scheme: s, + Namespace: test.HostOperatorNs, + MemberClusters: clusters, + } +} + +func requestFor(s *toolchainv1alpha1.SpaceBinding) reconcile.Request { + if s != nil { + return reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: s.Namespace, + Name: s.Name, + }, + } + } + return reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: test.HostOperatorNs, + Name: "unknown", + }, + } +} + +func mockGetSpaceBindingFail(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.SpaceBinding); ok { + return fmt.Errorf("mock error") + } + return cl.Get(ctx, key, obj, opts...) + } +} diff --git a/controllers/spacebindingrequestmigration/spacebindingrequest_spacebinding_mapper.go b/controllers/spacebindingrequestmigration/spacebindingrequest_spacebinding_mapper.go new file mode 100644 index 000000000..051ad3c07 --- /dev/null +++ b/controllers/spacebindingrequestmigration/spacebindingrequest_spacebinding_mapper.go @@ -0,0 +1,44 @@ +package spacebindingrequestmigration + +import ( + "context" + + toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1" + ctrl "sigs.k8s.io/controller-runtime" + + "k8s.io/apimachinery/pkg/types" + runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// MapSpaceBindingRequestToSpaceBinding returns an event for the spacebinding that owns. +func MapSpaceBindingRequestToSpaceBinding(cl runtimeclient.Client, watchNamespace string) func(spaceBindingRequest runtimeclient.Object) []reconcile.Request { + mapperLog := ctrl.Log.WithName("SpaceBindingRequestToSpaceBinding") + return func(obj runtimeclient.Object) []reconcile.Request { + spaceBindings := &toolchainv1alpha1.SpaceBindingList{} + err := cl.List(context.TODO(), spaceBindings, + runtimeclient.InNamespace(watchNamespace), + runtimeclient.MatchingLabels{ + toolchainv1alpha1.SpaceBindingRequestLabelKey: obj.GetName(), + toolchainv1alpha1.SpaceBindingRequestNamespaceLabelKey: obj.GetNamespace(), + }) + if err != nil { + mapperLog.Error(err, "cannot list spacebindings") + return []reconcile.Request{} + } + if len(spaceBindings.Items) == 0 { + return []reconcile.Request{} + } + + req := make([]reconcile.Request, len(spaceBindings.Items)) + for i, item := range spaceBindings.Items { + req[i] = reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: item.Namespace, + Name: item.Name, + }, + } + } + return req + } +} diff --git a/controllers/spacebindingrequestmigration/spacebindingrequest_spacebinding_mapper_test.go b/controllers/spacebindingrequestmigration/spacebindingrequest_spacebinding_mapper_test.go new file mode 100644 index 000000000..b9e70ae3a --- /dev/null +++ b/controllers/spacebindingrequestmigration/spacebindingrequest_spacebinding_mapper_test.go @@ -0,0 +1,47 @@ +package spacebindingrequestmigration_test + +import ( + "testing" + + "github.com/codeready-toolchain/api/api/v1alpha1" + "github.com/codeready-toolchain/host-operator/controllers/spacebindingrequestmigration" + sb "github.com/codeready-toolchain/host-operator/test/spacebinding" + spacebindingrequesttest "github.com/codeready-toolchain/host-operator/test/spacebindingrequest" + "github.com/codeready-toolchain/toolchain-common/pkg/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func TestMapSpaceBindingRequestToSpaceBinding(t *testing.T) { + // given + restore := test.SetEnvVarAndRestore(t, "WATCH_NAMESPACE", test.HostOperatorNs) + defer restore() + spaceBindingRequest := spacebindingrequesttest.NewSpaceBindingRequest("mySpaceBindingRequest", "jane") + // following spaceBinding has a spaceBindingRequest associated + spaceBinding := sb.NewSpaceBinding("jane", "jane", "admin", "signupAdmin") + spaceBinding.Labels[v1alpha1.SpaceBindingRequestLabelKey] = spaceBindingRequest.Name + spaceBinding.Labels[v1alpha1.SpaceBindingRequestNamespaceLabelKey] = spaceBindingRequest.Namespace + + cl := test.NewFakeClient(t, spaceBinding) + + t.Run("should return SpaceBinding requests for SpaceBindingRequest", func(t *testing.T) { + // when + requests := spacebindingrequestmigration.MapSpaceBindingRequestToSpaceBinding(cl, test.HostOperatorNs)(spaceBindingRequest) + + // then + require.Len(t, requests, 1) + assert.Contains(t, requests, newRequest(spaceBinding.Name)) + }) + +} + +func newRequest(name string) reconcile.Request { + return reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: test.HostOperatorNs, + Name: name, + }, + } +} diff --git a/main.go b/main.go index 8e97464a7..aec7519a1 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ import ( "github.com/codeready-toolchain/host-operator/controllers/space" "github.com/codeready-toolchain/host-operator/controllers/spacebindingcleanup" "github.com/codeready-toolchain/host-operator/controllers/spacebindingrequest" + "github.com/codeready-toolchain/host-operator/controllers/spacebindingrequestmigration" "github.com/codeready-toolchain/host-operator/controllers/spacecleanup" "github.com/codeready-toolchain/host-operator/controllers/spacecompletion" "github.com/codeready-toolchain/host-operator/controllers/spacerequest" @@ -323,6 +324,19 @@ func main() { // nolint:gocyclo os.Exit(1) } } + // TEMPORARY controller that converts spacebindings created via sandbox-cli into spacebinding requests + // once the migration effort is completed , the controller can be disabled and deleted. + if crtConfig.SpaceConfig().SpaceBindingRequestIsEnabled() { + if err = (&spacebindingrequestmigration.Reconciler{ + Client: mgr.GetClient(), + Namespace: namespace, + MemberClusters: clusterScopedMemberClusters, + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr, clusterScopedMemberClusters); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "SpaceBindingRequestMigration") + os.Exit(1) + } + } if err = (&space.Reconciler{ Client: mgr.GetClient(), Namespace: namespace, diff --git a/test/spacebindingrequest/spacebindingrequest.go b/test/spacebindingrequest/spacebindingrequest.go index 301d2ccba..2943b578b 100644 --- a/test/spacebindingrequest/spacebindingrequest.go +++ b/test/spacebindingrequest/spacebindingrequest.go @@ -37,6 +37,15 @@ func WithSpaceRole(spaceRole string) Option { } } +func WithLabel(key, value string) Option { + return func(space *toolchainv1alpha1.SpaceBindingRequest) { + if space.ObjectMeta.Labels == nil { + space.ObjectMeta.Labels = map[string]string{} + } + space.ObjectMeta.Labels[key] = value + } +} + func WithDeletionTimestamp() Option { return func(spaceBindingRequest *toolchainv1alpha1.SpaceBindingRequest) { now := metav1.NewTime(time.Now()) diff --git a/test/spacebindingrequest/spacebindingrequest_assertions.go b/test/spacebindingrequest/spacebindingrequest_assertions.go index 3f7198531..2527c1c95 100644 --- a/test/spacebindingrequest/spacebindingrequest_assertions.go +++ b/test/spacebindingrequest/spacebindingrequest_assertions.go @@ -77,6 +77,14 @@ func (a *Assertion) HasConditions(expected ...toolchainv1alpha1.Condition) *Asse return a } +func (a *Assertion) HasLabelWithValue(key, value string) *Assertion { + err := a.loadResource() + require.NoError(a.t, err) + require.NotNil(a.t, a.spaceBindingRequest.Labels) + assert.Equal(a.t, value, a.spaceBindingRequest.Labels[key]) + return a +} + func (a *Assertion) DoesNotExist() *Assertion { err := a.loadResource() require.Error(a.t, err)