Skip to content
4 changes: 4 additions & 0 deletions api/v1alpha1/careinstruction_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ const (

// ShootStatusExcluded indicates the shoot was excluded by the ShootSelector filter criteria.
ShootStatusExcluded = "Excluded"

// ShootAuthConfiguredByLabel is the label placed on a Shoot to identify which CareInstruction

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.

On a second thought I think we can use the more generic shoot-grafter.cloudoperators.dev/careinstruction label, to not reduce the information to auth only?
WDYT?

// configured its OIDC authentication.
ShootAuthConfiguredByLabel = "shoot-grafter.cloudoperators.dev/auth-configured-by"
)

// ShootSelector combines label-based and CEL expression-based filtering for shoots.
Expand Down
164 changes: 46 additions & 118 deletions controller/shoot/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,22 @@ import (
gardenerv1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apiserverv1beta1 "k8s.io/apiserver/pkg/apis/apiserver/v1beta1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/yaml"
)

const authConfigMapKey = "config.yaml"
const (
authConfigMapKey = "config.yaml"
authConfigMapManagedBy = "shoot-grafter.cloudoperators.dev/managed-by"
Comment thread
Zaggy21 marked this conversation as resolved.
Outdated
)

// configureOIDCAuthentication configures OIDC authentication for the Shoot by:
// ConfigureOIDCAuthentication configures OIDC authentication for the Shoot by:
// 1. Reading the AuthenticationConfiguration from the Greenhouse auth ConfigMap
// 2. Merging it with any existing configuration on the Garden cluster
// 3. Updating the Shoot spec to reference the merged configuration
func (r *ShootController) configureOIDCAuthentication(ctx context.Context, shoot *gardenerv1beta1.Shoot) error {
// Label the Greenhouse auth ConfigMap so the CareInstruction controller's watch predicate can
// identify it and associate it with this CareInstruction.
// We fetch the live CM to get the current metadata, then patch only the labels.
// 2. Writing it verbatim to a ConfigMap in the Garden cluster (always overwrite)
// 3. Updating the Shoot spec to reference that ConfigMap
func (r *ShootController) ConfigureOIDCAuthentication(ctx context.Context, shoot *gardenerv1beta1.Shoot) error {
// Fetch the Greenhouse auth ConfigMap and ensure it carries the watch label.
var greenhouseAuthConfigMap corev1.ConfigMap
if err := r.GreenhouseClient.Get(ctx, client.ObjectKey{
Namespace: r.CareInstruction.Namespace,
Expand All @@ -42,14 +41,8 @@ func (r *ShootController) configureOIDCAuthentication(ctx context.Context, shoot
if greenhouseAuthConfigMap.Labels == nil {
greenhouseAuthConfigMap.Labels = make(map[string]string)
}
labelsNeedUpdate := false

if _, hasAuthLabel := greenhouseAuthConfigMap.Labels[v1alpha1.AuthConfigMapLabel]; !hasAuthLabel {
greenhouseAuthConfigMap.Labels[v1alpha1.AuthConfigMapLabel] = "true"
labelsNeedUpdate = true
}

if labelsNeedUpdate {
if patchErr := r.GreenhouseClient.Patch(ctx, &greenhouseAuthConfigMap, client.MergeFrom(base)); patchErr != nil {
r.Info("failed to patch labels on auth ConfigMap", "configMap", greenhouseAuthConfigMap.Name, "error", patchErr)
}
Expand All @@ -60,52 +53,40 @@ func (r *ShootController) configureOIDCAuthentication(ctx context.Context, shoot
r.CareInstruction.Spec.AuthenticationConfigMapName, authConfigMapKey)
}

// Parse the Greenhouse authentication configuration
var greenhouseAuthConfig apiserverv1beta1.AuthenticationConfiguration
if err := yaml.Unmarshal([]byte(greenhouseAuthConfigMap.Data[authConfigMapKey]), &greenhouseAuthConfig); err != nil {
return fmt.Errorf("failed to parse Greenhouse AuthenticationConfiguration: %w", err)
}

// Determine the ConfigMap name for Garden cluster
// We create one CM per CareInstruction unless Shoot already has one configured
// Determine the ConfigMap name for the Garden cluster.
// Preserve an existing reference on the Shoot; otherwise use the default name.
configMapName := r.CareInstruction.Name + "-greenhouse-auth"
useExistingConfigMap := false

// Check if Shoot already has a ConfigMap configured
if shoot.Spec.Kubernetes.KubeAPIServer != nil &&
shoot.Spec.Kubernetes.KubeAPIServer.StructuredAuthentication != nil &&
shoot.Spec.Kubernetes.KubeAPIServer.StructuredAuthentication.ConfigMapName != "" {
configMapName = shoot.Spec.Kubernetes.KubeAPIServer.StructuredAuthentication.ConfigMapName
useExistingConfigMap = true
r.Info("Shoot already has AuthenticationConfiguration ConfigMap", "shoot", shoot.Name, "configMap", configMapName)
}

var gardenConfigMap corev1.ConfigMap

if useExistingConfigMap {
// Fetch the existing ConfigMap from Garden cluster
if err := r.GardenClient.Get(ctx, client.ObjectKey{
Namespace: shoot.Namespace,
// Always overwrite the garden-cluster CM with the Greenhouse content verbatim.
gardenConfigMap := corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configMapName,
}, &gardenConfigMap); err != nil {
return fmt.Errorf("failed to fetch existing AuthenticationConfiguration ConfigMap %s from Garden cluster: %w", configMapName, err)
}
} else {
// Create new ConfigMap structure
gardenConfigMap = corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configMapName,
Namespace: shoot.Namespace,
},
Data: map[string]string{
authConfigMapKey: "",
},
}
Namespace: shoot.Namespace,
},
}
authContent := greenhouseAuthConfigMap.Data[authConfigMapKey]

// Create or update the ConfigMap, merging configurations
configMapResult, err := ctrl.CreateOrUpdate(ctx, r.GardenClient, &gardenConfigMap, func() error {
return r.mergeAuthenticationConfigurations(&gardenConfigMap, &greenhouseAuthConfig)
if gardenConfigMap.Labels == nil {
gardenConfigMap.Labels = make(map[string]string)
}
gardenConfigMap.Labels[v1alpha1.CareInstructionLabel] = r.CareInstruction.Name

if gardenConfigMap.Annotations == nil {
gardenConfigMap.Annotations = make(map[string]string)
}
gardenConfigMap.Annotations[authConfigMapManagedBy] = "shoot-grafter - do not edit by hand, this is maintained by automation"

if gardenConfigMap.Data == nil {
gardenConfigMap.Data = make(map[string]string)
}
gardenConfigMap.Data[authConfigMapKey] = authContent
Comment thread
Zaggy21 marked this conversation as resolved.
Outdated
return nil
})
if err != nil {
return fmt.Errorf("failed to create/update AuthenticationConfiguration ConfigMap: %w", err)
Expand All @@ -118,7 +99,19 @@ func (r *ShootController) configureOIDCAuthentication(ctx context.Context, shoot
r.Info("AuthenticationConfiguration ConfigMap updated", "name", configMapName, "shoot", shoot.Name)
}

// Update Shoot spec to reference the ConfigMap if needed
// Ensure the Shoot carries the auth-configured-by label.
if shoot.Labels == nil || shoot.Labels[v1alpha1.ShootAuthConfiguredByLabel] != r.CareInstruction.Name {
shootBase := shoot.DeepCopy()
if shoot.Labels == nil {
shoot.Labels = make(map[string]string)
}
shoot.Labels[v1alpha1.ShootAuthConfiguredByLabel] = r.CareInstruction.Name
if patchErr := r.GardenClient.Patch(ctx, shoot, client.MergeFrom(shootBase)); patchErr != nil {
return fmt.Errorf("failed to patch auth-configured-by label on Shoot: %w", patchErr)
}
}

// Update the Shoot spec to reference the ConfigMap if not already pointing to it.
shootNeedsUpdate := false
if shoot.Spec.Kubernetes.KubeAPIServer == nil {
shoot.Spec.Kubernetes.KubeAPIServer = &gardenerv1beta1.KubeAPIServerConfig{}
Expand All @@ -141,8 +134,7 @@ func (r *ShootController) configureOIDCAuthentication(ctx context.Context, shoot
return nil // Spec change triggers reconciliation automatically
}

// At this point, Shoot spec doesn't need updates (ConfigMapName reference already exists)
// Trigger Shoot reconciliation if ConfigMap content was updated
// Trigger Shoot reconciliation if ConfigMap content was updated.
// Reference: https://gardener.cloud/docs/gardener/shoot-operations/shoot_operations/#immediate-reconciliation
if configMapResult == controllerutil.OperationResultUpdated {
if err := AnnotateShootForReconcile(ctx, r.GardenClient, shoot.Namespace, shoot.Name); err != nil {
Comment thread
Zaggy21 marked this conversation as resolved.
Outdated
Expand All @@ -155,67 +147,3 @@ func (r *ShootController) configureOIDCAuthentication(ctx context.Context, shoot

return nil
}

// mergeAuthenticationConfigurations merges the Greenhouse AuthenticationConfiguration
// with any existing configuration in the Garden ConfigMap. It intelligently merges:
// - JWT authenticators from both configurations
// - Deduplicates issuers by URL (Greenhouse config takes precedence)
// - Preserves Garden-specific configurations that don't conflict
func (r *ShootController) mergeAuthenticationConfigurations(gardenConfigMap *corev1.ConfigMap, greenhouseAuthConfig *apiserverv1beta1.AuthenticationConfiguration) error {
var gardenAuthConfig apiserverv1beta1.AuthenticationConfiguration

// Parse existing Garden configuration if present
if gardenConfigMap.Data != nil && gardenConfigMap.Data[authConfigMapKey] != "" {
existingConfigYAML := gardenConfigMap.Data[authConfigMapKey]
if err := yaml.Unmarshal([]byte(existingConfigYAML), &gardenAuthConfig); err != nil {
return fmt.Errorf("failed to parse existing Garden AuthenticationConfiguration: %w", err)
}
} else {
// Create new configuration structure
gardenAuthConfig = apiserverv1beta1.AuthenticationConfiguration{
TypeMeta: metav1.TypeMeta{
APIVersion: "apiserver.config.k8s.io/v1beta1",
Kind: "AuthenticationConfiguration",
},
JWT: []apiserverv1beta1.JWTAuthenticator{},
}
}

if gardenAuthConfig.JWT == nil {
gardenAuthConfig.JWT = []apiserverv1beta1.JWTAuthenticator{}
}

// Create a map of existing Garden issuer URLs for quick lookup
gardenIssuerURLs := make(map[string]int) // URL -> index in gardenAuthConfig.JWT
for i, jwtAuth := range gardenAuthConfig.JWT {
gardenIssuerURLs[jwtAuth.Issuer.URL] = i
}

// Merge JWT authenticators from Greenhouse configuration
// Greenhouse issuers take precedence over Garden issuers with the same URL
for _, greenhouseJWT := range greenhouseAuthConfig.JWT {
if existingIndex, exists := gardenIssuerURLs[greenhouseJWT.Issuer.URL]; exists {
// Update existing issuer with Greenhouse configuration
r.Info("Updating issuer from Greenhouse configuration", "url", greenhouseJWT.Issuer.URL)
gardenAuthConfig.JWT[existingIndex] = greenhouseJWT
} else {
// Add new issuer from Greenhouse configuration, no conflict since not added to gardenIssuerURLs map
r.Info("Adding new issuer from Greenhouse configuration", "url", greenhouseJWT.Issuer.URL)
gardenAuthConfig.JWT = append(gardenAuthConfig.JWT, greenhouseJWT)
}
}

// Marshal merged configuration back to YAML
mergedConfigYAML, err := yaml.Marshal(&gardenAuthConfig)
if err != nil {
return fmt.Errorf("failed to marshal merged AuthenticationConfiguration: %w", err)
}

// Update ConfigMap data
if gardenConfigMap.Data == nil {
gardenConfigMap.Data = make(map[string]string)
}
gardenConfigMap.Data[authConfigMapKey] = string(mergedConfigYAML)

return nil
}
Loading
Loading