From 76c46c7c2240462e94e413bf7d23064f8cb7efaa Mon Sep 17 00:00:00 2001 From: Miguel Soriano Date: Fri, 4 Sep 2026 14:59:07 +0200 Subject: [PATCH] poc db identities replacement simplif --- internal/api/coreapi/types_cluster.go | 103 ++- .../coreapi/types_serviceprovider_cluster.go | 176 +---- ...viceprovider_cluster_managed_identities.go | 660 ++++++++++++++++++ 3 files changed, 771 insertions(+), 168 deletions(-) create mode 100644 internal/api/coreapi/types_serviceprovider_cluster_managed_identities.go diff --git a/internal/api/coreapi/types_cluster.go b/internal/api/coreapi/types_cluster.go index 866e2c03c23..6b1446fb26a 100644 --- a/internal/api/coreapi/types_cluster.go +++ b/internal/api/coreapi/types_cluster.go @@ -38,7 +38,7 @@ type HCPOpenShiftCluster struct { ServiceProviderProperties HCPOpenShiftClusterServiceProviderProperties `json:"serviceProviderProperties,omitempty"` // Written by: Frontend PUT/PATCH Cluster (Create/Update), ClusterIdentitySync Identity *ManagedServiceIdentity `json:"identity,omitempty"` - // Written by: ClusterDegradedAggregator, ClusterRequirementsValidAggregator + // Written by: ClusterDegradedAggregator, ClusterRequirementsValidAggregator, ManagedIdentitiesStatusAggregator Status HCPOpenShiftClusterStatus `json:"status"` } @@ -69,6 +69,84 @@ type HCPOpenShiftClusterStatus struct { // +listType=map // +listMapKey=type UserFacingConditions []metav1.Condition `json:"userFacingConditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + // OperatorIdentities is a distilled rollup of identity replacement for + // Cluster-document readers (Desired vs Active ResourceID per operator). + // The full slot/instance ledger lives on + // ServiceProviderCluster.Spec/Status.ManagedIdentities; see + // ServiceProviderClusterManagedIdentitiesSpec. This field is not mapped to + // ARM ResourceStatus. ARM only exposes UserFacingConditions. + // Written by: ManagedIdentitiesStatusAggregator + OperatorIdentities ClusterOperatorIdentitiesStatus `json:"operatorIdentities,omitempty"` +} + +// ClusterOperatorIdentitiesStatus is the Cluster-document rollup of identity +// replacement. Each map entry is one slot (one operator, or the SMI): Desired +// is what the user asked for, Active is what is in service. Instance history, +// role-assignment progress, and overlapping replacements live on +// ServiceProviderCluster.Status.ManagedIdentities. +type ClusterOperatorIdentitiesStatus struct { + // Conditions are per-cluster identity observations. Keep this list small. + // Known types: + // - "Configured": True when every desired slot is Active on its desired + // instance and no instance is still deconfiguring. + // Written by: ManagedIdentitiesStatusAggregator + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + // ControlPlaneOperators is keyed by operator name as stored on + // Cluster.CustomerProperties.Platform.OperatorsAuthentication.UserAssignedIdentities.ControlPlaneOperators. + // Keys are not lowercased; they keep the Cluster map's casing. Lookups must + // use that same name, not a lowercased form and not the identity ResourceID. + // Written by: ManagedIdentitiesStatusAggregator + ControlPlaneOperators map[string]ClusterOperatorIdentityStatus `json:"controlPlaneOperators,omitempty"` + // DataPlaneOperators is keyed by operator name as stored on + // Cluster.CustomerProperties.Platform.OperatorsAuthentication.UserAssignedIdentities.DataPlaneOperators. + // Keys are not lowercased; they keep the Cluster map's casing. Lookups must + // use that same name, not a lowercased form and not the identity ResourceID. + // Written by: ManagedIdentitiesStatusAggregator + DataPlaneOperators map[string]ClusterOperatorIdentityStatus `json:"dataPlaneOperators,omitempty"` + // ServiceManagedIdentity is the distilled observed state of the cluster's + // service managed identity. Nil when that slot is absent. + // Written by: ManagedIdentitiesStatusAggregator + ServiceManagedIdentity *ClusterOperatorIdentityStatus `json:"serviceManagedIdentity,omitempty"` +} + +// ClusterOperatorIdentityStatus is the distilled observed state of one slot +// (one operator or the SMI). Desired* is the latest Cluster ResourceID. Active* +// is what is in service. They differ during replacement. +type ClusterOperatorIdentityStatus struct { + // DesiredResourceID is the latest Cluster-desired identity ResourceID for + // this slot, copied from Spec.ManagedIdentities. + // Written by: ManagedIdentitiesStatusAggregator + DesiredResourceID *azcorearm.ResourceID `json:"desiredResourceID,omitempty"` + // ActiveResourceID is the identity ResourceID currently in service for this + // slot, copied from Status.ManagedIdentities slot ActiveResourceID. Empty + // until an instance is activated. + // Written by: ManagedIdentitiesStatusAggregator + ActiveResourceID *azcorearm.ResourceID `json:"activeResourceID,omitempty"` + // ClientID is the Client ID of the Active identity. Empty until an instance + // is activated and metadata is resolved. + // Written by: ManagedIdentitiesStatusAggregator + ClientID *string `json:"clientID,omitempty"` + // PrincipalID is the Principal ID of the Active identity. Empty until an + // instance is activated and metadata is resolved. + // Written by: ManagedIdentitiesStatusAggregator + PrincipalID *string `json:"principalID,omitempty"` + + // Conditions for this slot. Known types: + // - "Configured": True when Active matches Desired and that instance is fully configured. + // Written by: ManagedIdentitiesStatusAggregator + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } var _ CosmosPersistable = &HCPOpenShiftCluster{} @@ -298,13 +376,24 @@ type OperatorsAuthenticationProfile struct { UserAssignedIdentities UserAssignedIdentitiesProfile `json:"userAssignedIdentities,omitempty"` } -// UserAssignedIdentitiesProfile represents authentication configuration for -// OpenShift operators using user-assigned managed identities. -// Visibility for the entire struct is "read create". +// UserAssignedIdentitiesProfile is the ARM-visible latest desired ResourceID +// for each operator and the SMI. Visibility for the entire struct is "read create". +// Frontend PUT/PATCH overwrites these maps. Replacement history (old ResourceIDs +// still being deconfigured, overlapping A then B then C changes) lives on +// ServiceProviderCluster Spec/Status ManagedIdentities, not here. See +// ServiceProviderClusterManagedIdentitiesSpec. type UserAssignedIdentitiesProfile struct { - ControlPlaneOperators map[string]*azcorearm.ResourceID `json:"controlPlaneOperators,omitempty"` - DataPlaneOperators map[string]*azcorearm.ResourceID `json:"dataPlaneOperators,omitempty"` - ServiceManagedIdentity *azcorearm.ResourceID `json:"serviceManagedIdentity,omitempty"` + // ControlPlaneOperators is keyed by operator name (for example + // "cloud-controller-manager"). Keys are not lowercased; they keep the + // casing from the ARM request. Values are identity ResourceIDs. + ControlPlaneOperators map[string]*azcorearm.ResourceID `json:"controlPlaneOperators,omitempty"` + // DataPlaneOperators is keyed by operator name (for example + // "image-registry"). Keys are not lowercased; they keep the casing from + // the ARM request. Values are identity ResourceIDs. + DataPlaneOperators map[string]*azcorearm.ResourceID `json:"dataPlaneOperators,omitempty"` + // ServiceManagedIdentity is the latest desired ResourceID of the cluster's + // service managed identity. Nil when the user has not provided one. + ServiceManagedIdentity *azcorearm.ResourceID `json:"serviceManagedIdentity,omitempty"` } // ClusterImageRegistryProfile - OpenShift cluster image registry diff --git a/internal/api/coreapi/types_serviceprovider_cluster.go b/internal/api/coreapi/types_serviceprovider_cluster.go index cfd7dbbe2a8..c7418915650 100644 --- a/internal/api/coreapi/types_serviceprovider_cluster.go +++ b/internal/api/coreapi/types_serviceprovider_cluster.go @@ -114,6 +114,13 @@ type ServiceProviderClusterSpec struct { // BackupScheduleState is the desired backup scheduling state: Enabled or Disabled. // Default is Enabled. Set to Disabled via Admin API to pause scheduled backups. BackupScheduleState BackupScheduleState `json:"backupScheduleState,omitempty"` + + // ManagedIdentities is the desired identity per operator slot and the SMI. + // A slot is one operator (or the SMI) asking which Azure identity to use. + // See ServiceProviderClusterManagedIdentitiesSpec for the + // slot/generation/instance model. + // Written by: ManagedIdentitiesCoordinator + ManagedIdentities ServiceProviderClusterManagedIdentitiesSpec `json:"managedIdentities,omitempty"` } // ServiceProviderClusterSpecVersion contains the desired version information. @@ -233,167 +240,14 @@ type ServiceProviderClusterStatus struct { // the cluster, including deny assignments and the managed resource group. AzureResources AzureResources `json:"azureResources,omitempty"` - // MSIManagedIdentities tracks resolved ClientID/PrincipalID for - // the Managed Service Identity (MSI) based Azure User-Assigned Managed Identities - // associated to the cluster. Those are the cluster's control plane operators and - // the cluster's service managed identity. - // A cluster's control plane operator is a kubernetes operator associated to - // the cluster that runs in the cluster's control plane. For example, - // the Cluster's CustomerProperties.Platform.OperatorsAuthentication.UserAssignedIdentities.ControlPlaneOperators - // map contains (and is not limited to) the set of required control plane operators associated to a Cluster. - // The cluster's service managed identity is used to read and modify - // customer-provided Azure resources (for example the cluster subnet), - // subject to the permissions granted to that identity. - // MSI-based user-assigned managed identities are the identities defined in - // the Cluster's `identity` section. Credentials for those identities can be - // obtained from Microsoft's Managed Identities Data Plane service. - // In ARO-HCP environments where Microsoft's Managed Identities Data Plane - // service is unavailable, a fake Managed Identities Data Plane client is - // used. That client always returns the same identity metadata and - // credentials, regardless of which identity is requested. The returned - // values belong to the "MI Mock" identity, so the ClientID and PrincipalID - // stored for each entry here will not match that entry's ResourceID key, - // nor the real ClientID/PrincipalID of the corresponding identity in the - // Cluster's `identity` section. - // Additionally, this also tracks when Azure should next be re-queried for that info. - // Written by: FetchMSIIdentitiesInfo - MSIManagedIdentities ServiceProviderClusterMSIManagedIdentities `json:"msiManagedIdentities,omitempty"` - - // DataPlaneOperatorsManagedIdentities tracks resolved ClientID/PrincipalID for - // the Azure User Assigned Managed Identities associated with the cluster's data - // plane operators, plus when Azure should next be re-queried for that info. - // A cluster's data plane operator is a Kubernetes operator associated with the - // cluster that runs in the cluster's data plane. - // For example, the Cluster's CustomerProperties.Platform.OperatorsAuthentication.UserAssignedIdentities.DataPlaneOperators map - // contains the set of required data plane operators associated with a Cluster. - // Written by: FetchDataPlaneOperatorsManagedIdentitiesInfoController - DataPlaneOperatorsManagedIdentities ServiceProviderClusterDataPlaneOperatorsManagedIdentities `json:"dataPlaneOperatorsManagedIdentities,omitempty"` -} - -// ServiceProviderClusterMSIManagedIdentities holds Managed Service Identity (MSI) -// based identity metadata resolved by FetchMSIIdentitiesInfo and consumed by ClusterIdentitySync to -// populate HCPOpenShiftCluster.Identity.UserAssignedIdentities. -type ServiceProviderClusterMSIManagedIdentities struct { - // EarliestRecheckTime is the earliest time at which the controller - // should re-query Azure for ClientID/PrincipalID of ControlPlaneOperatorsIdentities - // and ServiceManagedIdentity. - // Nil means recheck immediately. - // The same recheck time applies across all entries in ControlPlaneOperatorsIdentities - // and ServiceManagedIdentity. - // This allows the controller to avoid repeatedly hitting an Azure API to - // recheck that the desired state is true. - // Controllers should set this field with substantial jitter: without another - // concern, jitter of 50% is considered normal so that any storms are quickly - // dissipated. Additionally, long recheck times are recommended for resources - // outside of their active phases. Order of at least six hours is, with - // durations up to 24 hours considered normal. - // Written by: FetchMSIIdentitiesInfo - EarliestRecheckTime *metav1.Time `json:"earliestRecheckTime,omitempty"` - // ControlPlaneOperatorsIdentities is a map containing resolved ClientID/PrincipalID - // for Managed Service Identity (MSI) based Azure User-Assigned Managed Identities - // used by the cluster's control plane operators. The key is the fully lowercased - // Azure Resource ID of the identity. Which operators reference each identity is - // tracked on Cluster.CustomerProperties, not here. Multiple operators may share - // one identity entry. - // Written by: FetchMSIIdentitiesInfo - ControlPlaneOperatorsIdentities map[string]*ServiceProviderClusterControlPlaneOperatorIdentity `json:"controlPlaneOperatorsIdentities,omitempty"` - // ServiceManagedIdentity holds resolved ClientID/PrincipalID for the cluster's - // service managed identity. - // Written by: FetchMSIIdentitiesInfo - ServiceManagedIdentity *ServiceProviderClusterServiceManagedIdentity `json:"serviceManagedIdentity,omitempty"` -} - -// ServiceProviderClusterControlPlaneOperatorIdentity is the resolved metadata for a -// single Managed Service Identity (MSI) based Azure User-Assigned Managed Identity -// used by one or more control plane operators. -// Which operators reference this identity is tracked on -// Cluster.CustomerProperties.Platform.OperatorsAuthentication.UserAssignedIdentities.ControlPlaneOperators. -type ServiceProviderClusterControlPlaneOperatorIdentity struct { - // ResourceID is the Azure Resource ID of the Azure User Assigned Managed Identity. - // Its value comes from the Cluster's CustomerProperties. - // The ControlPlaneOperatorsIdentities map key is the fully lowercased form - // of this ID used for lookups. - ResourceID *azcorearm.ResourceID `json:"resourceId,omitempty"` - // ClientID is the Client ID of the Azure User Assigned Managed Identity represented by ResourceID. - // Fetched from Azure and written here by the FetchMSIIdentitiesInfo. - // It may be nil or empty. - ClientID *string `json:"clientId,omitempty"` - // PrincipalID is the Principal ID of the Azure User Assigned Managed Identity represented by ResourceID. - // Fetched from Azure and written here by the FetchMSIIdentitiesInfo. - // It may be nil or empty. - PrincipalID *string `json:"principalId,omitempty"` -} - -// ServiceProviderClusterServiceManagedIdentity is the resolved metadata for the -// cluster's service managed identity. -type ServiceProviderClusterServiceManagedIdentity struct { - // ResourceID is the Azure Resource ID of the Azure User Assigned Managed Identity that is associated to the cluster's Service Managed Identity. - // Its value comes from the Cluster's CustomerProperties. - ResourceID *azcorearm.ResourceID `json:"resourceId,omitempty"` - // ClientID is the Client ID of the Azure User Assigned Managed Identity represented by ResourceID. - // Fetched from Azure and written here by the FetchMSIIdentitiesInfo. - // It may be nil or empty. - ClientID *string `json:"clientId,omitempty"` - // PrincipalID is the Principal ID of the Azure User Assigned Managed Identity represented by ResourceID. - // Fetched from Azure and written here by the FetchMSIIdentitiesInfo. - // It may be nil or empty. - PrincipalID *string `json:"principalId,omitempty"` -} - -// ServiceProviderClusterDataPlaneOperatorsManagedIdentities holds the resolved -// managed-identity metadata for all data plane operators on a cluster, together -// with a single EarliestRecheckTime that applies to every entry in Identities. -type ServiceProviderClusterDataPlaneOperatorsManagedIdentities struct { - // Identities is a map containing resolved ClientID/PrincipalID for the Azure - // User Assigned Managed Identities associated with the cluster's data plane - // operators. The key is the fully lowercased Azure Resource ID of the - // identity. Which operators reference each identity is tracked on - // Cluster.CustomerProperties, not here. Multiple operators may share one - // identity entry. - // Written by: FetchDataPlaneOperatorsManagedIdentitiesInfoController - Identities map[string]*ServiceProviderClusterDataPlaneOperatorManagedIdentity `json:"identities,omitempty"` - // EarliestRecheckTime is the earliest time at which the controller should - // re-query Azure for ClientID/PrincipalID of Identities. Nil means recheck - // immediately. The same recheck time applies across all elements of Identities. - // This allows the controller to avoid repeatedly hitting an Azure API to - // recheck that the desired state is true. - // Controllers should set this field with substantial jitter: without another - // concern, jitter of 50% is considered normal so that any storms are quickly - // dissipated. Additionally, long recheck times are recommended for resources - // outside of their active phases. Order of at least six hours is, with - // durations up to 24 hours considered normal. - // Written by: FetchDataPlaneOperatorsManagedIdentitiesInfoController - EarliestRecheckTime *metav1.Time `json:"earliestRecheckTime,omitempty"` -} - -// ServiceProviderClusterDataPlaneOperatorManagedIdentity contains resolved -// ClientID/PrincipalID for an Azure User Assigned Managed Identity used by one -// or more of a cluster's data plane operators. -// A cluster's data plane operator is a customer operator associated with the cluster that runs in the cluster's data plane. -// Which operators reference this identity is tracked on -// Cluster.CustomerProperties.Platform.OperatorsAuthentication.UserAssignedIdentities.DataPlaneOperators. -type ServiceProviderClusterDataPlaneOperatorManagedIdentity struct { - // ResourceID is the Azure Resource ID of the Azure User Assigned Managed Identity. - // This field is an input: its value is mirrored from the Cluster's CustomerProperties - // (Platform.OperatorsAuthentication.UserAssignedIdentities.DataPlaneOperators) into this status. - // Written by: FetchDataPlaneOperatorsManagedIdentitiesInfoController - ResourceID *azcorearm.ResourceID `json:"resourceID,omitempty"` - // ClientID is the Client ID of the Azure User Assigned Managed Identity represented by ResourceID. - // This field is an output: it is fetched from Azure and written here by the controller. - // Written by: FetchDataPlaneOperatorsManagedIdentitiesInfoController - ClientID *string `json:"clientId,omitempty"` - // PrincipalID is the Principal ID of the Azure User Assigned Managed Identity represented by ResourceID. - // This field is an output: it is fetched from Azure and written here by the controller. - // Written by: FetchDataPlaneOperatorsManagedIdentitiesInfoController - PrincipalID *string `json:"principalId,omitempty"` - // RetrievalError, when non-nil, is the error (truncated to the first 1024 characters) from the - // most recent attempt to retrieve this identity's metadata from Azure. When set, ClientID and - // PrincipalID are nil because the last retrieval attempt failed - either the identity was not - // found in Azure or the Get call returned an error - and any previously resolved values are no - // longer trustworthy. It is nil when the last retrieval succeeded. - // This field is an output: it is written here by the controller. - // Written by: FetchDataPlaneOperatorsManagedIdentitiesInfoController - RetrievalError *string `json:"retrievalError,omitempty"` + // ManagedIdentities is the observed replacement ledger: Active identity per + // slot, shared generations, control-plane / SMI / data-plane instance maps, + // nested conditions, and fetch recheck times. Spec.ManagedIdentities is what + // each slot wants; this Status is what is in service and in flight. See + // ServiceProviderClusterManagedIdentitiesSpec. + // Written by: ManagedIdentitiesCoordinator, FetchMSIIdentitiesInfo, + // FetchDataPlaneOperatorsManagedIdentitiesInfo, ManagedIdentitiesStatusAggregator + ManagedIdentities ServiceProviderClusterManagedIdentitiesStatus `json:"managedIdentities,omitempty"` } // AzureResources groups the Azure resource references associated with a cluster. diff --git a/internal/api/coreapi/types_serviceprovider_cluster_managed_identities.go b/internal/api/coreapi/types_serviceprovider_cluster_managed_identities.go new file mode 100644 index 00000000000..7985d2a57d4 --- /dev/null +++ b/internal/api/coreapi/types_serviceprovider_cluster_managed_identities.go @@ -0,0 +1,660 @@ +// Copyright 2026 Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package coreapi + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" +) + +// Managed identity replacement is tracked on ServiceProviderCluster, not on the +// Cluster ARM document. Read ServiceProviderClusterManagedIdentitiesSpec for +// the slot/generation/instance model and the configure/replace/deconfigure +// flow. +// +// Managed identity condition types used on ServiceProviderCluster.Status.ManagedIdentities +// (subsystem, slot, generation, and instance). Top-level +// ServiceProviderCluster Status.Conditions stay limited to Progressing and Degraded. +const ( + // ManagedIdentityConditionConfigured is True when the object is fully configured. + // On a generation: every identity-level configure step (metadata, MRG role + // assignment union, deny-assignment exclude) has succeeded. + // On a control-plane instance: the generation is Configured plus Cluster + // Service dispatch (Key Vault later). + // On a data-plane instance: the generation is Configured plus OIDC federation + // and Cluster Service dispatch. + // On an SMI instance: the generation is Configured plus Cluster Service + // dispatch and customer-scope roles (Key Vault later). + // On a slot: ActiveInstanceID equals Spec.InstanceID and that instance is + // Configured. + // On the subsystem: every spec slot is Configured and no generation or + // instance is still deconfiguring. + ManagedIdentityConditionConfigured = "Configured" + + // ManagedIdentityConditionIdentityMetadataResolved is True when ClientID and + // PrincipalID are non-empty on the generation. + ManagedIdentityConditionIdentityMetadataResolved = "IdentityMetadataResolved" + + // ManagedIdentityConditionRoleAssignmentsConfigured is True when expected + // role assignments exist in Azure. On a generation: the identity-level MRG + // role assignment union. On an SMI instance: customer-scope SMI role + // assignments. + ManagedIdentityConditionRoleAssignmentsConfigured = "RoleAssignmentsConfigured" + + // ManagedIdentityConditionDenyAssignmentExcludesPrincipal is True when the + // cluster deny assignment excludes this generation's PrincipalID. + ManagedIdentityConditionDenyAssignmentExcludesPrincipal = "DenyAssignmentExcludesPrincipal" + + // ManagedIdentityConditionClusterServiceDispatched is True when Cluster + // Service has been told to use this identity for this instance (one + // operator on a control-plane or data-plane instance; the cluster SMI + // on an SMI instance). + ManagedIdentityConditionClusterServiceDispatched = "ClusterServiceDispatched" + + // ManagedIdentityConditionOIDCFederationConfigured is True when federated + // identity credentials for this data-plane instance's operator subjects exist. + ManagedIdentityConditionOIDCFederationConfigured = "OIDCFederationConfigured" + + // ManagedIdentityConditionDeconfigured is True when the object is fully + // retired and can be dropped. On a generation: identity-level teardown is + // done and no instance still names it. On a control-plane instance: Cluster + // Service dispatch teardown is done. On a data-plane instance: OIDC teardown + // is done. On an SMI instance: type-particular teardown is done, including + // customer-scope roles. + ManagedIdentityConditionDeconfigured = "Deconfigured" +) + +// ManagedIdentityReplacementTrigger is why a generation or a type instance +// was created. +type ManagedIdentityReplacementTrigger string + +const ( + // ManagedIdentityReplacementTriggerCreate is set when the generation or + // instance is the first identity for a slot (cluster create or a newly + // added operator). + ManagedIdentityReplacementTriggerCreate ManagedIdentityReplacementTrigger = "Create" + // ManagedIdentityReplacementTriggerUserResourceIDChange is set when the + // slot's desired ResourceID changed (user PUT/PATCH). + ManagedIdentityReplacementTriggerUserResourceIDChange ManagedIdentityReplacementTrigger = "UserResourceIDChange" + // ManagedIdentityReplacementTriggerObservedClientOrPrincipalIDChange is set + // when fetch observed that ClientID and/or PrincipalID for the same ResourceID + // no longer match the values stored on the previous generation. + ManagedIdentityReplacementTriggerObservedClientOrPrincipalIDChange ManagedIdentityReplacementTrigger = "ObservedClientOrPrincipalIDChange" +) + +// ServiceProviderClusterManagedIdentitiesSpec is the desired identity for each +// operator and for the service managed identity (SMI). +// +// # Why this lives on ServiceProviderCluster +// +// Cluster.CustomerProperties.Platform.OperatorsAuthentication.UserAssignedIdentities +// is the ARM-visible **latest** desired ResourceID per operator. Frontend PUT/PATCH +// overwrites those maps. +// +// This Spec, plus Status.ManagedIdentities, is the internal mechanism used to +// configure the new identity, deconfigure the old one, and survive a sequence +// of replacements before intermediate ones finish (A then B then C) so we can +// clean up the old ones. +// +// # Slot +// +// A slot is one operator (or the SMI) asking "which identity should I use?". +// There is one slot per control-plane operator name, one per data-plane +// operator name, and one SMI slot. Slots are keyed by operator name as stored +// on the Cluster maps (not lowercased, not by ResourceID). +// +// # Generation +// +// A generation is one Azure identity (ResourceID + ClientID + PrincipalID) plus +// identity-level configure/deconfigure progress that is independent of operator +// and of configuration process: metadata fetch, deny-assignment exclude of that +// principal, and the managed-resource-group role-assignment union across every +// operator using that principal. +// +// It is keyed by a UUID (GenerationID), not by ResourceID, because +// ClientID/PrincipalID can change for the same ResourceID and that is a new +// generation. Control-plane, SMI, and data-plane that use the same ResourceID +// share one generation so identity-level work does not run twice. +// +// # Instance +// +// An instance is one slot's occupancy of one generation: configure/deconfigure +// progress for that slot against that Azure identity. Control-plane operators, +// data-plane operators, and SMI each have their own instance map. Two operators +// that Spec the same ResourceID get two instances and share one generation. +// A control-plane slot and the SMI slot never share an instance; they share a +// generation. A control-plane slot and a data-plane slot share only a generation. +// +// Control-plane and data-plane instances store OperatorName (as on Cluster, not +// lowercased) so the instance map is readable without scanning slots. SMI has +// no operator; there is one SMI slot per cluster. +// +// Each instance's GenerationID is assigned at create and is never rewritten. +// Replacement opens a new instance and moves the slot's InstanceID. Two +// instances can share OperatorName during replacement (Active vs Spec). +// +// Spec.InstanceID is the instance this slot wants in service. Status.Active* +// is the instance operators / Cluster Service are actually using. They differ +// during replacement until the desired instance (and its generation) is +// Configured. +// +// # Flow +// +// 1. Frontend writes new desired ResourceIDs on the Cluster and starts an +// Update operation. It does not write this Spec, generations, or instances. +// 2. ManagedIdentitiesCoordinator copies Cluster ResourceIDs onto these slots. +// A new ResourceID, or observed ClientID/PrincipalID drift, opens a new +// generation when none is already Spec-desired for that ResourceID, opens +// a new instance for this slot pointing at that generation, and points +// Spec.InstanceID at the instance. Two operators share a generation, not +// an instance. Control-plane and SMI share a generation, not an instance. +// Control-plane and data-plane share a generation, not an instance. +// 3. Fetch controllers fill generation ClientID/PrincipalID. If Azure later +// returns different values they set Observed* instead of overwriting, and +// the coordinator opens a new generation and a new instance for every slot +// that Spec'd the old generation. +// 4. On create, Active is set once ClientID and PrincipalID are known. On +// replacement, Active moves only when the desired instance is Configured +// (generation plus this slot's steps). Until then the previous instance +// stays Active. +// 5. An instance with no slot Spec and no slot Active starts deconfiguring. +// When Deconfigured it is dropped from its type map. +// 6. A generation with no remaining instance starts identity-level +// deconfiguring. When Deconfigured it is dropped from Generations. +// +// Overlapping replacements (user changes A to B, then B to C before B is +// ready): Spec points at C. B is abandoned and deconfigures immediately. A +// stays Active until C is Configured for this slot. +// +// Cluster.Status.OperatorIdentities is a distilled copy of this ledger for +// Cluster-document readers. It is not an ARM ResourceStatus field. +type ServiceProviderClusterManagedIdentitiesSpec struct { + // ControlPlaneOperators is keyed by operator name as stored on + // Cluster.CustomerProperties.Platform.OperatorsAuthentication.UserAssignedIdentities.ControlPlaneOperators + // (for example "cloud-controller-manager"). Keys are not lowercased; they + // keep the Cluster map's casing. Lookups must use that same name, not a + // lowercased form and not the identity ResourceID. + // Written by: ManagedIdentitiesCoordinator + ControlPlaneOperators map[string]*ManagedIdentitySlotSpec `json:"controlPlaneOperators,omitempty"` + // DataPlaneOperators is keyed by operator name as stored on + // Cluster.CustomerProperties.Platform.OperatorsAuthentication.UserAssignedIdentities.DataPlaneOperators + // (for example "image-registry"). Keys are not lowercased; they keep the + // Cluster map's casing. Lookups must use that same name, not a lowercased + // form and not the identity ResourceID. + // Written by: ManagedIdentitiesCoordinator + DataPlaneOperators map[string]*ManagedIdentitySlotSpec `json:"dataPlaneOperators,omitempty"` + // ServiceManagedIdentity is the desired identity for the cluster's service + // managed identity. Nil when the Cluster has no ServiceManagedIdentity ResourceID. + // Written by: ManagedIdentitiesCoordinator + ServiceManagedIdentity *ManagedIdentitySlotSpec `json:"serviceManagedIdentity,omitempty"` +} + +// ManagedIdentitySlotSpec is the desired Azure identity for one slot (one +// control-plane operator, one data-plane operator, or the SMI). See +// ServiceProviderClusterManagedIdentitiesSpec for what a slot is. +type ManagedIdentitySlotSpec struct { + // ResourceID is the latest desired Azure user-assigned identity ResourceID, + // mirrored from Cluster.CustomerProperties.Platform.OperatorsAuthentication.UserAssignedIdentities. + // Written by: ManagedIdentitiesCoordinator + ResourceID *azcorearm.ResourceID `json:"resourceID,omitempty"` + // InstanceID is the type-map key this slot wants in service. Control-plane + // slots resolve it in Status.ManagedIdentities.ControlPlaneInstances. SMI + // slots resolve it in Status.ManagedIdentities.ServiceManagedIdentityInstances. + // Data-plane slots resolve it in Status.ManagedIdentities.DataPlaneInstances. + // It is the instance UUID as generated (uuid.NewString), not a ResourceID + // and not lowercased. The instance's GenerationID is the generation-map key. + // Written by: ManagedIdentitiesCoordinator + InstanceID string `json:"instanceID,omitempty"` +} + +// ServiceProviderClusterManagedIdentitiesStatus is the observed identity ledger: +// which instance is Active on each slot, the shared generation map, the three +// type instance maps, nested conditions, and the two fetch recheck times +// (MSI dataplane vs data-plane ARM). See +// ServiceProviderClusterManagedIdentitiesSpec for the +// slot/generation/instance model. +type ServiceProviderClusterManagedIdentitiesStatus struct { + // Conditions for the identity subsystem as a whole. + // Known types: Configured. + // Written by: ManagedIdentitiesStatusAggregator + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + // MSIIdentitiesEarliestRecheckTime is when FetchMSIIdentitiesInfo should next + // query the Managed Identities Data Plane for ClientID/PrincipalID of + // Spec-desired control-plane and SMI generations. Nil means recheck immediately. + // One timestamp for that whole fetch (one dataplane call), not per generation. + // Honor it only when the desired MSI ResourceID set is unchanged; on ResourceID + // set change, query immediately. Jitter ~50%; idle recheck on the order of + // 6-24 hours. + // Written by: FetchMSIIdentitiesInfo + MSIIdentitiesEarliestRecheckTime *metav1.Time `json:"msiIdentitiesEarliestRecheckTime,omitempty"` + // DataPlaneOperatorsIdentitiesEarliestRecheckTime is when + // FetchDataPlaneOperatorsManagedIdentitiesInfo should next query Azure ARM + // for ClientID/PrincipalID of Spec-desired data-plane generations. Nil means + // recheck immediately. Separate from MSIIdentitiesEarliestRecheckTime because + // that is a different controller and API. Same honor/jitter rules as MSI. + // Written by: FetchDataPlaneOperatorsManagedIdentitiesInfo + DataPlaneOperatorsIdentitiesEarliestRecheckTime *metav1.Time `json:"dataPlaneOperatorsIdentitiesEarliestRecheckTime,omitempty"` + + // ControlPlaneOperators is keyed by operator name as stored on + // Cluster.CustomerProperties.Platform.OperatorsAuthentication.UserAssignedIdentities.ControlPlaneOperators. + // Keys are not lowercased; they keep the Cluster map's casing and match + // Spec.ManagedIdentities.ControlPlaneOperators. Lookups must use that same + // name, not a lowercased form and not the identity ResourceID. + // Written by: ManagedIdentitiesCoordinator (Active* fields), ManagedIdentitiesStatusAggregator (Conditions) + ControlPlaneOperators map[string]*ManagedIdentitySlotStatus `json:"controlPlaneOperators,omitempty"` + // DataPlaneOperators is keyed by operator name as stored on + // Cluster.CustomerProperties.Platform.OperatorsAuthentication.UserAssignedIdentities.DataPlaneOperators. + // Keys are not lowercased; they keep the Cluster map's casing and match + // Spec.ManagedIdentities.DataPlaneOperators. Lookups must use that same + // name, not a lowercased form and not the identity ResourceID. + // Written by: ManagedIdentitiesCoordinator (Active* fields), ManagedIdentitiesStatusAggregator (Conditions) + DataPlaneOperators map[string]*ManagedIdentitySlotStatus `json:"dataPlaneOperators,omitempty"` + // ServiceManagedIdentity is the observed Active identity for the cluster's + // service managed identity. Nil when Spec.ServiceManagedIdentity is absent. + // Written by: ManagedIdentitiesCoordinator (Active* fields), ManagedIdentitiesStatusAggregator (Conditions) + ServiceManagedIdentity *ManagedIdentitySlotStatus `json:"serviceManagedIdentity,omitempty"` + + // Generations is the durable set of Azure identity generations + // (ResourceID + ClientID + PrincipalID) we have started configuring and + // have not finished deconfiguring. The key is ManagedIdentityGeneration.GenerationID + // (the UUID assigned when the generation is created). It is not a ResourceID + // and is not lowercased. Control-plane, SMI, and data-plane instances that + // use the same ResourceID share one generation. + // Written by: ManagedIdentitiesCoordinator (membership, Trigger, DeconfigurationStarted), + // FetchMSIIdentitiesInfo, FetchDataPlaneOperatorsManagedIdentitiesInfo + // (ClientID/PrincipalID/Observed*/RetrievalError) + Generations map[string]*ManagedIdentityGeneration `json:"generations,omitempty"` + + // ControlPlaneInstances is the durable set of control-plane instances we + // have started configuring and have not finished deconfiguring. The key is + // ControlPlaneManagedIdentityInstance.InstanceID (the UUID assigned when + // the instance is created). It is not an operator name, not a ResourceID, + // and is not lowercased. One Spec control-plane slot names one live + // instance; the map also holds instances still deconfiguring. Two operators + // that Spec the same ResourceID get two instances and share one generation. + // OperatorName on the instance is the slot it belongs to. Each instance + // points at a GenerationID. + // Written by: ManagedIdentitiesCoordinator (membership, Trigger, DeconfigurationStarted) + ControlPlaneInstances map[string]*ControlPlaneManagedIdentityInstance `json:"controlPlaneInstances,omitempty"` + + // ServiceManagedIdentityInstances is the durable set of SMI type instances + // we have started configuring and have not finished deconfiguring. The key + // is ServiceManagedIdentityInstance.InstanceID. There is at most one Spec + // SMI slot, so this map is typically one live instance plus any still + // deconfiguring. Each instance points at a GenerationID. + // Written by: ManagedIdentitiesCoordinator (membership, Trigger, DeconfigurationStarted) + ServiceManagedIdentityInstances map[string]*ServiceManagedIdentityInstance `json:"serviceManagedIdentityInstances,omitempty"` + + // DataPlaneInstances is the durable set of data-plane instances we have + // started configuring and have not finished deconfiguring. The key is + // DataPlaneManagedIdentityInstance.InstanceID. One Spec data-plane slot + // names one live instance; the map also holds instances still deconfiguring. + // Two operators that Spec the same ResourceID get two instances and share + // one generation. OperatorName on the instance is the slot it belongs to. + // Each instance points at a GenerationID. + // Written by: ManagedIdentitiesCoordinator (membership, Trigger, DeconfigurationStarted) + DataPlaneInstances map[string]*DataPlaneManagedIdentityInstance `json:"dataPlaneInstances,omitempty"` +} + +// ManagedIdentitySlotStatus is what is actually in service for one slot. +// Active* is the identity operators / Cluster Service are using. During +// replacement it stays on the previous instance until the Spec-desired +// instance is Configured (generation plus this slot's steps). See +// ServiceProviderClusterManagedIdentitiesSpec. +type ManagedIdentitySlotStatus struct { + // Conditions: + // - "Configured": ActiveInstanceID equals Spec.InstanceID and that instance + // is Configured. + // Written by: ManagedIdentitiesStatusAggregator + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + // ActiveInstanceID is the type-map key of the identity Cluster Service / + // operators are using for this slot. Same form as Spec.InstanceID: the instance + // UUID, not a ResourceID and not lowercased. Empty means nothing has been + // activated yet (create path). Control-plane slots resolve it in + // ControlPlaneInstances; SMI slots in ServiceManagedIdentityInstances; + // data-plane slots in DataPlaneInstances. + // Written by: ManagedIdentitiesCoordinator + ActiveInstanceID string `json:"activeInstanceID,omitempty"` + // ActiveResourceID is the Azure ResourceID of the Active instance. Copied + // from the Active instance's generation so consumers need not look up + // the generation. Empty until ActiveInstanceID is set. + // Written by: ManagedIdentitiesCoordinator + ActiveResourceID *azcorearm.ResourceID `json:"activeResourceID,omitempty"` + // ClientID is the Client ID of the Active instance. Copied from + // the Active instance's generation. Empty until metadata is resolved + // and ActiveInstanceID is set. + // Written by: ManagedIdentitiesCoordinator + ClientID *string `json:"clientID,omitempty"` + // PrincipalID is the Principal ID of the Active instance. Copied from + // the Active instance's generation. Empty until metadata is resolved + // and ActiveInstanceID is set. + // Written by: ManagedIdentitiesCoordinator + PrincipalID *string `json:"principalID,omitempty"` +} + +// ManagedIdentityGeneration is one Azure identity (ResourceID + ClientID + +// PrincipalID) plus identity-level configure/deconfigure progress. Control-plane, +// SMI, and data-plane instances that use this identity share this object so +// metadata fetch, deny-assignment exclude, and the MRG role-assignment union +// run once. See ServiceProviderClusterManagedIdentitiesSpec. +type ManagedIdentityGeneration struct { + // GenerationID is this generation's UUID and the key of Status.ManagedIdentities.Generations. + // It is assigned at creation (uuid.NewString). It is not a ResourceID and is + // not lowercased. + // Written by: ManagedIdentitiesCoordinator + GenerationID string `json:"generationID"` + + // ResourceID is the Azure user-assigned identity this generation represents. + // The same ResourceID can appear on more than one generation across replacements + // (ClientID/PrincipalID drift creates a new generation of the same ResourceID). + // Written by: ManagedIdentitiesCoordinator + ResourceID *azcorearm.ResourceID `json:"resourceID,omitempty"` + // ClientID is the Client ID stored for this generation. Filled by fetch when + // empty. Never overwritten in place when Azure later returns a different + // value; that difference is written to ObservedClientID instead. On a + // ClientID/PrincipalID-drift generation the coordinator copies the observed + // value in at creation. + // Written by: ManagedIdentitiesCoordinator (drift create), FetchMSIIdentitiesInfo, + // FetchDataPlaneOperatorsManagedIdentitiesInfo + ClientID *string `json:"clientID,omitempty"` + // PrincipalID is the Principal ID stored for this generation. Filled by fetch + // when empty. Never overwritten in place when Azure later returns a different + // value; that difference is written to ObservedPrincipalID instead. On a + // ClientID/PrincipalID-drift generation the coordinator copies the observed + // value in at creation. + // Written by: ManagedIdentitiesCoordinator (drift create), FetchMSIIdentitiesInfo, + // FetchDataPlaneOperatorsManagedIdentitiesInfo + PrincipalID *string `json:"principalID,omitempty"` + + // ObservedClientID is set by fetch controllers when Azure returns a ClientID + // that differs from ClientID. The coordinator opens a new generation rather + // than overwriting ClientID in place. Cleared on the old generation after the + // new generation is created. + // Written by: FetchMSIIdentitiesInfo, FetchDataPlaneOperatorsManagedIdentitiesInfo, + // ManagedIdentitiesCoordinator (clears after retarget) + ObservedClientID *string `json:"observedClientID,omitempty"` + // ObservedPrincipalID is set by fetch controllers when Azure returns a + // PrincipalID that differs from PrincipalID. Cleared on the old generation + // after the new generation is created. + // Written by: FetchMSIIdentitiesInfo, FetchDataPlaneOperatorsManagedIdentitiesInfo, + // ManagedIdentitiesCoordinator (clears after retarget) + ObservedPrincipalID *string `json:"observedPrincipalID,omitempty"` + + // RetrievalError, when non-nil, is the error (truncated to the first 1024 + // characters) from the most recent attempt to retrieve this generation's + // metadata from Azure. When set, the last retrieval failed (not found or + // Get error) and ClientID/PrincipalID from that attempt are untrustworthy. + // Nil when the last retrieval succeeded. Successful retrieval with different + // IDs uses Observed* instead of this field. + // Written by: FetchMSIIdentitiesInfo, FetchDataPlaneOperatorsManagedIdentitiesInfo + RetrievalError *string `json:"retrievalError,omitempty"` + + // Trigger is why this generation was created. See ManagedIdentityReplacementTrigger. + // Written by: ManagedIdentitiesCoordinator + Trigger ManagedIdentityReplacementTrigger `json:"trigger,omitempty"` + + Status ManagedIdentityGenerationStatus `json:"status,omitempty"` +} + +// ManagedIdentityGenerationStatus is identity-level configure/deconfigure +// progress. Configured means metadata, MRG role assignments, and +// deny-assignment exclude are True. Slot-particular progress lives on +// instances. +type ManagedIdentityGenerationStatus struct { + // Conditions: + // - IdentityMetadataResolved + // - RoleAssignmentsConfigured + // - DenyAssignmentExcludesPrincipal + // - Configured (all identity-level configure steps True) + // - Deconfigured (identity-level retirement complete and no instances remain) + // Written by: ManagedIdentitiesStatusAggregator + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + // DeconfigurationStarted is set when no type instance still names this + // generation. Identity-level teardown (deny exclude, MRG role assignments) + // runs after that. + // Written by: ManagedIdentitiesCoordinator + DeconfigurationStarted *metav1.Time `json:"deconfigurationStarted,omitempty"` + + // RoleAssignments tracks managed-resource-group role assignments created + // for this generation's PrincipalID. This is the union of role definitions of + // every operator currently using this principal, across control-plane, SMI, + // and data-plane instances. PendingAzureResources are requested but not yet + // confirmed in Azure; AzureResources are confirmed. + // Written by: (not yet; role-assignment controller will write per generation) + RoleAssignments AzureMultiReference `json:"roleAssignments,omitempty"` +} + +// ControlPlaneManagedIdentityInstance is one control-plane operator's occupancy +// of one generation. Cluster Service dispatch lives on Status. The Azure +// generation lives on Status.ManagedIdentities.Generations[GenerationID]. See +// ServiceProviderClusterManagedIdentitiesSpec. +type ControlPlaneManagedIdentityInstance struct { + // InstanceID is this instance's UUID and the key of ControlPlaneInstances. + // Written by: ManagedIdentitiesCoordinator + InstanceID string `json:"instanceID"` + // GenerationID is the key of Status.ManagedIdentities.Generations this + // instance uses. Assigned at create and never rewritten. ClientID or + // PrincipalID drift, or a slot ResourceID change, opens a new instance + // (and a new generation when needed) and moves Spec.InstanceID. + // Written by: ManagedIdentitiesCoordinator + GenerationID string `json:"generationID,omitempty"` + // OperatorName is the control-plane operator this instance belongs to, + // as stored on Cluster (not lowercased). Assigned at create. Two instances + // can share this name during replacement (Active vs Spec). + // Written by: ManagedIdentitiesCoordinator + OperatorName string `json:"operatorName,omitempty"` + // Trigger is why this instance was created. See + // ManagedIdentityReplacementTrigger. Distinct from the generation Trigger + // when a later slot starts using an already-created generation. + // Written by: ManagedIdentitiesCoordinator + Trigger ManagedIdentityReplacementTrigger `json:"trigger,omitempty"` + Status ControlPlaneManagedIdentityInstanceStatus `json:"status,omitempty"` +} + +// ServiceManagedIdentityInstance is one SMI occupancy of one generation plus +// SMI configure/deconfigure progress. The SMI is cluster-scoped, not an +// operator. The Azure generation lives on +// Status.ManagedIdentities.Generations[GenerationID]. See +// ServiceProviderClusterManagedIdentitiesSpec. +type ServiceManagedIdentityInstance struct { + // InstanceID is this instance's UUID and the key of ServiceManagedIdentityInstances. + // Written by: ManagedIdentitiesCoordinator + InstanceID string `json:"instanceID"` + // GenerationID is the key of Status.ManagedIdentities.Generations this + // instance uses. Assigned at create and never rewritten. + // Written by: ManagedIdentitiesCoordinator + GenerationID string `json:"generationID,omitempty"` + // Trigger is why this instance was created. See + // ManagedIdentityReplacementTrigger. + // Written by: ManagedIdentitiesCoordinator + Trigger ManagedIdentityReplacementTrigger `json:"trigger,omitempty"` + Status ServiceManagedIdentityInstanceStatus `json:"status,omitempty"` +} + +// DataPlaneManagedIdentityInstance is one data-plane operator's occupancy of +// one generation. OIDC federation and Cluster Service dispatch live on Status. +// The Azure generation lives on Status.ManagedIdentities.Generations[GenerationID]. +// See ServiceProviderClusterManagedIdentitiesSpec. +type DataPlaneManagedIdentityInstance struct { + // InstanceID is this instance's UUID and the key of DataPlaneInstances. + // Written by: ManagedIdentitiesCoordinator + InstanceID string `json:"instanceID"` + // GenerationID is the key of Status.ManagedIdentities.Generations this + // instance uses. Assigned at create and never rewritten. ClientID or + // PrincipalID drift, or a slot ResourceID change, opens a new instance + // (and a new generation when needed) and moves Spec.InstanceID. + // Written by: ManagedIdentitiesCoordinator + GenerationID string `json:"generationID,omitempty"` + // OperatorName is the data-plane operator this instance belongs to, + // as stored on Cluster (not lowercased). Assigned at create. Two instances + // can share this name during replacement (Active vs Spec). + // Written by: ManagedIdentitiesCoordinator + OperatorName string `json:"operatorName,omitempty"` + // Trigger is why this instance was created. See + // ManagedIdentityReplacementTrigger. + // Written by: ManagedIdentitiesCoordinator + Trigger ManagedIdentityReplacementTrigger `json:"trigger,omitempty"` + Status DataPlaneManagedIdentityInstanceStatus `json:"status,omitempty"` +} + +// ControlPlaneManagedIdentityInstanceStatus is this operator's configure/deconfigure +// progress. Configured means the generation is Configured and Cluster Service +// has been told to use this identity for this operator (Key Vault later). +type ControlPlaneManagedIdentityInstanceStatus struct { + // Conditions: + // - ClusterServiceDispatched + // - Configured (generation Configured, Cluster Service dispatched) + // - Deconfigured (Cluster Service dispatch teardown complete) + // Written by: ManagedIdentitiesStatusAggregator + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + // DeconfigurationStarted is set when no slot Spec or Active still names this + // instance. For a previously Active instance that is after the successor is + // Active. For an abandoned intermediate that is as soon as no slot desires it. + // Written by: ManagedIdentitiesCoordinator + DeconfigurationStarted *metav1.Time `json:"deconfigurationStarted,omitempty"` +} + +// ServiceManagedIdentityInstanceStatus is SMI configure/deconfigure progress. +// Configured means the generation is Configured, Cluster Service has been +// told to use this identity, and customer-scope SMI role assignments exist +// (Key Vault later; none today). +type ServiceManagedIdentityInstanceStatus struct { + // Conditions: + // - ClusterServiceDispatched + // - RoleAssignmentsConfigured + // - Configured (generation Configured, Cluster Service dispatched, customer-scope roles) + // - Deconfigured (type-particular retirement complete, including customer-scope roles) + // Written by: ManagedIdentitiesStatusAggregator + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + // DeconfigurationStarted is set when no slot Spec or Active still names this + // instance. For a previously Active instance that is after the successor is + // Active. For an abandoned intermediate that is as soon as no slot desires it. + // Written by: ManagedIdentitiesCoordinator + DeconfigurationStarted *metav1.Time `json:"deconfigurationStarted,omitempty"` + + // ServiceManagedIdentityRoleAssignments tracks customer-scope role + // assignments for the service managed identity. + // Written by: (not yet; SMI role-assignment controller will write per instance) + ServiceManagedIdentityRoleAssignments AzureMultiReference `json:"serviceManagedIdentityRoleAssignments,omitempty"` +} + +// DataPlaneManagedIdentityInstanceStatus is this operator's configure/deconfigure +// progress. Configured means the generation is Configured, OIDC federation for +// this operator's subjects exists, and Cluster Service has been told to use +// this identity for this operator. +type DataPlaneManagedIdentityInstanceStatus struct { + // Conditions: + // - OIDCFederationConfigured + // - ClusterServiceDispatched + // - Configured (generation Configured, OIDC, Cluster Service dispatched) + // - Deconfigured (OIDC teardown complete) + // Written by: ManagedIdentitiesStatusAggregator + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + // DeconfigurationStarted is set when no slot Spec or Active still names this + // instance. For a previously Active instance that is after the successor is + // Active. For an abandoned intermediate that is as soon as no slot desires it. + // Written by: ManagedIdentitiesCoordinator + DeconfigurationStarted *metav1.Time `json:"deconfigurationStarted,omitempty"` + + // OIDCFederation tracks federated identity credentials for this operator's + // Kubernetes service account subjects. + // Written by: (not yet; OIDC federation controller will write per instance) + OIDCFederation DataPlaneOIDCFederationStatus `json:"oidcFederation,omitempty"` +} + +// DataPlaneOIDCFederationStatus is OIDC federation progress for one data-plane +// instance. Subjects are that operator's KubernetesServiceAccounts. +type DataPlaneOIDCFederationStatus struct { + // FederatedIdentityCredentials tracks federated identity credentials created + // on the user-assigned identity for this operator's subjects. + // Written by: (not yet; OIDC federation controller will write per instance) + FederatedIdentityCredentials AzureMultiReference `json:"federatedIdentityCredentials,omitempty"` +} + +// IdentityMetadataResolved reports whether ClientID and PrincipalID are both non-empty. +func (g *ManagedIdentityGeneration) IdentityMetadataResolved() bool { + if g == nil { + return false + } + return nonEmptyStringPtr(g.ClientID) && nonEmptyStringPtr(g.PrincipalID) +} + +// HasObservedIdentityDrift reports whether fetch observed ClientID or PrincipalID +// values that differ from the ones stored on this generation. +func (g *ManagedIdentityGeneration) HasObservedIdentityDrift() bool { + if g == nil { + return false + } + if nonEmptyStringPtr(g.ObservedClientID) && !stringPtrsEqual(g.ClientID, g.ObservedClientID) { + return true + } + if nonEmptyStringPtr(g.ObservedPrincipalID) && !stringPtrsEqual(g.PrincipalID, g.ObservedPrincipalID) { + return true + } + return false +} + +// nonEmptyStringPtr reports whether s points at a non-empty string. +func nonEmptyStringPtr(s *string) bool { + return s != nil && len(*s) > 0 +} + +// stringPtrsEqual reports whether a and b are both nil or both point at equal strings. +func stringPtrsEqual(a, b *string) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + return *a == *b +}