Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,11 @@ For each CareInstruction, a dedicated Shoot controller is dynamically created an
- Extracts cluster connection details (API server URL, CA certificate)
- Creates or updates corresponding Secret resources with OIDC configuration
- Generates Greenhouse Cluster resources with appropriate labels
- Optionally configures OIDC authentication on Shoot clusters for Greenhouse access. Also see respective [Greenhouse docs](https://cloudoperators.github.io/greenhouse/docs/user-guides/cluster/oidc_connectivity/) and [Gardener docs](https://gardener.cloud/docs/guides/administer-shoots/oidc-login/#configure-the-shoot-cluster)
- Optionally configures OIDC authentication on Shoot clusters for Greenhouse access. Also see respective [Greenhouse docs](https://cloudoperators.github.io/greenhouse/docs/user-guides/cluster/oidc-login/) and [Gardener docs](https://gardener.cloud/docs/guides/administer-shoots/oidc-login/#configure-the-shoot-cluster)
- Optionally configures RBAC on the Shoot cluster for Greenhouse access

> **Auth ConfigMap labeling & watch**: When `authenticationConfigMapName` is set, the shoot controller labels the referenced Greenhouse ConfigMap with `shoot-grafter.cloudoperators.dev/auth-configmap: "true"` and `shoot-grafter.cloudoperators.dev/careinstruction: <ci-name>` on first interaction (creation or update). The controller also watches these labeled ConfigMaps on the Greenhouse cluster; any change to the auth ConfigMap automatically re-enqueues all matching Shoots so the Garden-side OIDC configuration stays in sync without waiting for the next Shoot event.
Comment thread
Zaggy21 marked this conversation as resolved.

## Custom Resource: CareInstruction

A `CareInstruction` defines the configuration for onboarding Shoots from a specific Garden cluster.
Expand Down Expand Up @@ -208,7 +210,7 @@ spec:
| `shootSelector.expression` | string | No | CEL expression for filtering shoots by status or other fields (max 1024 chars). The shoot object is available as `object` |
| `propagateLabels` | []string | No | List of label keys to copy from Shoot to Greenhouse Cluster |
| `additionalLabels` | map[string]string | No | Additional labels to add to all created Greenhouse Clusters |
| `authenticationConfigMapName` | string | No | Name of ConfigMap in Greenhouse cluster containing AuthenticationConfiguration [(config.yaml with apiserver.config.k8s.io/v1beta1 content)](https://gardener.cloud/docs/guides/administer-shoots/oidc-login/#configure-the-shoot-cluster)|
| `authenticationConfigMapName` | string | No | Name of ConfigMap in Greenhouse cluster containing AuthenticationConfiguration [(config.yaml with apiserver.config.k8s.io/v1beta1 content)](https://gardener.cloud/docs/guides/administer-shoots/oidc-login/#configure-the-shoot-cluster). The ConfigMap is labeled by the shoot controller on first interaction and watched for changes to trigger automatic re-reconciliation of Shoots. |
| `enableRBAC` | bool | No | When false, skips automatic RBAC setup on Shoot clusters (default: true‚) |

*Note: Either `gardenClusterName` or `gardenClusterKubeConfigSecretName` must be provided (priority: kubeconfig secret > cluster name)
Expand Down
2 changes: 1 addition & 1 deletion api/v1alpha1/careinstruction_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const (
CareInstructionLabel = "shoot-grafter.cloudoperators.dev/careinstruction"

// AuthConfigMapLabel is the label used to identify AuthenticationConfiguration ConfigMaps
AuthConfigMapLabel = "shoot-grafter.cloudoperators/authconfigmap"
AuthConfigMapLabel = "shoot-grafter.cloudoperators.dev/auth-configmap"

// ShootStatusOnboarded indicates the shoot has been onboarded as a Greenhouse Cluster.
ShootStatusOnboarded = "Onboarded"
Expand Down
4 changes: 3 additions & 1 deletion controller/careinstruction/careinstruction_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,11 @@ func (r *CareInstructionReconciler) reconcileManager(ctx context.Context, careIn
}

// Register the ShootController with the garden manager
// Note: EventRecorder is obtained from the Greenhouse manager to emit events on the Greenhouse cluster
// Note: EventRecorder is obtained from the Greenhouse manager to emit events on the Greenhouse cluster.
// GreenhouseMgr is passed so the ShootController can watch Greenhouse cluster resources (e.g. auth CMs).
sc := &shoot.ShootController{
GreenhouseClient: r.Client,
GreenhouseMgr: r.Manager,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not super convinced that watching resources on two different Clusters is the way to go here.
I would opt to actually watch the CM changes in the CareInstruction controller for separation of concerns.
That would involve:

Looping in @abhijith-darshan for his opinion

GardenClient: gardenClient,
Logger: r.WithValues("careInstruction", careInstruction.Name),
Name: shoot.GenerateName(careInstruction.Name),
Expand Down
34 changes: 29 additions & 5 deletions controller/shoot/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,38 @@ func (r *ShootController) configureOIDCAuthentication(ctx context.Context, shoot
r.CareInstruction.Spec.AuthenticationConfigMapName, err)
}

// Add the auth ConfigMap label if it doesn't exist
// Label the ConfigMap so the watch predicate can identify it and associate it with this CareInstruction.
// Take a snapshot before any mutations so the patch only touches metadata.labels.
base := greenhouseAuthConfigMap.DeepCopy()
if greenhouseAuthConfigMap.Labels == nil {
greenhouseAuthConfigMap.Labels = make(map[string]string)
}
if _, hasLabel := greenhouseAuthConfigMap.Labels[v1alpha1.AuthConfigMapLabel]; !hasLabel {
greenhouseAuthConfigMap.Labels[v1alpha1.AuthConfigMapLabel] = "true"
if err := r.GreenhouseClient.Update(ctx, &greenhouseAuthConfigMap); err != nil {
r.Info("failed to add auth ConfigMap label", "configMap", greenhouseAuthConfigMap.Name, "error", err)
labelsNeedUpdate := false

// If the CM is already owned by a different CareInstruction, skip relabelling to
// avoid breaking its watch predicate. OIDC merging still proceeds normally.
existingOwner, hasCILabel := greenhouseAuthConfigMap.Labels[v1alpha1.CareInstructionLabel]
if hasCILabel && existingOwner != r.CareInstruction.Name {
r.Info("auth ConfigMap is already owned by another CareInstruction; skipping relabel to avoid breaking its watch",
"configMap", greenhouseAuthConfigMap.Name,
"existingOwner", existingOwner,
"thisCareInstruction", r.CareInstruction.Name)
} else {
if _, hasAuthLabel := greenhouseAuthConfigMap.Labels[v1alpha1.AuthConfigMapLabel]; !hasAuthLabel {
greenhouseAuthConfigMap.Labels[v1alpha1.AuthConfigMapLabel] = "true"
labelsNeedUpdate = true
}
if !hasCILabel {
greenhouseAuthConfigMap.Labels[v1alpha1.CareInstructionLabel] = r.CareInstruction.Name
labelsNeedUpdate = true
}
}

if labelsNeedUpdate {
// Use a merge patch so only metadata.labels is sent to the API server.
// This avoids overwriting concurrent changes to config.yaml or other fields.
if err := r.GreenhouseClient.Patch(ctx, &greenhouseAuthConfigMap, client.MergeFrom(base)); err != nil {
r.Info("failed to patch labels on auth ConfigMap", "configMap", greenhouseAuthConfigMap.Name, "error", err)
// Don't fail the reconciliation for this, just log it
}
Comment thread
Zaggy21 marked this conversation as resolved.
}
Expand Down
51 changes: 47 additions & 4 deletions controller/shoot/shoot_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)

const (
Expand All @@ -36,6 +39,7 @@ const (

type ShootController struct {
GreenhouseClient client.Client
GreenhouseMgr ctrl.Manager // GreenhouseMgr provides the Greenhouse cluster cache for watches
GardenClient client.Client
logr.Logger
Name string
Expand Down Expand Up @@ -75,11 +79,50 @@ func (r *ShootController) SetupWithManager(mgr ctrl.Manager) error {
r.Error(nil, "EventRecorder is not set for ShootController", "name", r.Name)
}

// Setup the shoot controller with the manager
return ctrl.NewControllerManagedBy(mgr).
b := ctrl.NewControllerManagedBy(mgr).
Named(r.Name).
For(&gardenerv1beta1.Shoot{}, builder.WithPredicates(predicates...)).
Complete(r)
For(&gardenerv1beta1.Shoot{}, builder.WithPredicates(predicates...))

// Watch the auth ConfigMap on the Greenhouse cluster; re-enqueue all Shoots when it changes
// so the Garden-side OIDC config stays in sync.
if r.CareInstruction.Spec.AuthenticationConfigMapName != "" && r.GreenhouseMgr != nil {
authCMPredicate := predicate.NewTypedPredicateFuncs(func(cm *corev1.ConfigMap) bool {
return cm.GetName() == r.CareInstruction.Spec.AuthenticationConfigMapName &&
cm.GetNamespace() == r.CareInstruction.GetNamespace() &&
cm.GetLabels()[v1alpha1.AuthConfigMapLabel] == "true" &&
cm.GetLabels()[v1alpha1.CareInstructionLabel] == r.CareInstruction.Name
})
b = b.WatchesRawSource(source.Kind(
r.GreenhouseMgr.GetCache(),
&corev1.ConfigMap{},
handler.TypedEnqueueRequestsFromMapFunc(r.enqueueShootsForAuthConfigMap),
authCMPredicate,
))
Comment thread
Zaggy21 marked this conversation as resolved.
}

// Setup the shoot controller with the manager
return b.Complete(r)
}

func (r *ShootController) enqueueShootsForAuthConfigMap(ctx context.Context, _ *corev1.ConfigMap) []reconcile.Request {
shoots, err := r.CareInstruction.ListShoots(ctx, r.GardenClient)
if err != nil {
r.Info("failed to list shoots for auth ConfigMap change", "error", err)
return nil
}
requests := make([]reconcile.Request, 0, len(shoots.Items))
for _, shoot := range shoots.Items {
if !r.matchesCEL(&shoot) {
continue
}
requests = append(requests, reconcile.Request{
NamespacedName: client.ObjectKey{
Namespace: shoot.Namespace,
Name: shoot.Name,
},
})
}
Comment thread
Zaggy21 marked this conversation as resolved.
return requests
}

func (r *ShootController) newCELPredicate() predicate.Predicate {
Expand Down
Loading
Loading