feat: add containerRegistry to ARM API for ACR pull via managed identity (ARO-24037) - #5562
Conversation
|
Skipping CI for Draft Pull Request. |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds support for configuring Azure Container Registry (ACR) authentication on HCP OpenShift clusters (2025-12-23-preview), wiring the new containerRegistry field through API schemas, internal types, conversions, and validation.
Changes:
- Introduces
containerRegistryinto the platform profile schema (TypeSpec/OpenAPI) and updates example payloads. - Extends internal API types/enums plus external<->internal conversion logic to carry the new profile across versions.
- Updates validation to allow
platform.containerRegistryupdates while keeping the rest ofplatformimmutable.
Reviewed changes
Copilot reviewed 18 out of 22 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/validation/validate_cluster.go | Allows mutability for platform.containerRegistry and adds validation for the new profile. |
| internal/api/v20251223preview/hcpopenshiftclusters_methods.go | Maps containerRegistry in model conversions and normalizes incoming payloads. |
| internal/api/v20240610preview/hcpopenshiftclusters_methods.go | Preserves unknown containerRegistry field across older API version conversions. |
| internal/api/v20240610preview/conversion_fuzz_test.go | Updates fuzz round-trip customizations for version-skewed fields, including containerRegistry. |
| internal/api/types_cluster.go | Adds internal API structs for ContainerRegistryProfile and related types. |
| internal/api/enums.go | Adds ContainerRegistryCredentialType enum and allowed set. |
| api/.../openapi.json | Defines containerRegistry schemas and embeds it into PlatformProfile (and update variants). |
| api/.../examples/*.json | Adds containerRegistry to generated example payloads for the 2025-12-23-preview API. |
| api/.../hcpCluster-models.tsp | Adds TypeSpec models for containerRegistry and supporting shapes/enums. |
Files not reviewed (1)
- internal/api/zz_generated.deepcopy.go: Language not supported
73dab29 to
2d82b53
Compare
Manyanda Chitimbo (machi1990)
left a comment
There was a problem hiding this comment.
lgtm
Not adding the command as I'd defer that to SMEs
|
/hold
|
Matthew Barnes (mbarnes)
left a comment
There was a problem hiding this comment.
Suggested simplification for consistency with other places in the API that take a user-assigned managed identity resource ID.
2d82b53 to
70d4923
Compare
70d4923 to
2bee5be
Compare
2bee5be to
0bb66e9
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 42 changed files in this pull request and generated 2 comments.
Files not reviewed (1)
- internal/api/zz_generated.deepcopy.go: Generated file
Comments suppressed due to low confidence (2)
internal/ocm/convert.go:1
- On update, this only sends
containerRegistrywhen it’s non-nil, which prevents users from clearing/removing an existing container registry config (nil becomes “no change”). If the CS API supports clearing, explicitly set the field to nil on updates whenhcpCluster.CustomerProperties.Platform.ContainerRegistryis nil; otherwise document/validate that removal isn’t supported.
internal/validation/validate_cluster.go:1 - The required-field message here doesn’t match the conventions used elsewhere in this repo’s validation tests (which often assert
"Required"). Consider using the same capitalization/wording as otherfield.Requiredusages (or centralizing the message) to keep error outputs consistent and reduce brittle test expectations.
0bb66e9 to
b2094f1
Compare
77397f5 to
1a47c94
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 52 out of 64 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
internal/validation/validate_cluster.go:654
- This comment appears to have an incorrect JSON tag reference (
json:"containerRegistry,omitempty"). The internal field isContainerRegistryPullManagedIdentitywith JSON tagcontainerRegistryPullManagedIdentity, while the external ARM shape isplatform.containerRegistry.managedIdentity. Keeping the comment accurate helps avoid confusion when maintaining validation vs. API surface mappings.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 51 out of 64 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- internal/api/zz_generated.deepcopy.go: Generated file
Suppressed comments (2)
backend/pkg/utils/validationutils/container_registry_pull_credentials_permission_validation.go:82
- The user-assigned identities ARM client is always built with the cluster’s subscription ID, but both the containerRegistry pull MI and CAPZ MI resource IDs may be in a different subscription (the PR explicitly targets cross-subscription identities). This will cause false "not found or not accessible" failures whenever either identity lives outside the cluster subscription.
Build the UserAssignedIdentitiesClient using the subscription ID from the resource being queried (and create a second client if CAPZ and the pull MI are in different subscriptions).
uaisClient, err := v.smiClientBuilder.UserAssignedIdentitiesClient(ctx, clusterIdentityURL, smiResourceID, cluster.ID.SubscriptionID)
if err != nil {
return utils.TrackError(fmt.Errorf("failed to get user assigned identities client: %w", err))
}
_, err = uaisClient.Get(ctx, containerRegistryPullMI.ResourceGroupName, containerRegistryPullMI.Name, nil)
if err != nil {
return utils.TrackError(fmt.Errorf("container registry pull managed identity %s not found or not accessible: %w", containerRegistryPullMI, err))
}
capzMI, err := uaisClient.Get(ctx, capzResourceID.ResourceGroupName, capzResourceID.Name, nil)
if err != nil {
return utils.TrackError(fmt.Errorf("failed to get CAPZ managed identity %s: %w", capzResourceID, err))
}
internal/validation/validate_cluster.go:654
- The field comment references an incorrect JSON tag (
json:"containerRegistry,omitempty"). The internal field iscontainerRegistryPullManagedIdentity, and externally it maps toplatform.containerRegistry.managedIdentity. This comment is likely to mislead future readers.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 44 out of 50 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 (3)
internal/validation/validate_cluster_test.go:962
for i := range typ.NumField()does not compile becauserangecan't iterate over anint. This test will fail to build as written.
test/e2e/cluster_container_registry.go:204- Same formatting issue as above: Gomega won't apply
%sformatting to this message, so the failure output is harder to read. Preferfmt.Sprintfor string concatenation.
test/e2e/cluster_container_registry.go:187 - These assertion messages pass printf-style format strings to Gomega, but Gomega does not format them; it will print the args separated (e.g.
"failed to create ACR %s" acrName). Usefmt.Sprintf(or build the string explicitly) so failures show the intended message.
This issue also appears on line 201 of the same file.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 44 out of 50 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- internal/api/coreapi/zz_generated.deepcopy.go: Generated file
Suppressed comments (1)
backend/pkg/utils/validationutils/container_registry_pull_credentials_permission_validation.go:76
UserAssignedIdentitiesClientis created withcluster.ID.SubscriptionID, but the container registry pull managed identity is explicitly allowed to be cross-subscription. WhencontainerRegistryPullMI.SubscriptionID != cluster.ID.SubscriptionID, the subsequentGetwill query the wrong subscription and incorrectly fail validation. Build the MSI client for the managed identity’s subscription (and reuse the cluster-subscription client for CAPZ if desired).
smiResourceID := cluster.CustomerProperties.Platform.OperatorsAuthentication.UserAssignedIdentities.ServiceManagedIdentity
clusterIdentityURL := cluster.ServiceProviderProperties.ManagedIdentitiesDataPlaneIdentityURL
uaisClient, err := v.smiClientBuilder.UserAssignedIdentitiesClient(ctx, clusterIdentityURL, smiResourceID, cluster.ID.SubscriptionID)
if err != nil {
return utils.TrackError(fmt.Errorf("failed to get user assigned identities client: %w", err))
}
_, err = uaisClient.Get(ctx, containerRegistryPullMI.ResourceGroupName, containerRegistryPullMI.Name, nil)
if err != nil {
return utils.TrackError(fmt.Errorf("container registry pull managed identity %s not found or not accessible: %w", containerRegistryPullMI, err))
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 44 out of 50 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)
test/e2e/cluster_container_registry.go:190
acrResp.Properties.LoginServeris dereferenced without checkingacrResp.Properties/LoginServerfor nil, which can panic the E2E test if the ARM response is missing these fields. Add explicit assertions before dereferencing.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 44 out of 50 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)
internal/ocm/cluster_update_dispatch_config.go:31
- The clearing sentinel contains a NUL byte ("\x00clear"). While it won't collide with real ARM resource IDs, using a control character makes the value harder to debug and can behave unexpectedly if it ever gets logged or surfaced in errors. A plain string sentinel that cannot be a real resource ID (e.g., anything not starting with "/") is sufficient here.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 44 out of 50 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)
internal/api/coreapi/types_cluster.go:242
- This adds a new Cosmos-persisted field on the cluster model (ContainerRegistryPullManagedIdentity). Per repo guidance, changes to
internal/api/types_*.gothat affect stored fields should be reflected indocs/cosmos-data-flow.md(regenerate/update it so the documented Cosmos read/write flows stay accurate).
// Written by: Frontend PUT Cluster (Create), Frontend PATCH Cluster (Update)
ContainerRegistryPullManagedIdentity *azcorearm.ResourceID `json:"containerRegistryPullManagedIdentity,omitempty"`
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 50 out of 56 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)
test/e2e/cluster_container_registry.go:192
acrResp.Properties.LoginServeris dereferenced without checking thatacrResp.PropertiesandLoginServerare non-nil. If the ARM response is missing these fields, this test will panic instead of failing with a clear assertion message.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 50 out of 56 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)
backend/pkg/controllers/cluster/validation/cluster_validation_controller.go:228
- For
InputKeyedClusterValidation,shouldProcesscurrently requires the stored conditionMessageto exactly equalInputKey(cluster). If a keyed validation ever wants to include a human-readable success message (or if existing data has extra text), this will force re-validation on every reconcile.
Consider parsing the key from the message (e.g. treat the first line as the key and ignore the rest) so keyed validations can safely append more context without breaking the gating logic.
if keyed, ok := c.validation.(validationutils.InputKeyedClusterValidation); ok {
return keyed.InputKey(cluster) != condition.Message
}
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: twolff-gh The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 51 out of 57 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)
backend/pkg/utils/validationutils/cluster_validation.go:34
- The
InputKeyedClusterValidationdoc says the controller stores the input key in the condition’sMessageon success, but the controller currently only comparesInputKey(cluster)against the existing condition’sMessage(it doesn’t overwrite it). This mismatch is easy to follow incorrectly when implementing future keyed validations, and can lead to the validation re-running every reconcile if the PassedValidation user message isn’t set to the input key.
// InputKeyedClusterValidation is an optional extension of ClusterValidation
// for validations whose input can change on day-2 updates. The validation
// controller stores the key in the condition's Message field on success and
// re-validates when the key changes.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 78 out of 84 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 (9)
api/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/hcpopenshiftclusters/preview/2026-06-30-preview/examples/HcpOpenShiftClusters_CreateOrUpdate_MaximumSet_Gen.json:151
- This example places
containerRegistryunderproperties.platform, but the OpenAPI schema definesproperties.containerRegistry(sibling ofplatform). As-is, the example JSON does not conform to the schema.
"issuerUrl": "https://oidc.contoso.com",
"containerRegistry": {
"managedIdentity": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myAcrPullIdentity"
}
api/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/hcpopenshiftclusters/preview/2026-06-30-preview/examples/HcpOpenShiftClusters_CreateOrUpdate_MaximumSet_Gen.json:272
- This example places
containerRegistryunderproperties.platform, but the OpenAPI schema definesproperties.containerRegistry(sibling ofplatform). As-is, the example JSON does not conform to the schema.
"issuerUrl": "https://oidc.contoso.com",
"containerRegistry": {
"managedIdentity": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myAcrPullIdentity"
}
test/e2e/cluster_container_registry.go:182
- Possible nil dereference:
acrResp.Properties.LoginServeris dereferenced without checkingacrResp.PropertiesandacrResp.Properties.LoginServerfor nil, which would panic and obscure the test failure cause. Add explicit nil assertions before dereferencing.
test/e2e/cluster_container_registry.go:108 - Possible nil dereference:
clusterParams.UserAssignedIdentitiesProfileis used without a nil check. If cluster params are ever constructed without callingCreateClusterCustomerResources..., this will panic rather than returning a useful error.
api/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/hcpopenshiftclusters/preview/2026-06-30-preview/examples/HcpOpenShiftClusters_Update_MaximumSet_Gen.json:68 - This example places
containerRegistryunderproperties.platform, but the OpenAPI schema definesproperties.containerRegistry(sibling ofplatform). As-is, the example JSON does not conform to the schema.
"issuerUrl": "https://oidc.contoso.com",
"containerRegistry": {
"managedIdentity": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myAcrPullIdentity"
}
api/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/hcpopenshiftclusters/preview/2026-06-30-preview/examples/HcpOpenShiftClusters_Get_MaximumSet_Gen.json:57
- This example places
containerRegistryunderproperties.platform, but the OpenAPI schema definesproperties.containerRegistry(sibling ofplatform). As-is, the example JSON does not conform to the schema.
"issuerUrl": "https://oidc.contoso.com",
"containerRegistry": {
"managedIdentity": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myAcrPullIdentity"
}
api/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/hcpopenshiftclusters/preview/2026-06-30-preview/examples/HcpOpenShiftClusters_ListBySubscription_MaximumSet_Gen.json:57
- This example places
containerRegistryunderproperties.platform, but the OpenAPI schema definesproperties.containerRegistry(sibling ofplatform). As-is, the example JSON does not conform to the schema.
"issuerUrl": "https://oidc.contoso.com",
"containerRegistry": {
"managedIdentity": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myAcrPullIdentity"
}
api/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/hcpopenshiftclusters/preview/2026-06-30-preview/examples/HcpOpenShiftClusters_ListByResourceGroup_MaximumSet_Gen.json:58
- This example places
containerRegistryunderproperties.platform, but the OpenAPI schema definesproperties.containerRegistry(sibling ofplatform). As-is, the example JSON does not conform to the schema.
"issuerUrl": "https://oidc.contoso.com",
"containerRegistry": {
"managedIdentity": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myAcrPullIdentity"
}
api/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/hcpopenshiftclusters/preview/2026-06-30-preview/examples/HcpOpenShiftClusters_CreateOrUpdate_MaximumSet_Gen.json:51
- This example places
containerRegistryunderproperties.platform, but the OpenAPI schema definesproperties.containerRegistry(sibling ofplatform). As-is, the example JSON does not conform to the schema.
This issue also appears in the following locations of the same file:
- line 148
- line 269
"containerRegistry": {
"managedIdentity": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myAcrPullIdentity"
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 76 out of 82 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 (3)
test/e2e/cluster_container_registry.go:182
acrResp.Properties.LoginServeris dereferenced without checking thatacrResp.PropertiesandLoginServerare non-nil, which can panic the test if the SDK response is partially populated. Add explicit expectations before dereferencing.
backend/pkg/utils/validationutils/cluster_validation.go:34- The
InputKeyedClusterValidationdocstring says the controller stores the key in the conditionMessagefield on success, but the controller currently just writesValidationResult.ToCondition(), which usesOutcome.Passed.UserMessageasMessage. This makes the contract easy to misunderstand and can lead to keyed validations re-running every reconcile if they don’t setUserMessageto the key.
// InputKeyedClusterValidation is an optional extension of ClusterValidation
// for validations whose input can change on day-2 updates. The validation
// controller stores the key in the condition's Message field on success and
// re-validates when the key changes.
api/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/hcpopenshiftclusters/preview/2026-06-30-preview/openapi.json:2406
- The PR description says this change adds
platform.containerRegistry.managedIdentity, but the schema (and the rest of the implementation/tests) addsproperties.containerRegistry.managedIdentity(sibling toplatform). Please reconcile the description with the actual ARM shape (or move the field if the intended location really is underplatform).
"containerRegistry": {
"$ref": "#/definitions/ContainerRegistryProfile",
"description": "Azure Container Registry configuration for authenticating image pulls\non the cluster's worker nodes.",
"x-ms-mutability": [
"read",
"update",
"create"
]
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 64 out of 68 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- internal/api/coreapi/zz_generated.deepcopy.go: Generated file
Suppressed comments (3)
api/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/hcpopenshiftclusters/hcpCluster-models.tsp:539
ContainerRegistryProfileitself is also marked as@added(Versions.v2026_09_01_preview), which prevents it (andcontainerRegistry) from being available in v2026_06_30_preview even though this PR updates v20260630preview clients/tests. Update the@addedversion to v2026_06_30_preview (and regenerate outputs).
@added(Versions.v2026_09_01_preview)
test/e2e/cluster_container_registry.go:181
- Potential nil dereference:
acrResp.Propertiesand/oracrResp.Properties.LoginServercan be nil, which would panic the test at*acrResp.Properties.LoginServer. Add assertions before dereferencing so failures are reported as test failures instead of panics.
internal/validation/validate_cluster.go:266 - Remove the stray commented-out struct-tag line. It looks like a copy/paste artifact and doesn’t add useful context; it also references
omitemptyeven though the actual tag isomitzeroon the internal type.
// ContainerRegistry ContainerRegistryProfile `json:"containerRegistry,omitempty"`
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 65 out of 69 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 (6)
internal/validation/validate_cluster.go:264
validateClusterCustomerPropertiescurrently enforces platform immutability twice: a struct-levelimmutableByReflectonplatformplus the per-field immutability checks insidevalidateCustomerPlatformProfile. This leads to duplicate immutability errors (one atplatform, one at the specific field) and makes the per-field immutability coverage test less meaningful. Rely onvalidateCustomerPlatformProfileand drop the struct-level immutability check.
// Platform CustomerPlatformProfile `json:"platform,omitempty"`
errs = append(errs, immutableByReflect(ctx, op, fldPath.Child("platform"), &newObj.Platform, safe.Field(oldObj, toCustomerPlatform))...)
errs = append(errs, validateCustomerPlatformProfile(ctx, op, fldPath.Child("platform"), &newObj.Platform, safe.Field(oldObj, toCustomerPlatform))...)
internal/azureapi/v20260630preview/conversion_fuzz_test.go:43
- The PR description says
containerRegistry.managedIdentityis added to v20260630preview, but this fuzz test explicitly says ContainerRegistry does not exist in v20260630preview and forces it to the zero value for round-tripping. Please reconcile the implementation with the PR description (either add ContainerRegistry to the v20260630preview API surface, or update the PR description/notes to reflect the actual versioning).
fuzzer := coreapitesting.FuzzerFor(
append(coreapitesting.CommonRoundTripFuzzFuncs(),
// ContainerRegistry does not exist in v20260630preview.
func(j *coreapi.HCPOpenShiftClusterCustomerProperties, c randfill.Continue) {
c.FillNoCustom(j)
j.ContainerRegistry = coreapi.ContainerRegistryProfile{}
},
internal/azureapi/v20251223preview/conversion_fuzz_test.go:45
- This comment says ContainerRegistry was added in v20260630preview, but the TypeSpec/OpenAPI changes add it in v2026_09_01_preview. The mismatch makes it harder to reason about why the fuzzer needs to zero this field for older API versions.
fuzzer := coreapitesting.FuzzerFor(append(coreapitesting.CommonRoundTripFuzzFuncs(),
// Ingress, CryptoRestrictions, and ContainerRegistry were added in v20260630preview and do not exist in v20251223preview.
func(j *coreapi.HCPOpenShiftClusterCustomerProperties, c randfill.Continue) {
c.FillNoCustom(j)
j.Ingress = coreapi.CustomerIngressProfile{}
j.CryptoRestrictions = metadataapi.CryptoRestrictionsNone
j.ContainerRegistry = coreapi.ContainerRegistryProfile{}
},
test/e2e/cluster_container_registry.go:399
- The PR description claims the E2E scenario includes a day-2 clear via
"containerRegistry": nullPATCH, but this E2E test covers create + update only and never exercises clearing the field. Either update the PR description/testing section to match what’s actually covered here, or extend this test (or add a companion one) to validate the clear behavior end-to-end.
internal/azureapi/v20240610preview/hcpopenshiftclusters_methods.go:456 - Comment has the wrong API version: ContainerRegistry is introduced in the v2026_09_01_preview API (per the TypeSpec/OpenAPI changes), not v2026_06_30_preview.
// ContainerRegistry was added in v2026_06_30_preview.
to.CustomerProperties.ContainerRegistry = from.CustomerProperties.ContainerRegistry
internal/azureapi/v20251223preview/hcpopenshiftclusters_methods.go:498
- Comment has the wrong API version: ContainerRegistry is introduced in the v2026_09_01_preview API (per the TypeSpec/OpenAPI changes), not v2026_06_30_preview. Keeping this accurate helps explain why the field is preserved for this older API version.
// ContainerRegistry was added in v2026_06_30_preview.
to.CustomerProperties.ContainerRegistry = from.CustomerProperties.ContainerRegistry
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 65 out of 69 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 (8)
internal/validation/validate_cluster_test.go:990
for i := range typ.NumField()does not compile becauserangecan't be used over anint. Use a conventional indexed loop overNumField()so this test builds.
typ := reflect.TypeOf(coreapi.CustomerPlatformProfile{})
for i := range typ.NumField() {
name := typ.Field(i).Name
api/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/hcpopenshiftclusters/hcpCluster-models.tsp:144
- The PR metadata summary states this field is added in both v2026-06-30-preview and v2026-09-01-preview, but the TypeSpec marks it as
@added(Versions.v2026_09_01_preview)only. If v2026_06_30_preview is intended to expose the field, it also needs schema/model/SDK updates for that API version; otherwise the PR description (and any internal comments referring to v2026_06_30_preview) should be updated to match the actual version boundary.
/** Azure Container Registry configuration for authenticating image pulls
* on the cluster's worker nodes. */
@added(Versions.v2026_09_01_preview)
@visibility(Lifecycle.Read, Lifecycle.Create, Lifecycle.Update)
containerRegistry?: ContainerRegistryProfile;
internal/validation/validate_cluster.go:267
validateCustomerPlatformProfilealready enforces per-field immutability forplatform. KeepingimmutableByReflect(..., &newObj.Platform, ...)here duplicates immutability checks and will also block any future platform fields intended to be day-2 mutable.
errs = append(errs, immutableByReflect(ctx, op, fldPath.Child("platform"), &newObj.Platform, safe.Field(oldObj, toCustomerPlatform))...)
errs = append(errs, validateCustomerPlatformProfile(ctx, op, fldPath.Child("platform"), &newObj.Platform, safe.Field(oldObj, toCustomerPlatform))...)
// ContainerRegistry ContainerRegistryProfile `json:"containerRegistry,omitempty"`
errs = append(errs, validateContainerRegistryPullCredentials(ctx, op, fldPath.Child("containerRegistry", "managedIdentity"), newObj.ContainerRegistry.PullManagedIdentity, safe.Field(oldObj, toCustomerPropertiesContainerRegistryPullMI), newObj.Platform.ManagedResourceGroup)...)
test/e2e/cluster_container_registry.go:176
acrResp.Properties.LoginServeris dereferenced without checking thatPropertiesandLoginServerare non-nil. A nil value here will panic the test instead of producing a readable failure.
test/e2e/cluster_container_registry.go:356- The PR description says the E2E covers day-2 clear via
nullPATCH, but this test currently stops after the day-2 update and never exercisescontainerRegistry: null/ verify the field is cleared via GET. Adding that step would ensure the documented clear semantics keep working end-to-end.
internal/azureapi/v20251223preview/hcpopenshiftclusters_methods.go:498 - This comment says
ContainerRegistrywas added inv2026_06_30_preview, but the v20260630preview conversion fuzz test explicitly treatsContainerRegistryas non-existent in that version. Update the version in the comment to avoid misleading future maintainers.
// CryptoRestrictions was added in v2026_06_30_preview
to.CustomerProperties.CryptoRestrictions = from.CustomerProperties.CryptoRestrictions
// ContainerRegistry was added in v2026_06_30_preview.
to.CustomerProperties.ContainerRegistry = from.CustomerProperties.ContainerRegistry
internal/azureapi/v20240610preview/hcpopenshiftclusters_methods.go:456
- This comment says
ContainerRegistrywas added inv2026_06_30_preview, but the v20260630preview conversion fuzz test explicitly treatsContainerRegistryas non-existent in that version. Update the version in the comment to avoid misleading future maintainers.
// CryptoRestrictions was added in v2026_06_30_preview
to.CustomerProperties.CryptoRestrictions = from.CustomerProperties.CryptoRestrictions
// ContainerRegistry was added in v2026_06_30_preview.
to.CustomerProperties.ContainerRegistry = from.CustomerProperties.ContainerRegistry
internal/azureapi/v20251223preview/conversion_fuzz_test.go:45
- The comment claims ContainerRegistry was added in
v20260630preview, but the v20260630preview conversion fuzz test and TypeSpec changes indicate it does not exist until later. Consider updating the version in this comment to match the actual API version boundary so the fuzz override rationale stays accurate.
fuzzer := coreapitesting.FuzzerFor(append(coreapitesting.CommonRoundTripFuzzFuncs(),
// Ingress, CryptoRestrictions, and ContainerRegistry were added in v20260630preview and do not exist in v20251223preview.
func(j *coreapi.HCPOpenShiftClusterCustomerProperties, c randfill.Continue) {
c.FillNoCustom(j)
j.Ingress = coreapi.CustomerIngressProfile{}
j.CryptoRestrictions = metadataapi.CryptoRestrictionsNone
j.ContainerRegistry = coreapi.ContainerRegistryProfile{}
},
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 65 out of 69 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 (6)
test/e2e/cluster_container_registry.go:391
- The second
VerifyImagePulled(...)call can succeed because it checks any pod in the namespace. Since the first pod (acr-pull-test) remains and already has a populatedImageID, the post-update verification can pass even if the new pod never pulls successfully.
To make this assertion meaningful, remove the initial pod (and wait for deletion) before creating/verifying the post-update pod, so the verifier is forced to observe the new pull attempt.
test/e2e/cluster_container_registry.go:176
acrResp.Properties.LoginServeris dereferenced without checkingacrResp.Properties/LoginServerfor nil. If the ARM response omits these fields, the test will panic before reaching the assertions.
Add explicit nil checks before dereferencing so failures surface as clear test assertions.
This issue also appears on line 387 of the same file.
internal/azureapi/v20251223preview/hcpopenshiftclusters_methods.go:498
- The comment says ContainerRegistry was added in
v2026_06_30_preview, but this PR introducescontainerRegistryin thev2026_09_01_previewARM API. This comment should reflect the correct API version to avoid confusion when future versions add/rename fields.
// CryptoRestrictions was added in v2026_06_30_preview
to.CustomerProperties.CryptoRestrictions = from.CustomerProperties.CryptoRestrictions
// ContainerRegistry was added in v2026_06_30_preview.
to.CustomerProperties.ContainerRegistry = from.CustomerProperties.ContainerRegistry
internal/azureapi/v20240610preview/hcpopenshiftclusters_methods.go:456
- The comment says ContainerRegistry was added in
v2026_06_30_preview, but this PR introducescontainerRegistryin thev2026_09_01_previewARM API. Update the comment to match the actual version where the field was introduced.
// CryptoRestrictions was added in v2026_06_30_preview
to.CustomerProperties.CryptoRestrictions = from.CustomerProperties.CryptoRestrictions
// ContainerRegistry was added in v2026_06_30_preview.
to.CustomerProperties.ContainerRegistry = from.CustomerProperties.ContainerRegistry
internal/azureapi/v20251223preview/conversion_fuzz_test.go:45
- This comment says ContainerRegistry was added in
v20260630preview, but the newcontainerRegistryfield is added inv20260901preview. Keeping this accurate matters because the fuzz test is specifically documenting fields that must be zeroed because they do not exist in this external API version.
fuzzer := coreapitesting.FuzzerFor(append(coreapitesting.CommonRoundTripFuzzFuncs(),
// Ingress, CryptoRestrictions, and ContainerRegistry were added in v20260630preview and do not exist in v20251223preview.
func(j *coreapi.HCPOpenShiftClusterCustomerProperties, c randfill.Continue) {
c.FillNoCustom(j)
j.Ingress = coreapi.CustomerIngressProfile{}
j.CryptoRestrictions = metadataapi.CryptoRestrictionsNone
j.ContainerRegistry = coreapi.ContainerRegistryProfile{}
},
backend/pkg/controllers/cluster/validation/cluster_validation_controller.go:229
shouldProcessusescondition.Messageas the stored InputKey for keyed validations. Sincecondition.Messageis also the user-facing condition message, keyed validations will end up showing opaque keys (e.g., a resource ID) instead of a meaningful success message.
Consider storing the input key in a dedicated field (e.g., a new status field alongside the condition) or encoding it in Message in a structured way (e.g., inputKey=<key>; <human message>) and parsing it here, so the condition message stays informative.
// shouldProcess returns true when the validation should run. This is the case when:
// - the condition does not exist or previously failed, OR
// - the validation implements InputKeyedClusterValidation and the input has changed
// since the last successful validation.
func (c *clusterValidationSyncer) shouldProcess(serviceProviderCluster *coreapi.ServiceProviderCluster, cluster *coreapi.HCPOpenShiftCluster) bool {
condition := meta.FindStatusCondition(serviceProviderCluster.Status.Validations, c.validation.Name())
if condition == nil || condition.Status != metav1.ConditionTrue {
return true
}
if keyed, ok := c.validation.(validationutils.InputKeyedClusterValidation); ok {
return keyed.InputKey(cluster) != condition.Message
}
return false
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 65 out of 69 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 (4)
backend/pkg/utils/validationutils/cluster_validation.go:34
- The
InputKeyedClusterValidationdoc says the controller stores the key in the conditionMessageon success, but the controller actually just comparescondition.MessagetoInputKey(...)and relies on the validation’sPassedValidation(UserMessage=...)to populate the message. This mismatch is easy to trip over when adding future keyed validations (they may keep a human-readable success message and unintentionally cause perpetual re-validation). Either update the doc to reflect the current contract, or change the controller to explicitly store the key when the outcome is Passed.
// InputKeyedClusterValidation is an optional extension of ClusterValidation
// for validations whose input can change on day-2 updates. The validation
// controller stores the key in the condition's Message field on success and
// re-validates when the key changes.
internal/azureapi/v20251223preview/hcpopenshiftclusters_methods.go:499
- Comment says
ContainerRegistrywas added inv2026_06_30_preview, but this PR introduces it inv2026_09_01_preview. Since this function documents cross-version field preservation, the version marker should match the actual API introduction to avoid confusing future maintainers.
// Ingress was added in v2026_06_30_preview.
to.CustomerProperties.Ingress = from.CustomerProperties.Ingress
// CryptoRestrictions was added in v2026_06_30_preview
to.CustomerProperties.CryptoRestrictions = from.CustomerProperties.CryptoRestrictions
// ContainerRegistry was added in v2026_06_30_preview.
to.CustomerProperties.ContainerRegistry = from.CustomerProperties.ContainerRegistry
}
internal/api/coreapi/types_cluster.go:93
- A new Cosmos-persisted customer property (
CustomerProperties.ContainerRegistry) is introduced here, butdocs/cosmos-data-flow.mdis not updated (no references tocontainerRegistry/pullManagedIdentityfound). Per repo guidance, changes tointernal/api/types_*.gothat affect stored Cosmos fields should update that document so the data-flow inventory stays accurate.
// Written by: Frontend PUT/PATCH Cluster
Ingress CustomerIngressProfile `json:"ingress,omitempty"`
// Written by: Frontend PUT/PATCH Cluster
Platform CustomerPlatformProfile `json:"platform,omitempty"`
// Written by: Frontend PUT/PATCH Cluster
ContainerRegistry ContainerRegistryProfile `json:"containerRegistry,omitzero"`
// Written by: Frontend PUT/PATCH Cluster
Autoscaling ClusterAutoscalingProfile `json:"autoscaling,omitempty"`
internal/azureapi/v20240610preview/hcpopenshiftclusters_methods.go:456
- Comment says
ContainerRegistrywas added inv2026_06_30_preview, but this PR adds it inv2026_09_01_preview. Updating the comment avoids implying the field exists in older API versions.
// CryptoRestrictions was added in v2026_06_30_preview
to.CustomerProperties.CryptoRestrictions = from.CustomerProperties.CryptoRestrictions
// ContainerRegistry was added in v2026_06_30_preview.
to.CustomerProperties.ContainerRegistry = from.CustomerProperties.ContainerRegistry
|
/test e2e-parallel |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 67 out of 71 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- internal/api/coreapi/zz_generated.deepcopy.go: Generated file
Suppressed comments (7)
test/e2e/cluster_container_registry.go:384
- Redundant assertion: this repeats the exact same nil check twice.
internal/azureapi/v20251223preview/hcpopenshiftclusters_methods.go:498 - The version annotation comment is incorrect: ContainerRegistry was added in v2026_09_01_preview, not v2026_06_30_preview.
// Ingress was added in v2026_06_30_preview.
to.CustomerProperties.Ingress = from.CustomerProperties.Ingress
// CryptoRestrictions was added in v2026_06_30_preview
to.CustomerProperties.CryptoRestrictions = from.CustomerProperties.CryptoRestrictions
// ContainerRegistry was added in v2026_06_30_preview.
to.CustomerProperties.Platform.ContainerRegistry = from.CustomerProperties.Platform.ContainerRegistry
internal/azureapi/v20251223preview/conversion_fuzz_test.go:45
- This comment misstates when ContainerRegistry was introduced. Ingress/CryptoRestrictions were added in v20260630preview, but ContainerRegistry was added in v20260901preview.
fuzzer := coreapitesting.FuzzerFor(append(coreapitesting.CommonRoundTripFuzzFuncs(),
// Ingress, CryptoRestrictions, and ContainerRegistry were added in v20260630preview and do not exist in v20251223preview.
func(j *coreapi.HCPOpenShiftClusterCustomerProperties, c randfill.Continue) {
c.FillNoCustom(j)
j.Ingress = coreapi.CustomerIngressProfile{}
j.CryptoRestrictions = metadataapi.CryptoRestrictionsNone
j.Platform.ContainerRegistry = coreapi.ContainerRegistryProfile{}
},
internal/api/coreapi/types_cluster.go:233
- The CustomerPlatformProfile visibility comment is now inaccurate: platform.containerRegistry is day-2 mutable (PATCH/update), so the platform struct is no longer entirely "read create".
// CustomerPlatformProfile represents the Azure platform configuration.
// Visibility for the entire struct is "read create".
test/e2e/cluster_container_registry.go:254
- Redundant assertion: this repeats the exact same nil check twice.
This issue also appears on line 383 of the same file.
internal/azureapi/v20240610preview/hcpopenshiftclusters_methods.go:455
- The version annotation comment is incorrect: ContainerRegistry was added in v2026_09_01_preview (see the TypeSpec/OpenAPI), not v2026_06_30_preview.
// ContainerRegistry was added in v2026_06_30_preview.
internal/azureapi/v20240610preview/conversion_fuzz_test.go:45
- This fuzz override now also clears Platform.ContainerRegistry (line 45), but the header comment for this override doesn't mention that ContainerRegistry doesn't exist in v20240610preview.
j.Platform.ContainerRegistry = coreapi.ContainerRegistryProfile{}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 68 out of 73 changed files in this pull request and generated no new comments.
Files not reviewed (2)
- backend/pkg/azure/client/mock_user_assigned_identities_client.go: Generated file
- internal/api/coreapi/zz_generated.deepcopy.go: Generated file
Suppressed comments (3)
backend/pkg/utils/validationutils/container_registry_pull_credentials_permission_validation.go:173
- PassedValidation currently uses v.InputKey(cluster) as the user-facing message, so the condition Message becomes the (key) string instead of an explanatory success message. If the controller is using Message for staleness detection, one option is to make InputKey itself be the stable success message and use that message for both UserMessage and InternalMessage here.
return PassedValidation(
coreapi.ControllerConditionReasonAsExpected,
v.InputKey(cluster),
fmt.Sprintf("CAPZ identity has assign/action permission on container registry pull MI %s.", containerRegistryPullMI),
)
internal/api/coreapi/types_cluster.go:233
- The comment above CustomerPlatformProfile says the entire struct is "read create", but this PR introduces platform.containerRegistry as updateable/day-2 mutable. Update the comment to avoid misleading future readers about platform mutability.
// Visibility for the entire struct is "read create".
backend/pkg/utils/validationutils/container_registry_pull_credentials_permission_validation.go:57
- InputKey is currently just the managed identity resource ID, which then gets surfaced as the condition Message on success (via PassedValidation). This makes the user-facing status condition message non-descriptive. Consider making InputKey return a stable, human-readable message (still keyed by the MI) so the stored condition remains actionable while still allowing day-2 change detection.
This issue also appears on line 169 of the same file.
mi := cluster.CustomerProperties.Platform.ContainerRegistry.PullManagedIdentity
if mi == nil {
return ""
}
return mi.String()
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 68 out of 73 changed files in this pull request and generated no new comments.
Files not reviewed (2)
- backend/pkg/azure/client/mock_user_assigned_identities_client.go: Generated file
- internal/api/coreapi/zz_generated.deepcopy.go: Generated file
Suppressed comments (3)
test/e2e/cluster_container_registry.go:163
- ACR registry names must be lowercase and between 5–50 alphanumeric characters.
acrNameis derived from the resource group name without lowercasing or enforcing the minimum length, which can causeBeginCreateto fail for some generated RG names.
test/e2e/cluster_container_registry.go:176 acrResp.PropertiesandacrResp.Properties.LoginServerare dereferenced without nil checks. If the ARM response ever omits these fields, this test will panic instead of producing a clear failure.
internal/api/coreapi/types_cluster.go:234- The comment says the entire
CustomerPlatformProfileis "read create", but this PR introduces a day-2 mutable field (containerRegistry). Update the comment so it doesn't mislead future changes/reviewers about mutability expectations.
// CustomerPlatformProfile represents the Azure platform configuration.
// Visibility for the entire struct is "read create".
type CustomerPlatformProfile struct {
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 69 out of 74 changed files in this pull request and generated no new comments.
Files not reviewed (2)
- backend/pkg/azure/client/mock_user_assigned_identities_client.go: Generated file
- internal/api/coreapi/zz_generated.deepcopy.go: Generated file
Suppressed comments (4)
internal/api/coreapi/types_cluster.go:234
- The struct-level comment says CustomerPlatformProfile is entirely "read create", but this PR introduces a day-2 mutable field (containerRegistry). This comment is now inaccurate and can mislead future API/validation work.
// CustomerPlatformProfile represents the Azure platform configuration.
// Visibility for the entire struct is "read create".
type CustomerPlatformProfile struct {
internal/api/coreapi/types_cluster.go:249
- Adding ContainerRegistry to CustomerPlatformProfile introduces a Cosmos-stored field. Per the repo's Cosmos data flow documentation rules, docs/cosmos-data-flow.md should be regenerated/updated when internal/api/types_*.go changes impact persisted fields.
OperatorsAuthentication OperatorsAuthenticationProfile `json:"operatorsAuthentication,omitempty"`
// Written by: Frontend PUT/PATCH Cluster
ContainerRegistry ContainerRegistryProfile `json:"containerRegistry,omitzero"`
}
// ContainerRegistryProfile represents Azure Container Registry pull
// configuration for the cluster.
type ContainerRegistryProfile struct {
// Written by: Frontend PUT Cluster (Create), Frontend PATCH Cluster (Update)
PullManagedIdentity *azcorearm.ResourceID `json:"pullManagedIdentity,omitempty"`
backend/pkg/utils/validationutils/cluster_validation.go:34
- The comment for InputKeyedClusterValidation says the controller stores the key in the condition Message field on success, but the controller always sets Condition.Message from ValidationResult.Outcome.*.UserMessage (via ToCondition). For keyed validations to work, PassedValidation must set UserMessage to the InputKey; consider clarifying this contract in the comment to avoid future keyed validations accidentally causing perpetual re-validation.
// InputKeyedClusterValidation is an optional extension of ClusterValidation
// for validations whose input can change on day-2 updates. The validation
// controller stores the key in the condition's Message field on success and
// re-validates when the key changes.
test/e2e/cluster_container_registry.go:176
- acrResp.Properties.LoginServer is dereferenced without checking that Properties/LoginServer are non-nil. If the Azure SDK returns a response missing these pointers (transiently or due to a partial failure), the test will panic instead of failing with a clear assertion.
…ity (ARO-24037) Add properties.platform.containerRegistry.managedIdentity to the v20260901preview ARM API, allowing customers to configure a user-assigned managed identity for worker node ACR image pulls — no imagePullSecrets, no per-pod setup. ARM API: - ContainerRegistryProfile on PlatformProfile (v20260901preview). Day-2 mutable via PATCH: set, change, or clear. - TypeSpec gated to v2026_09_01_preview. Cross-version preservation for older API versions. Validation and admission: - OCP >= 4.22 version gate (CAPZ credential provider support). - Resource type check, managed resource group rejection. - Per-field platform immutability: ContainerRegistry is mutable; other platform fields retain their individual immutability checks. - Immutability enforcement test verifies every classified platform field produces an error when mutated. Backend: - ContainerRegistryPullCredentialsPermissionValidation checks CAPZ has assign/action on the MI via CheckAccess V2. - InputKeyedClusterValidation re-validates when MI changes on day-2. - Dispatch config wiring for day-2 updates and clearing. - Operation state convergence tracking. Testing: - E2E (v20260901preview): create cluster with MI, pull from private ACR, day-2 update to different MI, verify pull with new MI. - Integration: PATCH to add containerRegistry, PATCH null to clear. - Unit: validation, admission, backend permission validation, OCM conversion, dispatch config, immutability enforcement. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| managedIdentityOperatorRoleID = "f1a07417-d97a-45cb-824c-7a7467783830" | ||
| acrPullRoleID = "7f951dda-4ed3-4680-a7ca-43fe172d538d" |
There was a problem hiding this comment.
Just mentioning: The docs team needs to capture these and the entire flow so that users will know what to do in regards to role assignment
There was a problem hiding this comment.
There should already be instructions for it, but Ill confirm 🙏
There was a problem hiding this comment.
Why this change?
There was a problem hiding this comment.
Needs to be cleaned up. I had the PR originally for the previous preview release, but that has already been done. Ill clean this one up
| return errs | ||
| } | ||
|
|
||
| func validateContainerRegistryPullCredentials(ctx context.Context, op operation.Operation, fldPath *field.Path, newObj *azcorearm.ResourceID, oldObj *azcorearm.ResourceID, managedResourceGroup string) field.ErrorList { |
There was a problem hiding this comment.
Is this missing SameSubscription check?
There was a problem hiding this comment.
After our discussions, I thought we had landed on no check for the same sub due to that since it is an azure resource, it could be in any sub in the same tenant.
But if we want to just constrain it anyway, I can add that.
| } | ||
|
|
||
| var clusterVersion semver.Version | ||
| switch op.Type { |
There was a problem hiding this comment.
does the operation type matter? can this be restructure in the way that it's agnostic to that?
There was a problem hiding this comment.
Yes, I can do that 👍
|
/test images |
Summary
Add
properties.containerRegistry.managedIdentityto the v20260901preview ARM API, allowing customers to configure a user-assigned managed identity for worker node ACR image pulls.ARO-24037
What changes
ContainerRegistryProfileonHcpOpenShiftClusterProperties(v20260901preview). Day-2 mutable via PATCH. Set, change, or clear (null).ContainerRegistryPullCredentialsPermissionValidation— checks CAPZ has assign/action on the MI. Re-validates on day-2 MI changes viaInputKeyedClusterValidation.clusterUpdateDispatchConfigfor day-2 updates and clearing.Testing
Companion changes