Skip to content

[controllers] Fix maxUnhealthyCount bypass - #4420

Draft
jrvaldes wants to merge 1 commit into
openshift:masterfrom
jrvaldes:adjust-test-2machines
Draft

[controllers] Fix maxUnhealthyCount bypass#4420
jrvaldes wants to merge 1 commit into
openshift:masterfrom
jrvaldes:adjust-test-2machines

Conversation

@jrvaldes

@jrvaldes jrvaldes commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

The authentication-failure remediation path in the Machine controller deleted Machines unconditionally, bypassing the maxUnhealthyCount safety check already enforced on the private-key-rotation path. This commit lets a MachineSet exceed its disruption budget when both remediation paths triggered deletion around the same time.

Route both paths through a single deleteMachineIfAllowed helper so the gate applies consistently, and back off with RequeueAfter instead of an immediate requeue while a restricted deletion waits on a sibling's replacement to become healthy. Also fixes a nil pointer panic in isWindowsMachineHealthy for a Running Machine with no NodeRef, adds unit coverage for the gate, and widens e2e deletion/reconfiguration timeouts to account for the gate delaying deletion until a sibling's replacement finishes configuring.

Summary by CodeRabbit

  • Bug Fixes
    • Improved Windows Machine deletion safety by enforcing a 30-second retry backoff when deletion limits are reached.
    • Added clearer restriction events that identify the remediation reason.
    • Improved handling of stale private keys and authentication failures.
    • Windows health checks now safely handle missing or invalid status and node references.
    • Improved private-key secret replacement by updating existing secrets instead of deleting them first.
  • Documentation
    • Added guidance for remediation paths and expected deletion-safety behavior.

The authentication-failure remediation path in the Machine
controller deleted Machines unconditionally, bypassing the
maxUnhealthyCount safety check already enforced on the
private-key-rotation path. This commit lets a MachineSet exceed its
disruption budget when both remediation paths triggered deletion
around the same time.

Route both paths through a single deleteMachineIfAllowed helper so
the gate applies consistently, and back off with RequeueAfter
instead of an immediate requeue while a restricted deletion waits
on a sibling's replacement to become healthy. Also fixes a nil
pointer panic in isWindowsMachineHealthy for a Running Machine with
no NodeRef, adds unit coverage for the gate, and widens e2e
deletion/reconfiguration timeouts to account for the gate delaying
deletion until a sibling's replacement finishes configuring.
@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 Aug 4, 2026
@openshift-ci

openshift-ci Bot commented Aug 4, 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

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Windows Machine remediation now routes stale-key and authentication-failure deletions through a shared safety gate. Restricted deletions emit events and requeue after 30 seconds. Health checks reject invalid status or missing Node references. Tests cover deletion limits, events, backoff, and nil Node references. End-to-end tests update secret replacement, timeout, logging, and remediation handling.

Suggested reviewers: mansikulkarni96


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 2 warnings)

Check name Status Explanation Resolution
Stable And Deterministic Test Names ❌ Error Test names in TestDeleteMachineIfAllowed use string concatenation with a loop variable (reason+"/allowed", reason+"/restricted") instead of hardcoded strings. Replace t.Run(reason+"/allowed",...) and t.Run(reason+"/restricted",...) with four hardcoded calls: t.Run("authentication failure/allowed",...), t.Run("authentication failure/restricted",...), t.Run("private key out of date/allowed",...)...
Go Best Practices & Build Tags ⚠️ Warning Controller files have no //go:build !windows despite Linux-only operator builds; isAllowedDeletion also indexes ma.OwnerReferences[0] without checking it. Add consistent !windows tags to controller sources and guard each Machine's OwnerReferences before indexing; retain the existing NodeRef nil check.
Kubernetes Controller Patterns ⚠️ Warning deleteMachineIfAllowed gates before deleteMachine, so an already-deleting Machine can be requeued as restricted; isAllowedDeletion also indexes ma.OwnerReferences[0] without validating it. Short-circuit deletion when DeletionTimestamp is set, and validate/filter each sibling owner reference before indexing; add tests for both cases.
✅ Passed checks (17 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: fixing the maxUnhealthyCount bypass in the controller.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Security: Secrets, Ssh & Csr ✅ Passed The PR changes only deletion gating, health checks, and e2e Secret replacement; credential, crypto, CSR, and SSH implementations are unchanged, and changed paths log names and reasons only.
Windows Service Management ✅ Passed The PR changes Machine deletion/health logic and e2e timing only; Windows service schema, lifecycle, cleanup, reboot, and SCM files are unchanged.
Platform-Specific Requirements ✅ Passed PR changes shared deletion logic and e2e timing only; vSphere limits, AWS EC2LaunchV2 prerequisite, Azure cloud-node-manager service, and GCP hostname script remain implemented or documented.
Test Structure And Quality ✅ Passed The changed tests use Go testing/testify, not Ginkgo; no Describe, It, BeforeEach, AfterEach, Eventually, or Consistently calls occur in the PR changes.
Microshift Test Compatibility ✅ Passed The PR adds no new Ginkgo e2e tests. E2E changes use Go testing.T and existing t.Run; added controller tests are standard Test functions, so MicroShift API screening is not applicable.
Single Node Openshift (Sno) Test Compatibility ✅ Passed The PR adds no Ginkgo e2e tests: changed e2e files use package e2e and testing.T/t.Run, while controller additions are unit tests; SNO compatibility checks are therefore not applicable.
Topology-Aware Scheduling Compatibility ✅ Passed The diff adds no deployment manifests or pod scheduling constraints; controller changes only gate Machine deletion and validate Node health, with no topology or node-role assumptions.
Ote Binary Stdout Contract ✅ Passed The PR adds no OTE process-level stdout writes. New log.Printf calls remain in e2e test bodies, while the OTE main uses standard logging initialized before execution.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed No new Ginkgo e2e tests were added; changed tests use standard testing.T and fake clients, with no added IPv4-only literals or external connectivity.
No-Weak-Crypto ✅ Passed The PR adds no MD5, SHA1, DES, 3DES, RC4, Blowfish, ECB, custom crypto, or secret/token comparisons; added Go imports are time, clients, and test utilities only.
Container-Privileges ✅ Passed The patch adds no privilege controls or container manifests; added-line scanning found no prohibited settings, and the existing hostNetwork setting is unchanged.
No-Sensitive-Data-In-Logs ✅ Passed Added logs contain only Machine name, fixed remediation reason, counts, and status errors; no password, token, API key, private-key bytes, PII, or customer data is logged.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@jrvaldes

jrvaldes commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

/test ?

@jrvaldes

jrvaldes commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

/test unit

@openshift-ci

openshift-ci Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: jrvaldes

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

The pull request process is described here

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

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

@openshift-ci openshift-ci Bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Aug 4, 2026
@jrvaldes

jrvaldes commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

/test lint

@jrvaldes

jrvaldes commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

/test images

@jrvaldes

jrvaldes commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

/test vsphere-e2e-operator

@jrvaldes

jrvaldes commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@jrvaldes

jrvaldes commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai help

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

CodeRabbit commands

These commands are invoked using PR/Issue comments.

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai rate limit to show your current review rate limit status.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai evaluate custom pre-merge check --instructions <custom-checks-instructions> --name <custom-checks-title> [--mode <error|warning>] to validate the custom pre-merge checks instructions. Defaults to error when --mode is omitted.
  • @coderabbitai ignore pre-merge checks to override pre-merge checks and get an approval on PR.
  • @coderabbitai run pre-merge checks to run pre-merge checks on the pull request.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai emit path instructions to emit generated path-specific review instructions for this repository.
  • @coderabbitai resolve merge conflict to automatically resolve merge conflicts.
  • @coderabbitai autofix to automatically fix issues identified in unresolved review comments.
  • @coderabbitai fix-ci to automatically fix failing CI checks in a stacked pull request.
  • @coderabbitai fix-ci commit to automatically fix failing CI checks by committing fixes to the current branch.
  • @coderabbitai resolve to resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai generate configuration to create a PR that adds the current resolved configuration as .coderabbit.yaml (or show it if already present).
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, support, documentation and community

  • Visit our status page to check the current availability of CodeRabbit.
  • Create a ticket on our support page for assistance with any issues or questions.
  • Visit our documentation site for detailed information on how to use CodeRabbit.
  • Join our Discord community to connect with other users and get help from the community.
  • Follow us on X/Twitter for updates and announcements.

@jrvaldes

jrvaldes commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai rate limit

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Your plan includes PR reviews subject to rate limits. Reviews are available now.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 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 `@controllers/windowsmachine_controller_deletion_test.go`:
- Line 1: Add the //go:build !windows constraint to the top of the
WindowsMachine deletion test file, before the package declaration, matching the
constraint used by WindowsMachineReconciler in windowsmachine_controller.go.

In `@controllers/windowsmachine_controller.go`:
- Line 339: Update deleteMachineIfAllowed and its authentication-failure caller
to evaluate the unhealthy budget after removing the target: exclude the target
from the sibling health count and permit deletion when the resulting count is <=
maxUnhealthyCount, including Provisioned targets. Add a regression case to
TestDeleteMachineIfAllowed covering a Provisioned target with reason
"authentication failure".
- Around line 494-498: Update the condition in the loop using ma so it checks
len(ma.OwnerReferences) before accessing ma.OwnerReferences[0], replacing the
redundant machine.OwnerReferences guard while preserving the existing
owner-name, health, and deletion checks. Add a regression fixture covering an
ownerless sibling Windows-labeled Machine during deletion evaluation.
- Line 371: Update the reconciliation return around deleteMachine to wrap its
error with fmt.Errorf using %w, including context that identifies the
safety-gated machine deletion operation. Preserve the existing ctrl.Result{}
return and deleteMachine(ctx, machine) behavior.
- Around line 355-371: Update deleteMachineIfAllowed to return successfully
before calling isAllowedDeletion when machine.DeletionTimestamp is already set,
preserving deleteMachine’s no-op behavior for in-progress deletion. Add a
wrapper-level test covering an already-deleting Machine and verifying it neither
checks the deletion budget nor emits a restriction event or requeues.

In `@test/e2e/secrets_test.go`:
- Around line 253-275: Update the helper containing the Secret Get/Create/Update
calls to create one context.WithTimeout context with an appropriate deadline,
ensure it is canceled, and pass that context to all three Kubernetes API
operations instead of context.TODO().
🪄 Autofix

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: 91512654-ad60-4a11-aec8-2623a620bc14

📥 Commits

Reviewing files that changed from the base of the PR and between 1afa357 and aefdcf9.

⛔ Files ignored due to path filters (36)
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/awsfailuredomain.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/awsfailuredomainplacement.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/awsresourcefilter.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/awsresourcereference.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/azurefailuredomain.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/controlplanemachineset.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/controlplanemachinesetspec.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/controlplanemachinesetstatus.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/controlplanemachinesetstrategy.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/controlplanemachinesettemplate.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/controlplanemachinesettemplateobjectmeta.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/failuredomains.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/gcpfailuredomain.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/nutanixfailuredomainreference.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/openshiftmachinev1beta1machinetemplate.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/openstackfailuredomain.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/rootvolume.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/machine/v1/vspherefailuredomain.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/applyconfigurations/utils.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/clientset/versioned/clientset.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/clientset/versioned/fake/clientset_generated.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/clientset/versioned/fake/doc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/clientset/versioned/fake/register.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/clientset/versioned/typed/machine/v1/controlplanemachineset.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/clientset/versioned/typed/machine/v1/doc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/clientset/versioned/typed/machine/v1/fake/doc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/clientset/versioned/typed/machine/v1/fake/fake_controlplanemachineset.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/clientset/versioned/typed/machine/v1/fake/fake_machine_client.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/clientset/versioned/typed/machine/v1/generated_expansion.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/clientset/versioned/typed/machine/v1/machine_client.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/clientset/versioned/typed/machine/v1beta1/fake/doc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/clientset/versioned/typed/machine/v1beta1/fake/fake_machine.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/clientset/versioned/typed/machine/v1beta1/fake/fake_machine_client.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/clientset/versioned/typed/machine/v1beta1/fake/fake_machinehealthcheck.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/client-go/machine/clientset/versioned/typed/machine/v1beta1/fake/fake_machineset.go is excluded by !vendor/**, !**/vendor/**
  • vendor/modules.txt is excluded by !vendor/**, !**/vendor/**
📒 Files selected for processing (4)
  • controllers/windowsmachine_controller.go
  • controllers/windowsmachine_controller_deletion_test.go
  • test/e2e/create_test.go
  • test/e2e/secrets_test.go

@@ -0,0 +1,317 @@
package controllers

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add the Linux build constraint to this controller test.

This file has no //go:build !windows constraint. A Windows-targeted test build can select this test while excluding Linux-only controller code that defines WindowsMachineReconciler.

Proposed fix
+//go:build !windows
+
 package controllers

As per path instructions, “Controllers must have //go:build !windows tag.”

#!/bin/bash
set -euo pipefail

for file in \
  controllers/windowsmachine_controller.go \
  controllers/windowsmachine_controller_deletion_test.go; do
  echo "== $file =="
  sed -n '1,8p' "$file"
done
🤖 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 `@controllers/windowsmachine_controller_deletion_test.go` at line 1, Add the
//go:build !windows constraint to the top of the WindowsMachine deletion test
file, before the package declaration, matching the constraint used by
WindowsMachineReconciler in windowsmachine_controller.go.

r.recorder.Eventf(machine, core.EventTypeWarning, "MachineSetupFailure",
"Machine %s authentication failure", machine.Name)
return ctrl.Result{}, r.deleteMachine(ctx, machine)
return r.deleteMachineIfAllowed(ctx, machine, "authentication failure")

@coderabbitai coderabbitai Bot Aug 4, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Evaluate the deletion budget after deletion.

The authentication-failure path can run for a Provisioned Machine. That target is already unhealthy. With two replicas, one healthy sibling, and maxUnhealthyCount == 1, the gate returns RequeueAfter instead of deleting the failed target. The target then cannot be replaced.

Calculate the post-deletion unhealthy count. Exclude the target from the sibling health count, then allow deletion when the post-deletion count is <= maxUnhealthyCount. Add a regression case in TestDeleteMachineIfAllowed for a Provisioned target and reason "authentication failure".

Also applies to: 355-371

🤖 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 `@controllers/windowsmachine_controller.go` at line 339, Update
deleteMachineIfAllowed and its authentication-failure caller to evaluate the
unhealthy budget after removing the target: exclude the target from the sibling
health count and permit deletion when the resulting count is <=
maxUnhealthyCount, including Provisioned targets. Add a regression case to
TestDeleteMachineIfAllowed covering a Provisioned target with reason
"authentication failure".

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.

@coderabbitai create a commit; verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @controllers/windowsmachine_controller.go at line 339, Update
deleteMachineIfAllowed and its authentication-failure caller to evaluate the
unhealthy budget after removing the target: exclude the target from the sibling
health count and permit deletion when the resulting count is <=
maxUnhealthyCount, including Provisioned targets. Add a regression case to
TestDeleteMachineIfAllowed covering a Provisioned target with reason
"authentication failure".

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline controllers/windowsmachine_controller.go --items all --type function --match 'deleteMachineIfAllowed|isAllowedDeletion|isWindowsMachineHealthy|Reconcile'
ast-grep outline controllers/windowsmachine_controller_deletion_test.go --items all --type function --match 'TestDeleteMachineIfAllowed|TestIsAllowedDeletion|new|fixture|event'

rg -n -C 8 'func \(r \*WindowsMachineReconciler\) (deleteMachineIfAllowed|isAllowedDeletion)|authentication failure' \
  controllers/windowsmachine_controller.go

rg -n -C 12 'func TestDeleteMachineIfAllowed|authentication failure|Provisioned|MachineDeletionRestricted' \
  controllers/windowsmachine_controller_deletion_test.go

Length of output: 9277


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '36,138p' controllers/windowsmachine_controller_deletion_test.go
sed -n '139,282p' controllers/windowsmachine_controller_deletion_test.go
sed -n '463,525p' controllers/windowsmachine_controller.go

rg -n -C 4 'OwnerReferences\[0\]|UID|ObjectKey|newTestMachine\(' \
  controllers/windowsmachine_controller_deletion_test.go controllers/windowsmachine_controller.go

Length of output: 31207


@jrvaldes, I verified the finding. It remains valid.

The authentication-failure caller at Line 339 already calls deleteMachineIfAllowed. Do not change that caller.

The minimal fix is in isAllowedDeletion:

  • Exclude the target from the healthy sibling count.
  • Check ma.OwnerReferences before reading ma.OwnerReferences[0].
  • Calculate the post-deletion unhealthy count as MachineSet replicas - healthy siblings.
  • Allow deletion when that count is <= maxUnhealthyCount.

This preserves the existing restricted case where a healthy target has an unhealthy sibling. Deleting that target produces two unhealthy Machines. It allows the requested Provisioned target case because the healthy sibling remains and the post-deletion unhealthy count is one.

I cannot create a commit directly. The verified patch should also add one focused TestDeleteMachineIfAllowed case:

  • reason: "authentication failure"
  • target phase: Provisioned
  • sibling: healthy and Running
  • MachineSet replicas: 2
  • expected result: no requeue and target deletion
  • expected events: no MachineDeletionRestricted event

You are interacting with an AI system.

Comment on lines +355 to +371
func (r *WindowsMachineReconciler) deleteMachineIfAllowed(ctx context.Context, machine *mapi.Machine,
reason string) (ctrl.Result, error) {
deletionAllowed, err := r.isAllowedDeletion(ctx, machine)
if err != nil {
return ctrl.Result{}, fmt.Errorf("unable to determine if Machine can be deleted: %w", err)
}
if !deletionAllowed {
r.log.Info("machine deletion restricted", "name", machine.GetName(), "reason", reason,
"maxUnhealthyCount", maxUnhealthyCount)
r.recorder.Eventf(machine, core.EventTypeWarning, "MachineDeletionRestricted",
"Machine %v deletion restricted (%s) as the maximum unhealthy machines can`t exceed %v count",
machine.Name, reason, maxUnhealthyCount)
// Requeue with a backoff rather than immediately, to avoid a tight reconcile loop and event spam while
// waiting for a sibling Machine's remediation to complete.
return ctrl.Result{RequeueAfter: machineDeletionRestrictedRequeueInterval}, nil
}
return ctrl.Result{}, r.deleteMachine(ctx, machine)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Preserve the no-op for a Machine already deleting.

deleteMachine returns successfully when DeletionTimestamp is set. This wrapper checks the budget first. If the budget is full, an already-deleting Machine emits MachineDeletionRestricted and requeues instead of returning success.

Return before isAllowedDeletion when deletion has already started. Add a wrapper-level test for this case.

Proposed fix
 func (r *WindowsMachineReconciler) deleteMachineIfAllowed(ctx context.Context, machine *mapi.Machine,
 	reason string) (ctrl.Result, error) {
+	if !machine.GetDeletionTimestamp().IsZero() {
+		return ctrl.Result{}, nil
+	}
 	deletionAllowed, err := r.isAllowedDeletion(ctx, machine)

As per path instructions, “Check reconciliation loop logic and idempotency.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (r *WindowsMachineReconciler) deleteMachineIfAllowed(ctx context.Context, machine *mapi.Machine,
reason string) (ctrl.Result, error) {
deletionAllowed, err := r.isAllowedDeletion(ctx, machine)
if err != nil {
return ctrl.Result{}, fmt.Errorf("unable to determine if Machine can be deleted: %w", err)
}
if !deletionAllowed {
r.log.Info("machine deletion restricted", "name", machine.GetName(), "reason", reason,
"maxUnhealthyCount", maxUnhealthyCount)
r.recorder.Eventf(machine, core.EventTypeWarning, "MachineDeletionRestricted",
"Machine %v deletion restricted (%s) as the maximum unhealthy machines can`t exceed %v count",
machine.Name, reason, maxUnhealthyCount)
// Requeue with a backoff rather than immediately, to avoid a tight reconcile loop and event spam while
// waiting for a sibling Machine's remediation to complete.
return ctrl.Result{RequeueAfter: machineDeletionRestrictedRequeueInterval}, nil
}
return ctrl.Result{}, r.deleteMachine(ctx, machine)
func (r *WindowsMachineReconciler) deleteMachineIfAllowed(ctx context.Context, machine *mapi.Machine,
reason string) (ctrl.Result, error) {
if !machine.GetDeletionTimestamp().IsZero() {
return ctrl.Result{}, nil
}
deletionAllowed, err := r.isAllowedDeletion(ctx, machine)
if err != nil {
return ctrl.Result{}, fmt.Errorf("unable to determine if Machine can be deleted: %w", err)
}
if !deletionAllowed {
r.log.Info("machine deletion restricted", "name", machine.GetName(), "reason", reason,
"maxUnhealthyCount", maxUnhealthyCount)
r.recorder.Eventf(machine, core.EventTypeWarning, "MachineDeletionRestricted",
"Machine %v deletion restricted (%s) as the maximum unhealthy machines can`t exceed %v count",
machine.Name, reason, maxUnhealthyCount)
// Requeue with a backoff rather than immediately, to avoid a tight reconcile loop and event spam while
// waiting for a sibling Machine's remediation to complete.
return ctrl.Result{RequeueAfter: machineDeletionRestrictedRequeueInterval}, nil
}
return ctrl.Result{}, r.deleteMachine(ctx, machine)
}
🤖 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 `@controllers/windowsmachine_controller.go` around lines 355 - 371, Update
deleteMachineIfAllowed to return successfully before calling isAllowedDeletion
when machine.DeletionTimestamp is already set, preserving deleteMachine’s no-op
behavior for in-progress deletion. Add a wrapper-level test covering an
already-deleting Machine and verifying it neither checks the deletion budget nor
emits a restriction event or requeues.

Source: Path instructions

// waiting for a sibling Machine's remediation to complete.
return ctrl.Result{RequeueAfter: machineDeletionRestrictedRequeueInterval}, nil
}
return ctrl.Result{}, r.deleteMachine(ctx, machine)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Wrap the delete error with operation context.

Line 371 returns the raw deleteMachine error. Wrap it so the reconciliation error identifies the safety-gated delete operation.

Proposed fix
-	return ctrl.Result{}, r.deleteMachine(ctx, machine)
+	if err := r.deleteMachine(ctx, machine); err != nil {
+		return ctrl.Result{}, fmt.Errorf("delete machine %q: %w", machine.Name, err)
+	}
+	return ctrl.Result{}, nil

As per coding guidelines, “In Go code, wrap errors with context using fmt.Errorf with %w.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return ctrl.Result{}, r.deleteMachine(ctx, machine)
if err := r.deleteMachine(ctx, machine); err != nil {
return ctrl.Result{}, fmt.Errorf("delete machine %q: %w", machine.Name, err)
}
return ctrl.Result{}, nil
🤖 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 `@controllers/windowsmachine_controller.go` at line 371, Update the
reconciliation return around deleteMachine to wrap its error with fmt.Errorf
using %w, including context that identifies the safety-gated machine deletion
operation. Preserve the existing ctrl.Result{} return and deleteMachine(ctx,
machine) behavior.

Source: Coding guidelines

Comment on lines +494 to 498
// Note: `len(machine.OwnerReferences) != 0` here refers to the outer `machine` parameter (already validated
// non-empty at the top of this function), not the loop variable `ma`. It is redundant but kept for
// clarity/safety in case this function is refactored again in the future.
if len(machine.OwnerReferences) != 0 && ma.OwnerReferences[0].Name == machinesetName &&
r.isWindowsMachineHealthy(ctx, &ma) && ma.DeletionTimestamp.IsZero() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the loop item's owner references.

The condition validates machine.OwnerReferences, but it indexes ma.OwnerReferences[0]. A Windows-labeled Machine without owner references panics the controller during deletion evaluation.

Check len(ma.OwnerReferences) before indexing. Add a regression fixture for an ownerless sibling Machine.

Proposed fix
-		if len(machine.OwnerReferences) != 0 && ma.OwnerReferences[0].Name == machinesetName &&
+		if len(ma.OwnerReferences) != 0 && ma.OwnerReferences[0].Name == machinesetName &&

As per path instructions, “Verify ... owner references.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Note: `len(machine.OwnerReferences) != 0` here refers to the outer `machine` parameter (already validated
// non-empty at the top of this function), not the loop variable `ma`. It is redundant but kept for
// clarity/safety in case this function is refactored again in the future.
if len(machine.OwnerReferences) != 0 && ma.OwnerReferences[0].Name == machinesetName &&
r.isWindowsMachineHealthy(ctx, &ma) && ma.DeletionTimestamp.IsZero() {
// Note: `len(machine.OwnerReferences) != 0` here refers to the outer `machine` parameter (already validated
// non-empty at the top of this function), not the loop variable `ma`. It is redundant but kept for
// clarity/safety in case this function is refactored again in the future.
if len(ma.OwnerReferences) != 0 && ma.OwnerReferences[0].Name == machinesetName &&
r.isWindowsMachineHealthy(ctx, &ma) && ma.DeletionTimestamp.IsZero() {
🤖 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 `@controllers/windowsmachine_controller.go` around lines 494 - 498, Update the
condition in the loop using ma so it checks len(ma.OwnerReferences) before
accessing ma.OwnerReferences[0], replacing the redundant machine.OwnerReferences
guard while preserving the existing owner-name, health, and deletion checks. Add
a regression fixture covering an ownerless sibling Windows-labeled Machine
during deletion evaluation.

Source: Path instructions

Comment thread test/e2e/secrets_test.go
Comment on lines 253 to +275
for _, ns := range []string{wmcoNamespace, tc.workloadNamespace} {
_, err := tc.client.K8s.CoreV1().Secrets(ns).Create(context.TODO(), &privateKeySecret, meta.CreateOptions{})
if err != nil {
return fmt.Errorf("could not create private key secret in namespace %s: %w", ns, err)
existing, getErr := tc.client.K8s.CoreV1().Secrets(ns).Get(context.TODO(), secrets.PrivateKeySecret,
meta.GetOptions{})
if getErr != nil {
if !apierrors.IsNotFound(getErr) {
return fmt.Errorf("error getting private key secret in namespace %s: %w", ns, getErr)
}
newSecret := &core.Secret{
Data: map[string][]byte{secrets.PrivateKeySecretKey: keyData},
ObjectMeta: meta.ObjectMeta{
Name: secrets.PrivateKeySecret,
},
}
if _, err := tc.client.K8s.CoreV1().Secrets(ns).Create(context.TODO(), newSecret,
meta.CreateOptions{}); err != nil {
return fmt.Errorf("could not create private key secret in namespace %s: %w", ns, err)
}
continue
}
existing.Data = map[string][]byte{secrets.PrivateKeySecretKey: keyData}
if _, err := tc.client.K8s.CoreV1().Secrets(ns).Update(context.TODO(), existing,
meta.UpdateOptions{}); err != nil {
return fmt.Errorf("could not update private key secret in namespace %s: %w", ns, err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Use a deadline-bound context for Secret API calls.

context.TODO() does not cancel or time-limit the new Get, Create, and Update calls. If the API server stops responding, createPrivateKeySecret can block without a request deadline.

Create a context.WithTimeout context for this helper and pass it to all three calls.

🤖 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 `@test/e2e/secrets_test.go` around lines 253 - 275, Update the helper
containing the Secret Get/Create/Update calls to create one context.WithTimeout
context with an appropriate deadline, ensure it is canceled, and pass that
context to all three Kubernetes API operations instead of context.TODO().

Source: Coding guidelines

@openshift-ci

openshift-ci Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

@jrvaldes: 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

approved Indicates a PR has been approved by an approver from all required OWNERS files. do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant