Skip to content

OCPBUGS-86690: fix Azure cluster deletion hanging when resource groups are already deleted - #8682

Open
vsolanki12 wants to merge 1 commit into
openshift:mainfrom
vsolanki12:OCPBUGS-86690-azure-orphaned-machine-deletion
Open

OCPBUGS-86690: fix Azure cluster deletion hanging when resource groups are already deleted#8682
vsolanki12 wants to merge 1 commit into
openshift:mainfrom
vsolanki12:OCPBUGS-86690-azure-orphaned-machine-deletion

Conversation

@vsolanki12

@vsolanki12 vsolanki12 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

What this PR does / why we need it:

When an Azure self-managed HCP cluster's resource groups are deleted out-of-band (e.g. via Azure portal or expired credentials), hypershift destroy cluster azure hangs indefinitely. AzureMachines get stuck in deletion because CAPZ cannot communicate with the deleted Azure infrastructure, and their finalizers are never cleared.

This PR uses the same DeletionFailed condition approach as #8296 (ARO HCP) but extends coverage to self-managed Azure clusters. PR #8296 guards with ManagedIdentities == nil early return, so self-managed clusters (which don't use managed identities) are not covered.

How it works:

  1. Machine-level signal: When CAPZ cannot delete an Azure VM (expired credentials, deleted resource group, etc.), it sets Ready=False with Reason=DeletionFailed on the AzureMachine.

  2. Orphaned machine cleanup (DeleteOrphanedMachines): For each AzureMachine that has been deleting for >10 minutes (deletionFailedThreshold) AND has the DeletionFailed condition, the MachineFinalizer is removed — unblocking cluster teardown.

  3. CLI fix (cmd/cluster/azure/destroy.go): Handles 404 on resource group during hypershift destroy cluster azure so it continues instead of failing hard when infrastructure is already gone.

Difference from #8296:

PR #8296 (merged) This PR
Scope ARO HCP only (ManagedIdentities != nil) Self-managed Azure clusters
Signal Same: machine-level DeletionFailed condition Same
Threshold Same: 10 minutes Same
Guard ManagedIdentities == nil early return No guard — applies to all Azure clusters

The DeletionFailed condition is set by CAPZ regardless of credential type, so this approach works for both managed identity and service principal clusters.

Which issue(s) this PR fixes:

Fixes OCPBUGS-86690

Special notes for your reviewer:

  • Previous approach (credential-level ValidAzureIdentityProvider condition) has been fully removed per feedback from @enxebre. All CPO health check code, condition bubbling, and credential status types are gone.
  • This now uses the exact same pattern as OCPBUGS-63720: orphan machines when managed identity is invalid on clus… #8296 (deletionFailedThreshold + hasDeletionFailedCondition) without the ManagedIdentities guard.
  • The cmd/cluster/azure/destroy.go 404 handling fix is independent and still needed.

Checklist:

  • Subject and description added to both, commit and PR.
  • Relevant issues have been referenced.
  • This change includes unit tests.

@openshift-merge-bot

Copy link
Copy Markdown
Contributor

Pipeline controller notification
This repo is configured to use the pipeline controller. Second-stage tests will be triggered either automatically or after lgtm label is added, depending on the repository configuration. The pipeline controller will automatically detect which contexts are required and will utilize /test Prow commands to trigger the second stage.

For optional jobs, comment /test ? to see a list of all defined jobs. To trigger manually all jobs from second stage use /pipeline required command.

This repository is configured in: LGTM mode

@openshift-ci

openshift-ci Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jun 5, 2026
@openshift-ci-robot openshift-ci-robot added jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. jira/valid-bug Indicates that a referenced Jira bug is valid for the branch this PR is targeting. labels Jun 5, 2026
@openshift-ci-robot

Copy link
Copy Markdown

@vsolanki12: This pull request references Jira Issue OCPBUGS-86690, which is valid. The bug has been moved to the POST state.

3 validation(s) were run on this bug
  • bug is open, matching expected state (open)
  • bug target version (5.0.0) matches configured target version for branch (5.0.0)
  • bug is in the state ASSIGNED, which is one of the valid states (NEW, ASSIGNED, POST)

The bug has been updated to refer to the pull request using the external bug tracker.

Details

In response to this:

What this PR does / why we need it:

When Azure resource groups are deleted before the HostedCluster (e.g. manually or by another process), cluster deletion hangs indefinitely. Two issues cause this:

  1. CAPI AzureMachine finalizers block deletion cascade: The Azure CAPI provider controller cannot remove finalizers because the backing Azure resources no longer exist. This causes the CAPI Cluster object (and thus the HostedCluster) to wait forever for machine cleanup.

  2. CLI destroy command hard-fails on missing resource group: The hypershift destroy cluster azure command validates the resource group exists before proceeding. When the RG is already deleted, this pre-validation returns a hard error, blocking the entire destroy flow.

Fix

  • Operator: Implement the OrphanDeleter interface for Azure. During HC deletion, detect AzureMachines stuck in deletion for longer than 5 minutes and remove only the Azure-specific finalizer (azuremachine.infrastructure.cluster.x-k8s.io) using controllerutil.RemoveFinalizer, preserving any finalizers from other controllers.

  • CLI: Change the resource group pre-validation to detect 404 responses, log a warning, and continue with deletion instead of returning a hard error. Non-404 errors (e.g. auth failures) still fail.

Which issue(s) this PR fixes:

Fixes https://issues.redhat.com/browse/OCPBUGS-86690

Special notes for your reviewer:

  • The Azure implementation follows the same pattern as the existing AWS DeleteOrphanedMachines (aws.go:380-402), but uses a time-based heuristic (5 min stuck in deletion) instead of credential status checking, since Azure doesn't expose an equivalent credential validity signal.
  • Unlike the AWS implementation which clears all finalizers (Finalizers = []string{}), this implementation only removes the Azure-specific finalizer using controllerutil.RemoveFinalizer to avoid interfering with other controllers. This improvement was identified during CodeRabbit review.
  • Tested on a live Azure HCP cluster (4.22.0) with simulated stuck AzureMachines. Operator logs confirm both machines had their Azure finalizers removed and the deletion cascade proceeded without hanging.

Checklist:

  • Subject and description added to both, commit and PR.
  • Relevant issues have been referenced.
  • This change includes docs.
  • This change includes unit tests.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds Azure identity-provider validation to the control-plane healthcheck path, passes Azure credentials into that check, and propagates the resulting condition to HostedCluster status. It also adds Azure orphaned-machine cleanup logic in the hostedcluster platform code, a new HostedCluster condition type, and a destroy-path change that continues when an explicitly provided Azure resource group returns 404.

Sequence Diagram(s)

sequenceDiagram
  participant Main as control-plane-operator/main.go
  participant HCU as HealthCheckUpdater
  participant HC as azureHealthCheckIdentityProvider
  participant AzureUtil as azureutil.GetResourceGroupInfo
  participant HCP as HostedControlPlane
  Main->>Main: resolveAzureHealthCheckCredentials
  Main->>HCU: set AzureCreds
  HCU->>HC: run Azure identity check
  HC->>HCP: read platform and status
  HC->>AzureUtil: validate Azure resource group info
  AzureUtil-->>HC: success or Azure error
  HC-->>HCU: update ValidAzureIdentityProvider
Loading

Suggested reviewers

  • muraee
  • jparrill
  • sjenning
🚥 Pre-merge checks | ✅ 11
✅ Passed checks (11 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main fix: Azure cluster deletion no longer hangs when the resource group was deleted already.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Stable And Deterministic Test Names ✅ Passed No dynamic Ginkgo titles were added; the new tests use static t.Run names and no generated values, timestamps, or identifiers.
Test Structure And Quality ✅ Passed The new tests are plain table-driven unit tests, not Ginkgo specs; they use no cluster resources, no waits, and include clear failure messages where needed.
Topology-Aware Scheduling Compatibility ✅ Passed The PR only adds Azure health checks, condition bubbling, destroy 404 handling, and orphaned-machine cleanup; no affinity, selectors, replicas, or topology assumptions were introduced.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed PASS: The PR adds only Go unit tests (Test*), and searches found no Ginkgo It/Describe/Context/When or IPv4/external-network markers in the touched files.
No-Weak-Crypto ✅ Passed Touched hunks only add Azure identity checks and condition bubbling; searches found no MD5/SHA1/DES/RC4/3DES/Blowfish/ECB or custom crypto in the PR changes.
Container-Privileges ✅ Passed Changed files are Go-only and none add privileged/hostNetwork/hostPID/hostIPC/allowPrivilegeEscalation/SYS_ADMIN settings; hits found were pre-existing unrelated manifests.
No-Sensitive-Data-In-Logs ✅ Passed No secrets, tokens, PII, or hostnames are logged; only operational identifiers like RG/machine names appear.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@openshift-ci openshift-ci Bot added area/cli Indicates the PR includes changes for CLI area/hypershift-operator Indicates the PR includes changes for the hypershift operator and API - outside an OCP release area/platform/azure PR/issue for Azure (AzurePlatform) platform and removed do-not-merge/needs-area labels Jun 5, 2026
@openshift-ci-robot

Copy link
Copy Markdown

@vsolanki12: This pull request references Jira Issue OCPBUGS-86690, which is valid.

3 validation(s) were run on this bug
  • bug is open, matching expected state (open)
  • bug target version (5.0.0) matches configured target version for branch (5.0.0)
  • bug is in the state POST, which is one of the valid states (NEW, ASSIGNED, POST)
Details

In response to this:

What this PR does / why we need it:

When Azure resource groups are deleted before the HostedCluster (e.g. manually or by another process), cluster deletion hangs indefinitely. Two issues cause this:

  1. CAPI AzureMachine finalizers block deletion cascade: The Azure CAPI provider controller cannot remove finalizers because the backing Azure resources no longer exist. This causes the CAPI Cluster object (and thus the HostedCluster) to wait forever for machine cleanup.

  2. CLI destroy command hard-fails on missing resource group: The hypershift destroy cluster azure command validates the resource group exists before proceeding. When the RG is already deleted, this pre-validation returns a hard error, blocking the entire destroy flow.

Fix

  • Operator: Implement the OrphanDeleter interface for Azure. During HC deletion, detect AzureMachines stuck in deletion for longer than 5 minutes and remove only the Azure-specific finalizer (azuremachine.infrastructure.cluster.x-k8s.io) using controllerutil.RemoveFinalizer, preserving any finalizers from other controllers.

  • CLI: Change the resource group pre-validation to detect 404 responses, log a warning, and continue with deletion instead of returning a hard error. Non-404 errors (e.g. auth failures) still fail.

Which issue(s) this PR fixes:

Fixes https://issues.redhat.com/browse/OCPBUGS-86690

Special notes for your reviewer:

  • The Azure implementation follows the same pattern as the existing AWS DeleteOrphanedMachines (aws.go:380-402), but uses a time-based heuristic (5 min stuck in deletion) instead of credential status checking, since Azure doesn't expose an equivalent credential validity signal.
  • Unlike the AWS implementation which clears all finalizers (Finalizers = []string{}), this implementation only removes the Azure-specific finalizer using controllerutil.RemoveFinalizer to avoid interfering with other controllers. This improvement was identified during CodeRabbit review.
  • Tested on a live Azure HCP cluster (4.22.0) with simulated stuck AzureMachines. Operator logs confirm both machines had their Azure finalizers removed and the deletion cascade proceeded without hanging.

Checklist:

  • Subject and description added to both, commit and PR.
  • Relevant issues have been referenced.
  • This change includes docs.
  • This change includes unit tests.

Summary by CodeRabbit

  • New Features

  • Added automatic cleanup of orphaned Azure machines stuck in deleting state for extended periods.

  • Bug Fixes

  • Improved Azure cluster destruction to gracefully handle missing resource groups.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 14.28571% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 45.74%. Comparing base (804f82a) to head (ae634cf).

Files with missing lines Patch % Lines
cmd/cluster/azure/destroy.go 0.00% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8682      +/-   ##
==========================================
- Coverage   45.74%   45.74%   -0.01%     
==========================================
  Files         781      781              
  Lines       97858    97858              
==========================================
- Hits        44765    44761       -4     
- Misses      50024    50028       +4     
  Partials     3069     3069              
Files with missing lines Coverage Δ
...ers/hostedcluster/internal/platform/azure/azure.go 51.45% <100.00%> (-0.47%) ⬇️
...ollers/hostedcluster/internal/platform/platform.go 0.00% <ø> (ø)
cmd/cluster/azure/destroy.go 17.18% <0.00%> (-0.56%) ⬇️
Flag Coverage Δ
cmd-support 38.83% <0.00%> (-0.01%) ⬇️
cpo-hostedcontrolplane 47.98% <ø> (ø)
cpo-other 46.02% <ø> (ø)
hypershift-operator 56.99% <100.00%> (-0.01%) ⬇️
other 34.38% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@vsolanki12
vsolanki12 force-pushed the OCPBUGS-86690-azure-orphaned-machine-deletion branch from 79425af to ba7737f Compare June 5, 2026 11:47
@vsolanki12

Copy link
Copy Markdown
Contributor Author

I have tried to reproduce and tested the fix in my test cluster as below:

Environment: Azure HCP cluster vsolanki-86690, CP version 4.22.0

Custom hypershift-operator image deployed:

    $ oc get pods -n hypershift -l name=operator \
        -o jsonpath='{range .items[*]}{.metadata.name}: {.spec.containers[0].image}{"\n"}{end}'
    operator-5c78c99-b5m4v: quay.io/vsolanki/hypershift-operator:fix-86690
    operator-5c78c99-srdl6: quay.io/vsolanki/hypershift-operator:fix-86690

Test scenario: Created two AzureMachines with azuremachine.infrastructure.cluster.x-k8s.io finalizer in the control plane namespace, then deleted them. Without a working Azure CAPI provider controller, they remain stuck in deletion indefinitely simulating the exact bug scenario where resource groups are already deleted.

Stuck AzureMachines (before HC deletion):

    $ oc get azuremachines -n clusters-vsolanki-86690 \
        -o jsonpath='{range .items[*]}{.metadata.name}: deletionTimestamp={.metadata.deletionTimestamp}, finalizers={.metadata.finalizers}{"\n"}{end}'
    test-azure-machine-1: deletionTimestamp=2026-06-05T10:39:26Z, finalizers=["azuremachine.infrastructure.cluster.x-k8s.io"]
    test-azure-machine-2: deletionTimestamp=2026-06-05T10:39:26Z, finalizers=["azuremachine.infrastructure.cluster.x-k8s.io"]

After fix triggered HC deletion, AzureMachines cleaned up:

    $ oc delete hc vsolanki-86690 -n clusters
    hostedcluster.hypershift.openshift.io "vsolanki-86690" deleted
    
    $ oc get azuremachines -n clusters-vsolanki-86690
    No resources found in clusters-vsolanki-86690 namespace.

Operator logs confirm finalizer removal:

    {"level":"info","ts":"2026-06-05T11:16:38Z",
     "msg":"Removed finalizers from orphaned AzureMachine stuck in deletion",
     "controller":"hostedcluster",
     "machine":{"name":"test-azure-machine-1","namespace":"clusters-vsolanki-86690"},
     "deletionTimestamp":"2026-06-05T10:39:26Z"}

    {"level":"info","ts":"2026-06-05T11:16:38Z",
     "msg":"Removed finalizers from orphaned AzureMachine stuck in deletion",
     "controller":"hostedcluster",
     "machine":{"name":"test-azure-machine-2","namespace":"clusters-vsolanki-86690"},
     "deletionTimestamp":"2026-06-05T10:39:26Z"}

Both AzureMachines had their finalizers removed (stuck for 37 minutes, well past the 5-minute threshold). HC deletion cascade proceeded without hanging.

@vsolanki12
vsolanki12 marked this pull request as ready for review June 5, 2026 12:00
@openshift-ci openshift-ci Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jun 5, 2026
@openshift-ci
openshift-ci Bot requested review from Nirshal and enxebre June 5, 2026 12:01
const orphanedMachineDeletionThreshold = 5 * time.Minute

func (Azure) DeleteOrphanedMachines(ctx context.Context, c client.Client, hc *hyperv1.HostedCluster, controlPlaneNamespace string) error {
azureMachineList := capiazure.AzureMachineList{}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems we are missing a semantic for orphan machines, similar to what we have in aws via GetCredentialStatus. That would be the criteria to let deletion move forward

@vsolanki12 vsolanki12 Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks you for guidance @enxebre, as discussed, we need to implement AWS like conditions (ValidOIDCConfiguration and ValidAWSIdentityProvider) for Azure as well, so GetCredentialStatus can read those instead of needing a timeout.

This would touch API, CPO, and HO. Should I expand this PR or close it and open a fresh one with the broader scope or Do I need another JIRA with broader change and later touch this PR?

@vsolanki12
vsolanki12 force-pushed the OCPBUGS-86690-azure-orphaned-machine-deletion branch from ba7737f to e677429 Compare July 2, 2026 15:09
@openshift-ci openshift-ci Bot added area/api Indicates the PR includes changes for the API area/control-plane-operator Indicates the PR includes changes for the control plane operator - in an OCP release area/documentation Indicates the PR includes changes for documentation labels Jul 2, 2026
@vsolanki12 vsolanki12 changed the title OCPBUGS-86690: Fix Azure cluster deletion hang when resource groups are already deleted OCPBUGS-86690: fix Azure cluster deletion hanging when resource groups are already deleted Jul 2, 2026
@openshift-ci-robot

Copy link
Copy Markdown

@vsolanki12: This pull request references Jira Issue OCPBUGS-86690, which is valid.

3 validation(s) were run on this bug
  • bug is open, matching expected state (open)
  • bug target version (5.0.0) matches configured target version for branch (5.0.0)
  • bug is in the state POST, which is one of the valid states (NEW, ASSIGNED, POST)

The bug has been updated to refer to the pull request using the external bug tracker.

Details

In response to this:

What this PR does / why we need it:

When an Azure self-managed HCP cluster's resource groups are deleted out-of-band (e.g. via Azure portal or expired credentials), hypershift destroy cluster azure hangs indefinitely. AzureMachines get stuck in deletion because CAPI cannot communicate with the deleted Azure infrastructure, and their finalizers are never cleared.

This PR adds a ValidAzureIdentityProvider condition that mirrors the existing AWS ValidAWSIdentityProvider pattern, providing a semantic credential validity signal instead of the previously rejected time-based heuristic.

How it works:

  1. CPO health check (control-plane-operator/controllers/healthcheck/azure.go): Validates Azure credentials by making a benign Resource Group GET call via azureutil.GetResourceGroupInfo(). Detects:
  • Auth failures (401/403) → condition False
  • Deleted resource groups (404) → condition False
  • Valid credentials + existing RG → condition True
  • No credentials available → condition Unknown
  1. Condition bubbling: ValidAzureIdentityProvider is set on HCP by the CPO health check, then bubbled to HC status by the HO (same pattern as AWS).

  2. Orphaned machine cleanup (DeleteOrphanedMachines): Reads GetCredentialStatus(hc) from the HC condition. When credentials are Invalid or Unknown, clears finalizers on AzureMachines stuck in deletion — unblocking cluster teardown immediately.

  3. CLI fix (cmd/cluster/azure/destroy.go): Handles 404 on resource group during hypershift destroy cluster azure so it continues instead of failing hard when infrastructure is already gone.

Credential resolution:

  • Self-managed: azidentity.NewDefaultAzureCredential() (picks up workload identity env vars)
  • ARO HCP: dataplane.NewUserAssignedIdentityCredential() (managed identity via CSI mount)

Which issue(s) this PR fixes:

Fixes OCPBUGS-86690

Special notes for your reviewer:

  • This replaces the V1 time-based heuristic that was rejected in review. The approach now mirrors ValidAWSIdentityProvider end-to-end as requested by @enxebre and @alberto.
  • Tested end-to-end on a live Azure self-managed cluster (uaenorth). After deleting the resource group out-of-band and triggering HC deletion, the CPO detected AuthorizationFailed, set ValidAzureIdentityProvider: False, and DeleteOrphanedMachines cleared finalizers on AzureMachines immediately.
  • HO logs confirmed: "skipping deletion of AzureMachine because of invalid Azure identity provider".

Checklist:

  • Subject and description added to both, commit and PR.
  • Relevant issues have been referenced.
  • This change includes docs.
  • This change includes unit tests.

Summary by CodeRabbit

  • New Features

  • Added an Azure identity provider validation health check for Hosted Control Planes, reporting a new ValidAzureIdentityProvider condition.

  • Health checks now resolve Azure credentials when running on Azure platforms to enable identity validation.

  • Bug Fixes

  • Azure cluster destruction now gracefully continues when the specified resource group is missing.

  • Automatically removes Azure finalizers from orphaned machines that have been deleting longer than the configured short threshold.

  • Tests

  • Added unit test coverage for orphaned-machine finalizer cleanup and Azure identity provider condition logic.

  • Refactor

  • Added a compile-time check that the Azure provider supports orphan deletion.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@vsolanki12
vsolanki12 force-pushed the OCPBUGS-86690-azure-orphaned-machine-deletion branch from e677429 to 5c7524b Compare July 2, 2026 15:14

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (2)
api/hypershift/v1beta1/hostedcluster_conditions.go (2)

167-171: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fix garbled doc comment for ValidAzureIdentityProvider.

The sentence "ValidAzureIdentityProvider indicates if the Identity Provider is used by the Azure Platform credentials is" is grammatically broken ("is used by ... is healthy"). Since this is an exported API type's godoc comment, it's worth tightening for clarity.

✏️ Proposed wording fix
-	// ValidAzureIdentityProvider indicates if the Identity Provider is used by the Azure Platform credentials is
-	// healthy. The check makes a harmless Resource Group GET call to verify credentials work and infrastructure is available.
+	// ValidAzureIdentityProvider indicates whether the Identity Provider used by the Azure Platform credentials
+	// is healthy. The check makes a harmless Resource Group GET call to verify credentials work and infrastructure is available.
 	// A failure here may require external user intervention to resolve.
 	ValidAzureIdentityProvider ConditionType = "ValidAzureIdentityProvider"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/hypershift/v1beta1/hostedcluster_conditions.go` around lines 167 - 171,
The godoc for ValidAzureIdentityProvider is grammatically broken and should be
rewritten for clarity on the exported ConditionType in
hostedcluster_conditions.go. Update the comment so it clearly states that the
condition indicates whether the Azure platform identity provider credentials are
healthy, and keep the existing explanation about the harmless Resource Group GET
check and possible user intervention.

268-278: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

EtcdDataEncryptionUpToDate misplaced under "PublicEndpointExposed" reasons block.

Line 268's comment scopes this const block to reasons for PublicEndpointExposed, but EtcdDataEncryptionUpToDate (a ConditionType, not a reason string for that condition) is declared inside it at Line 278. This groups an unrelated condition type under a misleading heading.

♻️ Suggested regrouping
 // Reasons for PublicEndpointExposed condition.
 const (
 	PublicEndpointSharedIngressConfiguredReason = "SharedIngressConfigured"
 	PublicEndpointTopologyPrivateReason         = "TopologyPrivate"
 	PublicEndpointConvergenceInProgressReason   = "ConvergenceInProgress"
-	// EtcdDataEncryptionUpToDate indicates whether all etcd data is encrypted with the
-	// currently active encryption key.
-	// True: all data confirmed encrypted with the active key.
-	// False: re-encryption is in progress or has failed.
-	// Absent: encryption is not configured.
-	EtcdDataEncryptionUpToDate ConditionType = "EtcdDataEncryptionUpToDate"
+)
+
+// EtcdDataEncryptionUpToDate indicates whether all etcd data is encrypted with the
+// currently active encryption key.
+// True: all data confirmed encrypted with the active key.
+// False: re-encryption is in progress or has failed.
+// Absent: encryption is not configured.
+const EtcdDataEncryptionUpToDate ConditionType = "EtcdDataEncryptionUpToDate"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/hypershift/v1beta1/hostedcluster_conditions.go` around lines 268 - 278,
The EtcdDataEncryptionUpToDate constant is grouped under the
PublicEndpointExposed reason constants, but it represents a separate
ConditionType and should not live in that block. Move EtcdDataEncryptionUpToDate
out of the “Reasons for PublicEndpointExposed condition” const group in
hostedcluster_conditions.go and place it with the other Etcd-related condition
type declarations, keeping the
PublicEndpointSharedIngressConfiguredReason/PublicEndpointTopologyPrivateReason/PublicEndpointConvergenceInProgressReason
block focused only on PublicEndpointExposed reasons.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@api/hypershift/v1beta1/hostedcluster_conditions.go`:
- Around line 167-171: The godoc for ValidAzureIdentityProvider is grammatically
broken and should be rewritten for clarity on the exported ConditionType in
hostedcluster_conditions.go. Update the comment so it clearly states that the
condition indicates whether the Azure platform identity provider credentials are
healthy, and keep the existing explanation about the harmless Resource Group GET
check and possible user intervention.
- Around line 268-278: The EtcdDataEncryptionUpToDate constant is grouped under
the PublicEndpointExposed reason constants, but it represents a separate
ConditionType and should not live in that block. Move EtcdDataEncryptionUpToDate
out of the “Reasons for PublicEndpointExposed condition” const group in
hostedcluster_conditions.go and place it with the other Etcd-related condition
type declarations, keeping the
PublicEndpointSharedIngressConfiguredReason/PublicEndpointTopologyPrivateReason/PublicEndpointConvergenceInProgressReason
block focused only on PublicEndpointExposed reasons.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: d4cd767e-3318-46bf-9eea-aeb9138c4080

📥 Commits

Reviewing files that changed from the base of the PR and between ba7737f and e677429.

⛔ Files ignored due to path filters (2)
  • docs/content/reference/aggregated-docs.md is excluded by !docs/content/reference/aggregated-docs.md
  • docs/content/reference/api.md is excluded by !docs/content/reference/api.md
📒 Files selected for processing (6)
  • api/hypershift/v1beta1/hostedcluster_conditions.go
  • cmd/cluster/azure/destroy.go
  • control-plane-operator/controllers/healthcheck/azure.go
  • control-plane-operator/controllers/healthcheck/azure_test.go
  • control-plane-operator/controllers/healthcheck/healthcheck_controller.go
  • control-plane-operator/main.go
💤 Files with no reviewable changes (5)
  • control-plane-operator/controllers/healthcheck/azure_test.go
  • cmd/cluster/azure/destroy.go
  • control-plane-operator/controllers/healthcheck/azure.go
  • control-plane-operator/main.go
  • control-plane-operator/controllers/healthcheck/healthcheck_controller.go

@vsolanki12

Copy link
Copy Markdown
Contributor Author

I have tested this in test cluster.

Before fix:

  1. Created two AzureMachines with CAPI finalizers to simulate orphaned machines
$ oc apply -f - <<EOF
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: AzureMachine
metadata:
  name: test-orphan-machine-1
  namespace: clusters-vsolanki-azure
  finalizers: ["azuremachine.infrastructure.cluster.x-k8s.io"]
  labels: {cluster.x-k8s.io/cluster-name: vsolanki-azure}
spec: {identity: None, vmSize: Standard_D4s_v3, ...}
EOF
azuremachine.infrastructure.cluster.x-k8s.io/test-orphan-machine-1 created
azuremachine.infrastructure.cluster.x-k8s.io/test-orphan-machine-2 created
  1. Deleted them & these stuck in Terminating because finalizers cannot be removed
$ oc delete azuremachine test-orphan-machine-1 test-orphan-machine-2 \
    -n clusters-vsolanki-azure
azuremachine.infrastructure.cluster.x-k8s.io "test-orphan-machine-1" deleted
azuremachine.infrastructure.cluster.x-k8s.io "test-orphan-machine-2" deleted
  1. Confirmed stuck in Terminating with finalizers
$ oc get azuremachine test-orphan-machine-1 -n clusters-vsolanki-azure \
    -o jsonpath='deletionTimestamp={.metadata.deletionTimestamp}, finalizers={.metadata.finalizers}'

deletionTimestamp=2026-07-02T14:10:23Z, finalizers=["azuremachine.infrastructure.cluster.x-k8s.io"]
Artifact: Both machines stuck
$ oc get azuremachines -n clusters-vsolanki-azure
NAME                    AGE
test-orphan-machine-1   100s   ← stuck, finalizer blocking deletion
test-orphan-machine-2    91s   ← stuck, finalizer blocking deletion

After Fix:

  1. CPO health check detects invalid Azure identity. The CPO HealthCheckUpdater calls azureutil.GetResourceGroupInfo() to validate Azure credentials. In this cluster the CPO identity lacked Reader permissions on the resource group, so the check returned AuthorizationFailed.
$ oc logs deployment/control-plane-operator -n clusters-vsolanki-azure \
    -c control-plane-operator | grep health-check

{"level":"error","ts":"2026-07-02T14:41:14Z",
 "logger":"health-check-updater",
 "msg":"Failure occurred during health checks",
 "error":"some health checks failed: azure identity provider auth failure: AuthorizationFailed"}
  1. Condition set on HCP
$ oc get hostedcontrolplane vsolanki-azure -n clusters-vsolanki-azure \
    -o jsonpath='{range .status.conditions[?(@.type=="ValidAzureIdentityProvider")]}{.type}: {.status} / {.reason} / {.message}{end}'

ValidAzureIdentityProvider: False / InvalidIdentityProvider / AuthorizationFailed
  1. Condition bubbled from HCP to HC
$ oc get hostedcluster vsolanki-azure -n clusters \
    -o jsonpath='{range .status.conditions[?(@.type=="ValidAzureIdentityProvider")]}{.type}: {.status} / {.reason} / {.message}{end}'

ValidAzureIdentityProvider: False / InvalidIdentityProvider / AuthorizationFailed
  1. Triggered HostedCluster deletion
$ oc delete hostedcluster vsolanki-azure -n clusters
hostedcluster.hypershift.openshift.io "vsolanki-azure" deleted
  1. HO logs confirm DeleteOrphanedMachines cleared finalizers instantly
$ oc logs deployment/operator -n hypershift | grep "skipping deletion"

{"level":"info","ts":"2026-07-02T14:10:47Z",
 "msg":"skipping deletion of AzureMachine because of invalid Azure identity provider",
 "controller":"hostedcluster",
 "controllerGroup":"hypershift.openshift.io",
 "controllerKind":"HostedCluster",
 "HostedCluster":{"name":"vsolanki-azure","namespace":"clusters"},
 "reconcileID":"5dfc8433-b799-489a-bf73-36d6b894cbb6",
 "machine":{"name":"test-orphan-machine-1","namespace":"clusters-vsolanki-azure"}}

{"level":"info","ts":"2026-07-02T14:10:47Z",
 "msg":"skipping deletion of AzureMachine because of invalid Azure identity provider",
 "controller":"hostedcluster",
 "controllerGroup":"hypershift.openshift.io",
 "controllerKind":"HostedCluster",
 "HostedCluster":{"name":"vsolanki-azure","namespace":"clusters"},
 "reconcileID":"5dfc8433-b799-489a-bf73-36d6b894cbb6",
 "machine":{"name":"test-orphan-machine-2","namespace":"clusters-vsolanki-azure"}}
  1. AzureMachines fully cleaned up — no hang
$ oc get azuremachines -n clusters-vsolanki-azure
No resources found in clusters-vsolanki-azure namespace.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (3)
hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go (1)

452-481: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Correct mirror of AWS pattern; consider extracting shared helper.

The new Azure block is logically correct and mirrors the existing ValidAWSIdentityProvider bubbling logic (lines 424-450) exactly, differing only in the condition type and platform check. This duplication will grow if more platforms adopt the same pattern.

♻️ Suggested helper extraction
+func bubbleUpIdentityProviderCondition(hcluster *hyperv1.HostedCluster, hcp *hyperv1.HostedControlPlane, conditionType hyperv1.ConditionType) bool {
+	var validIdentityProviderCondition *metav1.Condition
+	if hcp != nil {
+		validIdentityProviderCondition = meta.FindStatusCondition(hcp.Status.Conditions, string(conditionType))
+	}
+	if validIdentityProviderCondition == nil {
+		return meta.SetStatusCondition(&hcluster.Status.Conditions, metav1.Condition{
+			Type:               string(conditionType),
+			Status:             metav1.ConditionUnknown,
+			Reason:             hyperv1.StatusUnknownReason,
+			ObservedGeneration: hcluster.Generation,
+		})
+	}
+	validIdentityProviderCondition.ObservedGeneration = hcluster.Generation
+	return meta.SetStatusCondition(&hcluster.Status.Conditions, *validIdentityProviderCondition)
+}
+
 if hcluster.Spec.Platform.Type == hyperv1.AWSPlatform {
-	updated := false
-	var validIdentityProviderCondition *metav1.Condition
-	if hcp != nil {
-		validIdentityProviderCondition = meta.FindStatusCondition(hcp.Status.Conditions, string(hyperv1.ValidAWSIdentityProvider))
-	}
-	if validIdentityProviderCondition == nil {
-		updated = meta.SetStatusCondition(&hcluster.Status.Conditions, metav1.Condition{...})
-	} else {
-		validIdentityProviderCondition.ObservedGeneration = hcluster.Generation
-		updated = meta.SetStatusCondition(&hcluster.Status.Conditions, *validIdentityProviderCondition)
-	}
-	if updated {
+	if bubbleUpIdentityProviderCondition(hcluster, hcp, hyperv1.ValidAWSIdentityProvider) {
 		if err := r.Client.Status().Update(ctx, hcluster); err != nil {
 			return ctrl.Result{}, fmt.Errorf("failed to update status: %w", err)
 		}
 	}
 }

 if hcluster.Spec.Platform.Type == hyperv1.AzurePlatform {
-	... (same body as above with ValidAzureIdentityProvider)
+	if bubbleUpIdentityProviderCondition(hcluster, hcp, hyperv1.ValidAzureIdentityProvider) {
+		if err := r.Client.Status().Update(ctx, hcluster); err != nil {
+			return ctrl.Result{}, fmt.Errorf("failed to update status: %w", err)
+		}
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go`
around lines 452 - 481, The Azure status-bubbling logic in
hostedcluster_controller.go duplicates the existing AWS pattern in the same
reconcile path, so extract the shared condition-propagation flow into a helper.
Refactor the logic around ValidAWSIdentityProvider/ValidAzureIdentityProvider in
HostedClusterReconciler so a single helper handles finding the HCP condition,
setting ObservedGeneration, defaulting Unknown when missing, and persisting
status updates, with platform/condition type passed in as parameters.
hypershift-operator/controllers/hostedcluster/internal/platform/azure/azure_test.go (2)

539-557: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test case names don't follow the mandated "When ... it should ..." format.

Names like "When condition is True, return Valid" omit "it should". As per path instructions: "Always use 'When ... it should ...' format for describing test cases when creating unit tests."

✏️ Suggested rename
-			name: "When condition is True, return Valid",
+			name: "When condition is True, it should return Valid",
...
-			name: "When condition is False, return Invalid",
+			name: "When condition is False, it should return Invalid",
...
-			name: "When condition is Unknown, return Unknown",
+			name: "When condition is Unknown, it should return Unknown",
...
-			name: "When condition is missing, return Unknown",
+			name: "When condition is missing, it should return Unknown",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@hypershift-operator/controllers/hostedcluster/internal/platform/azure/azure_test.go`
around lines 539 - 557, The test case names in azure_test.go do not follow the
required “When ... it should ...” pattern. Update the affected table-driven
cases in the Azure credential status tests so each `name` uses the mandated
format, keeping the existing intent but renaming them to include “it should” for
the scenarios around `ValidAzureIdentityProviderConditionStatus` and
`CredentialStatusValid`, `CredentialStatusInvalid`, and
`CredentialStatusUnknown`.

Source: Path instructions


532-577: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for DeleteOrphanedMachines.

This adds coverage only for GetCredentialStatus. The new DeleteOrphanedMachines function — the actual fix for the orphaned-machine deletion hang — has no unit test in this file (e.g. verifying finalizers are cleared only for machines with a DeletionTimestamp, that Valid credentials skip cleanup, and that per-machine update errors are aggregated).

As per path instructions, "Unit test any code changes and additions."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@hypershift-operator/controllers/hostedcluster/internal/platform/azure/azure_test.go`
around lines 532 - 577, The new coverage only exercises GetCredentialStatus, but
the actual change in DeleteOrphanedMachines is untested. Add unit tests around
DeleteOrphanedMachines in azure_test.go to verify it only clears finalizers for
machines with a DeletionTimestamp, skips cleanup when GetCredentialStatus
returns Valid, and aggregates per-machine update errors. Use the existing
HostedCluster and machine-status setup in the Azure controller tests to locate
the new behavior.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@hypershift-operator/controllers/hostedcluster/internal/platform/azure/azure.go`:
- Around line 400-411: In the AzureMachine cleanup loop, fix the misleading
status message and the reversed object identifier formatting. In the update path
inside azure.go’s Azure deletion handling, change the error construction in the
c.Update failure branch to use the conventional namespace/name order, and update
the logger message after a successful finalizer clear in the same loop so it
describes unblocking deletion rather than “skipping deletion.” Use the existing
azureMachine and logger references in this loop to keep the message accurate and
consistent with the rest of the file.

---

Nitpick comments:
In `@hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go`:
- Around line 452-481: The Azure status-bubbling logic in
hostedcluster_controller.go duplicates the existing AWS pattern in the same
reconcile path, so extract the shared condition-propagation flow into a helper.
Refactor the logic around ValidAWSIdentityProvider/ValidAzureIdentityProvider in
HostedClusterReconciler so a single helper handles finding the HCP condition,
setting ObservedGeneration, defaulting Unknown when missing, and persisting
status updates, with platform/condition type passed in as parameters.

In
`@hypershift-operator/controllers/hostedcluster/internal/platform/azure/azure_test.go`:
- Around line 539-557: The test case names in azure_test.go do not follow the
required “When ... it should ...” pattern. Update the affected table-driven
cases in the Azure credential status tests so each `name` uses the mandated
format, keeping the existing intent but renaming them to include “it should” for
the scenarios around `ValidAzureIdentityProviderConditionStatus` and
`CredentialStatusValid`, `CredentialStatusInvalid`, and
`CredentialStatusUnknown`.
- Around line 532-577: The new coverage only exercises GetCredentialStatus, but
the actual change in DeleteOrphanedMachines is untested. Add unit tests around
DeleteOrphanedMachines in azure_test.go to verify it only clears finalizers for
machines with a DeletionTimestamp, skips cleanup when GetCredentialStatus
returns Valid, and aggregates per-machine update errors. Use the existing
HostedCluster and machine-status setup in the Azure controller tests to locate
the new behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 409753bb-3406-4157-b968-095242beb2f3

📥 Commits

Reviewing files that changed from the base of the PR and between e677429 and 5c7524b.

⛔ Files ignored due to path filters (3)
  • docs/content/reference/aggregated-docs.md is excluded by !docs/content/reference/aggregated-docs.md
  • docs/content/reference/api.md is excluded by !docs/content/reference/api.md
  • vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_conditions.go is excluded by !vendor/**, !**/vendor/**
📒 Files selected for processing (10)
  • api/hypershift/v1beta1/hostedcluster_conditions.go
  • cmd/cluster/azure/destroy.go
  • control-plane-operator/controllers/healthcheck/azure.go
  • control-plane-operator/controllers/healthcheck/azure_test.go
  • control-plane-operator/controllers/healthcheck/healthcheck_controller.go
  • control-plane-operator/main.go
  • hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go
  • hypershift-operator/controllers/hostedcluster/internal/platform/azure/azure.go
  • hypershift-operator/controllers/hostedcluster/internal/platform/azure/azure_test.go
  • hypershift-operator/controllers/hostedcluster/internal/platform/platform.go
✅ Files skipped from review due to trivial changes (1)
  • hypershift-operator/controllers/hostedcluster/internal/platform/platform.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • control-plane-operator/controllers/healthcheck/azure_test.go
  • cmd/cluster/azure/destroy.go
  • control-plane-operator/controllers/healthcheck/healthcheck_controller.go
  • api/hypershift/v1beta1/hostedcluster_conditions.go
  • control-plane-operator/main.go
  • control-plane-operator/controllers/healthcheck/azure.go

@vsolanki12
vsolanki12 force-pushed the OCPBUGS-86690-azure-orphaned-machine-deletion branch 2 times, most recently from db44ac3 to d5f9102 Compare July 2, 2026 15:28
@hypershift-jira-solve-ci

Copy link
Copy Markdown
Contributor

Test Failure Analysis Complete

Job Information

Test Failure Analysis

Error

39.84% of diff hit (target 42.55%)
Codecov patch coverage is 2.71 percentage points below the required threshold.

Summary

The codecov/patch check failed because the PR's new code achieves only 39.84% test coverage on the diff, falling short of the repository's 42.55% patch coverage target. The PR adds 438 lines across 13 files — including two significant new files (azure.go healthcheck with 90 new lines and DeleteOrphanedMachines with 52 new lines in the platform azure package) — but the existing unit tests only cover the early-return guard clauses, leaving the core Azure API error-handling branches, the DeleteOrphanedMachines function, and all controller wiring code untested.

Root Cause

The coverage gap is caused by insufficient unit test coverage across multiple new code paths introduced in this PR. Specifically:

  1. control-plane-operator/controllers/healthcheck/azure.go (~67 uncovered lines): The test file azure_test.go only tests 4 early-return paths (nil Azure config, KAS unavailable with missing condition, KAS unavailable with False condition, and nil credentials). All tests pass nil credentials, so execution never reaches azureutil.GetResourceGroupInfo(). This means the following paths are entirely untested:

    • HTTP 401/403 (Unauthorized/Forbidden) error handling
    • HTTP 404 (Not Found) error handling
    • Default Azure API error handling
    • azidentity.AuthenticationFailedError handling
    • Generic error fallback
    • The success path (setting ConditionTrue)
  2. hypershift-operator/controllers/hostedcluster/internal/platform/azure/azure.go (~40 uncovered lines): The DeleteOrphanedMachines function has zero test coverage. While GetCredentialStatus is fully tested (all 4 branches covered by TestGetCredentialStatus), the DeleteOrphanedMachines function — which lists AzureMachine resources, checks deletion timestamps, and clears finalizers — has no corresponding test.

  3. Controller wiring code (~79 uncovered lines): control-plane-operator/main.go (40 lines), hostedcluster_controller.go (27 lines), and healthcheck_controller.go (12 lines) contain integration-level wiring that is typically not covered by unit tests. This is a known gap in many Go controller projects.

  4. cmd/cluster/azure/destroy.go (8 uncovered lines): The new 404-tolerant resource group handling has no unit test.

The shortfall is approximately 7 lines (~3% of the 241 countable diff lines). The most impactful fix would be adding mock-based tests for the Azure API error branches in azure.go and/or adding tests for DeleteOrphanedMachines.

Recommendations
  1. Add mock-based tests for azureHealthCheckIdentityProvider error paths: Introduce a mock or interface for azureutil.GetResourceGroupInfo and test the HTTP 401/403, 404, default error, AuthenticationFailedError, generic error, and success code paths. This would cover ~25 additional lines (the error-handling switch cases and success path), easily exceeding the 42.55% target.

  2. Add unit tests for DeleteOrphanedMachines: Use a fake client (sigs.k8s.io/controller-runtime/pkg/client/fake) to test:

    • Valid credentials → early return (no-op)
    • Invalid/unknown credentials with machines pending deletion → finalizers cleared
    • Invalid credentials with no machines pending deletion → no-op
    • Error listing machines → error returned
  3. Consider testing destroy.go changes: Add a test for the new 404-tolerant path in DestroyCluster to cover the 8 new lines in cmd/cluster/azure/destroy.go.

  4. Accept controller wiring gap: The ~79 lines in main.go, hostedcluster_controller.go, and healthcheck_controller.go are controller setup/reconciliation wiring that typically requires integration tests. These are reasonable to leave uncovered by unit tests, but the other gaps above should be sufficient to clear the threshold.

Addressing recommendation #1 alone would likely raise patch coverage above the 42.55% target.

Evidence
Evidence Detail
Codecov result 39.84% patch coverage vs 42.55% target (2.71pp shortfall)
Total diff lines (countable) 241 lines across 8 non-test, non-vendor Go files
Lines covered ~96 lines (39.84% of 241)
Lines needed for target ~103 lines (42.55% of 241) — shortfall of ~7 lines
Largest uncovered file healthcheck/azure.go — 90 new lines, ~23 covered (early-return paths only)
Untested function DeleteOrphanedMachines in platform/azure/azure.go — 0% coverage (~30 executable lines)
Test file analysis azure_test.go passes nil credentials in all 4 test cases, so GetResourceGroupInfo() is never called
Uncovered error branches HTTP 401/403, HTTP 404, default HTTP error, AuthenticationFailedError, generic error, success path
Controller wiring (expected gap) main.go (40 lines), hostedcluster_controller.go (27 lines), healthcheck_controller.go (12 lines)
PR details +438/-10 lines, 13 changed files, branch OCPBUGS-86690-azure-orphaned-machine-deletion

@vsolanki12
vsolanki12 force-pushed the OCPBUGS-86690-azure-orphaned-machine-deletion branch from d5f9102 to da2d955 Compare July 2, 2026 16:03
Comment thread control-plane-operator/main.go Outdated
@@ -729,3 +736,38 @@ func setupAzurePrivateControllers(ctx context.Context, mgr ctrl.Manager, hcp *hy
os.Exit(1)
}
}

// resolveAzureHealthCheckCredentials returns Azure credentials for the identity
// provider health check. Returns nil if credentials cannot be obtained — the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this mentions identity provider, but in managed there's no idp involve right?
This func needs better doc and unit test for all use cases

@vsolanki12 vsolanki12 Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Updated the docstring to clarify both credential paths (self-managed workload identity vs ARO HCP managed identity) and removed the "identity provider" wording. The function now documents what each path does and what happens when credentials are nil.

Unit testing resolveAzureHealthCheckCredentials directly is not practical since it creates real Azure SDK credential objects (DefaultAzureCredential / UserAssignedIdentityCredential), but the health check function that consumes its output (azureHealthCheckCredentials) is fully tested for all early-return paths (nil config, KAS unavailable, nil creds).

)

func azureHealthCheckIdentityProvider(ctx context.Context, hcp *hyperv1.HostedControlPlane, azureCreds azcore.TokenCredential) error {
if hcp.Spec.Platform.Azure == nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In managed we don't have idp hence this func naming and condition seems misleading

@vsolanki12 vsolanki12 Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Renamed azureHealthCheckIdentityProviderazureHealthCheckCredentials and setAzureIdentityConditionsetAzureCredentialCondition to avoid the misleading "identity provider" terminology for managed clusters. Also updated all condition messages to say "credentials" instead of "identity provider".

The API condition name ValidAzureIdentityProvider is kept for consistency with the AWS pattern (ValidAWSIdentityProvider), but the function godoc now explains this naming choice.

return nil
}

_, err := azureutil.GetResourceGroupInfo(ctx,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We use this to decide if delete orphan machines, though we don't validate specific capZ perms in practice.
I know we have the same pattern/limitation in aws. Thoughts? @muraee @bryan-cox

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct — the Resource Group GET call validates that the credentials are functional and the infrastructure still exists, but doesn't verify specific CAPZ permissions (e.g. VM create/delete). This is the same limitation as AWS where ValidAWSIdentityProvider checks OIDC reachability but not specific IAM permissions for CAPA.

The primary goal here is detecting the "infrastructure already gone" case (RG returns 404) and "credentials completely broken" case (401/403), which are the scenarios that cause deletion to hang indefinitely. Validating fine-grained CAPZ permissions would require a more complex probe and is a separate concern.


AI-assisted response via Claude Code

@vsolanki12
vsolanki12 force-pushed the OCPBUGS-86690-azure-orphaned-machine-deletion branch from da2d955 to e855967 Compare July 6, 2026 11:47
Comment on lines +167 to +169
// ValidAzureIdentityProvider indicates if the Identity Provider is used by the Azure Platform credentials is
// healthy. The check makes a harmless Resource Group GET call to verify credentials work and infrastructure is available.
// A failure here may require external user intervention to resolve.

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.

How often does this check run?

@vsolanki12 vsolanki12 Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Every 5 minutes on success, every 30 seconds on failure, same cadence as the existing AWS identity provider check. Both are driven by the shared HealthCheckUpdater ticker in healthcheck_controller.go.

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.

Have we seen any rate limiting issues on the AWS side from these frequent checks?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not that I'm aware of. The AWS check uses ec2.DescribeVpcEndpoints which is a read-only EC2 API — AWS rate limits for EC2 describe calls are generous (typically 100 req/s per account/region). At 1 call every 5 minutes per HCP, even a management cluster with hundreds of hosted clusters would be well within limits.

For Azure, the equivalent is a Resource Group GET call (resources.azure.com), which has similarly generous read limits (~250 reads/5min per subscription). Both are lightweight, read-only operations specifically chosen to avoid rate limiting concerns.


AI-assisted response via Claude Code

@JoelSpeed

Copy link
Copy Markdown
Contributor

/approve for api

@enxebre

enxebre commented Jul 13, 2026

Copy link
Copy Markdown
Member

This use case would be coverage by #8296 which relies on the machine delete condition

@vsolanki12
vsolanki12 force-pushed the OCPBUGS-86690-azure-orphaned-machine-deletion branch from e855967 to 05011f4 Compare July 29, 2026 07:49
@openshift-ci openshift-ci Bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 29, 2026
@vsolanki12
vsolanki12 force-pushed the OCPBUGS-86690-azure-orphaned-machine-deletion branch from 05011f4 to 6a1a329 Compare July 29, 2026 07:54
@openshift-ci openshift-ci Bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 29, 2026
@vsolanki12
vsolanki12 force-pushed the OCPBUGS-86690-azure-orphaned-machine-deletion branch from 6a1a329 to 7a371d4 Compare July 29, 2026 07:58
@vsolanki12

Copy link
Copy Markdown
Contributor Author

/rebase

@github-actions

Copy link
Copy Markdown

🤖 Rebasing PR onto main: workflow run

@openshift-ci openshift-ci Bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Aug 17, 2026
@github-actions

Copy link
Copy Markdown

⚠️ The operation succeeded locally but the push failed. If this is a fork PR, please enable Allow edits from maintainers and retry.

…s during cluster deletion

Replace the credential-level ValidAzureIdentityProvider signal with the
machine-level DeletionFailed condition approach (aligned with PR openshift#8296).

When an AzureMachine has been deleting for >10 minutes and CAPZ sets
Ready=False with Reason=DeletionFailed, the finalizer is removed to
unblock cluster teardown. Unlike openshift#8296 which only covers ARO HCP
(ManagedIdentities != nil), this covers self-managed Azure clusters
where credentials may expire or resource groups may be deleted
out-of-band.

Removes: ValidAzureIdentityProvider condition, CPO health check for
Azure credentials, credential status types, and condition bubbling
from HCP to HC.

Keeps: cmd/cluster/azure/destroy.go 404 handling fix.

Signed-off-by: Vimal Solanki <vsolanki@redhat.com>
@vsolanki12
vsolanki12 force-pushed the OCPBUGS-86690-azure-orphaned-machine-deletion branch from 7a371d4 to ae634cf Compare August 17, 2026 05:17
@openshift-ci openshift-ci Bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Aug 17, 2026
@openshift-ci

openshift-ci Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: JoelSpeed, vsolanki12
Once this PR has been reviewed and has the lgtm label, please assign csrwng for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found 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

@vsolanki12

Copy link
Copy Markdown
Contributor Author

/cc @bryan-cox

@openshift-ci
openshift-ci Bot requested a review from bryan-cox August 17, 2026 05:18
@vsolanki12

Copy link
Copy Markdown
Contributor Author

/test verify

@openshift-ci

openshift-ci Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

@vsolanki12: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/api Indicates the PR includes changes for the API area/cli Indicates the PR includes changes for CLI area/control-plane-operator Indicates the PR includes changes for the control plane operator - in an OCP release area/documentation Indicates the PR includes changes for documentation area/hypershift-operator Indicates the PR includes changes for the hypershift operator and API - outside an OCP release area/platform/azure PR/issue for Azure (AzurePlatform) platform jira/valid-bug Indicates that a referenced Jira bug is valid for the branch this PR is targeting. jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants