Skip to content

Commit d63c0e3

Browse files
committed
refactor: add status reporting for cluster context requeuing
On-behalf-of: @SAP christopher.junk@sap.com Signed-off-by: Christopher Junk <christopher.junk@sap.com>
1 parent 18a50a1 commit d63c0e3

3 files changed

Lines changed: 74 additions & 87 deletions

File tree

‎pkg/serviceprovider/apireconciler.go‎

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,10 @@ import (
99
controllerutil2 "github.com/openmcp-project/controller-utils/pkg/controller"
1010
clustersv1alpha1 "github.com/openmcp-project/openmcp-operator/api/clusters/v1alpha1"
1111
apiconst "github.com/openmcp-project/openmcp-operator/api/constants"
12-
mcpv2alpha1 "github.com/openmcp-project/openmcp-operator/api/core/v2alpha1"
1312
corev1 "k8s.io/api/core/v1"
1413
"k8s.io/apimachinery/pkg/api/equality"
1514
apierrors "k8s.io/apimachinery/pkg/api/errors"
1615
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
17-
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
1816
ctrl "sigs.k8s.io/controller-runtime"
1917
"sigs.k8s.io/controller-runtime/pkg/builder"
2018
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -111,12 +109,8 @@ func (b *APIReconcilerBuilder[T, C]) PlatformCluster(c *clusters.Cluster) *APIRe
111109
return b
112110
}
113111

114-
// OnboardingCluster sets the onboarding cluster. The runtime registers core.openmcp.cloud/v2alpha1
115-
// onto the cluster's scheme so it can read ManagedControlPlaneV2 to surface a clear error when the
116-
// implicitly-referenced MCP for an SP CR doesn't exist; consumers don't need to register this type
117-
// themselves.
112+
// OnboardingCluster sets the onboarding cluster.
118113
func (b *APIReconcilerBuilder[T, C]) OnboardingCluster(c *clusters.Cluster) *APIReconcilerBuilder[T, C] {
119-
utilruntime.Must(mcpv2alpha1.AddToScheme(c.Scheme()))
120114
b.apiReconciler.onboardingCluster = c
121115
return b
122116
}
@@ -281,7 +275,7 @@ func (r *APIReconciler[T, C]) delete(ctx context.Context, obj T, config C, addit
281275
return ctrl.Result{}, err
282276
}
283277
if res.RequeueAfter > 0 {
284-
StatusTerminatingWithReason(obj, "Reconciling", "cluster cleanup")
278+
StatusTerminatingWithReason(obj, "WaitingForClusterContext", waitingForClusterContextMessage(req, r.withWorkloadCluster))
285279
return res, nil
286280
}
287281
res, err = r.reconciler.Delete(ctx, obj, config, clusterContext)
@@ -329,27 +323,20 @@ func (r *APIReconciler[T, C]) createOrUpdate(ctx context.Context, obj T, config
329323
return ctrl.Result{}, err
330324
}
331325
if res.RequeueAfter > 0 {
326+
StatusProgressing(obj, "WaitingForClusterContext", waitingForClusterContextMessage(req, r.withWorkloadCluster))
332327
return res, nil
333328
}
334329
return r.reconciler.CreateOrUpdate(ctx, obj, config, clusterContext)
335330
}
336331

337-
// managedControlPlaneNotFoundMessage returns a user-facing message for the ManagedControlPlaneNotFound
338-
// condition. It names the convention so users can self-resolve.
339-
func managedControlPlaneNotFoundMessage(req ctrl.Request) string {
340-
return fmt.Sprintf(
341-
"ManagedControlPlaneV2 %q not found on the onboarding cluster. "+
342-
"A ServiceProviderAPI requires a matching ManagedControlPlaneV2 with the same name and namespace. "+
343-
"Create the ManagedControlPlaneV2 first, then re-apply.",
344-
req.Namespace+"/"+req.Name,
345-
)
346-
}
347-
348-
// managedControlPlaneExists reports whether the ManagedControlPlaneV2 implicitly referenced by the
349-
// reconciled object exists on the onboarding cluster. It returns false with a nil error when the MCP V2
350-
// is missing. The onboarding cluster's scheme must have core.openmcp.cloud/v2alpha1 registered.
351-
func (r *APIReconciler[T, C]) managedControlPlaneExists(ctx context.Context, req ctrl.Request) (bool, error) {
352-
return true, nil
332+
// waitingForClusterContextMessage returns a user-facing message to identify issues like a non-matching request to ControlPlane mapping.
333+
func waitingForClusterContextMessage(req ctrl.Request, workloadCluster bool) string {
334+
msg := fmt.Sprintf("Waiting for ControlPlane (%s/%s)", req.Namespace, req.Name)
335+
if workloadCluster {
336+
msg += " and workload cluster"
337+
}
338+
msg += " to become accessible"
339+
return msg
353340
}
354341

355342
// areAccessRequestsInDeletion determines if the access requests for a reconcile request are in deletion.

‎pkg/serviceprovider/apireconciler_test.go‎

Lines changed: 62 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
clustersv1alpha1 "github.com/openmcp-project/openmcp-operator/api/clusters/v1alpha1"
1717
"github.com/openmcp-project/openmcp-operator/api/common"
1818
apiconst "github.com/openmcp-project/openmcp-operator/api/constants"
19-
mcpv2alpha1 "github.com/openmcp-project/openmcp-operator/api/core/v2alpha1"
2019
"github.com/stretchr/testify/assert"
2120
"github.com/stretchr/testify/require"
2221
"go.uber.org/zap"
@@ -60,17 +59,15 @@ func TestAPIReconciler_Reconcile(t *testing.T) {
6059
tests := []struct {
6160
name string // description of this test case
6261
// Named input parameters for target function.
63-
apiObj API
64-
providerConfig *fakeProviderConfigImpl
65-
req ctrl.Request
66-
want ctrl.Result
67-
wantStatusPhase string
68-
wantReason string
69-
wantReconciliation bool
70-
wantErr bool
71-
// noMCP, when true, omits the seeded ManagedControlPlaneV2 from the onboarding cluster
72-
// to simulate the issue #9 scenario.
73-
noMCP bool
62+
apiObj API
63+
providerConfig *fakeProviderConfigImpl
64+
req ctrl.Request
65+
want ctrl.Result
66+
wantStatusPhase string
67+
wantReason string
68+
wantReconciliation bool
69+
wantErr bool
70+
missingClusterAccess bool
7471
}{
7572
{
7673
name: "CreateOrUpdate ok -> requeue with pc poll interval",
@@ -284,14 +281,17 @@ func TestAPIReconciler_Reconcile(t *testing.T) {
284281
wantErr: true,
285282
},
286283
{
287-
name: "ManagedControlPlaneV2 missing -> Progressing with ManagedControlPlaneNotFound condition",
284+
name: "CreateOrUpdate with missing cluster access -> requeue with reason WaitingForClusterContext",
288285
apiObj: &fakeApiImpl{
289286
ObjectMeta: metav1.ObjectMeta{
290287
Name: testObjectName,
291288
Namespace: testNamespaceName,
292289
},
293290
},
294291
providerConfig: &fakeProviderConfigImpl{
292+
ObjectMeta: metav1.ObjectMeta{
293+
Name: testObjectName,
294+
},
295295
FakePollInterval: time.Hour,
296296
},
297297
req: ctrl.Request{
@@ -303,14 +303,14 @@ func TestAPIReconciler_Reconcile(t *testing.T) {
303303
want: ctrl.Result{
304304
RequeueAfter: time.Hour,
305305
},
306-
wantStatusPhase: StatusPhaseProgressing,
307-
wantReason: reasonManagedControlPlaneNotFound,
308-
wantReconciliation: false,
309-
wantErr: false,
310-
noMCP: true,
306+
wantStatusPhase: StatusPhaseProgressing,
307+
wantReason: "WaitingForClusterContext",
308+
wantReconciliation: false,
309+
wantErr: false,
310+
missingClusterAccess: true,
311311
},
312312
{
313-
name: "Delete succeeds even when ManagedControlPlaneV2 is missing",
313+
name: "Delete with missing cluster access -> requeue with reason WaitingForClusterContext",
314314
apiObj: &fakeApiImpl{
315315
ObjectMeta: metav1.ObjectMeta{
316316
Name: testObjectName,
@@ -322,6 +322,9 @@ func TestAPIReconciler_Reconcile(t *testing.T) {
322322
},
323323
},
324324
providerConfig: &fakeProviderConfigImpl{
325+
ObjectMeta: metav1.ObjectMeta{
326+
Name: testObjectName,
327+
},
325328
FakePollInterval: time.Hour,
326329
},
327330
req: ctrl.Request{
@@ -333,61 +336,56 @@ func TestAPIReconciler_Reconcile(t *testing.T) {
333336
want: ctrl.Result{
334337
RequeueAfter: time.Hour,
335338
},
336-
// SP-domain Delete is intentionally skipped (no MCP cluster to clean up on);
337-
// AccessRequest cleanup and finalizer removal still run via clusterAccessProvider.ReconcileDelete.
338-
wantStatusPhase: StatusPhaseTerminating,
339-
wantReconciliation: false,
340-
wantErr: false,
341-
noMCP: true,
339+
wantStatusPhase: StatusPhaseTerminating,
340+
wantReason: "WaitingForClusterContext",
341+
wantReconciliation: false,
342+
wantErr: false,
343+
missingClusterAccess: true,
342344
},
343345
}
344346
for _, tt := range tests {
345347
t.Run(tt.name, func(t *testing.T) {
346348
onboardingObjects := []client.Object{tt.apiObj}
347-
if !tt.noMCP {
348-
// onboardingObjects = append(onboardingObjects, &mcpv2alpha1.ManagedControlPlaneV2{
349-
// ObjectMeta: metav1.ObjectMeta{
350-
// Name: tt.req.Name,
351-
// Namespace: tt.req.Namespace,
352-
// },
353-
// })
354-
}
355349
onboardingCluster := createFakeCluster(t, "onboarding", onboardingObjects...)
356350
platformCluster := createFakeCluster(t, "platform")
357351
mockReconciler := &MockServiceProviderReconciler{
358352
wantError: tt.wantErr,
359353
}
354+
clusterAccessProvider := FakeClusterAccessProvider{
355+
ManagedControlPlane: createFakeCluster(t, testMCPName),
356+
ManagedControlPlaneAR: &clustersv1alpha1.AccessRequest{
357+
ObjectMeta: metav1.ObjectMeta{
358+
Name: testMCPName,
359+
Namespace: testNamespaceName,
360+
},
361+
Status: clustersv1alpha1.AccessRequestStatus{
362+
SecretRef: &common.LocalObjectReference{
363+
Name: testMCPKubeconfig,
364+
},
365+
},
366+
},
367+
Workload: createFakeCluster(t, testWorkloadName),
368+
WorkloadAR: &clustersv1alpha1.AccessRequest{
369+
ObjectMeta: metav1.ObjectMeta{
370+
Name: testWorkloadName,
371+
Namespace: testNamespaceName,
372+
},
373+
Status: clustersv1alpha1.AccessRequestStatus{
374+
SecretRef: &common.LocalObjectReference{
375+
Name: testWorkloadKubeconfig,
376+
},
377+
},
378+
},
379+
}
380+
if tt.missingClusterAccess {
381+
clusterAccessProvider.RequeueAfter = time.Hour
382+
}
360383
builder := NewAPIReconcilerBuilder[*fakeApiImpl, *fakeProviderConfigImpl]().
361384
EmptyObjectProvider(func() *fakeApiImpl { return &fakeApiImpl{} }).
362385
EmptyConfigProvider(func() *fakeProviderConfigImpl { return &fakeProviderConfigImpl{} }).
363386
OnboardingCluster(onboardingCluster).
364387
PlatformCluster(platformCluster).
365-
ClusterAccessReconciler(FakeClusterAccessProvider{
366-
ManagedControlPlane: createFakeCluster(t, testMCPName),
367-
ManagedControlPlaneAR: &clustersv1alpha1.AccessRequest{
368-
ObjectMeta: metav1.ObjectMeta{
369-
Name: testMCPName,
370-
Namespace: testNamespaceName,
371-
},
372-
Status: clustersv1alpha1.AccessRequestStatus{
373-
SecretRef: &common.LocalObjectReference{
374-
Name: testMCPKubeconfig,
375-
},
376-
},
377-
},
378-
Workload: createFakeCluster(t, testWorkloadName),
379-
WorkloadAR: &clustersv1alpha1.AccessRequest{
380-
ObjectMeta: metav1.ObjectMeta{
381-
Name: testWorkloadName,
382-
Namespace: testNamespaceName,
383-
},
384-
Status: clustersv1alpha1.AccessRequestStatus{
385-
SecretRef: &common.LocalObjectReference{
386-
Name: testWorkloadKubeconfig,
387-
},
388-
},
389-
},
390-
}).
388+
ClusterAccessReconciler(clusterAccessProvider).
391389
Reconciler(mockReconciler).
392390
WorkloadCluster(true)
393391
r := builder.MustBuild()
@@ -473,6 +471,7 @@ func assertReconcileAnnotationRemoved(t *testing.T, c client.Client, req ctrl.Re
473471
obj := &fakeApiImpl{}
474472
obj.SetName(req.Name)
475473
obj.SetNamespace(req.Namespace)
474+
require.NoError(t, c.Get(context.Background(), client.ObjectKeyFromObject(obj), obj))
476475
_, hasAnnotation := obj.GetAnnotations()[apiconst.OperationAnnotation]
477476
assert.False(t, hasAnnotation, "Operation annotation should have been removed")
478477
}
@@ -532,6 +531,7 @@ type FakeClusterAccessProvider struct {
532531
ManagedControlPlaneAR *clustersv1alpha1.AccessRequest
533532
Workload *clusters.Cluster
534533
WorkloadAR *clustersv1alpha1.AccessRequest
534+
RequeueAfter time.Duration
535535
}
536536

537537
// MCPAccessRequest implements [ClusterAccessProvider].
@@ -549,7 +549,9 @@ func (f FakeClusterAccessProvider) Reconcile(ctx context.Context, request reconc
549549
if request.Name == testObjectNameClusterAccessError {
550550
return reconcile.Result{}, errors.New("cluster access reconcile failed")
551551
}
552-
return reconcile.Result{}, nil
552+
return reconcile.Result{
553+
RequeueAfter: f.RequeueAfter,
554+
}, nil
553555
}
554556

555557
// ReconcileDelete implements [ClusterAccessProvider].
@@ -969,7 +971,6 @@ func createFakeCluster(t *testing.T, id string, clusterObjects ...client.Object)
969971
_ = clientgoscheme.AddToScheme(scheme)
970972
_ = apiextv1.AddToScheme(scheme)
971973
_ = clustersv1alpha1.AddToScheme(scheme)
972-
_ = mcpv2alpha1.AddToScheme(scheme)
973974
scheme.AddKnownTypes(testGV, &fakeApiImpl{}, &fakeProviderConfigImpl{})
974975

975976
// init cluster with objects

‎pkg/serviceprovider/status.go‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ const (
1515
// StatusPhaseTerminating indicates that the resource is not ready and in deletion.
1616
StatusPhaseTerminating = "Terminating"
1717

18-
reasonReconcileError = "ReconcileError"
19-
reasonManagedControlPlaneNotFound = "ManagedControlPlaneNotFound"
18+
reasonReconcileError = "ReconcileError"
2019
)
2120

2221
// StatusProgressing indicates progressing with synced false

0 commit comments

Comments
 (0)