Skip to content

[NO-ISSUE] Wait for pull-secret rollout to MCP master before boot returns ready - #37

Open
eliorerz wants to merge 1 commit into
osac-project:mainfrom
eliorerz:wait-for-pull-secret-rollout
Open

[NO-ISSUE] Wait for pull-secret rollout to MCP master before boot returns ready#37
eliorerz wants to merge 1 commit into
osac-project:mainfrom
eliorerz:wait-for-pull-secret-rollout

Conversation

@eliorerz

@eliorerz eliorerz commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Why

After #33 (caching the etcd image at snapshot time), the golden snapshot's /var/lib/kubelet/config.json ends up with an empty authfile instead of a stale-but-present one. cmd_boot already pushes a fresh pull-secret into openshift-config/pull-secret once the API is healthy, but nothing waits for MCO to actually roll that out to the node before cmd_boot reports the cluster ready.

That rollout takes several minutes, and recert makes it worse: it writes kubelet's config.json straight to disk instead of through MCO, so machine-config-daemon can boot believing the rendered-master MachineConfig doesn't match what's on disk and latch Degraded.

Anything that pulls an authenticated image right after cmd_boot returns — metal3/ironic in BMaaS, marketplace catalog sources in CaaS-full-install — can race this and fail with unauthorized, eventually succeeding after backoff retries, but sometimes outliving the caller's own readiness timeout (seen on osac-3 and osac-9 with oc wait --timeout=600s on the metal3 pod).

infra/netris/roles/restore-snapshot in osac-test-infra already hit this exact recert side effect and fixed it: force an MCD resync, wait for mcp/master to report Updated=True, and self-heal (clear stuck degraded annotations, restart machine-config-daemon) if it doesn't. This PR ports that same fix into cmd_boot so every caller gets it for free instead of patching each workflow's timeout individually.

What

  • New step in cmd_boot between "Waiting for cluster health" and "Configuring access": force /run/machine-config-daemon-force, poll mcp/master for Updated=True (90 x 10s), and if still not synced, clear the stuck node annotation + restart machine-config-daemon and poll again (60 x 10s) before giving up.
  • Bumped cmd_boot's step count 9 → 10 and renumbered accordingly.

Test plan

  • python3 -m pytest test_cluster_tool.py -q — 161 passed (was 158; added 3 new tests), same 11 pre-existing failures unrelated to this change (root/NetworkManager-permission-dependent tests that fail identically on main in a sandboxed environment).
  • New tests: TestPullSecretRollout covers ordering (resync forced after pull-secret set, before "Configuring access"), the self-heal path, and the give-up-after-timeout path.

Summary by CodeRabbit

  • New Features

    • Added a pull-secret rollout verification phase during cluster boot.
    • Boot now waits for the cluster configuration update to complete before configuring access.
    • Added automatic recovery for stalled configuration updates by refreshing node configuration and restarting related services.
  • Bug Fixes

    • Boot now reports an error when the configuration update does not complete.
    • Updated progress steps and completion messages to reflect the expanded boot workflow.

recert writes kubelet's config.json straight to disk instead of through
MCO, so machine-config-daemon can boot believing the rendered-master
MachineConfig's content doesn't match what's on disk and latch Degraded.
Until MachineConfigPool master reports Updated, the node's kubelet may
still be running with the stale/empty authfile baked into the snapshot,
so anything that pulls an authenticated image right after boot (metal3,
marketplace catalogs, etc.) can race that rollout and fail.

Force a resync and wait for MCP master to report Updated, with the same
stuck-Degraded self-heal used by infra/netris/roles/restore-snapshot for
this exact recert side effect, before cmd_boot reports the cluster ready.

Signed-off-by: Elior Erez <eerez@redhat.com>
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

cmd_boot now waits for pull-secret rollout through MCP master, forces machine-config-daemon resynchronization, recovers stuck rollout state, updates progress numbering, and adds tests for success, recovery, and timeout paths.

Changes

Pull-secret rollout during boot

Layer / File(s) Summary
Boot rollout synchronization and progress reporting
cluster-tool
Adds MCP master polling, machine-config-daemon resynchronization, recovery actions, updated progress steps, and operator count output.
Rollout test coverage
test_cluster_tool.py
Updates SSH mocks for MCP status queries and adds tests covering successful rollout, stuck MCP recovery, and rollout failure.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant cmd_boot
  participant machine-config-daemon
  participant MCP_master
  participant node
  cmd_boot->>machine-config-daemon: force resynchronization
  cmd_boot->>MCP_master: poll Updated condition
  MCP_master-->>cmd_boot: return rollout status
  cmd_boot->>node: clear and restate annotations
  cmd_boot->>machine-config-daemon: delete daemon pods
  cmd_boot->>MCP_master: poll Updated condition again
  MCP_master-->>cmd_boot: return final rollout status
Loading

Possibly related PRs

Suggested reviewers: omer-vishlitzky, danielerez

🚥 Pre-merge checks | ✅ 11
✅ Passed checks (11 passed)
Check name Status Explanation
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.
No-Hardcoded-Secrets ✅ Passed No hardcoded secrets, credentialed URLs, or secret-named literal assignments were added; the new base64 value is a short test fixture, not secret-shaped.
No-Weak-Crypto ✅ Passed No forbidden crypto, custom crypto, or non-constant-time secret comparison was added; the changes only add rollout waits and MCO recovery logic.
No-Injection-Vectors ✅ Passed No new eval/pickle/yaml/os.system/shell=True patterns were added; the only new shell interpolation is a Kubernetes node name from a constrained API field.
Container-Privileges ✅ Passed Patch only changes boot logic/tests; no container/K8s manifests or privilege flags were added.
No-Sensitive-Data-In-Logs ✅ Passed PASS: The added boot-step logs are generic, and the new rollout path does not print secrets, tokens, PII, or internal hostnames.
Ai-Attribution ✅ Passed No AI-tool usage was mentioned in the PR/commit, and the commit only has a Signed-off-by trailer—no Assisted-by, Generated-by, or Co-Authored-By found.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: waiting for pull-secret rollout to MCP master before boot reports ready.
✨ 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.

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

🤖 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 `@cluster-tool`:
- Around line 1126-1128: Update the timeout text in the pull-secret rollout
handling near the synced check to report the actual maximum wait of 25 minutes
instead of 30 minutes, while preserving the existing exit behavior and message
context.
- Line 1216: Update the completion message in the operator-waiting flow around
print_step to remove the tautological len(items)/len(items) count, replacing it
with concise wording that reports completion without redundant totals.
- Around line 1109-1119: In the unsynced recovery block, remove the `env.run_vm`
node lookup and use the already-known `vm_name` directly as the node identifier
in the patch command. Keep the existing patch and machine-config-daemon pod
deletion behavior unchanged.
- Around line 1077-1079: Remove the redundant kc_path and oc_vm assignments in
the “Waiting for pull-secret rollout” section of cmd_boot, reusing the identical
variables already initialized earlier in the same function. Preserve the
existing command execution behavior and single source of truth for the
kubeconfig path.
🪄 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: osac-project/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 70b5dad8-e59d-4894-a789-29daf41d3e2b

📥 Commits

Reviewing files that changed from the base of the PR and between 3a2d0b0 and c7e9c09.

📒 Files selected for processing (2)
  • cluster-tool
  • test_cluster_tool.py

Comment thread cluster-tool
Comment on lines +1077 to +1079
print_step(7, total, "Waiting for pull-secret rollout")
kc_path = "/etc/kubernetes/static-pod-resources/kube-apiserver-certs/secrets/node-kubeconfigs/lb-ext.kubeconfig"
oc_vm = f"sudo oc --kubeconfig={kc_path} --insecure-skip-tls-verify"

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 | 🔵 Trivial | ⚡ Quick win

Redundant kc_path/oc_vm redefinition.

These are already defined identically a few lines earlier (lines 1066-1067) in the same function scope and remain valid here. Recomputing them is dead duplication that can drift if the kubeconfig path ever changes (it's now defined 3 times in cmd_boot).

♻️ Proposed fix
         print_step(7, total, "Waiting for pull-secret rollout")
-        kc_path = "/etc/kubernetes/static-pod-resources/kube-apiserver-certs/secrets/node-kubeconfigs/lb-ext.kubeconfig"
-        oc_vm = f"sudo oc --kubeconfig={kc_path} --insecure-skip-tls-verify"
-
         # recert writes kubelet's config.json straight to disk instead of
📝 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
print_step(7, total, "Waiting for pull-secret rollout")
kc_path = "/etc/kubernetes/static-pod-resources/kube-apiserver-certs/secrets/node-kubeconfigs/lb-ext.kubeconfig"
oc_vm = f"sudo oc --kubeconfig={kc_path} --insecure-skip-tls-verify"
print_step(7, total, "Waiting for pull-secret rollout")
🤖 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 `@cluster-tool` around lines 1077 - 1079, Remove the redundant kc_path and
oc_vm assignments in the “Waiting for pull-secret rollout” section of cmd_boot,
reusing the identical variables already initialized earlier in the same
function. Preserve the existing command execution behavior and single source of
truth for the kubeconfig path.

Comment thread cluster-tool
Comment on lines +1109 to +1119
if not synced:
r = env.run_vm(vm_ip, f"{oc_vm} get nodes -o jsonpath='{{.items[0].metadata.name}}'", identity=key_path)
node_name = r.stdout.strip()
env.run_vm(
vm_ip,
f"{oc_vm} patch node {node_name} --type merge --patch "
f"'{{\"metadata\":{{\"annotations\":{{\"machineconfiguration.openshift.io/reason\":\"\","
f"\"machineconfiguration.openshift.io/state\":\"Done\"}}}}}}'",
identity=key_path,
)
env.run_vm(vm_ip, f"{oc_vm} delete pod -n openshift-machine-config-operator -l k8s-app=machine-config-daemon", identity=key_path)

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

Unnecessary, fragile API call to look up the node name; vm_name is already known.

node_name is fetched via oc get nodes -o jsonpath=... with the default check=True. If this single call fails transiently (plausible right after forcing an MCD resync, which can itself disrupt the API briefly on SNO), the whole self-heal path aborts and the entire clone gets rolled back for what could be a transient blip — an unguarded external call on a recovery path.

This lookup is also redundant: vm_name (test-infra-cluster-{clone_id}-master-0, set at line 892) is explicitly set as the node's hostname earlier via hostnamectl set-hostname in the recert fix-up script (line 1030), so it's already known and guaranteed to match. Using it directly removes both the fragility and the extra round trip.

🔧 Proposed fix
         if not synced:
-            r = env.run_vm(vm_ip, f"{oc_vm} get nodes -o jsonpath='{{.items[0].metadata.name}}'", identity=key_path)
-            node_name = r.stdout.strip()
             env.run_vm(
                 vm_ip,
-                f"{oc_vm} patch node {node_name} --type merge --patch "
+                f"{oc_vm} patch node {vm_name} --type merge --patch "
📝 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
if not synced:
r = env.run_vm(vm_ip, f"{oc_vm} get nodes -o jsonpath='{{.items[0].metadata.name}}'", identity=key_path)
node_name = r.stdout.strip()
env.run_vm(
vm_ip,
f"{oc_vm} patch node {node_name} --type merge --patch "
f"'{{\"metadata\":{{\"annotations\":{{\"machineconfiguration.openshift.io/reason\":\"\","
f"\"machineconfiguration.openshift.io/state\":\"Done\"}}}}}}'",
identity=key_path,
)
env.run_vm(vm_ip, f"{oc_vm} delete pod -n openshift-machine-config-operator -l k8s-app=machine-config-daemon", identity=key_path)
if not synced:
env.run_vm(
vm_ip,
f"{oc_vm} patch node {vm_name} --type merge --patch "
f"'{{\"metadata\":{{\"annotations\":{{\"machineconfiguration.openshift.io/reason\":\"\","
f"\"machineconfiguration.openshift.io/state\":\"Done\"}}}}}}'",
identity=key_path,
)
env.run_vm(vm_ip, f"{oc_vm} delete pod -n openshift-machine-config-operator -l k8s-app=machine-config-daemon", identity=key_path)
🤖 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 `@cluster-tool` around lines 1109 - 1119, In the unsynced recovery block,
remove the `env.run_vm` node lookup and use the already-known `vm_name` directly
as the node identifier in the patch command. Keep the existing patch and
machine-config-daemon pod deletion behavior unchanged.

Comment thread cluster-tool
Comment on lines +1126 to +1128
if not synced:
sys.exit("MachineConfigPool 'master' not Updated after pull-secret rollout (30 minutes).")
print_step(7, total, "Waiting for pull-secret rollout", "done")

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

Timeout message overstates the actual wait.

The exit message says "(30 minutes)" but the actual worst case is 90×10s + 60×10s = 1500s = 25 minutes. This can mislead on-call responders diagnosing a stuck boot.

📝 Proposed fix
-            sys.exit("MachineConfigPool 'master' not Updated after pull-secret rollout (30 minutes).")
+            sys.exit("MachineConfigPool 'master' not Updated after pull-secret rollout (25 minutes).")
📝 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
if not synced:
sys.exit("MachineConfigPool 'master' not Updated after pull-secret rollout (30 minutes).")
print_step(7, total, "Waiting for pull-secret rollout", "done")
if not synced:
sys.exit("MachineConfigPool 'master' not Updated after pull-secret rollout (25 minutes).")
print_step(7, total, "Waiting for pull-secret rollout", "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 `@cluster-tool` around lines 1126 - 1128, Update the timeout text in the
pull-secret rollout handling near the synced check to report the actual maximum
wait of 25 minutes instead of 30 minutes, while preserving the existing exit
behavior and message context.

Comment thread cluster-tool
detail = "\n".join(f" - {u}" for u in last_unhealthy)
sys.exit(f"Operators not healthy after 20 minutes.\nUnhealthy operators:\n{detail}")
print_step(8, total, "Waiting for operators", f"done ({len(items)}/{len(items)})")
print_step(9, total, "Waiting for operators", f"done ({len(items)}/{len(items)})")

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

Tautological (N/N) in the completion message.

The preceding loop only breaks when last_unhealthy is empty (i.e., all items are already healthy), so len(items)}/{len(items)} always prints an identical numerator and denominator, e.g. done (3/3). It conveys no real signal beyond the total count.

📝 Proposed fix
-        print_step(9, total, "Waiting for operators", f"done ({len(items)}/{len(items)})")
+        print_step(9, total, "Waiting for operators", f"done ({len(items)} operators)")
📝 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
print_step(9, total, "Waiting for operators", f"done ({len(items)}/{len(items)})")
print_step(9, total, "Waiting for operators", f"done ({len(items)} operators)")
🤖 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 `@cluster-tool` at line 1216, Update the completion message in the
operator-waiting flow around print_step to remove the tautological
len(items)/len(items) count, replacing it with concise wording that reports
completion without redundant totals.

@eliorerz eliorerz changed the title Wait for pull-secret rollout to MCP master before boot returns ready [NO-ISSUE] Wait for pull-secret rollout to MCP master before boot returns ready Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant