Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 58 additions & 40 deletions lib/clusteraccess/clusteraccess.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -106,6 +110,7 @@ func NewClusterAccessReconciler(platformClusterClient client.Client, controllerN
workloadRoleRefs: []commonapi.RoleRef{},
mcpScheme: runtime.NewScheme(),
workloadScheme: runtime.NewScheme(),
skipWorkloadCluster: false,
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
128 changes: 122 additions & 6 deletions lib/clusteraccess/clusteraccess_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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...).
Expand All @@ -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))
Expand All @@ -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,
}
Expand All @@ -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",
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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-<onboarding-namespace>"
// 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
Expand Down Expand Up @@ -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")
Expand All @@ -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")
})
})
})
})
Expand Down