Skip to content

feat: add controllers that retrieve and sync information about MSI based identities - #6589

Merged
openshift-merge-bot[bot] merged 1 commit into
Azure:mainfrom
redhat-chai-bot:msoriano-fetchinfo-controlplaneoperators-and-smi-mis
Aug 20, 2026
Merged

feat: add controllers that retrieve and sync information about MSI based identities#6589
openshift-merge-bot[bot] merged 1 commit into
Azure:mainfrom
redhat-chai-bot:msoriano-fetchinfo-controlplaneoperators-and-smi-mis

Conversation

@redhat-chai-bot

Copy link
Copy Markdown
Collaborator

Summary

Adds two new controllers in backend/pkg/controllers/cluster/identity/ that retrieve and synchronize MSI-based identity information for ARO-HCP clusters:

  1. ClusterIdentitySync — Keeps HCPOpenShiftCluster.Identity.UserAssignedIdentities ClientID/PrincipalID in sync with ServiceProviderCluster (replaces the old CS-backed IdentityMigration controller from cluster/properties/)
  2. FetchMSIIdentitiesInfo — Fetches ClientID/PrincipalID for control-plane operator identities and the service managed identity via Microsoft's Managed Identities Data Plane service, storing results on ServiceProviderCluster.Status.MSIManagedIdentities

Changes

  • New controllers: cluster_identity_sync.go, fetch_msi_identities_info.go
  • New API types: IdentityDetails, MSIManagedIdentities in types_serviceprovider_cluster.go + generated deepcopy
  • Removed old identity_migration.go / identity_migration_test.go from cluster/properties/
  • Updated backend wiring (backend.go), docs (cosmos-data-flow.md, resource-creation.mm)
  • Fixed inertia_test.go regex to match renamed controller

Based on

Rebased from PR #6301 onto current main. Addresses review comments from that PR:

  • Completed unit tests for both controllers (11 SyncOnce test cases for FetchMSIIdentitiesInfo covering happy path, case-insensitive matching, identity-not-found, deleting-cluster skip, recheck gating, error handling, and nil-guard edge cases)
  • Audited and verified nil-dereference safety (the Copilot-flagged patterns targeted the old design; current code is nil-safe with explicit test coverage)
  • Fixed CI failures: inertia_test.go regex mismatch and lint issues
  • Minor grammar fix in doc comment

make lint and go test ./pkg/controllers/cluster/identity/... both pass cleanly.


AI-generated. Review for accuracy.

David Eads (@deads2k) requested in Slack thread

Copilot AI left a comment

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.

Pull request overview

This PR moves MSI identity metadata resolution/syncing fully into Cosmos by introducing two new backend controllers that (1) fetch ClientID/PrincipalID for MSI-based identities via the Managed Identities Data Plane and store them on ServiceProviderCluster.Status.MSIManagedIdentities, and (2) sync those resolved IDs into HCPOpenShiftCluster.Identity.UserAssignedIdentities. It also removes the legacy Cluster Service–backed identity migration controller and updates docs/wiring accordingly.

Changes:

  • Added FetchMSIIdentitiesInfo and ClusterIdentitySync controllers under backend/pkg/controllers/cluster/identity/.
  • Added new ServiceProviderCluster status types + deepcopy generation for persisted MSI identity metadata.
  • Removed the legacy IdentityMigration controller/tests and updated backend wiring + docs (Cosmos data flow, resource-creation map).

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
internal/api/coreapi/zz_generated.deepcopy.go Adds deepcopy support for new ServiceProviderCluster MSI identity status types.
internal/api/coreapi/types_serviceprovider_cluster.go Introduces Status.MSIManagedIdentities schema and related identity detail structs.
internal/api/coreapi/types_cluster.go Updates writer annotation for HCPOpenShiftCluster.Identity to ClusterIdentitySync.
backend/pkg/controllers/cluster/identity/fetch_msi_identities_info.go New controller to query MI Data Plane and persist resolved identity metadata onto SPC.
backend/pkg/controllers/cluster/identity/fetch_msi_identities_info_test.go Unit tests for identity-set matching and recheck gating.
backend/pkg/controllers/cluster/identity/fetch_msi_identities_info_synconce_test.go SyncOnce-focused unit tests covering dataplane responses, errors, and gating behavior.
backend/pkg/controllers/cluster/identity/cluster_identity_sync.go New controller to sync resolved ClientID/PrincipalID from SPC into Cluster Identity UAIs.
backend/pkg/controllers/cluster/identity/cluster_identity_sync_test.go Unit tests validating syncing behavior, casing, nil handling, and “no-op” scenarios.
backend/pkg/app/backend.go Wires in the new controllers and removes the old identity migration controller startup.
backend/pkg/utils/statusutils/inertia_test.go Updates regex test data to align with renamed controller naming in the suite.
docs/cosmos-data-flow.md Updates Cosmos read/write documentation for the new controllers and fields.
docs/resource-creation.mm Updates controller taxonomy to reflect the new identity-sync controller.
backend/pkg/controllers/cluster/properties/identity_migration.go Removes legacy CS-backed identity migration controller implementation.
backend/pkg/controllers/cluster/properties/identity_migration_test.go Removes legacy controller unit tests.
Files not reviewed (1)
  • internal/api/coreapi/zz_generated.deepcopy.go: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +257 to +266
identitiesToSyncResourceIDStrs := msiBasedIdentitiesToFetch.resourceIDStrings()

// On environments where the real Managed Identities Data Plane service is not available, a
// fake implementation of the Managed Identities Data Plane client is used, which always returns the same information and
// same set of credentials for all requests, independently of which identity is requested. The returned information is
// the information associated to the "MI Mock" identity.
fpaMIDataplaneClient, err := c.fpaMIdataplaneClientBuilder.ManagedIdentitiesDataplane(existingCluster.ServiceProviderProperties.ManagedIdentitiesDataPlaneIdentityURL)
if err != nil {
return utils.TrackError(fmt.Errorf("failed to get Managed Identities Data Plane Client: %w", err))
}
Comment on lines +307 to +330
// ClientID is Client ID of the Azure User Assigned Managed Identity represented by ResourceID.
// Fetched from Azure and written here by the FetchMSIIdentitiesInfo.
// It may be be nil or empty.
ClientID *string `json:"clientId,omitempty"`
// PrincipalID Principal ID of the Azure User Assigned Managed Identity represented by ResourceID.
// Fetched from Azure and written here by the FetchMSIIdentitiesInfo.
// It may be 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. Its value comes from the Cluster's CustomerProperties.
ResourceID *azcorearm.ResourceID `json:"resourceId,omitempty"`
// ClientID is Client ID of the Azure User Assigned Managed Identity represented by ResourceID.
// Fetched from Azure and written here by the FetchMSIIdentitiesInfo.
// It may be be nil or empty.
ClientID *string `json:"clientId,omitempty"`
// PrincipalID Principal ID of the Azure User Assigned Managed Identity represented by ResourceID.
// Fetched from Azure and written here by the FetchMSIIdentitiesInfo.
// It may be be nil or empty.
PrincipalID *string `json:"principalId,omitempty"`
}

// Get the cluster from Cosmos
clusterCRUD := c.resourcesDBClient.HCPClusters(key.SubscriptionID, key.ResourceGroupName)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

dont' do this. Just used the cached value.

// PrincipalID are updated in place. Keys missing from SPC are left as-is.
func (c *clusterIdentitySyncer) updateIdentityUserAssignedIdentitiesFromSPC(
identityUserAssignedIdentities map[string]*coreapi.UserAssignedIdentity,
spcControlPlaneOperatorsIdentities map[string]*coreapi.ServiceProviderClusterControlPlaneOperatorIdentity,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

dont' abbreviate serviceProviderCluster as spc anywhere.

clientID = spcServiceManagedIdentity.ClientID
principalID = spcServiceManagedIdentity.PrincipalID
} else { // otherwise, we leave the identity as-is
continue

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this is incorrect. When we don't have data, we need to set the value to &coreapi.UserAssignedIdentity{}

var clientID, principalID *string

// If we found the identity in the SPC control plane operators identities, we use the ClientID and PrincipalID from the SPC.
if spcIdentity, ok := spcControlPlaneOperatorsIdentities[lowerResourceIDStr]; ok && spcIdentity != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

factor as a switch/case

Comment on lines +184 to +185
clientID = spcIdentity.ClientID
principalID = spcIdentity.PrincipalID

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

just inline assigned to the identityUserAssignedIdentities map

// If we found the identity in the SPC service managed identity, we use the ClientID and PrincipalID from the SPC.
} else if spcServiceManagedIdentity != nil && spcServiceManagedIdentity.ResourceID != nil &&
strings.ToLower(spcServiceManagedIdentity.ResourceID.String()) == lowerResourceIDStr {
clientID = spcServiceManagedIdentity.ClientID

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

just inline assigned to the identityUserAssignedIdentities map

return nil
}

spcCRUD := c.resourcesDBClient.ServiceProviderClusters(key.SubscriptionID, key.ResourceGroupName, key.HCPClusterName)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

use lister

controlPlaneOperatorsUnchanged := equality.Semantic.DeepEqual(replacement.Status.MSIManagedIdentities.ControlPlaneOperatorsIdentities, existingSPC.Status.MSIManagedIdentities.ControlPlaneOperatorsIdentities)
serviceManagedIdentityUnchanged := equality.Semantic.DeepEqual(replacement.Status.MSIManagedIdentities.ServiceManagedIdentity, existingSPC.Status.MSIManagedIdentities.ServiceManagedIdentity)
recheckUnchanged := equality.Semantic.DeepEqual(replacement.Status.MSIManagedIdentities.EarliestRecheckTime, existingSPC.Status.MSIManagedIdentities.EarliestRecheckTime)
if controlPlaneOperatorsUnchanged && serviceManagedIdentityUnchanged && recheckUnchanged {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

how about just doing equality.Semantic.DeepEqual(replacement, existingSPC) right here instead of partial checks.

…ased identities

refactor: move MSI identity resolution onto SPC and sync into cluster Identity

We add a controller that retrieves the Client ID and Principal ID associated
to the following identities associated to an ARO-HCP Cluster:
- The Control Plane operators identities
- The Service Managed Identity

We leverage Microsoft's Managed Identities Data Plane service to retrieve the information.
When the service is not available (outside of AME tenants) the fake managed identities
data plane client is leveraged which returns the information associated to the MI Mock Identity
for all requests/responses to it. We do not directly use Azure Go SDK's UserAssignedIdentities
client because otherwise we would return the information of clientid+principalid of the passed
identities in the payload instead of the actual clientid+principalid that ends
up being used in the management cluster.

Additionally, we replace IdentityMigration (CS-backed) with ClusterIdentitySync, which keeps
HCPOpenShiftCluster.Identity.UserAssignedIdentities ClientID/PrincipalID in
sync with ServiceProviderCluster.Status.MSIManagedIdentities. Keys are
never deleted from the
HCPOpenShiftCluster.Identity.UserAssignedIdentities map.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 18, 2026 19:17
@redhat-chai-bot
Chai-bot (redhat-chai-bot) force-pushed the msoriano-fetchinfo-controlplaneoperators-and-smi-mis branch from 4ccd0f8 to 4b6af93 Compare August 18, 2026 19:17
@redhat-chai-bot

Copy link
Copy Markdown
Collaborator Author

Addressed all review feedback in the latest force-push (4b6af93). Summary of changes:

David Eads (@deads2k)'s review:

  1. Cached value (L121): Dropped the Cosmos re-Get — SyncOnce now works from the cached cluster. Replace relies on the cached etag for optimistic concurrency (stale cache → precondition-fail + requeue).
  2. No spc abbreviation (L174): Renamed all spc/SPCserviceProviderCluster across controllers and tests (including method names like desiredMSIResourceIDsMatchServiceProviderCluster).
  3. Empty struct for missing data (L192): default case now sets &coreapi.UserAssignedIdentity{} instead of nil — added a test that asserts stale values are cleared.
  4. switch/case (L183): Refactored if/else chain to switch/case.
  5. Inline assignments (L185/L189): Each case now inline-assigns directly into the identityUserAssignedIdentities map.
  6. Use lister (L239): Cluster and ServiceProviderCluster reads now go through informer listers; DB client is kept only for the Replace call.
  7. DeepEqual (L348): Replaced partial field checks with equality.Semantic.DeepEqual(replacement, existingServiceProviderCluster).

Copilot suggestions:
8. Empty identity URL (L266): Added early-return when ManagedIdentitiesDataPlaneIdentityURL is empty (+ test).
9. Identity dedup (L393): De-duplicating control-plane operators by lowercased resource ID in collectMSIBasedIdentitiesToFetch (+ tests).
10. Doc typos (L330): Fixed "It may be be…", "PrincipalID Principal ID…", and duplicated ResourceID sentence.

All changes amend the single commit. make lint and go test ./pkg/controllers/cluster/identity/... pass locally.


AI-generated. Review for accuracy.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • internal/api/coreapi/zz_generated.deepcopy.go: Generated file
Suppressed comments (1)

docs/cosmos-data-flow.md:948

  • Documentation mismatch: ClusterIdentitySync clears ClientID/PrincipalID for identity keys that don't have a matching entry in ServiceProviderCluster.Status.MSIManagedIdentities (sets an empty UserAssignedIdentity), but the data-flow table says those keys are left unchanged. This should reflect the actual behavior so readers don't assume stale values are preserved.
| **Write** | **`HCPOpenShiftCluster`** | <ul><li>**`Identity.UserAssignedIdentities[key].ClientID` / `PrincipalID`** = from SPC when a lowercased match exists; keys absent from SPC are left unchanged</li></ul> |

@deads2k

Copy link
Copy Markdown
Collaborator

/approve
/assign Manyanda Chitimbo (@machi1990)

@redhat-chai-bot

Copy link
Copy Markdown
Collaborator Author

/retest e2e-parallel


AI-generated. Review for accuracy.

@redhat-chai-bot

Copy link
Copy Markdown
Collaborator Author

/test e2e-parallel


AI-generated. Review for accuracy.

@machi1990

Copy link
Copy Markdown
Collaborator

/lgtm

@openshift-ci openshift-ci Bot added the lgtm label Aug 20, 2026
@openshift-ci

openshift-ci Bot commented Aug 20, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: deads2k, machi1990, redhat-chai-bot

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@redhat-chai-bot

Copy link
Copy Markdown
Collaborator Author

/test e2e-parallel


AI-generated. Review for accuracy.

@openshift-merge-bot
openshift-merge-bot Bot merged commit 15fb6f1 into Azure:main Aug 20, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants