Skip to content

Commit 2fc9a7d

Browse files
committed
accessrequest controller now also adds profile label
1 parent 3527800 commit 2fc9a7d

File tree

12 files changed

+224
-12
lines changed

12 files changed

+224
-12
lines changed

api/clusters/v1alpha1/constants.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ const (
8181
DeleteWithoutRequestsLabel = GroupName + "/delete-without-requests"
8282
// ProviderLabel is used to indicate the provider that is responsible for an AccessRequest.
8383
ProviderLabel = "provider." + GroupName
84+
// ProfileLabel is used to make the profile information easily accessible for the ClusterProviders.
85+
ProfileLabel = "profile." + GroupName
8486
)
8587

8688
const (

docs/controller/accessrequest.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,17 @@ The _AccessRequest Controller_ is responsible for labelling `AccessRequest` reso
44

55
This is needed because the information, which ClusterProvider is responsible for answering the `AccessRequest` is contained in the referenced `ClusterProfile`. Depending on `AccessRequest`'s spec, a `Cluster` and potentially also a `ClusterRequest` must be fetched before the `ClusterProfile` is known, which then has to be fetched too. If multiple ClusterProviders are running in the cluster, all of them would need to fetch these resources, only for all but one of them to notice that they are not responsible and don't have to do anything.
66

7-
To increase performance and simplify reconciliation logic in the individual ClusterProviders, this central AccessRequest controller takes over the task of figuring out the responsible ClusterProvider and adds a `provider.clusters.openmcp.cloud` label with its name to the `AccessRequest` resource. It reacts only on resources which do not yet have this label, so it should reconcile each `AccessRequest` only once (excluding repeated reconciliations due to errors).
7+
To increase performance and simplify reconciliation logic in the individual ClusterProviders, this central AccessRequest controller takes over the task of figuring out the ClusterProfile and the responsible ClusterProvider and it adds these as labels to the `AccessRequest` resource. It reacts only on resources which do not yet have both of these labels set, so it should reconcile each `AccessRequest` only once (excluding repeated reconciliations due to errors).
88

9-
ClusterProviders should only reconcile `AccessRequest` resources where the value of the `provider.clusters.openmcp.cloud` label matches their own provider name and ignore resources with other values or if the label is missing completely.
9+
The added labels are:
10+
```yaml
11+
provider.clusters.openmcp.cloud: <provider-name>
12+
profile.clusters.openmcp.cloud: <profile-name>
13+
```
14+
15+
ClusterProviders should only reconcile `AccessRequest` resources where both labels are set and the value of the provider label matches their own provider name. Resources where either label is missing or the value of the provider label does not match the own provider name must be ignored.
16+
17+
Note that if a reconciled `AccessRequest` already has one of the labels set, but its value differs from the expected one, the controller will log an error, but not update the resource in any way, to not accidentally move the responsibility from one provider to another. This also means that `AccessRequest` resources that have only one of the labels set, and that one to a wrong value, will not be handled - this controller won't update the resource and the ClusterProvider should not pick it up because one of the labels is missing. It is therefore strongly recommended to not set the labels when creating a new `AccessRequest` resource.
1018

1119
## Configuration
1220

internal/controllers/accessrequest/controller.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ func (r *AccessRequestReconciler) reconcile(ctx context.Context, req reconcile.R
140140
// fetch ClusterProfile resource
141141
cp := &clustersv1alpha1.ClusterProfile{}
142142
cp.SetName(c.Spec.Profile)
143-
log.Debug("Fetching ClusterProfile", "clusterProfileName", cp.Name)
143+
log.Debug("Fetching ClusterProfile", "profileName", cp.Name)
144144
if err := r.PlatformCluster.Client().Get(ctx, client.ObjectKeyFromObject(cp), cp); err != nil {
145145
if apierrors.IsNotFound(err) {
146146
rr.ReconcileError = errutils.WithReason(fmt.Errorf("ClusterProfile '%s' not found", cp.Name), cconst.ReasonInvalidReference)
@@ -150,13 +150,22 @@ func (r *AccessRequestReconciler) reconcile(ctx context.Context, req reconcile.R
150150
return rr
151151
}
152152

153-
log.Info("Identified responsible ClusterProvider", "providerName", cp.Spec.ProviderRef.Name)
154-
if err := ctrlutils.EnsureLabel(ctx, r.PlatformCluster.Client(), ar, clustersv1alpha1.ProviderLabel, cp.Spec.ProviderRef.Name, true); err != nil {
155-
if ctrlutils.IsMetadataEntryAlreadyExistsError(err) {
156-
log.Error(err, "label '%s' already set on resource '%s'", clustersv1alpha1.ProviderLabel, req.String())
153+
log.Info("Identified responsible ClusterProvider", "providerName", cp.Spec.ProviderRef.Name, "profileName", cp.Name)
154+
arOld := ar.DeepCopy()
155+
if err := ctrlutils.EnsureLabel(ctx, nil, ar, clustersv1alpha1.ProviderLabel, cp.Spec.ProviderRef.Name, false); err != nil {
156+
if e, ok := err.(*ctrlutils.MetadataEntryAlreadyExistsError); ok {
157+
log.Error(err, "label '%s' already set on resource '%s', but with value '%s' instead of the desired value '%s'", e.Key, req.String(), e.ActualValue, e.DesiredValue)
157158
return rr
158159
}
159-
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error setting label '%s' with value '%s' on resource '%s': %w", clustersv1alpha1.ProviderLabel, cp.Spec.ProviderRef.Name, req.String(), err), cconst.ReasonPlatformClusterInteractionProblem)
160+
}
161+
if err := ctrlutils.EnsureLabel(ctx, nil, ar, clustersv1alpha1.ProfileLabel, cp.Name, false); err != nil {
162+
if e, ok := err.(*ctrlutils.MetadataEntryAlreadyExistsError); ok {
163+
log.Error(err, "label '%s' already set on resource '%s', but with value '%s' instead of the desired value '%s'", e.Key, req.String(), e.ActualValue, e.DesiredValue)
164+
return rr
165+
}
166+
}
167+
if err := r.PlatformCluster.Client().Patch(ctx, ar, client.MergeFrom(arOld)); err != nil {
168+
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error patching labels on resource '%s': %w", req.String(), err), cconst.ReasonPlatformClusterInteractionProblem)
160169
return rr
161170
}
162171

@@ -169,8 +178,11 @@ func (r *AccessRequestReconciler) SetupWithManager(mgr ctrl.Manager) error {
169178
// watch AccessRequest resources
170179
For(&clustersv1alpha1.AccessRequest{}).
171180
WithEventFilter(predicate.And(
172-
// ignore resources that already have the provider label set
173-
predicate.Not(ctrlutils.HasLabelPredicate(clustersv1alpha1.ProviderLabel, "")),
181+
// ignore resources that already have the provider AND profile label set
182+
predicate.Not(predicate.And(
183+
ctrlutils.HasLabelPredicate(clustersv1alpha1.ProviderLabel, ""),
184+
ctrlutils.HasLabelPredicate(clustersv1alpha1.ProfileLabel, ""),
185+
)),
174186
ctrlutils.LabelSelectorPredicate(r.Config.Selector.Completed()),
175187
predicate.Or(
176188
ctrlutils.GotAnnotationPredicate(clustersv1alpha1.OperationAnnotation, clustersv1alpha1.OperationAnnotationValueReconcile),

internal/controllers/accessrequest/controller_test.go

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,41 +26,48 @@ func arReconciler(c client.Client) reconcile.Reconciler {
2626

2727
var _ = Describe("AccessRequest Controller", func() {
2828

29-
It("should add the correct provider label to the AccessRequest if a Cluster is referenced directly", func() {
29+
It("should add the correct provider and profile labels to the AccessRequest if a Cluster is referenced directly", func() {
3030
env := testutils.NewEnvironmentBuilder().WithFakeClient(scheme).WithInitObjectPath("testdata", "test-01").WithReconcilerConstructor(arReconciler).Build()
3131
ar := &clustersv1alpha1.AccessRequest{}
3232
Expect(env.Client().Get(env.Ctx, ctrlutils.ObjectKey("mc-access", "bar"), ar)).To(Succeed())
3333
Expect(ar.Labels).ToNot(HaveKey(clustersv1alpha1.ProviderLabel))
34+
Expect(ar.Labels).ToNot(HaveKey(clustersv1alpha1.ProfileLabel))
3435
env.ShouldReconcile(testutils.RequestFromObject(ar))
3536
Expect(env.Client().Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed())
3637
Expect(ar.Labels).To(HaveKeyWithValue(clustersv1alpha1.ProviderLabel, "asdf"))
38+
Expect(ar.Labels).To(HaveKeyWithValue(clustersv1alpha1.ProfileLabel, "default"))
3739
})
3840

39-
It("should add the correct provider label to the AccessRequest if a Cluster is referenced via a ClusterRequest", func() {
41+
It("should add the correct provider and profile labels to the AccessRequest if a Cluster is referenced via a ClusterRequest", func() {
4042
env := testutils.NewEnvironmentBuilder().WithFakeClient(scheme).WithInitObjectPath("testdata", "test-01").WithReconcilerConstructor(arReconciler).Build()
4143
ar := &clustersv1alpha1.AccessRequest{}
4244
Expect(env.Client().Get(env.Ctx, ctrlutils.ObjectKey("mcr-access", "bar"), ar)).To(Succeed())
4345
Expect(ar.Labels).ToNot(HaveKey(clustersv1alpha1.ProviderLabel))
46+
Expect(ar.Labels).ToNot(HaveKey(clustersv1alpha1.ProfileLabel))
4447
env.ShouldReconcile(testutils.RequestFromObject(ar))
4548
Expect(env.Client().Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed())
4649
Expect(ar.Labels).To(HaveKeyWithValue(clustersv1alpha1.ProviderLabel, "asdf"))
50+
Expect(ar.Labels).To(HaveKeyWithValue(clustersv1alpha1.ProfileLabel, "default"))
4751
})
4852

4953
It("should fail if the AccessRequest references a ClusterRequest which is not Granted", func() {
5054
env := testutils.NewEnvironmentBuilder().WithFakeClient(scheme).WithInitObjectPath("testdata", "test-02").WithReconcilerConstructor(arReconciler).Build()
5155
ar := &clustersv1alpha1.AccessRequest{}
5256
Expect(env.Client().Get(env.Ctx, ctrlutils.ObjectKey("mcr-access", "bar"), ar)).To(Succeed())
5357
Expect(ar.Labels).ToNot(HaveKey(clustersv1alpha1.ProviderLabel))
58+
Expect(ar.Labels).ToNot(HaveKey(clustersv1alpha1.ProfileLabel))
5459
env.ShouldNotReconcile(testutils.RequestFromObject(ar))
5560
Expect(env.Client().Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed())
5661
Expect(ar.Status.Message).To(ContainSubstring("not granted"))
5762
})
5863

5964
It("should fail if the AccessRequest references an unknown Cluster or ClusterRequest", func() {
6065
env := testutils.NewEnvironmentBuilder().WithFakeClient(scheme).WithInitObjectPath("testdata", "test-03").WithReconcilerConstructor(arReconciler).Build()
66+
6167
ar := &clustersv1alpha1.AccessRequest{}
6268
Expect(env.Client().Get(env.Ctx, ctrlutils.ObjectKey("mc-access", "bar"), ar)).To(Succeed())
6369
Expect(ar.Labels).ToNot(HaveKey(clustersv1alpha1.ProviderLabel))
70+
Expect(ar.Labels).ToNot(HaveKey(clustersv1alpha1.ProfileLabel))
6471
env.ShouldNotReconcile(testutils.RequestFromObject(ar))
6572
Expect(env.Client().Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed())
6673
Expect(ar.Status.Reason).To(Equal(cconst.ReasonInvalidReference))
@@ -69,10 +76,55 @@ var _ = Describe("AccessRequest Controller", func() {
6976
ar = &clustersv1alpha1.AccessRequest{}
7077
Expect(env.Client().Get(env.Ctx, ctrlutils.ObjectKey("mcr-access", "bar"), ar)).To(Succeed())
7178
Expect(ar.Labels).ToNot(HaveKey(clustersv1alpha1.ProviderLabel))
79+
Expect(ar.Labels).ToNot(HaveKey(clustersv1alpha1.ProfileLabel))
7280
env.ShouldNotReconcile(testutils.RequestFromObject(ar))
7381
Expect(env.Client().Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed())
7482
Expect(ar.Status.Reason).To(Equal(cconst.ReasonInvalidReference))
7583
Expect(ar.Status.Message).To(ContainSubstring("not found"))
7684
})
7785

86+
It("should add the respective other label if either provider or profile label is already set", func() {
87+
env := testutils.NewEnvironmentBuilder().WithFakeClient(scheme).WithInitObjectPath("testdata", "test-04").WithReconcilerConstructor(arReconciler).Build()
88+
89+
ar := &clustersv1alpha1.AccessRequest{}
90+
Expect(env.Client().Get(env.Ctx, ctrlutils.ObjectKey("mc-access-provider", "bar"), ar)).To(Succeed())
91+
Expect(ar.Labels).To(HaveKey(clustersv1alpha1.ProviderLabel))
92+
Expect(ar.Labels).ToNot(HaveKey(clustersv1alpha1.ProfileLabel))
93+
env.ShouldReconcile(testutils.RequestFromObject(ar))
94+
Expect(env.Client().Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed())
95+
Expect(ar.Labels).To(HaveKeyWithValue(clustersv1alpha1.ProviderLabel, "asdf"))
96+
Expect(ar.Labels).To(HaveKeyWithValue(clustersv1alpha1.ProfileLabel, "default"))
97+
98+
ar = &clustersv1alpha1.AccessRequest{}
99+
Expect(env.Client().Get(env.Ctx, ctrlutils.ObjectKey("mc-access-profile", "bar"), ar)).To(Succeed())
100+
Expect(ar.Labels).ToNot(HaveKey(clustersv1alpha1.ProviderLabel))
101+
Expect(ar.Labels).To(HaveKey(clustersv1alpha1.ProfileLabel))
102+
env.ShouldReconcile(testutils.RequestFromObject(ar))
103+
Expect(env.Client().Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed())
104+
Expect(ar.Labels).To(HaveKeyWithValue(clustersv1alpha1.ProviderLabel, "asdf"))
105+
Expect(ar.Labels).To(HaveKeyWithValue(clustersv1alpha1.ProfileLabel, "default"))
106+
})
107+
108+
It("should not overwrite either label if already set to a different value", func() {
109+
env := testutils.NewEnvironmentBuilder().WithFakeClient(scheme).WithInitObjectPath("testdata", "test-05").WithReconcilerConstructor(arReconciler).Build()
110+
111+
ar := &clustersv1alpha1.AccessRequest{}
112+
Expect(env.Client().Get(env.Ctx, ctrlutils.ObjectKey("mc-access-provider", "bar"), ar)).To(Succeed())
113+
Expect(ar.Labels).To(HaveKey(clustersv1alpha1.ProviderLabel))
114+
Expect(ar.Labels).ToNot(HaveKey(clustersv1alpha1.ProfileLabel))
115+
env.ShouldReconcile(testutils.RequestFromObject(ar))
116+
Expect(env.Client().Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed())
117+
Expect(ar.Labels).ToNot(HaveKeyWithValue(clustersv1alpha1.ProviderLabel, "asdf"))
118+
Expect(ar.Labels).ToNot(HaveKey(clustersv1alpha1.ProfileLabel))
119+
120+
ar = &clustersv1alpha1.AccessRequest{}
121+
Expect(env.Client().Get(env.Ctx, ctrlutils.ObjectKey("mc-access-profile", "bar"), ar)).To(Succeed())
122+
Expect(ar.Labels).ToNot(HaveKey(clustersv1alpha1.ProviderLabel))
123+
Expect(ar.Labels).To(HaveKey(clustersv1alpha1.ProfileLabel))
124+
env.ShouldReconcile(testutils.RequestFromObject(ar))
125+
Expect(env.Client().Get(env.Ctx, client.ObjectKeyFromObject(ar), ar)).To(Succeed())
126+
Expect(ar.Labels).ToNot(HaveKey(clustersv1alpha1.ProviderLabel))
127+
Expect(ar.Labels).ToNot(HaveKeyWithValue(clustersv1alpha1.ProfileLabel, "default"))
128+
})
129+
78130
})
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apiVersion: clusters.openmcp.cloud/v1alpha1
2+
kind: AccessRequest
3+
metadata:
4+
name: mc-access-profile
5+
namespace: bar
6+
labels:
7+
profile.clusters.openmcp.cloud: default
8+
spec:
9+
clusterRef:
10+
name: my-cluster
11+
namespace: foo
12+
permissions:
13+
- rules:
14+
- apiGroups:
15+
- "*"
16+
resources:
17+
- "*"
18+
verbs:
19+
- "*"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apiVersion: clusters.openmcp.cloud/v1alpha1
2+
kind: AccessRequest
3+
metadata:
4+
name: mc-access-provider
5+
namespace: bar
6+
labels:
7+
provider.clusters.openmcp.cloud: asdf
8+
spec:
9+
clusterRef:
10+
name: my-cluster
11+
namespace: foo
12+
permissions:
13+
- rules:
14+
- apiGroups:
15+
- "*"
16+
resources:
17+
- "*"
18+
verbs:
19+
- "*"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
apiVersion: clusters.openmcp.cloud/v1alpha1
2+
kind: Cluster
3+
metadata:
4+
name: my-cluster
5+
namespace: foo
6+
spec:
7+
profile: default
8+
kubernetes:
9+
version: "1.32"
10+
purposes:
11+
- test
12+
tenancy: Exclusive
13+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
apiVersion: clusters.openmcp.cloud/v1alpha1
2+
kind: ClusterProfile
3+
metadata:
4+
generation: 1
5+
name: default
6+
spec:
7+
environment: default
8+
providerConfigRef:
9+
name: foobar
10+
providerRef:
11+
name: asdf
12+
supportedVersions:
13+
- version: 1.32.2
14+
- version: 1.31.7
15+
- deprecated: true
16+
version: 1.31.6
17+
- deprecated: true
18+
version: 1.31.5
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apiVersion: clusters.openmcp.cloud/v1alpha1
2+
kind: AccessRequest
3+
metadata:
4+
name: mc-access-profile
5+
namespace: bar
6+
labels:
7+
profile.clusters.openmcp.cloud: wrong
8+
spec:
9+
clusterRef:
10+
name: my-cluster
11+
namespace: foo
12+
permissions:
13+
- rules:
14+
- apiGroups:
15+
- "*"
16+
resources:
17+
- "*"
18+
verbs:
19+
- "*"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apiVersion: clusters.openmcp.cloud/v1alpha1
2+
kind: AccessRequest
3+
metadata:
4+
name: mc-access-provider
5+
namespace: bar
6+
labels:
7+
provider.clusters.openmcp.cloud: wrong
8+
spec:
9+
clusterRef:
10+
name: my-cluster
11+
namespace: foo
12+
permissions:
13+
- rules:
14+
- apiGroups:
15+
- "*"
16+
resources:
17+
- "*"
18+
verbs:
19+
- "*"

0 commit comments

Comments
 (0)