diff --git a/lib/clusteraccess/clusteraccess.go b/lib/clusteraccess/clusteraccess.go index 9db80bf..667ae34 100644 --- a/lib/clusteraccess/clusteraccess.go +++ b/lib/clusteraccess/clusteraccess.go @@ -53,6 +53,9 @@ type Reconciler interface { // WithWorkloadScheme sets the scheme for the Workload Kubernetes client. WithWorkloadScheme(scheme *runtime.Scheme) Reconciler + // SkipWorkloadCluster disables the request of a Workload cluster. + SkipWorkloadCluster() Reconciler + // MCPCluster creates a Cluster for the MCP AccessRequest. // This function will only be successful if the MCP AccessRequest is granted and Reconcile returned without an error // and a reconcile.Result with no RequeueAfter value. @@ -90,6 +93,7 @@ type reconcilerImpl struct { workloadRoleRefs []commonapi.RoleRef mcpScheme *runtime.Scheme workloadScheme *runtime.Scheme + skipWorkloadCluster bool } // NewClusterAccessReconciler creates a new ClusterAccessReconciler with the given parameters. @@ -106,6 +110,7 @@ func NewClusterAccessReconciler(platformClusterClient client.Client, controllerN workloadRoleRefs: []commonapi.RoleRef{}, mcpScheme: runtime.NewScheme(), workloadScheme: runtime.NewScheme(), + skipWorkloadCluster: false, } } @@ -144,6 +149,11 @@ func (r *reconcilerImpl) WithWorkloadScheme(scheme *runtime.Scheme) Reconciler { return r } +func (r *reconcilerImpl) SkipWorkloadCluster() Reconciler { + r.skipWorkloadCluster = true + return r +} + func (r *reconcilerImpl) MCPCluster(ctx context.Context, request reconcile.Request) (*clusters.Cluster, error) { platformNamespace, err := libutils.StableMCPNamespace(request.Name, request.Namespace) if err != nil { @@ -245,47 +255,49 @@ func (r *reconcilerImpl) Reconcile(ctx context.Context, request reconcile.Reques return reconcile.Result{RequeueAfter: r.retryInterval}, nil } - // Create or update the ClusterRequest for the Workload cluster and wait until it is ready. + if !r.skipWorkloadCluster { + // Create or update the ClusterRequest for the Workload cluster and wait until it is ready. - log.Debug("Create and wait for Workload cluster request", "clusterRequestName", requestNameWorkload, "clusterRequestNamespace", requestNamespace) + log.Debug("Create and wait for Workload cluster request", "clusterRequestName", requestNameWorkload, "clusterRequestNamespace", requestNamespace) - workloadRequest, err := ensureClusterRequest(ctx, r.platformClusterClient, requestNameWorkload, requestNamespace, clustersv1alpha1.PURPOSE_WORKLOAD, metadata) - if err != nil { - return reconcile.Result{}, fmt.Errorf("failed to create or update Workload ClusterRequest: %w", err) - } + workloadRequest, err := ensureClusterRequest(ctx, r.platformClusterClient, requestNameWorkload, requestNamespace, clustersv1alpha1.PURPOSE_WORKLOAD, metadata) + if err != nil { + return reconcile.Result{}, fmt.Errorf("failed to create or update Workload ClusterRequest: %w", err) + } - if workloadRequest.Status.IsDenied() { - return reconcile.Result{}, fmt.Errorf("workload ClusterRequest denied") - } + if workloadRequest.Status.IsDenied() { + return reconcile.Result{}, fmt.Errorf("workload ClusterRequest denied") + } - if !workloadRequest.Status.IsGranted() { - log.Debug("Workload ClusterRequest is not yet granted", - "clusterRequestName", requestNameWorkload, "clusterRequestNamespace", requestNamespace, "requestPhase", workloadRequest.Status.Phase) - return reconcile.Result{RequeueAfter: r.retryInterval}, nil - } + if !workloadRequest.Status.IsGranted() { + log.Debug("Workload ClusterRequest is not yet granted", + "clusterRequestName", requestNameWorkload, "clusterRequestNamespace", requestNamespace, "requestPhase", workloadRequest.Status.Phase) + return reconcile.Result{RequeueAfter: r.retryInterval}, nil + } - // Create or update the AccessRequest for the Workload cluster. + // Create or update the AccessRequest for the Workload cluster. - log.Debug("Create and wait for Workload cluster access request", "accessRequestName", requestNameWorkload, "accessRequestNamespace", requestNamespace) + log.Debug("Create and wait for Workload cluster access request", "accessRequestName", requestNameWorkload, "accessRequestNamespace", requestNamespace) - workloadAccessRequest, err := ensureAccessRequest(ctx, r.platformClusterClient, - requestNameWorkload, requestNamespace, &commonapi.ObjectReference{ - Name: requestNameWorkload, - Namespace: requestNamespace, - }, nil, r.workloadPermissions, r.workloadRoleRefs, metadata) + workloadAccessRequest, err := ensureAccessRequest(ctx, r.platformClusterClient, + requestNameWorkload, requestNamespace, &commonapi.ObjectReference{ + Name: requestNameWorkload, + Namespace: requestNamespace, + }, nil, r.workloadPermissions, r.workloadRoleRefs, metadata) - if err != nil { - return reconcile.Result{}, fmt.Errorf("failed to create or update Workload AccessRequest: %w", err) - } + if err != nil { + return reconcile.Result{}, fmt.Errorf("failed to create or update Workload AccessRequest: %w", err) + } - if workloadAccessRequest.Status.IsDenied() { - return reconcile.Result{}, fmt.Errorf("workload AccessRequest denied") - } + if workloadAccessRequest.Status.IsDenied() { + return reconcile.Result{}, fmt.Errorf("workload AccessRequest denied") + } - if !workloadAccessRequest.Status.IsGranted() { - log.Debug("Workload AccessRequest is not yet granted", - "accessRequestName", requestNameMCP, "accessRequestNamespace", requestNamespace, "requestPhase", workloadAccessRequest.Status.Phase) - return reconcile.Result{RequeueAfter: r.retryInterval}, nil + if !workloadAccessRequest.Status.IsGranted() { + log.Debug("Workload AccessRequest is not yet granted", + "accessRequestName", requestNameMCP, "accessRequestNamespace", requestNamespace, "requestPhase", workloadAccessRequest.Status.Phase) + return reconcile.Result{RequeueAfter: r.retryInterval}, nil + } } return reconcile.Result{}, nil @@ -300,16 +312,22 @@ func (r *reconcilerImpl) ReconcileDelete(ctx context.Context, request reconcile. requestNameMCP := StableRequestName(r.controllerName, request) + requestSuffixMCP requestNameWorkload := StableRequestName(r.controllerName, request) + requestSuffixWorkload - // Delete the Workload AccessRequest if it exists - workloadAccessDeleted, err := deleteAccessRequest(ctx, r.platformClusterClient, requestNameWorkload, requestNamespace) - if err != nil { - return reconcile.Result{}, fmt.Errorf("failed to delete Workload AccessRequest: %w", err) - } + workloadAccessDeleted := true + workloadClusterDeleted := true + + if !r.skipWorkloadCluster { + // Delete the Workload AccessRequest if it exists + workloadAccessDeleted, err = deleteAccessRequest(ctx, r.platformClusterClient, requestNameWorkload, requestNamespace) + if err != nil { + return reconcile.Result{}, fmt.Errorf("failed to delete Workload AccessRequest: %w", err) + } + + // Delete the Workload ClusterRequest if it exists + workloadClusterDeleted, err = deleteClusterRequest(ctx, r.platformClusterClient, requestNameWorkload, requestNamespace) + if err != nil { + return reconcile.Result{}, fmt.Errorf("failed to delete Workload ClusterRequest: %w", err) + } - // Delete the Workload ClusterRequest if it exists - workloadClusterDeleted, err := deleteClusterRequest(ctx, r.platformClusterClient, requestNameWorkload, requestNamespace) - if err != nil { - return reconcile.Result{}, fmt.Errorf("failed to delete Workload ClusterRequest: %w", err) } // Delete the MCP AccessRequest if it exists diff --git a/lib/clusteraccess/clusteraccess_test.go b/lib/clusteraccess/clusteraccess_test.go index 057f50d..8b5bc9e 100644 --- a/lib/clusteraccess/clusteraccess_test.go +++ b/lib/clusteraccess/clusteraccess_test.go @@ -37,7 +37,7 @@ func TestUtils(t *testing.T) { RunSpecs(t, "ClusterAccess Test Suite") } -func buildTestEnvironmentReconcile(testdataDir string, objectsWitStatus ...client.Object) *testutils.Environment { +func buildTestEnvironmentReconcile(testdataDir string, skipWorkloadCluster bool, objectsWitStatus ...client.Object) *testutils.Environment { scheme := runtime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(clustersv1alpha1.AddToScheme(scheme)) @@ -74,6 +74,9 @@ func buildTestEnvironmentReconcile(testdataDir string, objectsWitStatus ...clien WithWorkloadPermissions(permissions). WithWorkloadRoleRefs(roleRefs). WithRetryInterval(1 * time.Second) + if skipWorkloadCluster { + r.SkipWorkloadCluster() + } return r }). WithDynamicObjectsWithStatus(objectsWitStatus...). @@ -88,7 +91,7 @@ func (dr *deleteReconciler) Reconcile(ctx context.Context, req reconcile.Request return dr.r.ReconcileDelete(ctx, req) } -func buildTestEnvironmentDelete(testdataDir string, objectsWitStatus ...client.Object) *testutils.Environment { +func buildTestEnvironmentDelete(testdataDir string, skipWorkloadCluster bool, objectsWitStatus ...client.Object) *testutils.Environment { scheme := runtime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(clustersv1alpha1.AddToScheme(scheme)) @@ -100,6 +103,10 @@ func buildTestEnvironmentDelete(testdataDir string, objectsWitStatus ...client.O r := clusteraccess.NewClusterAccessReconciler(c, controllerName) r.WithRetryInterval(1 * time.Second) + if skipWorkloadCluster { + r.SkipWorkloadCluster() + } + dr := &deleteReconciler{ r: r, } @@ -122,13 +129,15 @@ func buildTestEnvironmentNoReconcile(testdataDir string, objectsWitStatus ...cli Build() } +const ( + expectedRequestNamespace = "mcp--80158a25-6874-80a6-a75d-94f57da600c0" +) + var _ = Describe("ClusterAccessReconciler", func() { Context("Reconcile", func() { It("should create MCP-/Workload ClusterRequests/AccessRequests", func() { var reconcileResult reconcile.Result - expectedRequestNamespace := "mcp--80158a25-6874-80a6-a75d-94f57da600c0" - request := reconcile.Request{ NamespacedName: client.ObjectKey{ Name: "instance", @@ -157,7 +166,7 @@ var _ = Describe("ClusterAccessReconciler", func() { }, } - env := buildTestEnvironmentReconcile("test-01", accessRequestMCP, clusterRequestWorkload, accessRequestWorkload) + env := buildTestEnvironmentReconcile("test-01", false, accessRequestMCP, clusterRequestWorkload, accessRequestWorkload) reconcileResult = env.ShouldReconcile(request, "reconcilerImpl should not return an error") Expect(reconcileResult.RequeueAfter).ToNot(BeZero(), "reconcile should requeue after a delay") @@ -244,6 +253,85 @@ var _ = Describe("ClusterAccessReconciler", func() { Expect(workloadCluster).ToNot(BeNil(), "should return a valid Workload cluster") }) + It("should create MCP-/Workload ClusterRequests/AccessRequests without Workload Cluster", func() { + var reconcileResult reconcile.Result + + request := reconcile.Request{ + NamespacedName: client.ObjectKey{ + Name: "instance", + Namespace: "test", + }, + } + + accessRequestMCP := &clustersv1alpha1.AccessRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusteraccess.StableRequestName(controllerName, request) + "--mcp", + Namespace: expectedRequestNamespace, + }, + } + + env := buildTestEnvironmentReconcile("test-01", true, accessRequestMCP) + + reconcileResult = env.ShouldReconcile(request, "reconcilerImpl should not return an error") + Expect(reconcileResult.RequeueAfter).ToNot(BeZero(), "reconcile should requeue after a delay") + + // reconcile now waits until the request namespace is being created + // the format if the request namespace is "ob-" + // create the expected request namespace + requestNamespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: expectedRequestNamespace, + }, + } + + Expect(env.Client().Create(env.Ctx, requestNamespace)).To(Succeed()) + + // reconcile again to process the request + env.ShouldReconcile(request, "reconcilerImpl should not return an error") + + // there should be an access request for the MCP cluster created + Expect(env.Client().Get(env.Ctx, client.ObjectKeyFromObject(accessRequestMCP), accessRequestMCP)).To(Succeed()) + + // set the access request status to "Granted" + accessRequestMCP.Status = clustersv1alpha1.AccessRequestStatus{ + Status: commonapi.Status{ + Phase: clustersv1alpha1.REQUEST_GRANTED, + }, + } + Expect(env.Client().Status().Update(env.Ctx, accessRequestMCP)).To(Succeed()) + + // reconcile again to process the granted access request + env.ShouldReconcile(request, "reconcilerImpl should not return an error") + + // set the secret reference for the MCP access request + accessRequestMCP.Status.SecretRef = &commonapi.ObjectReference{ + Name: "mcp-access", + Namespace: expectedRequestNamespace, + } + Expect(env.Client().Status().Update(env.Ctx, accessRequestMCP)).To(Succeed()) + + // reconcile again to process the granted access request + env.ShouldReconcile(request, "reconcilerImpl should not return an error") + + // cast to ClusterAccessReconciler to access the reconcilerImpl methods + reconciler, ok := env.Reconciler().(clusteraccess.Reconciler) // nolint:staticcheck + Expect(ok).To(BeTrue(), "reconcilerImpl should be of type ClusterAccessReconciler") + + mcpCluster, err := reconciler.MCPCluster(env.Ctx, request) + Expect(err).ToNot(HaveOccurred(), "should not return an error when getting MCP cluster") + Expect(mcpCluster).ToNot(BeNil(), "should return a valid MCP cluster") + + _, err = reconciler.WorkloadCluster(env.Ctx, request) + Expect(err).To(HaveOccurred(), "should return an error when trying to get the Workload cluster") + + accessRequestList := &clustersv1alpha1.AccessRequestList{} + Expect(env.Client().List(env.Ctx, accessRequestList, client.InNamespace(expectedRequestNamespace))).To(Succeed()) + Expect(accessRequestList.Items).To(HaveLen(1), "there should be only one access request (for the MCP cluster)") + clusterRequestList := &clustersv1alpha1.ClusterRequestList{} + Expect(env.Client().List(env.Ctx, clusterRequestList, client.InNamespace(expectedRequestNamespace))).To(Succeed()) + Expect(clusterRequestList.Items).To(BeEmpty(), "there should be no cluster request (for the Workload cluster)") + }) + Context("Delete", func() { It("should delete MCP-/Workload ClusterRequests/AccessRequests", func() { var reconcileResult reconcile.Result @@ -278,7 +366,7 @@ var _ = Describe("ClusterAccessReconciler", func() { }, } - env := buildTestEnvironmentDelete("test-02") + env := buildTestEnvironmentDelete("test-02", false) reconcileResult = env.ShouldReconcile(request, "reconcilerImpl should not return an error") Expect(reconcileResult.RequeueAfter).To(BeZero(), "reconcile should requeue after a delay") @@ -288,6 +376,34 @@ var _ = Describe("ClusterAccessReconciler", func() { Expect(env.Client().Get(env.Ctx, client.ObjectKeyFromObject(clusterRequestWorkload), clusterRequestWorkload)).ToNot(Succeed(), "cluster request for Workload cluster should not exist") Expect(env.Client().Get(env.Ctx, client.ObjectKeyFromObject(accessRequestWorkload), accessRequestWorkload)).ToNot(Succeed(), "access request for Workload cluster should not exist") }) + + It("should delete only MCP AccessRequest with skipWorkloadCluster", func() { + var reconcileResult reconcile.Result + + expectedRequestNamespace := "mcp--80158a25-6874-80a6-a75d-94f57da600c0" + + request := reconcile.Request{ + NamespacedName: client.ObjectKey{ + Name: "instance", + Namespace: "test", + }, + } + + accessRequestMCP := &clustersv1alpha1.AccessRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusteraccess.StableRequestName(controllerName, request) + "--mcp", + Namespace: expectedRequestNamespace, + }, + } + + env := buildTestEnvironmentDelete("test-02", true) + + reconcileResult = env.ShouldReconcile(request, "reconcilerImpl should not return an error") + Expect(reconcileResult.RequeueAfter).To(BeZero(), "reconcile should requeue after a delay") + + // access request should be deleted + Expect(env.Client().Get(env.Ctx, client.ObjectKeyFromObject(accessRequestMCP), accessRequestMCP)).ToNot(Succeed(), "access request for MCP cluster should not exist") + }) }) }) })