|
| 1 | +package managedcontrolplane |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + "strings" |
| 7 | + |
| 8 | + corev1 "k8s.io/api/core/v1" |
| 9 | + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| 10 | + "k8s.io/apimachinery/pkg/util/sets" |
| 11 | + "sigs.k8s.io/controller-runtime/pkg/client" |
| 12 | + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" |
| 13 | + |
| 14 | + "github.com/openmcp-project/controller-utils/pkg/conditions" |
| 15 | + ctrlutils "github.com/openmcp-project/controller-utils/pkg/controller" |
| 16 | + errutils "github.com/openmcp-project/controller-utils/pkg/errors" |
| 17 | + "github.com/openmcp-project/controller-utils/pkg/logging" |
| 18 | + |
| 19 | + clustersv1alpha1 "github.com/openmcp-project/openmcp-operator/api/clusters/v1alpha1" |
| 20 | + cconst "github.com/openmcp-project/openmcp-operator/api/clusters/v1alpha1/constants" |
| 21 | + commonapi "github.com/openmcp-project/openmcp-operator/api/common" |
| 22 | + apiconst "github.com/openmcp-project/openmcp-operator/api/constants" |
| 23 | + corev2alpha1 "github.com/openmcp-project/openmcp-operator/api/core/v2alpha1" |
| 24 | + libutils "github.com/openmcp-project/openmcp-operator/lib/utils" |
| 25 | +) |
| 26 | + |
| 27 | +// manageAccessRequests aligns the existing AccessRequests for the MCP with the currently configured OIDC providers. |
| 28 | +// It uses the given createCon function to create conditions for AccessRequests and removes outdated AccessRequest related conditions directly from the MCP status. |
| 29 | +// The bool return value specifies whether everything related to MCP access is in the desired state or not. If 'false', it is recommended to requeue the MCP. |
| 30 | +func (r *ManagedControlPlaneReconciler) manageAccessRequests(ctx context.Context, mcp *corev2alpha1.ManagedControlPlane, cr *clustersv1alpha1.ClusterRequest, createCon func(conType string, status metav1.ConditionStatus, reason, message string)) (bool, errutils.ReasonableError) { |
| 31 | + updatedAccessRequests, rerr := r.createOrUpdateDesiredAccessRequests(ctx, mcp, cr, createCon) |
| 32 | + if rerr != nil { |
| 33 | + return false, rerr |
| 34 | + } |
| 35 | + |
| 36 | + accessRequestsInDeletion, rerr := r.deleteUndesiredAccessRequests(ctx, mcp, updatedAccessRequests, createCon) |
| 37 | + if rerr != nil { |
| 38 | + return false, rerr |
| 39 | + } |
| 40 | + |
| 41 | + allAccessReady, rerr := r.syncAccessSecrets(ctx, mcp, updatedAccessRequests, createCon) |
| 42 | + if rerr != nil { |
| 43 | + return false, rerr |
| 44 | + } |
| 45 | + |
| 46 | + accessSecretsInDeletion, rerr := r.deleteUndesiredAccessSecrets(ctx, mcp, updatedAccessRequests, createCon) |
| 47 | + if rerr != nil { |
| 48 | + return false, rerr |
| 49 | + } |
| 50 | + |
| 51 | + // remove conditions for AccessRequests that are neither required nor in deletion (= have been deleted already) |
| 52 | + cu := conditions.ConditionUpdater(mcp.Status.Conditions, false).WithEventRecorder(r.eventRecorder, conditions.EventPerChange) |
| 53 | + for _, con := range mcp.Status.Conditions { |
| 54 | + if !strings.HasPrefix(con.Type, corev2alpha1.ConditionPrefixOIDCAccessReady) { |
| 55 | + continue |
| 56 | + } |
| 57 | + providerName := strings.TrimPrefix(con.Type, corev2alpha1.ConditionPrefixOIDCAccessReady) |
| 58 | + if _, ok := updatedAccessRequests[providerName]; !ok && !accessRequestsInDeletion.Has(providerName) { |
| 59 | + cu.RemoveCondition(corev2alpha1.ConditionPrefixOIDCAccessReady + providerName) |
| 60 | + } |
| 61 | + } |
| 62 | + mcp.Status.Conditions, _ = cu.Record(mcp).Conditions() |
| 63 | + |
| 64 | + everythingReady := accessRequestsInDeletion.Len() == 0 && accessSecretsInDeletion.Len() == 0 && allAccessReady |
| 65 | + if everythingReady { |
| 66 | + createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionTrue, "", "All accesses are ready") |
| 67 | + } else { |
| 68 | + createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequest, "Not all accesses are ready") |
| 69 | + } |
| 70 | + |
| 71 | + return everythingReady, nil |
| 72 | +} |
| 73 | + |
| 74 | +// createOrUpdateDesiredAccessRequests creates/updates all AccessRequests that are desired according to the ManagedControlPlane's configured OIDC providers. |
| 75 | +// It returns a mapping from OIDC provider names to the corresponding AccessRequests. |
| 76 | +// If the ManagedControlPlane has a non-zero DeletionTimestamp, no AccessRequests will be created or updated and the returned map will be empty. |
| 77 | +func (r *ManagedControlPlaneReconciler) createOrUpdateDesiredAccessRequests(ctx context.Context, mcp *corev2alpha1.ManagedControlPlane, cr *clustersv1alpha1.ClusterRequest, createCon func(conType string, status metav1.ConditionStatus, reason, message string)) (map[string]*clustersv1alpha1.AccessRequest, errutils.ReasonableError) { |
| 78 | + log := logging.FromContextOrPanic(ctx) |
| 79 | + |
| 80 | + namespace := libutils.StableRequestNamespace(mcp.Namespace) |
| 81 | + updatedAccessRequests := map[string]*clustersv1alpha1.AccessRequest{} |
| 82 | + var oidcProviders []*commonapi.OIDCProviderConfig |
| 83 | + |
| 84 | + // create or update AccessRequests for the ManagedControlPlane |
| 85 | + if mcp.DeletionTimestamp.IsZero() { |
| 86 | + oidcProviders = make([]*commonapi.OIDCProviderConfig, 0, len(mcp.Spec.IAM.OIDCProviders)+1) |
| 87 | + if r.Config.StandardOIDCProvider != nil && len(mcp.Spec.IAM.RoleBindings) > 0 { |
| 88 | + // add default OIDC provider, unless it has been disabled |
| 89 | + defaultOidc := r.Config.StandardOIDCProvider.DeepCopy() |
| 90 | + defaultOidc.Name = corev2alpha1.DefaultOIDCProviderName |
| 91 | + defaultOidc.RoleBindings = mcp.Spec.IAM.RoleBindings |
| 92 | + oidcProviders = append(oidcProviders, defaultOidc) |
| 93 | + } |
| 94 | + oidcProviders = append(oidcProviders, mcp.Spec.IAM.OIDCProviders...) |
| 95 | + } |
| 96 | + |
| 97 | + for _, oidc := range oidcProviders { |
| 98 | + log.Debug("Creating/updating AccessRequest for OIDC provider", "oidcProviderName", oidc.Name) |
| 99 | + arName := ctrlutils.K8sNameHash(mcp.Name, oidc.Name) |
| 100 | + ar := &clustersv1alpha1.AccessRequest{} |
| 101 | + ar.Name = arName |
| 102 | + ar.Namespace = namespace |
| 103 | + if _, err := controllerutil.CreateOrUpdate(ctx, r.PlatformCluster.Client(), ar, func() error { |
| 104 | + ar.Spec.RequestRef = &commonapi.ObjectReference{ |
| 105 | + Name: cr.Name, |
| 106 | + Namespace: cr.Namespace, |
| 107 | + } |
| 108 | + ar.Spec.OIDCProvider = oidc |
| 109 | + |
| 110 | + // set labels |
| 111 | + if ar.Labels == nil { |
| 112 | + ar.Labels = map[string]string{} |
| 113 | + } |
| 114 | + ar.Labels[corev2alpha1.MCPLabel] = mcp.Name |
| 115 | + ar.Labels[apiconst.ManagedByLabel] = ControllerName |
| 116 | + ar.Labels[corev2alpha1.OIDCProviderLabel] = corev2alpha1.DefaultOIDCProviderName |
| 117 | + |
| 118 | + return nil |
| 119 | + }); err != nil { |
| 120 | + rerr := errutils.WithReason(fmt.Errorf("error creating/updating AccessRequest '%s/%s': %w", ar.Namespace, ar.Name, err), cconst.ReasonPlatformClusterInteractionProblem) |
| 121 | + createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+oidc.Name, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) |
| 122 | + createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequest, "Error creating/updating AccessRequest for OIDC provider "+oidc.Name) |
| 123 | + return nil, rerr |
| 124 | + } |
| 125 | + updatedAccessRequests[corev2alpha1.DefaultOIDCProviderName] = ar |
| 126 | + } |
| 127 | + |
| 128 | + return updatedAccessRequests, nil |
| 129 | +} |
| 130 | + |
| 131 | +// deleteUndesiredAccessRequests deletes all AccessRequests that belong to the given ManagedControlPlane, but are not in the updatedAccessRequests map. |
| 132 | +// These are AccessRequests that have been created for a previous version of the ManagedControlPlane and are not needed anymore. |
| 133 | +// It returns a set of OIDC provider names for which the AccessRequests are still in deletion. If the set is empty, all undesired AccessRequests have been deleted. |
| 134 | +func (r *ManagedControlPlaneReconciler) deleteUndesiredAccessRequests(ctx context.Context, mcp *corev2alpha1.ManagedControlPlane, updatedAccessRequests map[string]*clustersv1alpha1.AccessRequest, createCon func(conType string, status metav1.ConditionStatus, reason, message string)) (sets.Set[string], errutils.ReasonableError) { |
| 135 | + log := logging.FromContextOrPanic(ctx) |
| 136 | + |
| 137 | + namespace := libutils.StableRequestNamespace(mcp.Namespace) |
| 138 | + accessRequestsInDeletion := sets.New[string]() |
| 139 | + |
| 140 | + // delete all AccessRequests that have previously been created for this ManagedControlPlane but are not needed anymore |
| 141 | + oidcARs := &clustersv1alpha1.AccessRequestList{} |
| 142 | + if err := r.PlatformCluster.Client().List(ctx, oidcARs, client.InNamespace(namespace), client.HasLabels{corev2alpha1.OIDCProviderLabel}, client.MatchingLabels{ |
| 143 | + corev2alpha1.MCPLabel: mcp.Name, |
| 144 | + apiconst.ManagedByLabel: ControllerName, |
| 145 | + }); err != nil { |
| 146 | + rerr := errutils.WithReason(fmt.Errorf("error listing AccessRequests for ManagedControlPlane '%s/%s': %w", mcp.Namespace, mcp.Name, err), cconst.ReasonPlatformClusterInteractionProblem) |
| 147 | + createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) |
| 148 | + return accessRequestsInDeletion, rerr |
| 149 | + } |
| 150 | + errs := errutils.NewReasonableErrorList() |
| 151 | + for _, ar := range oidcARs.Items { |
| 152 | + if _, ok := updatedAccessRequests[ar.Spec.OIDCProvider.Name]; ok { |
| 153 | + continue |
| 154 | + } |
| 155 | + providerName := "<unknown>" |
| 156 | + if ar.Spec.OIDCProvider != nil { |
| 157 | + providerName = ar.Spec.OIDCProvider.Name |
| 158 | + } |
| 159 | + accessRequestsInDeletion.Insert(ar.Name) |
| 160 | + if !ar.DeletionTimestamp.IsZero() { |
| 161 | + log.Debug("Waiting for deletion of AccessRequest that is no longer required", "accessRequestName", ar.Name, "accessRequestNamespace", ar.Namespace, "oidcProviderName", providerName) |
| 162 | + createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequest, "AccessRequest is being deleted") |
| 163 | + continue |
| 164 | + } |
| 165 | + log.Debug("Deleting AccessRequest that is no longer needed", "accessRequestName", ar.Name, "accessRequestNamespace", ar.Namespace, "oidcProviderName", providerName) |
| 166 | + if err := r.PlatformCluster.Client().Delete(ctx, &ar); client.IgnoreNotFound(err) != nil { |
| 167 | + rerr := errutils.WithReason(fmt.Errorf("error deleting AccessRequest '%s/%s': %w", ar.Namespace, ar.Name, err), cconst.ReasonPlatformClusterInteractionProblem) |
| 168 | + errs.Append(rerr) |
| 169 | + createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) |
| 170 | + } |
| 171 | + createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequest, "AccessRequest is being deleted") |
| 172 | + } |
| 173 | + if rerr := errs.Aggregate(); rerr != nil { |
| 174 | + createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequest, "Error deleting AccessRequests that are no longer needed") |
| 175 | + return accessRequestsInDeletion, rerr |
| 176 | + } |
| 177 | + |
| 178 | + return accessRequestsInDeletion, nil |
| 179 | +} |
| 180 | + |
| 181 | +// deleteUndesiredAccessSecrets deletes all access secrets belonging to the ManagedControlPlane that are not copied from an up-to-date AccessRequest. |
| 182 | +// It returns a set of OIDC provider names for which the AccessRequest secrets are still in deletion. |
| 183 | +func (r *ManagedControlPlaneReconciler) deleteUndesiredAccessSecrets(ctx context.Context, mcp *corev2alpha1.ManagedControlPlane, updatedAccessRequests map[string]*clustersv1alpha1.AccessRequest, createCon func(conType string, status metav1.ConditionStatus, reason, message string)) (sets.Set[string], errutils.ReasonableError) { |
| 184 | + log := logging.FromContextOrPanic(ctx) |
| 185 | + |
| 186 | + accessSecretsInDeletion := sets.New[string]() |
| 187 | + |
| 188 | + // delete all AccessRequest secrets that have been copied to the Onboarding cluster and belong to AccessRequests that are no longer needed |
| 189 | + mcpSecrets := &corev1.SecretList{} |
| 190 | + if err := r.OnboardingCluster.Client().List(ctx, mcpSecrets, client.InNamespace(mcp.Namespace), client.HasLabels{corev2alpha1.OIDCProviderLabel}, client.MatchingLabels{ |
| 191 | + corev2alpha1.MCPLabel: mcp.Name, |
| 192 | + apiconst.ManagedByLabel: ControllerName, |
| 193 | + }); err != nil { |
| 194 | + rerr := errutils.WithReason(fmt.Errorf("error listing secrets for ManagedControlPlane '%s/%s': %w", mcp.Namespace, mcp.Name, err), cconst.ReasonOnboardingClusterInteractionProblem) |
| 195 | + createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) |
| 196 | + return accessSecretsInDeletion, rerr |
| 197 | + } |
| 198 | + |
| 199 | + errs := errutils.NewReasonableErrorList() |
| 200 | + for _, mcpSecret := range mcpSecrets.Items { |
| 201 | + providerName := mcpSecret.Labels[corev2alpha1.OIDCProviderLabel] |
| 202 | + if providerName == "" { |
| 203 | + log.Error(nil, "Secret for ManagedControlPlane has an empty OIDCProvider label, this should not happen", "secretName", mcpSecret.Name, "secretNamespace", mcpSecret.Namespace) |
| 204 | + continue |
| 205 | + } |
| 206 | + if _, ok := updatedAccessRequests[providerName]; ok { |
| 207 | + continue |
| 208 | + } |
| 209 | + accessSecretsInDeletion.Insert(providerName) |
| 210 | + if !mcpSecret.DeletionTimestamp.IsZero() { |
| 211 | + log.Debug("Waiting for deletion of access secret that is no longer required", "secretName", mcpSecret.Name, "secretNamespace", mcpSecret.Namespace, "oidcProviderName", providerName) |
| 212 | + createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequest, "AccessRequest secret is being deleted") |
| 213 | + continue |
| 214 | + } |
| 215 | + log.Debug("Deleting access secret that is no longer required", "secretName", mcpSecret.Name, "secretNamespace", mcpSecret.Namespace, "oidcProviderName", providerName) |
| 216 | + if err := r.OnboardingCluster.Client().Delete(ctx, &mcpSecret); client.IgnoreNotFound(err) != nil { |
| 217 | + rerr := errutils.WithReason(fmt.Errorf("error deleting access secret '%s/%s': %w", mcpSecret.Namespace, mcpSecret.Name, err), cconst.ReasonOnboardingClusterInteractionProblem) |
| 218 | + errs.Append(rerr) |
| 219 | + createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) |
| 220 | + } |
| 221 | + createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequest, "access secret is being deleted") |
| 222 | + } |
| 223 | + if rerr := errs.Aggregate(); rerr != nil { |
| 224 | + createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequest, "Error deleting access secrets that are no longer needed") |
| 225 | + return accessSecretsInDeletion, rerr |
| 226 | + } |
| 227 | + |
| 228 | + return accessSecretsInDeletion, nil |
| 229 | +} |
| 230 | + |
| 231 | +// syncAccessSecrets checks if all AccessRequests belonging to the ManagedControlPlane are ready and copies their secrets to the Onboarding cluster and references them in the ManagedControlPlane status. |
| 232 | +// It returns a boolean indicating whether all AccessRequests are ready and their secrets have been copied successfully (true) or not (false). |
| 233 | +func (r *ManagedControlPlaneReconciler) syncAccessSecrets(ctx context.Context, mcp *corev2alpha1.ManagedControlPlane, updatedAccessRequests map[string]*clustersv1alpha1.AccessRequest, createCon func(conType string, status metav1.ConditionStatus, reason, message string)) (bool, errutils.ReasonableError) { |
| 234 | + log := logging.FromContextOrPanic(ctx) |
| 235 | + |
| 236 | + allAccessReady := true |
| 237 | + if mcp.Status.Access == nil { |
| 238 | + mcp.Status.Access = map[string]commonapi.LocalObjectReference{} |
| 239 | + } |
| 240 | + for providerName, ar := range updatedAccessRequests { |
| 241 | + if !ar.Status.IsGranted() || ar.Status.SecretRef == nil { |
| 242 | + log.Debug("AccessRequest is not ready yet", "accessRequestName", ar.Name, "accessRequestNamespace", ar.Namespace, "oidcProviderName", providerName) |
| 243 | + createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequest, "AccessRequest is not ready yet") |
| 244 | + allAccessReady = false |
| 245 | + } else { |
| 246 | + // copy access request secret and reference it in the ManagedControlPlane status |
| 247 | + arSecret := &corev1.Secret{} |
| 248 | + arSecret.Name = ar.Status.SecretRef.Name |
| 249 | + arSecret.Namespace = ar.Status.SecretRef.Namespace |
| 250 | + if err := r.PlatformCluster.Client().Get(ctx, client.ObjectKeyFromObject(arSecret), arSecret); err != nil { |
| 251 | + rerr := errutils.WithReason(fmt.Errorf("error getting AccessRequest secret '%s/%s': %w", arSecret.Namespace, arSecret.Name, err), cconst.ReasonPlatformClusterInteractionProblem) |
| 252 | + createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) |
| 253 | + createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequest, "Error getting AccessRequest secret for OIDC provider "+providerName) |
| 254 | + return false, rerr |
| 255 | + } |
| 256 | + mcpSecret := &corev1.Secret{} |
| 257 | + mcpSecret.Name = ctrlutils.K8sNameHash(mcp.Name, providerName) |
| 258 | + mcpSecret.Namespace = mcp.Namespace |
| 259 | + if _, err := controllerutil.CreateOrUpdate(ctx, r.OnboardingCluster.Client(), mcpSecret, func() error { |
| 260 | + mcpSecret.Data = arSecret.Data |
| 261 | + if mcpSecret.Labels == nil { |
| 262 | + mcpSecret.Labels = map[string]string{} |
| 263 | + } |
| 264 | + mcpSecret.Labels[corev2alpha1.MCPLabel] = mcp.Name |
| 265 | + mcpSecret.Labels[corev2alpha1.OIDCProviderLabel] = providerName |
| 266 | + mcpSecret.Labels[apiconst.ManagedByLabel] = ControllerName |
| 267 | + |
| 268 | + if err := controllerutil.SetControllerReference(mcp, mcpSecret, r.OnboardingCluster.Scheme()); err != nil { |
| 269 | + return err |
| 270 | + } |
| 271 | + return nil |
| 272 | + }); err != nil { |
| 273 | + rerr := errutils.WithReason(fmt.Errorf("error creating/updating AccessRequest secret '%s/%s': %w", mcpSecret.Namespace, mcpSecret.Name, err), cconst.ReasonOnboardingClusterInteractionProblem) |
| 274 | + createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionFalse, rerr.Reason(), rerr.Error()) |
| 275 | + createCon(corev2alpha1.ConditionAllAccessReady, metav1.ConditionFalse, cconst.ReasonWaitingForAccessRequest, "Error creating/updating AccessRequest secret for OIDC provider "+providerName) |
| 276 | + return false, rerr |
| 277 | + } |
| 278 | + log.Debug("Access secret for ManagedControlPlane created/updated", "secretName", mcpSecret.Name, "oidcProviderName", providerName) |
| 279 | + mcp.Status.Access[providerName] = commonapi.LocalObjectReference{ |
| 280 | + Name: mcpSecret.Name, |
| 281 | + } |
| 282 | + createCon(corev2alpha1.ConditionPrefixOIDCAccessReady+providerName, metav1.ConditionTrue, "", "") |
| 283 | + } |
| 284 | + } |
| 285 | + |
| 286 | + return allAccessReady, nil |
| 287 | +} |
0 commit comments