Skip to content

OCPBUGS-84695: feat(tnf): multi-node job controller framework#1655

Open
jaypoulz wants to merge 1 commit into
openshift:mainfrom
jaypoulz:jpoulin/commit-1-framework
Open

OCPBUGS-84695: feat(tnf): multi-node job controller framework#1655
jaypoulz wants to merge 1 commit into
openshift:mainfrom
jaypoulz:jpoulin/commit-1-framework

Conversation

@jaypoulz

@jaypoulz jaypoulz commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Implements the foundational framework for multi-node job scheduling with round-robin retry logic and drift detection:

  • Multi-node job support with NodeTarget struct for node-specific jobs
  • Round-robin retry logic with state tracking (AttemptNumber, NodeIndex, LastJobConfig)
  • Job config drift detection (resets retry state when config changes)
  • Degradation when retry attempts exhausted
  • IsControllerRunning() helper to prevent duplicate controllers
  • Shared node utility functions (GetNodeNames, DetermineReconciliationActions, IsNodeReady)
  • Job type labels for filtering and cleanup

See docs/tnf/job-controllers.md for detailed multi-node retry behavior and config drift examples.

Co-Authored-By: Claude Sonnet 4.5 noreply@anthropic.com

Summary by CodeRabbit

  • New Features

    • Coordinated TNF job execution now supports node-scoped and cluster-wide scheduling across eligible control-plane nodes.
    • Added “Blocked” and “Degraded” job conditions with automated retry progression, drift recovery, and node failover.
    • Introduced lifecycle-based restart helpers for node and cluster job controllers.
  • Bug Fixes

    • Stuck/failed Jobs are now auto-recovered by removing prolonged failures.
    • Update-setup now fails when the local Pacemaker cluster status can’t be verified, instead of being skipped.
  • Documentation

    • Added comprehensive TNF job-controller lifecycle and state-flow documentation.
  • Refactor

    • Reworked TNF job-controller orchestration to use the new lifecycle framework.

@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-robot openshift-ci-robot added jira/severity-important Referenced Jira bug's severity is important for the branch this PR is targeting. jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. labels Jul 21, 2026
@openshift-ci-robot

Copy link
Copy Markdown

@jaypoulz: This pull request references Jira Issue OCPBUGS-84695, 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:

Implements the foundational framework for multi-node job scheduling with round-robin retry logic and drift detection:

  • Multi-node job support with NodeTarget struct for node-specific jobs
  • Round-robin retry logic with state tracking (AttemptNumber, NodeIndex, LastJobConfig)
  • Job config drift detection (resets retry state when config changes)
  • Degradation when retry attempts exhausted
  • IsControllerRunning() helper to prevent duplicate controllers
  • Shared node utility functions (GetNodeNames, DetermineReconciliationActions, IsNodeReady)
  • Job type labels for filtering and cleanup

See docs/tnf/job-controllers.md for detailed multi-node retry behavior and config drift examples.

Co-Authored-By: Claude Sonnet 4.5 noreply@anthropic.com

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.

@openshift-ci-robot openshift-ci-robot added the jira/valid-bug Indicates that a referenced Jira bug is valid for the branch this PR is targeting. label Jul 21, 2026
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

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

Adds a TNF Job-controller lifecycle framework for node-scoped and cluster-wide Jobs, including readiness conditions, retry orchestration, restart helpers, stuck-Job recovery, operator wiring, utilities, tests, and documentation.

Changes

TNF Job lifecycle

Layer / File(s) Summary
Controller lifecycle and retry state
pkg/tnf/pkg/jobs/lifecycle.go
Adds controller tracking, readiness gating, blocked conditions, multi-node retry state, drift detection, round-robin scheduling, degraded-state handling, and serialized restart helpers.
TNF orchestration and node scheduling
pkg/tnf/operator/..., pkg/tnf/pkg/tools/nodes.go
Routes node listers and cluster-scoped controllers through setup, update-setup, after-setup, and fencing workflows.
Job recovery and condition contracts
pkg/tnf/pkg/jobs/jobcontroller.go, pkg/tnf/pkg/jobs/utils.go, pkg/tnf/pkg/tools/jobs.go
Adds stuck-Job deletion, expanded stopped-state detection, background propagation, and PascalCase condition names.
Lifecycle validation and framework documentation
pkg/tnf/pkg/jobs/*_test.go, docs/tnf/job-controllers.md
Tests routing, restart locking, retry progression, drift resets, blocked conditions, and stuck-Job recovery; documents framework behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant TNFOperator
  participant JobController
  participant KubernetesAPI
  participant OperatorStatus
  TNFOperator->>JobController: start or restart TNF Job controller
  JobController->>KubernetesAPI: inspect and configure Job
  JobController->>OperatorStatus: set Blocked or Degraded condition
  KubernetesAPI-->>JobController: report Job state
  JobController->>KubernetesAPI: delete or recreate Job
Loading

Possibly related PRs

Suggested reviewers: clobrano, everettraven


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
No-Sensitive-Data-In-Logs ❌ Error New logs emit node names/IPs and opaque config strings, which can expose internal hostnames or customer data. Redact or avoid raw node/IP/config values; log counts, job types, or hashed IDs instead.
Docstring Coverage ⚠️ Warning Docstring coverage is 65.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Test Structure And Quality ⚠️ Warning lifecycle_test.go still has compile-breaking test issues (nodeTarget/tt.node mismatch and a stray import block), so the test suite isn't structurally sound. Make the test struct fields/literals consistent, remove the orphaned import block, and rerun the pkg/tnf/pkg/jobs tests to confirm the suite compiles.
✅ Passed checks (12 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing a multi-node TNF job controller framework.
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 All test titles are static literals; none include dynamic node/namespace/pod/IP/UUID/date values or generated identifiers.
Microshift Test Compatibility ✅ Passed No Ginkgo e2e tests were added; the changed tests are plain Go unit tests and don’t introduce MicroShift-unsupported APIs/features.
Single Node Openshift (Sno) Test Compatibility ✅ Passed No new Ginkgo e2e specs were added; the changes are unit tests/docs and contain no It/Describe/Context/When or SNO assumptions.
Topology-Aware Scheduling Compatibility ✅ Passed TNF jobs are only started on DualReplica/external-etcd clusters, and handleNodes enforces exactly 2 control-plane nodes before scheduling.
Ote Binary Stdout Contract ✅ Passed No modified process-level entry points add stdout writes; touched Go files lack init/TestMain/RunSpecs setup or fmt.Print-style calls.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed No new Ginkgo/e2e specs were added; changed tests are plain testing.T unit tests with no IPv4-only or external-connectivity assumptions.
No-Weak-Crypto ✅ Passed No weak crypto primitives or new secret/token comparisons appear in the PR diff; the lone bytes.Equal secret check in starter.go pre-existed unchanged.
Container-Privileges ✅ Passed No new privileged/host settings appear in the PR diff; the only privileged YAML is pre-existing test fixture, and runtime manifests stay non-privileged.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@openshift-ci
openshift-ci Bot requested review from clobrano and everettraven July 21, 2026 17:04
@openshift-ci

openshift-ci Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign benluddy 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

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

🧹 Nitpick comments (2)
pkg/tnf/pkg/jobs/lifecycle_test.go (2)

691-691: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Don't ignore the Create return values in the retry-progression setup.

The fake-client Create calls at Lines 691, 700, 710, 714, and 731 discard the returned error. If a Create silently fails, the subsequent syncMultiNodeJobState would find no job (NotFound → no-op), the retry state wouldn't advance, and the test could pass or fail for the wrong reason. Assert require.NoError(...) on each so setup failures surface immediately.

As per path instructions: "Never ignore error returns".

💡 Example fix for one call
-	fakeKubeClient.BatchV1().Jobs(operatorclient.TargetNamespace).Create(ctx, failedJob, metav1.CreateOptions{})
+	_, err = fakeKubeClient.BatchV1().Jobs(operatorclient.TargetNamespace).Create(ctx, failedJob, metav1.CreateOptions{})
+	require.NoError(t, 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 `@pkg/tnf/pkg/jobs/lifecycle_test.go` at line 691, Update the retry-progression
test setup to check every fake-client Job Create call with require.NoError,
including the calls near lines 691, 700, 710, 714, and 731. Pass the test
context and each returned error so setup failures stop the test immediately
before syncMultiNodeJobState runs.

Source: Path instructions


211-223: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

expectControllerRun doesn't affect the assertion — the "skip when already running" case isn't actually verified.

Both branches assert require.True(t, isRunning), so the field never changes what is checked. For "Skip starting controller when already running", runningControllers[key] is pre-seeded to true, so the assertion passes whether or not a duplicate start was skipped. Consider distinguishing an actual skip (e.g., assert no new goroutine/controller was created, or that the controller count didn't change) so the dedup path is meaningfully exercised.

🤖 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 `@pkg/tnf/pkg/jobs/lifecycle_test.go` around lines 211 - 223, The lifecycle
test’s expectControllerRun branch does not distinguish starting a controller
from skipping an already-running one. Update the verification around
runningControllers and the “Skip starting controller when already running” case
to assert an observable deduplication outcome, such as unchanged
controller/goroutine count, while retaining the running-state assertion for
newly started controllers.
🤖 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 `@docs/tnf/job-controllers.md`:
- Around line 11-15: Update the Blocked Condition section in job-controllers.md
to remove the exhausted-retries bullet, keeping only the node-not-ready timeout
behavior. Add that exhausted retries set the <JobName>Degraded condition to the
Degraded description, using the existing condition naming and format.

---

Nitpick comments:
In `@pkg/tnf/pkg/jobs/lifecycle_test.go`:
- Line 691: Update the retry-progression test setup to check every fake-client
Job Create call with require.NoError, including the calls near lines 691, 700,
710, 714, and 731. Pass the test context and each returned error so setup
failures stop the test immediately before syncMultiNodeJobState runs.
- Around line 211-223: The lifecycle test’s expectControllerRun branch does not
distinguish starting a controller from skipping an already-running one. Update
the verification around runningControllers and the “Skip starting controller
when already running” case to assert an observable deduplication outcome, such
as unchanged controller/goroutine count, while retaining the running-state
assertion for newly started controllers.
🪄 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: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0c16170f-d107-4fba-bcb4-ba3d437bdd0c

📥 Commits

Reviewing files that changed from the base of the PR and between 1fe5873 and 708a96d.

📒 Files selected for processing (12)
  • docs/tnf/job-controllers.md
  • pkg/tnf/operator/nodehandler.go
  • pkg/tnf/operator/starter.go
  • pkg/tnf/pkg/jobs/jobcontroller.go
  • pkg/tnf/pkg/jobs/jobcontroller_test.go
  • pkg/tnf/pkg/jobs/lifecycle.go
  • pkg/tnf/pkg/jobs/lifecycle_test.go
  • pkg/tnf/pkg/jobs/tnf.go
  • pkg/tnf/pkg/jobs/tnf_test.go
  • pkg/tnf/pkg/jobs/utils.go
  • pkg/tnf/pkg/tools/jobs.go
  • pkg/tnf/pkg/tools/nodes.go
💤 Files with no reviewable changes (2)
  • pkg/tnf/pkg/jobs/tnf_test.go
  • pkg/tnf/pkg/jobs/tnf.go

Comment thread docs/tnf/job-controllers.md Outdated

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

🧹 Nitpick comments (2)
pkg/tnf/pkg/jobs/lifecycle.go (1)

90-153: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated blocked-condition tracking logic.

checkNodeReadinessAndSetCondition (single-node path) and the affected-nodes block inside syncMultiNodeJobState (multi-node path) both independently implement "track first-blocked timestamp, set/clear TNF<JobName>Blocked after 10 minutes" against the shared jobBlockedSince map. The logic has already started to diverge (message format differs: [%s] for a single node vs %v for a slice). Extracting a shared helper (e.g. updateBlockedCondition(ctx, jobName, notReadyNodeNames []string, ...)) would remove this duplication and prevent future drift.

Also applies to: 164-231

🤖 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 `@pkg/tnf/pkg/jobs/lifecycle.go` around lines 90 - 153, The blocked-condition
tracking is duplicated between checkNodeReadinessAndSetCondition and the
affected-nodes handling in syncMultiNodeJobState. Extract the shared timestamp
tracking and TNF<JobName>Blocked condition set/clear behavior into a helper such
as updateBlockedCondition, pass the relevant not-ready node names, and update
both call sites to use it with consistent message formatting while preserving
existing readiness error behavior.
pkg/operator/ceohelpers/node_helpers.go (1)

52-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse tools.IsNodeReady here. This duplicates the Ready-condition check already in pkg/tnf/pkg/tools/nodes.go and can be replaced directly.

🤖 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 `@pkg/operator/ceohelpers/node_helpers.go` around lines 52 - 62, Update the
Ready-node filtering loop to call the existing tools.IsNodeReady helper for each
node instead of manually iterating node.Status.Conditions and checking condition
type/status. Preserve appending each ready node once and retain the existing
filtering behavior.
🤖 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 `@pkg/tnf/pkg/jobs/lifecycle.go`:
- Around line 164-231: Prevent the blocked-node path in syncMultiNodeJobState
from reaching configureMultiNodeJob with an unset retryState[jobName].
Initialize the JobRetryState before returning from a blocked sync, or have
configureMultiNodeJob detect a nil state after its syncMultiNodeJobState call
and return without locking it, while preserving the existing job-blocking
behavior.
- Around line 360-403: Update the failed-job handling flow around
IsFailed/IsStopped so the next node and attempt values are calculated without
mutating state, delete the failed job with DeleteAndWait first, and only commit
NodeIndex and AttemptNumber after deletion succeeds. Preserve degraded-condition
updates and retry logging, while ensuring a delete error returns without
advancing retry state.

---

Nitpick comments:
In `@pkg/operator/ceohelpers/node_helpers.go`:
- Around line 52-62: Update the Ready-node filtering loop to call the existing
tools.IsNodeReady helper for each node instead of manually iterating
node.Status.Conditions and checking condition type/status. Preserve appending
each ready node once and retain the existing filtering behavior.

In `@pkg/tnf/pkg/jobs/lifecycle.go`:
- Around line 90-153: The blocked-condition tracking is duplicated between
checkNodeReadinessAndSetCondition and the affected-nodes handling in
syncMultiNodeJobState. Extract the shared timestamp tracking and
TNF<JobName>Blocked condition set/clear behavior into a helper such as
updateBlockedCondition, pass the relevant not-ready node names, and update both
call sites to use it with consistent message formatting while preserving
existing readiness error 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: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 484b8923-171e-4cb0-9423-d32c859e15c7

📥 Commits

Reviewing files that changed from the base of the PR and between 708a96d and 37a5d9f.

📒 Files selected for processing (10)
  • bindata/tnfdeployment/role.yaml
  • docs/tnf/job-controllers.md
  • pkg/operator/ceohelpers/node_helpers.go
  • pkg/tnf/pkg/jobs/jobcontroller.go
  • pkg/tnf/pkg/jobs/lifecycle.go
  • pkg/tnf/pkg/jobs/lifecycle_test.go
  • pkg/tnf/pkg/jobs/tnf.go
  • pkg/tnf/pkg/jobs/tnf_test.go
  • pkg/tnf/pkg/jobs/utils.go
  • pkg/tnf/pkg/tools/nodes.go
💤 Files with no reviewable changes (2)
  • pkg/tnf/pkg/jobs/tnf.go
  • pkg/tnf/pkg/jobs/tnf_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/tnf/pkg/jobs/utils.go
  • pkg/tnf/pkg/jobs/lifecycle_test.go

Comment thread pkg/tnf/pkg/jobs/lifecycle.go
Comment thread pkg/tnf/pkg/jobs/lifecycle.go
@jaypoulz
jaypoulz force-pushed the jpoulin/commit-1-framework branch from 37a5d9f to b09bb41 Compare July 21, 2026 18:44

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

🧹 Nitpick comments (1)
pkg/tnf/pkg/jobs/lifecycle.go (1)

262-263: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Compare target nodes independent of order. handleFencingSecretChange re-lists control-plane nodes on every sync, GetNodeNames preserves that list order, and slicesEqual treats different permutations as drift. Sorting or set-comparing here avoids deleting/recreating the Job and resetting retries when the API returns the same nodes in a different order.

🤖 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 `@pkg/tnf/pkg/jobs/lifecycle.go` around lines 262 - 263, Update the target-node
comparison in handleFencingSecretChange so state.TargetNodes and the current
GetNodeNames result are compared independent of ordering, using sorting or set
equality before slicesEqual. Preserve the existing nodesChanged behavior for
actual membership changes and avoid treating permutations as drift.
🤖 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 `@docs/tnf/job-controllers.md`:
- Around line 18-23: Update the Degraded Condition documentation in
docs/tnf/job-controllers.md to clarify that retry cycles continue after the
retry budget is exhausted: describe the limit as 6 tries per cycle, with cycles
continuing until recovery. Remove “permanent failure” and any wording implying
Degraded is terminal, while retaining the existing condition name, reason, and
active-until-success behavior.

---

Nitpick comments:
In `@pkg/tnf/pkg/jobs/lifecycle.go`:
- Around line 262-263: Update the target-node comparison in
handleFencingSecretChange so state.TargetNodes and the current GetNodeNames
result are compared independent of ordering, using sorting or set equality
before slicesEqual. Preserve the existing nodesChanged behavior for actual
membership changes and avoid treating permutations as drift.
🪄 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: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 9b247758-ff1a-4c98-98c7-ab9a47cd52d2

📥 Commits

Reviewing files that changed from the base of the PR and between 37a5d9f and b09bb41.

📒 Files selected for processing (12)
  • docs/tnf/job-controllers.md
  • pkg/tnf/operator/nodehandler.go
  • pkg/tnf/operator/starter.go
  • pkg/tnf/pkg/jobs/jobcontroller.go
  • pkg/tnf/pkg/jobs/jobcontroller_test.go
  • pkg/tnf/pkg/jobs/lifecycle.go
  • pkg/tnf/pkg/jobs/lifecycle_test.go
  • pkg/tnf/pkg/jobs/tnf.go
  • pkg/tnf/pkg/jobs/tnf_test.go
  • pkg/tnf/pkg/jobs/utils.go
  • pkg/tnf/pkg/tools/jobs.go
  • pkg/tnf/pkg/tools/nodes.go
💤 Files with no reviewable changes (2)
  • pkg/tnf/pkg/jobs/tnf_test.go
  • pkg/tnf/pkg/jobs/tnf.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • pkg/tnf/pkg/jobs/jobcontroller.go
  • pkg/tnf/pkg/jobs/utils.go
  • pkg/tnf/pkg/jobs/lifecycle_test.go

Comment thread docs/tnf/job-controllers.md Outdated
@jaypoulz
jaypoulz force-pushed the jpoulin/commit-1-framework branch from b09bb41 to c3f8662 Compare July 21, 2026 19:14

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

♻️ Duplicate comments (1)
pkg/tnf/pkg/jobs/lifecycle.go (1)

336-391: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Retry state is still advanced before the failed job's deletion is confirmed.

state.NodeIndex/state.AttemptNumber are mutated at Lines 379-380 before DeleteAndWait runs at Lines 383-386. If deletion fails, the error is returned but the state mutation already happened; the next sync re-detects the same still-existing failed Job and advances the index/attempt again, skipping a node/attempt due to a transient delete error. This is the same issue flagged in a prior review on this segment and does not appear to have been addressed yet (unlike the sibling nil-pointer fix, which was).

🐛 Proposed fix: only commit state after a successful delete
-		// Apply state changes
-		state.NodeIndex = nextNodeIndex
-		state.AttemptNumber = nextAttemptNumber
-
 		// Delete the failed job so it can be recreated on next node
 		klog.Infof("Deleting failed job %s to retry on node index %d", jobName, nextNodeIndex)
 		if err := DeleteAndWait(ctx, kubeClient, jobName, operatorclient.TargetNamespace); err != nil {
 			return fmt.Errorf("failed to delete failed job: %w", err)
 		}
+
+		// Apply state changes only after successful deletion, so a delete
+		// failure doesn't cause a node/attempt to be skipped on retry.
+		state.NodeIndex = nextNodeIndex
+		state.AttemptNumber = nextAttemptNumber
 	}
🤖 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 `@pkg/tnf/pkg/jobs/lifecycle.go` around lines 336 - 391, Move the state
assignments in the failed-job retry flow so state.NodeIndex and
state.AttemptNumber are committed only after DeleteAndWait succeeds. Keep the
calculated nextNodeIndex and nextAttemptNumber as local values during deletion,
and return the deletion error without mutating retry state if deletion fails.
🤖 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 `@pkg/tnf/operator/starter.go`:
- Around line 429-434: Update schedulableNodesFunc to exclude control-plane
nodes carrying the arbiter:NoSchedule taint before returning the node slice.
Preserve the existing control-plane label selection and ensure only nodes
eligible for scheduling are available to the lifecycle’s NodeName assignment.

In `@pkg/tnf/pkg/jobs/lifecycle.go`:
- Around line 3-25: Add the standard library "slices" import to the import block
in lifecycle.go so the slicesEqual function can resolve its calls to slices.Sort
and compile successfully.

---

Duplicate comments:
In `@pkg/tnf/pkg/jobs/lifecycle.go`:
- Around line 336-391: Move the state assignments in the failed-job retry flow
so state.NodeIndex and state.AttemptNumber are committed only after
DeleteAndWait succeeds. Keep the calculated nextNodeIndex and nextAttemptNumber
as local values during deletion, and return the deletion error without mutating
retry state if deletion fails.
🪄 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: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: bd8a999a-cd72-4a2e-9365-f75ad6b94e7d

📥 Commits

Reviewing files that changed from the base of the PR and between b09bb41 and c3f8662.

📒 Files selected for processing (12)
  • docs/tnf/job-controllers.md
  • pkg/tnf/operator/nodehandler.go
  • pkg/tnf/operator/starter.go
  • pkg/tnf/pkg/jobs/jobcontroller.go
  • pkg/tnf/pkg/jobs/jobcontroller_test.go
  • pkg/tnf/pkg/jobs/lifecycle.go
  • pkg/tnf/pkg/jobs/lifecycle_test.go
  • pkg/tnf/pkg/jobs/tnf.go
  • pkg/tnf/pkg/jobs/tnf_test.go
  • pkg/tnf/pkg/jobs/utils.go
  • pkg/tnf/pkg/tools/jobs.go
  • pkg/tnf/pkg/tools/nodes.go
💤 Files with no reviewable changes (2)
  • pkg/tnf/pkg/jobs/tnf_test.go
  • pkg/tnf/pkg/jobs/tnf.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • pkg/tnf/pkg/tools/nodes.go
  • pkg/tnf/pkg/jobs/jobcontroller.go
  • pkg/tnf/pkg/tools/jobs.go
  • pkg/tnf/operator/nodehandler.go
  • pkg/tnf/pkg/jobs/jobcontroller_test.go
  • pkg/tnf/pkg/jobs/utils.go
  • pkg/tnf/pkg/jobs/lifecycle_test.go

Comment thread pkg/tnf/operator/starter.go
Comment thread pkg/tnf/pkg/jobs/lifecycle.go
@jaypoulz
jaypoulz force-pushed the jpoulin/commit-1-framework branch from c3f8662 to d052f7b Compare July 21, 2026 19:51
if err != nil {
klog.Infof("Cluster not running (err: %v), skipping update-setup on this node", err)
return nil
return fmt.Errorf("pacemaker is not running on this node %s: %w", currentNodeName, err)

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.

This is necessary because we need the update-setup job to round-robin 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 `@pkg/tnf/pkg/jobs/lifecycle_test.go`:
- Around line 72-148: Reconcile the node field naming and types in both
table-driven suites in pkg/tnf/pkg/jobs/lifecycle_test.go:72-148 and
pkg/tnf/pkg/jobs/lifecycle_test.go:237-254. In TestRunTNFJobController, make
struct literals match the declared field and existing tt.node reads. In the
restart/run test table, unify the declaration, literals, and loop reads with the
node parameter type expected by RestartNodeJobOrRunController and
RunNodeJobController, passing the correct node value or pointer.
- Around line 747-760: Remove the orphaned duplicate import block appearing
after TestSyncMultiNodeJobState_RetryProgression; retain only the valid
top-of-file imports and leave the test implementation unchanged.

In `@pkg/tnf/pkg/jobs/lifecycle.go`:
- Around line 284-312: In the nodesChanged/configChanged branch of the lifecycle
logic, defer updating state.AttemptNumber, state.NodeIndex, state.TargetNodes,
and state.LastJobConfig until after the existing job lookup and any
DeleteAndWait call complete successfully. Preserve the current error returns for
lookup or deletion failures so retries still see the original state; commit the
new retry and configuration state immediately before returning nil on success.
- Around line 644-705: Fix the undefined nodeTarget reference in
RestartNodeJobOrRunController by passing the existing node parameter to
RunNodeJobController. Preserve the surrounding controller invocation and restart
handling unchanged.
- Around line 519-543: The job hook’s nil return for an unready node does not
prevent syncManaged from reaching ApplyJob. Update the syncManaged flow around
the hook execution to explicitly stop and skip ApplyJob when admission is
blocked, while preserving normal application for ready jobs and propagating
actual hook errors.

In `@pkg/tnf/pkg/tools/nodes.go`:
- Around line 54-66: The HasArbiterNoScheduleTaint function uses the wrong taint
key. Update its taint check to match OpenShift’s node-role.kubernetes.io/arbiter
key while preserving the NoSchedule effect and existing nil-node 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: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0f079ba3-fae7-48be-8b8c-0407a925d03f

📥 Commits

Reviewing files that changed from the base of the PR and between c3f8662 and d052f7b.

📒 Files selected for processing (13)
  • docs/tnf/job-controllers.md
  • pkg/tnf/operator/nodehandler.go
  • pkg/tnf/operator/starter.go
  • pkg/tnf/pkg/jobs/jobcontroller.go
  • pkg/tnf/pkg/jobs/jobcontroller_test.go
  • pkg/tnf/pkg/jobs/lifecycle.go
  • pkg/tnf/pkg/jobs/lifecycle_test.go
  • pkg/tnf/pkg/jobs/tnf.go
  • pkg/tnf/pkg/jobs/tnf_test.go
  • pkg/tnf/pkg/jobs/utils.go
  • pkg/tnf/pkg/tools/jobs.go
  • pkg/tnf/pkg/tools/nodes.go
  • pkg/tnf/update-setup/runner.go
💤 Files with no reviewable changes (2)
  • pkg/tnf/pkg/jobs/tnf.go
  • pkg/tnf/pkg/jobs/tnf_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • pkg/tnf/pkg/tools/jobs.go
  • docs/tnf/job-controllers.md
  • pkg/tnf/pkg/jobs/jobcontroller.go
  • pkg/tnf/operator/starter.go
  • pkg/tnf/pkg/jobs/utils.go
  • pkg/tnf/pkg/jobs/jobcontroller_test.go
  • pkg/tnf/operator/nodehandler.go

Comment thread pkg/tnf/pkg/jobs/lifecycle_test.go Outdated
Comment thread pkg/tnf/pkg/jobs/lifecycle_test.go Outdated
Comment thread pkg/tnf/pkg/jobs/lifecycle.go
Comment thread pkg/tnf/pkg/jobs/lifecycle.go
Comment on lines +644 to +705
func RestartNodeJobOrRunController(
ctx context.Context,
jobType tools.JobType,
node *corev1.Node,
retries int,
controllerContext *controllercmd.ControllerContext,
operatorClient v1helpers.StaticPodOperatorClient,
kubeClient kubernetes.Interface,
kubeInformersForNamespaces v1helpers.KubeInformersForNamespaces,
conditions []string,
existingJobCompletionTimeout time.Duration) error {

jobName := jobType.GetJobName(&node.Name)

// Acquire a lock for this specific job to prevent parallel execution
restartJobLocksMutex.Lock()
jobLock, exists := restartJobLocks[jobName]
if !exists {
jobLock = &sync.Mutex{}
restartJobLocks[jobName] = jobLock
}
restartJobLocksMutex.Unlock()

jobLock.Lock()
defer jobLock.Unlock()

// Check if job already exists
jobExists := true
_, err := kubeClient.BatchV1().Jobs(operatorclient.TargetNamespace).Get(ctx, jobName, v1.GetOptions{})
if err != nil {
if !apierrors.IsNotFound(err) {
return fmt.Errorf("failed to check for existing job %s: %w", jobName, err)
}
jobExists = false
}

// Always run the controller (CEO might have been restarted)
RunNodeJobController(ctx, jobType, nodeTarget, retries, controllerContext, operatorClient, kubeClient, kubeInformersForNamespaces, conditions)

if !jobExists {
// No existing job - reset retry state to start fresh
resetJobRetryState(jobName)
return nil
}

// Job exists, wait for it to stop
klog.Infof("Job %s already exists, waiting for it to stop", jobName)
if err := WaitForStopped(ctx, kubeClient, jobName, operatorclient.TargetNamespace, existingJobCompletionTimeout); err != nil {
return fmt.Errorf("failed to wait for job %s to stop: %w", jobName, err)
}

// Delete the job so the controller can recreate it
klog.Infof("Deleting existing job %s", jobName)
if err := DeleteAndWait(ctx, kubeClient, jobName, operatorclient.TargetNamespace); err != nil {
return fmt.Errorf("failed to delete existing job %s: %w", jobName, err)
}

// Reset retry state when starting fresh after deleting old job
resetJobRetryState(jobName)

return nil
}

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 | 🔴 Critical | ⚡ Quick win

Undefined identifier nodeTarget — this will not compile.

The function parameter is named node (line 647), but line 681 passes nodeTarget, which is never declared anywhere in this function. This is a build-breaking typo.

🐛 Proposed fix
 	// Always run the controller (CEO might have been restarted)
-	RunNodeJobController(ctx, jobType, nodeTarget, retries, controllerContext, operatorClient, kubeClient, kubeInformersForNamespaces, conditions)
+	RunNodeJobController(ctx, jobType, node, retries, controllerContext, operatorClient, kubeClient, kubeInformersForNamespaces, conditions)
📝 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 RestartNodeJobOrRunController(
ctx context.Context,
jobType tools.JobType,
node *corev1.Node,
retries int,
controllerContext *controllercmd.ControllerContext,
operatorClient v1helpers.StaticPodOperatorClient,
kubeClient kubernetes.Interface,
kubeInformersForNamespaces v1helpers.KubeInformersForNamespaces,
conditions []string,
existingJobCompletionTimeout time.Duration) error {
jobName := jobType.GetJobName(&node.Name)
// Acquire a lock for this specific job to prevent parallel execution
restartJobLocksMutex.Lock()
jobLock, exists := restartJobLocks[jobName]
if !exists {
jobLock = &sync.Mutex{}
restartJobLocks[jobName] = jobLock
}
restartJobLocksMutex.Unlock()
jobLock.Lock()
defer jobLock.Unlock()
// Check if job already exists
jobExists := true
_, err := kubeClient.BatchV1().Jobs(operatorclient.TargetNamespace).Get(ctx, jobName, v1.GetOptions{})
if err != nil {
if !apierrors.IsNotFound(err) {
return fmt.Errorf("failed to check for existing job %s: %w", jobName, err)
}
jobExists = false
}
// Always run the controller (CEO might have been restarted)
RunNodeJobController(ctx, jobType, nodeTarget, retries, controllerContext, operatorClient, kubeClient, kubeInformersForNamespaces, conditions)
if !jobExists {
// No existing job - reset retry state to start fresh
resetJobRetryState(jobName)
return nil
}
// Job exists, wait for it to stop
klog.Infof("Job %s already exists, waiting for it to stop", jobName)
if err := WaitForStopped(ctx, kubeClient, jobName, operatorclient.TargetNamespace, existingJobCompletionTimeout); err != nil {
return fmt.Errorf("failed to wait for job %s to stop: %w", jobName, err)
}
// Delete the job so the controller can recreate it
klog.Infof("Deleting existing job %s", jobName)
if err := DeleteAndWait(ctx, kubeClient, jobName, operatorclient.TargetNamespace); err != nil {
return fmt.Errorf("failed to delete existing job %s: %w", jobName, err)
}
// Reset retry state when starting fresh after deleting old job
resetJobRetryState(jobName)
return nil
}
func RestartNodeJobOrRunController(
ctx context.Context,
jobType tools.JobType,
node *corev1.Node,
retries int,
controllerContext *controllercmd.ControllerContext,
operatorClient v1helpers.StaticPodOperatorClient,
kubeClient kubernetes.Interface,
kubeInformersForNamespaces v1helpers.KubeInformersForNamespaces,
conditions []string,
existingJobCompletionTimeout time.Duration) error {
jobName := jobType.GetJobName(&node.Name)
// Acquire a lock for this specific job to prevent parallel execution
restartJobLocksMutex.Lock()
jobLock, exists := restartJobLocks[jobName]
if !exists {
jobLock = &sync.Mutex{}
restartJobLocks[jobName] = jobLock
}
restartJobLocksMutex.Unlock()
jobLock.Lock()
defer jobLock.Unlock()
// Check if job already exists
jobExists := true
_, err := kubeClient.BatchV1().Jobs(operatorclient.TargetNamespace).Get(ctx, jobName, v1.GetOptions{})
if err != nil {
if !apierrors.IsNotFound(err) {
return fmt.Errorf("failed to check for existing job %s: %w", jobName, err)
}
jobExists = false
}
// Always run the controller (CEO might have been restarted)
RunNodeJobController(ctx, jobType, node, retries, controllerContext, operatorClient, kubeClient, kubeInformersForNamespaces, conditions)
if !jobExists {
// No existing job - reset retry state to start fresh
resetJobRetryState(jobName)
return nil
}
// Job exists, wait for it to stop
klog.Infof("Job %s already exists, waiting for it to stop", jobName)
if err := WaitForStopped(ctx, kubeClient, jobName, operatorclient.TargetNamespace, existingJobCompletionTimeout); err != nil {
return fmt.Errorf("failed to wait for job %s to stop: %w", jobName, err)
}
// Delete the job so the controller can recreate it
klog.Infof("Deleting existing job %s", jobName)
if err := DeleteAndWait(ctx, kubeClient, jobName, operatorclient.TargetNamespace); err != nil {
return fmt.Errorf("failed to delete existing job %s: %w", jobName, err)
}
// Reset retry state when starting fresh after deleting old job
resetJobRetryState(jobName)
return 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 `@pkg/tnf/pkg/jobs/lifecycle.go` around lines 644 - 705, Fix the undefined
nodeTarget reference in RestartNodeJobOrRunController by passing the existing
node parameter to RunNodeJobController. Preserve the surrounding controller
invocation and restart handling unchanged.

Comment on lines +54 to +66
// HasArbiterNoScheduleTaint returns true if the node has the arbiter:NoSchedule taint.
// Arbiter nodes in SNO+1 configurations are not eligible for normal workload scheduling.
func HasArbiterNoScheduleTaint(node *corev1.Node) bool {
if node == nil {
return false
}
for _, taint := range node.Spec.Taints {
if taint.Key == "arbiter" && taint.Effect == corev1.TaintEffectNoSchedule {
return true
}
}
return false
}

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for the arbiter taint key elsewhere in the repo (installer/MCO/topology helpers)
rg -nP 'arbiter' --type=go -C2

Repository: openshift/cluster-etcd-operator

Length of output: 169


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file and inspect nearby code/tests for the helper usage.
ast-grep outline pkg/tnf/pkg/tools/nodes.go --view expanded || true

printf '\n--- nodes.go ---\n'
cat -n pkg/tnf/pkg/tools/nodes.go | sed -n '1,220p'

printf '\n--- repo search: arbiter / taint key / schedulable nodes ---\n'
rg -n --hidden --glob '!**/*vendor*' --glob '!**/node_modules/**' 'node-role\.kubernetes\.io/arbiter|arbiter:NoSchedule|HasArbiterNoScheduleTaint|FilterSchedulableNodes|NoSchedule' .

Repository: openshift/cluster-etcd-operator

Length of output: 4840


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check public docs references in the repo, if any, that define the arbiter topology or taint key.
rg -n --hidden --glob '!**/*vendor*' --glob '!**/node_modules/**' 'Two-Node-with-Arbiter|arbiter topology|arbiter node|node-role\.kubernetes\.io/arbiter' .

Repository: openshift/cluster-etcd-operator

Length of output: 1299


🌐 Web query:

OpenShift arbiter node taint key node-role.kubernetes.io/arbiter NoSchedule documentation

💡 Result:

The taint key node-role.kubernetes.io/arbiter with the effect NoSchedule is used in OpenShift to prevent standard application workloads and non-essential control plane components from being scheduled onto an arbiter node [1][2][3]. An arbiter node is a specialized, lightweight node role introduced to support topologies such as two-node OpenShift clusters [4][2][5]. Its primary function is to store etcd data to maintain quorum and prevent split-brain scenarios in compact, cost-effective deployments [4][5][6]. Because the arbiter node is intended solely for etcd consensus and does not run standard control plane components (like kube-apiserver or kube-controller-manager) or application workloads, this taint is applied to ensure it remains dedicated to its specific role [4][3][5]. If you need to schedule specific infrastructure components or workloads onto an arbiter node, you must add a corresponding toleration to the pod or deployment specification [3]: tolerations: - effect: NoSchedule key: node-role.kubernetes.io/arbiter While the arbiter node is a regular Red Hat CoreOS node running the Kubelet, the arbiter role prevents automatic scheduling of general pods [3]. Technical documentation and community resources emphasize that this node role is distinct and handled specifically by the OpenShift infrastructure [7][8][9].

Citations:


🌐 Web query:

site:docs.redhat.com OpenShift arbiter taint key effect NoSchedule "node-role.kubernetes.io/arbiter"

💡 Result:

In OpenShift Container Platform, the arbiter node is a specialized node type used in Two-Node OpenShift with Arbiter (TNA) cluster topologies [1][2]. Its primary purpose is to store etcd data to maintain quorum and prevent split-brain conditions in a configuration that only has two control plane nodes [1][3]. Arbiter nodes are explicitly restricted from running workloads and do not run additional control plane components like kube-apiserver or kube-controller-manager [1][2]. To enforce this, arbiter nodes are automatically tainted with the key node-role.kubernetes.io/arbiter and the effect NoSchedule [1]. Because of this NoSchedule taint, the Kubernetes scheduler will not place pods on an arbiter node unless those pods have a corresponding toleration that matches the taint [4][5]. This design ensures that the arbiter node remains dedicated to its etcd-quorum function and is not used for general-purpose application hosting [1][2].

Citations:


Use the OpenShift arbiter taint key here

HasArbiterNoScheduleTaint checks arbiter, but OpenShift taints arbiter nodes with node-role.kubernetes.io/arbiter:NoSchedule. As written, arbiter nodes will never match and FilterSchedulableNodes will keep including them.

Suggested fix
-		if taint.Key == "arbiter" && taint.Effect == corev1.TaintEffectNoSchedule {
+		if taint.Key == "node-role.kubernetes.io/arbiter" && taint.Effect == corev1.TaintEffectNoSchedule {
📝 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
// HasArbiterNoScheduleTaint returns true if the node has the arbiter:NoSchedule taint.
// Arbiter nodes in SNO+1 configurations are not eligible for normal workload scheduling.
func HasArbiterNoScheduleTaint(node *corev1.Node) bool {
if node == nil {
return false
}
for _, taint := range node.Spec.Taints {
if taint.Key == "arbiter" && taint.Effect == corev1.TaintEffectNoSchedule {
return true
}
}
return false
}
// HasArbiterNoScheduleTaint returns true if the node has the arbiter:NoSchedule taint.
// Arbiter nodes in SNO+1 configurations are not eligible for normal workload scheduling.
func HasArbiterNoScheduleTaint(node *corev1.Node) bool {
if node == nil {
return false
}
for _, taint := range node.Spec.Taints {
if taint.Key == "node-role.kubernetes.io/arbiter" && taint.Effect == corev1.TaintEffectNoSchedule {
return true
}
}
return false
}
🤖 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 `@pkg/tnf/pkg/tools/nodes.go` around lines 54 - 66, The
HasArbiterNoScheduleTaint function uses the wrong taint key. Update its taint
check to match OpenShift’s node-role.kubernetes.io/arbiter key while preserving
the NoSchedule effect and existing nil-node behavior.

@jaypoulz
jaypoulz force-pushed the jpoulin/commit-1-framework branch 4 times, most recently from 59545f5 to 22df05d Compare July 21, 2026 20:58
Implements the foundational framework for multi-node job scheduling with round-robin
retry logic and drift detection:

- Multi-node job support with NodeTarget struct for node-specific jobs
- Round-robin retry logic with state tracking (AttemptNumber, NodeIndex, LastJobConfig)
- Job config drift detection (resets retry state when config changes)
- Degradation when retry attempts exhausted
- IsControllerRunning() helper to prevent duplicate controllers
- Shared node utility functions (GetNodeNames, DetermineReconciliationActions, IsNodeReady)
- Job type labels for filtering and cleanup

See docs/tnf/job-controllers.md for detailed multi-node retry behavior and config drift examples.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Refactoring updates:

- Split API into RunNodeJobController/RunClusterJobController (eliminates conditionals)

- Unified node readiness checking to avoid duplicate condition setting

- Update ToPascalCase to capitalize 'tnf' as 'TNF' (acronym, not 'Tnf')

- Extract duplicate blocked condition logic into manageBlockedCondition() helper

- All exports verified as intentional (public API)

OCPBUGS-84695: feat(tnf): bootstrap job controllers and startup logic

Implements job controller startup with dual-mode behavior (bootstrap vs runtime) and
individual job implementations:

- StartJobControllers() with bootstrap vs runtime separation
- Bootstrap mode: exponential backoff retry, requires exactly 2 ready nodes, sets degraded condition
- Runtime mode: idempotent restart support, handles 1-2 nodes, fast path when setup job complete
- startTnfJobcontrollers(): waits for etcd bootstrap, stable revision, creates per-node and cluster-wide controllers
- Auth job implementation: sets hacluster password (required before Pacemaker operations)
- After-setup job implementation:
  * Pre-transition: waits for setup job completion
  * Post-transition: waits for update-setup job containing this node
  * Disables kubelet service (Pacemaker owns it now)
  * Prevents kubelet disable race during Day 2 node addition
- Update-setup controller check: on operator restart, starts controller for stopped update-setup job

See docs/tnf/job-controllers.md for dual-mode operation and bootstrap sequence.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@jaypoulz
jaypoulz force-pushed the jpoulin/commit-1-framework branch from 22df05d to 6ffede6 Compare July 21, 2026 21:04
@openshift-ci

openshift-ci Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

@jaypoulz: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/configmap-scale 6ffede6 link false /test configmap-scale
ci/prow/okd-scos-images 6ffede6 link true /test okd-scos-images
ci/prow/unit 6ffede6 link true /test unit
ci/prow/images 6ffede6 link true /test images
ci/prow/verify 6ffede6 link true /test verify

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

jira/severity-important Referenced Jira bug's severity is important for the branch this PR is targeting. 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.

2 participants