Skip to content

Commit 7d85659

Browse files
committed
feat: move auth ConfigMap watch from ShootController to CareInstruction controller, adapt tests
On-behalf-of: @SAP krzysztof.zagorski@sap.com Signed-off-by: Zaggy21 <k.zaggy@gmail.com>
1 parent 28511b9 commit 7d85659

4 files changed

Lines changed: 183 additions & 346 deletions

File tree

controller/careinstruction/careinstruction_controller.go

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package careinstruction
66
import (
77
"context"
88
"errors"
9+
"maps"
910
"reflect"
1011
"sync"
1112

@@ -18,9 +19,11 @@ import (
1819
"sigs.k8s.io/controller-runtime/pkg/client"
1920
"sigs.k8s.io/controller-runtime/pkg/config"
2021
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
22+
"sigs.k8s.io/controller-runtime/pkg/event"
2123
"sigs.k8s.io/controller-runtime/pkg/handler"
2224
"sigs.k8s.io/controller-runtime/pkg/log"
2325
"sigs.k8s.io/controller-runtime/pkg/metrics/server"
26+
"sigs.k8s.io/controller-runtime/pkg/predicate"
2427

2528
greenhouseapis "github.com/cloudoperators/greenhouse/api"
2629
greenhousemetav1alpha1 "github.com/cloudoperators/greenhouse/api/meta/v1alpha1"
@@ -52,6 +55,7 @@ type garden struct {
5255
careInstructionSpec *v1alpha1.CareInstructionSpec // The CareInstruction object for the garden cluster
5356
cancelFunc context.CancelFunc // Cancel function to stop the manager
5457
stopChan chan bool // Channel to know if the manager is stopped
58+
authConfigMapData map[string]string // In-memory cache of the latest auth ConfigMap Data
5559
}
5660

5761
type careInstructionContextKey struct{}
@@ -64,11 +68,35 @@ type careInstructionContextKey struct{}
6468
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch;delete
6569

6670
func (r *CareInstructionReconciler) SetupWithManager(mgr ctrl.Manager) error {
71+
// Auth ConfigMap predicate: only re-enqueue the CareInstruction when ConfigMap Data changes.
72+
// Label-only patches (e.g. from our own MergeFrom calls in auth.go) are ignored.
73+
authCMDataChangedPredicate := predicate.Funcs{
74+
CreateFunc: func(_ event.CreateEvent) bool { return false },
75+
UpdateFunc: func(e event.UpdateEvent) bool {
76+
oldCM, ok1 := e.ObjectOld.(*corev1.ConfigMap)
77+
newCM, ok2 := e.ObjectNew.(*corev1.ConfigMap)
78+
if !ok1 || !ok2 {
79+
return false
80+
}
81+
return !maps.Equal(oldCM.Data, newCM.Data)
82+
},
83+
DeleteFunc: func(_ event.DeleteEvent) bool { return false },
84+
}
85+
6786
// Setup the controller with the manager
6887
return ctrl.NewControllerManagedBy(mgr).
6988
For(&v1alpha1.CareInstruction{}).
7089
Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(r.enqueueCareInstructionForGardenCluster), builder.WithPredicates(clientutil.PredicateFilterBySecretTypes(greenhouseapis.SecretTypeKubeConfig, greenhouseapis.SecretTypeOIDCConfig))).
7190
Watches(&greenhousev1alpha1.Cluster{}, handler.EnqueueRequestsFromMapFunc(r.enqueueCareInstructionForCreatedClusters), builder.WithPredicates(clientutil.PredicateHasLabel(v1alpha1.CareInstructionLabel))).
91+
// Watch auth ConfigMaps on the Greenhouse cluster. When their Data changes, re-enqueue the owning
92+
// CareInstruction so reconcileManager detects the change and restarts the ShootController with
93+
// the updated CM data.
94+
Watches(&corev1.ConfigMap{}, handler.EnqueueRequestsFromMapFunc(r.enqueueCareInstructionForAuthConfigMap),
95+
builder.WithPredicates(
96+
clientutil.PredicateHasLabel(v1alpha1.AuthConfigMapLabel),
97+
authCMDataChangedPredicate,
98+
),
99+
).
72100
Complete(r)
73101
}
74102

@@ -172,6 +200,7 @@ func (r *CareInstructionReconciler) reconcileManager(ctx context.Context, careIn
172200
gardenClient: nil,
173201
careInstructionSpec: &careInstruction.Spec,
174202
cancelFunc: nil,
203+
authConfigMapData: nil,
175204
}
176205
}
177206
r.gardensMu.Unlock()
@@ -197,6 +226,12 @@ func (r *CareInstructionReconciler) reconcileManager(ctx context.Context, careIn
197226
),
198227
)
199228

229+
// Fetch the current auth ConfigMap data so we can detect changes and pass it to the ShootController.
230+
currentAuthCMData, authCMErr := r.fetchAuthConfigMapData(ctx, &careInstruction)
231+
if authCMErr != nil && !apierrors.IsNotFound(authCMErr) {
232+
r.Info("failed to fetch auth ConfigMap data, will proceed without it", "error", authCMErr)
233+
}
234+
200235
// Now we check the following to see if we need to recreate and restart the manager (with read lock):
201236
r.gardensMu.RLock()
202237
garden := r.gardens[gardenKey]
@@ -221,9 +256,11 @@ func (r *CareInstructionReconciler) reconcileManager(ctx context.Context, careIn
221256
channelOpen = true
222257
}
223258
}
259+
// 6. If the auth ConfigMap data has changed, the ShootController must be restarted with the new data
260+
authConfigMapDataChanged := !maps.Equal(garden.authConfigMapData, currentAuthCMData)
224261
r.gardensMu.RUnlock()
225262

226-
if mgrExists && shootControllerStarted && !gardenConfigChanged && !careInstructionSpecChanged && channelExists && channelOpen {
263+
if mgrExists && shootControllerStarted && !gardenConfigChanged && !careInstructionSpecChanged && channelExists && channelOpen && !authConfigMapDataChanged {
227264
r.Info("Manager is running, garden cluster config & careInstruction.Spec is unchanged, skipping client and manager recreation", "careInstruction", careInstruction.Name)
228265
return nil
229266
}
@@ -241,6 +278,8 @@ func (r *CareInstructionReconciler) reconcileManager(ctx context.Context, careIn
241278
reason = "stop channel is missing"
242279
case !channelOpen:
243280
reason = "manager stop channel is closed"
281+
case authConfigMapDataChanged:
282+
reason = "auth ConfigMap data has changed"
244283
default:
245284
reason = "unknown reason"
246285
}
@@ -287,17 +326,17 @@ func (r *CareInstructionReconciler) reconcileManager(ctx context.Context, careIn
287326
return err
288327
}
289328

290-
// Register the ShootController with the garden manager
329+
// Register the ShootController with the garden manager.
291330
// Note: EventRecorder is obtained from the Greenhouse manager to emit events on the Greenhouse cluster.
292-
// GreenhouseMgr is passed so the ShootController can watch Greenhouse cluster resources (e.g. auth CMs).
331+
// AuthConfigMapData is passed in-memory from the CareInstruction controller, which owns the auth CM watch.
293332
sc := &shoot.ShootController{
294-
GreenhouseClient: r.Client,
295-
GreenhouseMgr: r.Manager,
296-
GardenClient: gardenClient,
297-
Logger: r.WithValues("careInstruction", careInstruction.Name),
298-
Name: shoot.GenerateName(careInstruction.Name),
299-
CareInstruction: careInstruction.DeepCopy(),
300-
EventRecorder: r.GetEventRecorderFor(shoot.GenerateName(careInstruction.Name)),
333+
GreenhouseClient: r.Client,
334+
GardenClient: gardenClient,
335+
Logger: r.WithValues("careInstruction", careInstruction.Name),
336+
Name: shoot.GenerateName(careInstruction.Name),
337+
CareInstruction: careInstruction.DeepCopy(),
338+
EventRecorder: r.GetEventRecorderFor(shoot.GenerateName(careInstruction.Name)),
339+
AuthConfigMapData: currentAuthCMData,
301340
}
302341
if err := sc.SetupWithManager(shootControllerMgr); err != nil {
303342
return err
@@ -315,6 +354,7 @@ func (r *CareInstructionReconciler) reconcileManager(ctx context.Context, careIn
315354
r.gardens[gardenKey].cancelFunc = cancel
316355
r.gardens[gardenKey].stopChan = make(chan bool)
317356
r.gardens[gardenKey].careInstructionSpec = &careInstruction.Spec
357+
r.gardens[gardenKey].authConfigMapData = currentAuthCMData
318358
stopChan := r.gardens[gardenKey].stopChan
319359
r.gardensMu.Unlock()
320360

@@ -589,3 +629,43 @@ func (r *CareInstructionReconciler) enqueueCareInstructionForCreatedClusters(_ c
589629
},
590630
}
591631
}
632+
633+
// enqueueCareInstructionForAuthConfigMap enqueues the CareInstruction that references the changed auth ConfigMap.
634+
// The CareInstructionLabel on the ConfigMap identifies the owning CareInstruction.
635+
func (r *CareInstructionReconciler) enqueueCareInstructionForAuthConfigMap(_ context.Context, obj client.Object) []ctrl.Request {
636+
cm, ok := obj.(*corev1.ConfigMap)
637+
if !ok {
638+
return nil
639+
}
640+
641+
careInstructionName, exists := cm.Labels[v1alpha1.CareInstructionLabel]
642+
if !exists {
643+
return nil
644+
}
645+
646+
r.Info("Enqueuing CareInstruction for auth ConfigMap change", "configMap", cm.Name, "careInstruction", careInstructionName)
647+
return []ctrl.Request{
648+
{
649+
NamespacedName: client.ObjectKey{
650+
Name: careInstructionName,
651+
Namespace: cm.Namespace,
652+
},
653+
},
654+
}
655+
}
656+
657+
// fetchAuthConfigMapData fetches the current Data of the auth ConfigMap referenced by the CareInstruction.
658+
// Returns nil (no error) when no auth ConfigMap is configured or the CM does not exist yet.
659+
func (r *CareInstructionReconciler) fetchAuthConfigMapData(ctx context.Context, careInstruction *v1alpha1.CareInstruction) (map[string]string, error) {
660+
if careInstruction.Spec.AuthenticationConfigMapName == "" {
661+
return nil, nil
662+
}
663+
var cm corev1.ConfigMap
664+
if err := r.Get(ctx, client.ObjectKey{
665+
Namespace: careInstruction.Namespace,
666+
Name: careInstruction.Spec.AuthenticationConfigMapName,
667+
}, &cm); err != nil {
668+
return nil, err
669+
}
670+
return cm.Data, nil
671+
}

controller/shoot/auth.go

Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -20,65 +20,60 @@ import (
2020
)
2121

2222
// configureOIDCAuthentication configures OIDC authentication for the Shoot by:
23-
// 1. Fetching the AuthenticationConfiguration ConfigMap from Greenhouse cluster
23+
// 1. Reading the AuthenticationConfiguration from the in-memory auth ConfigMap data (provided by the CareInstruction controller)
2424
// 2. Merging it with any existing configuration on the Garden cluster
2525
// 3. Updating the Shoot spec to reference the merged configuration
2626
func (r *ShootController) configureOIDCAuthentication(ctx context.Context, shoot *gardenerv1beta1.Shoot) error {
27-
// Fetch the AuthenticationConfiguration ConfigMap from Greenhouse cluster
27+
// Use the in-memory auth ConfigMap data provided by the CareInstruction controller.
28+
// This avoids a cross-cluster watch; the CareInstruction controller is responsible for
29+
// fetching and caching the data, and restarts this ShootController when it changes.
30+
if r.AuthConfigMapData == nil || r.AuthConfigMapData["config.yaml"] == "" {
31+
return fmt.Errorf("AuthenticationConfiguration ConfigMap %s does not contain config.yaml (data not available)",
32+
r.CareInstruction.Spec.AuthenticationConfigMapName)
33+
}
34+
35+
// Label the Greenhouse auth ConfigMap so the CareInstruction controller's watch predicate can
36+
// identify it and associate it with this CareInstruction.
37+
// We fetch the live CM to get the current metadata, then patch only the labels.
2838
var greenhouseAuthConfigMap corev1.ConfigMap
2939
if err := r.GreenhouseClient.Get(ctx, client.ObjectKey{
3040
Namespace: r.CareInstruction.Namespace,
3141
Name: r.CareInstruction.Spec.AuthenticationConfigMapName,
32-
}, &greenhouseAuthConfigMap); err != nil {
33-
return fmt.Errorf("failed to fetch AuthenticationConfiguration ConfigMap %s from Greenhouse cluster: %w",
34-
r.CareInstruction.Spec.AuthenticationConfigMapName, err)
35-
}
36-
37-
// Label the ConfigMap so the watch predicate can identify it and associate it with this CareInstruction.
38-
// Take a snapshot before any mutations so the patch only touches metadata.labels.
39-
base := greenhouseAuthConfigMap.DeepCopy()
40-
if greenhouseAuthConfigMap.Labels == nil {
41-
greenhouseAuthConfigMap.Labels = make(map[string]string)
42-
}
43-
labelsNeedUpdate := false
44-
45-
// If the CM is already owned by a different CareInstruction, skip relabelling to
46-
// avoid breaking its watch predicate. OIDC merging still proceeds normally.
47-
existingOwner, hasCILabel := greenhouseAuthConfigMap.Labels[v1alpha1.CareInstructionLabel]
48-
if hasCILabel && existingOwner != r.CareInstruction.Name {
49-
r.Info("auth ConfigMap is already owned by another CareInstruction; skipping relabel to avoid breaking its watch",
50-
"configMap", greenhouseAuthConfigMap.Name,
51-
"existingOwner", existingOwner,
52-
"thisCareInstruction", r.CareInstruction.Name)
53-
} else {
54-
if _, hasAuthLabel := greenhouseAuthConfigMap.Labels[v1alpha1.AuthConfigMapLabel]; !hasAuthLabel {
55-
greenhouseAuthConfigMap.Labels[v1alpha1.AuthConfigMapLabel] = "true"
56-
labelsNeedUpdate = true
42+
}, &greenhouseAuthConfigMap); err == nil {
43+
base := greenhouseAuthConfigMap.DeepCopy()
44+
if greenhouseAuthConfigMap.Labels == nil {
45+
greenhouseAuthConfigMap.Labels = make(map[string]string)
5746
}
58-
if !hasCILabel {
59-
greenhouseAuthConfigMap.Labels[v1alpha1.CareInstructionLabel] = r.CareInstruction.Name
60-
labelsNeedUpdate = true
47+
labelsNeedUpdate := false
48+
49+
// If the CM is already owned by a different CareInstruction, skip relabelling.
50+
existingOwner, hasCILabel := greenhouseAuthConfigMap.Labels[v1alpha1.CareInstructionLabel]
51+
if hasCILabel && existingOwner != r.CareInstruction.Name {
52+
r.Info("auth ConfigMap is already owned by another CareInstruction; skipping relabel",
53+
"configMap", greenhouseAuthConfigMap.Name,
54+
"existingOwner", existingOwner,
55+
"thisCareInstruction", r.CareInstruction.Name)
56+
} else {
57+
if _, hasAuthLabel := greenhouseAuthConfigMap.Labels[v1alpha1.AuthConfigMapLabel]; !hasAuthLabel {
58+
greenhouseAuthConfigMap.Labels[v1alpha1.AuthConfigMapLabel] = "true"
59+
labelsNeedUpdate = true
60+
}
61+
if !hasCILabel {
62+
greenhouseAuthConfigMap.Labels[v1alpha1.CareInstructionLabel] = r.CareInstruction.Name
63+
labelsNeedUpdate = true
64+
}
6165
}
62-
}
6366

64-
if labelsNeedUpdate {
65-
// Use a merge patch so only metadata.labels is sent to the API server.
66-
// This avoids overwriting concurrent changes to config.yaml or other fields.
67-
if err := r.GreenhouseClient.Patch(ctx, &greenhouseAuthConfigMap, client.MergeFrom(base)); err != nil {
68-
r.Info("failed to patch labels on auth ConfigMap", "configMap", greenhouseAuthConfigMap.Name, "error", err)
69-
// Don't fail the reconciliation for this, just log it
67+
if labelsNeedUpdate {
68+
if patchErr := r.GreenhouseClient.Patch(ctx, &greenhouseAuthConfigMap, client.MergeFrom(base)); patchErr != nil {
69+
r.Info("failed to patch labels on auth ConfigMap", "configMap", greenhouseAuthConfigMap.Name, "error", patchErr)
70+
}
7071
}
7172
}
7273

73-
// Verify the ConfigMap contains config.yaml
74-
if greenhouseAuthConfigMap.Data == nil || greenhouseAuthConfigMap.Data["config.yaml"] == "" {
75-
return fmt.Errorf("AuthenticationConfiguration ConfigMap %s does not contain config.yaml",
76-
r.CareInstruction.Spec.AuthenticationConfigMapName)
77-
}
78-
79-
// Parse the Greenhouse authentication configuration
74+
// Parse the Greenhouse authentication configuration from in-memory data
8075
var greenhouseAuthConfig apiserverv1beta1.AuthenticationConfiguration
81-
if err := yaml.Unmarshal([]byte(greenhouseAuthConfigMap.Data["config.yaml"]), &greenhouseAuthConfig); err != nil {
76+
if err := yaml.Unmarshal([]byte(r.AuthConfigMapData["config.yaml"]), &greenhouseAuthConfig); err != nil {
8277
return fmt.Errorf("failed to parse Greenhouse AuthenticationConfiguration: %w", err)
8378
}
8479

controller/shoot/shoot_controller.go

Lines changed: 7 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,7 @@ import (
2626
"sigs.k8s.io/controller-runtime/pkg/client"
2727
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
2828
"sigs.k8s.io/controller-runtime/pkg/event"
29-
"sigs.k8s.io/controller-runtime/pkg/handler"
3029
"sigs.k8s.io/controller-runtime/pkg/predicate"
31-
"sigs.k8s.io/controller-runtime/pkg/reconcile"
32-
"sigs.k8s.io/controller-runtime/pkg/source"
3330
)
3431

3532
const (
@@ -38,9 +35,9 @@ const (
3835
)
3936

4037
type ShootController struct {
41-
GreenhouseClient client.Client
42-
GreenhouseMgr ctrl.Manager // GreenhouseMgr provides the Greenhouse cluster cache for watches
43-
GardenClient client.Client
38+
GreenhouseClient client.Client
39+
GardenClient client.Client
40+
AuthConfigMapData map[string]string // In-memory auth ConfigMap Data provided by the CareInstruction controller
4441
logr.Logger
4542
Name string
4643
CareInstruction *v1alpha1.CareInstruction
@@ -79,60 +76,11 @@ func (r *ShootController) SetupWithManager(mgr ctrl.Manager) error {
7976
r.Error(nil, "EventRecorder is not set for ShootController", "name", r.Name)
8077
}
8178

82-
b := ctrl.NewControllerManagedBy(mgr).
83-
Named(r.Name).
84-
For(&gardenerv1beta1.Shoot{}, builder.WithPredicates(predicates...))
85-
86-
// Watch the auth ConfigMap on the Greenhouse cluster; re-enqueue all Shoots when it changes
87-
// so the Garden-side OIDC config stays in sync.
88-
if r.CareInstruction.Spec.AuthenticationConfigMapName != "" && r.GreenhouseMgr != nil {
89-
authCMPredicate := predicate.TypedFuncs[*corev1.ConfigMap]{
90-
CreateFunc: func(_ event.TypedCreateEvent[*corev1.ConfigMap]) bool { return false },
91-
UpdateFunc: func(e event.TypedUpdateEvent[*corev1.ConfigMap]) bool {
92-
oldCM, newCM := e.ObjectOld, e.ObjectNew
93-
if newCM.GetName() != r.CareInstruction.Spec.AuthenticationConfigMapName ||
94-
newCM.GetNamespace() != r.CareInstruction.GetNamespace() {
95-
return false
96-
}
97-
if newCM.GetLabels()[v1alpha1.AuthConfigMapLabel] != "true" ||
98-
newCM.GetLabels()[v1alpha1.CareInstructionLabel] != r.CareInstruction.Name {
99-
return false
100-
}
101-
return !maps.Equal(oldCM.Data, newCM.Data)
102-
},
103-
DeleteFunc: func(_ event.TypedDeleteEvent[*corev1.ConfigMap]) bool { return false },
104-
}
105-
b = b.WatchesRawSource(source.Kind(
106-
r.GreenhouseMgr.GetCache(),
107-
&corev1.ConfigMap{},
108-
handler.TypedEnqueueRequestsFromMapFunc(r.enqueueShootsForAuthConfigMap),
109-
authCMPredicate,
110-
))
111-
}
112-
11379
// Setup the shoot controller with the manager
114-
return b.Complete(r)
115-
}
116-
117-
func (r *ShootController) enqueueShootsForAuthConfigMap(ctx context.Context, _ *corev1.ConfigMap) []reconcile.Request {
118-
shoots, err := r.CareInstruction.ListShoots(ctx, r.GardenClient)
119-
if err != nil {
120-
r.Info("failed to list shoots for auth ConfigMap change", "error", err)
121-
return nil
122-
}
123-
requests := make([]reconcile.Request, 0, len(shoots.Items))
124-
for _, shoot := range shoots.Items {
125-
if !r.matchesCEL(&shoot) {
126-
continue
127-
}
128-
requests = append(requests, reconcile.Request{
129-
NamespacedName: client.ObjectKey{
130-
Namespace: shoot.Namespace,
131-
Name: shoot.Name,
132-
},
133-
})
134-
}
135-
return requests
80+
return ctrl.NewControllerManagedBy(mgr).
81+
Named(r.Name).
82+
For(&gardenerv1beta1.Shoot{}, builder.WithPredicates(predicates...)).
83+
Complete(r)
13684
}
13785

13886
func (r *ShootController) newCELPredicate() predicate.Predicate {

0 commit comments

Comments
 (0)