Skip to content

OCPBUGS-84695: feat(tnf): TNF job controller framework and lifecycle management#1655

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

OCPBUGS-84695: feat(tnf): TNF job controller framework and lifecycle management#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 job controller framework for Two-Node Fencing (TNF) cluster management:

Job Controller Framework (pkg/tnf/pkg/jobs/):

  • Multi-node job pattern with round-robin retry across schedulable nodes
  • Single-node job pattern for node-specific operations
  • Drift detection: resets retry state when node list or config changes
  • Blocked condition when nodes not ready (10-minute timeout)
  • Degraded condition when retry budget exhausted (retries continue)
  • JobRetryState tracks AttemptNumber, NodeIndex, TargetNodes
  • RunNodeJobController/RunClusterJobController entry points

Lifecycle Manager (pkg/tnf/operator/lifecycle_manager.go):

  • PacemakerLifecycleManager: owns all TNF job lifecycle logic
  • Event handlers: node Ready transitions, fencing secret changes
  • Bootstrap mode: exponential backoff retry, requires 2 ready nodes
  • Runtime mode: idempotent restart, handles 1-2 nodes
  • Node selection: K8s ∩ Pacemaker intersection with fallback

Job Implementations:

  • auth: sets hacluster password (per-node, prerequisite for Pacemaker)
  • setup: creates Pacemaker cluster (cluster-wide, bootstrap only)
  • after-setup: disables kubelet service (per-node, Pacemaker owns it)
  • fencing: configures STONITH (cluster-wide, secret-driven restart)

Status Collector (pkg/tnf/operator/status_collector.go):

  • CronJob running every minute on active Pacemaker nodes
  • Sticky-on-success, rotate-on-failure strategy
  • Resets to first node when node list changes

Node Management (pkg/tnf/operator/helpers.go):

  • getActivePacemakerNodes(): returns K8s ∩ Pacemaker intersection
  • Staleness detection: 5-minute threshold for PacemakerCluster CR
  • Sorted output for deterministic round-robin behavior

Documentation:

  • docs/tnf/job-controllers.md: multi-node retry, drift detection, condition model
  • docs/tnf/lifecycle-manager.md: startup sequence, node selection, status collector

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

Summary by CodeRabbit

  • New Features
    • Added TNF lifecycle management for Pacemaker-related Jobs, including bootstrap/runtime coordination, multi-node round-robin retries, and drift-aware progression.
    • Introduced a Pacemaker lifecycle manager gated on PacemakerCluster readiness and a status-collector CronJob with sticky node selection and rotation on failure.
  • Documentation
    • Documented the TNF Job Controllers framework and PacemakerLifecycleManager startup/behavior.
  • Bug Fixes
    • Auto-recovers long-failed Jobs and improves Blocked/Degraded operator condition handling.
    • Avoids unnecessary Job recreation when only retry-state label differences exist for already-completed cluster Jobs.
    • Improves pcs host auth when node 2 details are missing and refines active node targeting behavior.
  • Tests
    • Added coverage for lifecycle state transitions, restart behavior, drift handling, and status-collector rotation.

@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
Contributor

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds Pacemaker lifecycle orchestration, active-node targeting, status-collector rotation, node and cluster TNF Job controllers, retry/restart handling, REST compatibility helpers, authentication updates, tests, and lifecycle documentation.

Changes

TNF lifecycle and Job execution

Layer / File(s) Summary
Lifecycle startup and node targeting
pkg/tnf/operator/lifecycle_manager.go, pkg/tnf/operator/job_controllers.go, pkg/tnf/operator/starter.go, pkg/tnf/operator/helpers.go
Adds CRD-gated lifecycle startup, bootstrap/runtime controller paths, readiness gates, Pacemaker-node selection, fencing-secret filtering, and startup retries.
TNF Job lifecycle and retry orchestration
pkg/tnf/pkg/jobs/lifecycle.go, pkg/tnf/pkg/jobs/jobcontroller.go, pkg/tnf/pkg/jobs/batch.go, pkg/tnf/pkg/jobs/utils.go
Adds node and cluster controllers, blocked/degraded conditions, round-robin retries, drift resets, restart serialization, stuck-Job recovery, and expanded stopped-state detection.
Status collector execution and rotation
pkg/tnf/operator/status_collector.go
Adds the minute-based status collector CronJob, active-node pinning, failure detection, and target rotation.
Supporting APIs and authentication
pkg/tnf/pkg/pacemaker/*, pkg/tnf/pkg/tools/*, pkg/tnf/pkg/pcs/auth.go, pkg/tnf/auth/runner.go
Adds REST/list-option helpers, node and Job-name utilities, and single-node authentication configuration handling.
Validation and documentation
pkg/tnf/operator/*_test.go, pkg/tnf/pkg/jobs/*_test.go, docs/tnf/*
Adds tests for lifecycle startup, targeting, retries, drift, recovery, and status rotation, plus corresponding TNF documentation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Starter
  participant LifecycleManager
  participant JobControllers
  participant Kubernetes
  Starter->>LifecycleManager: initialize after PacemakerCluster CRD establishment
  LifecycleManager->>Kubernetes: observe nodes and PacemakerCluster state
  LifecycleManager->>JobControllers: start bootstrap or runtime controllers
  JobControllers->>Kubernetes: create, retry, delete, and pin Jobs
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, 1 warning)

Check name Status Explanation Resolution
No-Sensitive-Data-In-Logs ❌ Error FAIL: lifecycle.go logs full fencing config (includes secret names/ResourceVersions), and pacemaker status logs node names/IPs plus cluster UUIDs. Remove or redact these values from logs; log only counts/IDs hashed or truncated to non-sensitive forms.
Docstring Coverage ⚠️ Warning Docstring coverage is 61.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (13 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main TNF job controller and lifecycle management changes.
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 PASS: The changed test files use only static Go test functions and t.Run titles; no Ginkgo It/Describe/When/Context titles or dynamic identifiers were added.
Test Structure And Quality ✅ Passed No Ginkgo tests here; the new table-driven unit tests use bounded timeouts, defer cleanup, and mostly include helpful assertion messages.
Microshift Test Compatibility ✅ Passed No new Ginkgo e2e tests were added; the touched tests are plain testing unit tests, so MicroShift compatibility is not applicable.
Single Node Openshift (Sno) Test Compatibility ✅ Passed No new Ginkgo/e2e tests were added; the changed files contain standard Go unit tests (testing.T) and no It/Describe/Context/When/By constructs.
Topology-Aware Scheduling Compatibility ✅ Passed The new controllers are TNF-specific and gate bootstrap on exactly 2 ready control-plane nodes; no worker-only, anti-affinity, or spread constraints were added.
Ote Binary Stdout Contract ✅ Passed Changed files add no process-level stdout writes; klog logs to stderr, and the only stdout path found is an unchanged cmd.Help() in the OTE main.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed The PR adds only Go unit tests/docs; no new Ginkgo e2e tests or external-network dependencies were introduced.
No-Weak-Crypto ✅ Passed No weak crypto, custom crypto, or secret/token comparisons were added in the changed files; the only crypto found is standard SHA-256 for event-name deduping.
Container-Privileges ✅ Passed The patch adds no new privileged/root/host* settings; the only matching Job manifest is pre-existing test data unchanged by this commit.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


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
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (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
Contributor

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
@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
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (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
Contributor

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 Outdated
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
Comment thread pkg/tnf/update-setup/runner.go Outdated
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
Contributor

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 thread pkg/tnf/pkg/jobs/lifecycle.go Outdated
Comment thread pkg/tnf/pkg/tools/nodes.go Outdated
@jaypoulz
jaypoulz force-pushed the jpoulin/commit-1-framework branch 10 times, most recently from d2ae9a8 to 034cd7d Compare July 22, 2026 16:53
@jaypoulz
jaypoulz force-pushed the jpoulin/commit-1-framework branch from cdeeae6 to adc8adc Compare July 23, 2026 21:45

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
pkg/tnf/operator/starter.go (1)

137-182: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Nested polling for CRD establishment works but is convoluted.

The inner wait.PollUntilContextTimeout (2s interval, 1 minute deadline) is wrapped in an outer for loop that re-polls every 30s on timeout. This is correct but adds an extra layer of indirection versus a single wait.PollUntilContextCancel with a fixed interval, which would achieve the same "poll indefinitely until CRD established or ctx cancelled" behavior with less code.

♻️ Simplification using a single infinite poll
-		for {
-			err = wait.PollUntilContextTimeout(ctx, 2*time.Second, time.Minute, true, func(ctx context.Context) (bool, error) {
-				crd, getErr := apiextClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, "pacemakerclusters.etcd.openshift.io", metav1.GetOptions{})
-				if getErr != nil {
-					klog.V(2).Infof("waiting for PacemakerCluster CRD: %v", getErr)
-					return false, nil
-				}
-				for _, cond := range crd.Status.Conditions {
-					if cond.Type == apiextensionsv1.Established && cond.Status == apiextensionsv1.ConditionTrue {
-						return true, nil
-					}
-				}
-				klog.V(2).Infof("PacemakerCluster CRD not yet established")
-				return false, nil
-			})
-			if err == nil {
-				break
-			}
-			if ctx.Err() != nil {
-				klog.Infof("context done while waiting for PacemakerCluster CRD: %v", err)
-				return
-			}
-			klog.Warningf("PacemakerCluster CRD not established yet, will retry in 30s: %v", err)
-			select {
-			case <-time.After(30 * time.Second):
-				continue
-			case <-ctx.Done():
-				return
-			}
-		}
+		err = wait.PollUntilContextCancel(ctx, 5*time.Second, true, func(ctx context.Context) (bool, error) {
+			crd, getErr := apiextClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, "pacemakerclusters.etcd.openshift.io", metav1.GetOptions{})
+			if getErr != nil {
+				klog.V(2).Infof("waiting for PacemakerCluster CRD: %v", getErr)
+				return false, nil
+			}
+			for _, cond := range crd.Status.Conditions {
+				if cond.Type == apiextensionsv1.Established && cond.Status == apiextensionsv1.ConditionTrue {
+					return true, nil
+				}
+			}
+			klog.V(2).Infof("PacemakerCluster CRD not yet established")
+			return false, nil
+		})
+		if err != nil {
+			klog.Infof("context done while waiting for PacemakerCluster CRD: %v", err)
+			return
+		}
🤖 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/operator/starter.go` around lines 137 - 182, Replace the nested retry
structure in runPacemakerControllers with a single wait.PollUntilContextCancel
that polls for the PacemakerCluster CRD at a fixed interval until it is
established or ctx is cancelled. Preserve the existing CRD lookup,
established-condition check, waiting logs, and cancellation handling, while
removing the outer for loop and timeout-based retry logic.
🤖 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 381-427: Explicitly delete the failed or stopped existingJob in
the IsFailed/IsStopped retry branch before committing the updated
state.NodeIndex and state.AttemptNumber. Use the existing Kubernetes job
deletion mechanism and handle/report deletion errors consistently, so ApplyJob
can recreate the job even when the next node index resolves to the same node.
Keep the existing retry calculation and degraded-condition behavior unchanged.

---

Nitpick comments:
In `@pkg/tnf/operator/starter.go`:
- Around line 137-182: Replace the nested retry structure in
runPacemakerControllers with a single wait.PollUntilContextCancel that polls for
the PacemakerCluster CRD at a fixed interval until it is established or ctx is
cancelled. Preserve the existing CRD lookup, established-condition check,
waiting logs, and cancellation handling, while removing the outer for loop and
timeout-based retry logic.
🪄 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: fb03a9ab-31d9-4a07-b79a-fe57ae269681

📥 Commits

Reviewing files that changed from the base of the PR and between 22a57fa and cdeeae6.

📒 Files selected for processing (29)
  • docs/tnf/job-controllers.md
  • docs/tnf/lifecycle-manager.md
  • pkg/tnf/auth/runner.go
  • pkg/tnf/operator/helpers.go
  • pkg/tnf/operator/helpers_test.go
  • pkg/tnf/operator/job_controllers.go
  • pkg/tnf/operator/job_controllers_test.go
  • pkg/tnf/operator/lifecycle_manager.go
  • pkg/tnf/operator/nodehandler.go
  • pkg/tnf/operator/nodehandler_test.go
  • pkg/tnf/operator/starter.go
  • pkg/tnf/operator/starter_test.go
  • pkg/tnf/operator/status_collector.go
  • pkg/tnf/operator/status_collector_test.go
  • pkg/tnf/pkg/jobs/batch.go
  • pkg/tnf/pkg/jobs/batch_test.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/pacemaker/healthcheck.go
  • pkg/tnf/pkg/pacemaker/helpers.go
  • pkg/tnf/pkg/pacemaker/statuscollector.go
  • pkg/tnf/pkg/pcs/auth.go
  • pkg/tnf/pkg/tools/jobs.go
  • pkg/tnf/pkg/tools/nodes.go
💤 Files with no reviewable changes (5)
  • pkg/tnf/operator/nodehandler.go
  • pkg/tnf/pkg/jobs/tnf_test.go
  • pkg/tnf/operator/nodehandler_test.go
  • pkg/tnf/operator/starter_test.go
  • pkg/tnf/pkg/jobs/tnf.go
🚧 Files skipped from review as they are similar to previous changes (17)
  • pkg/tnf/pkg/pacemaker/statuscollector.go
  • pkg/tnf/auth/runner.go
  • pkg/tnf/pkg/tools/jobs.go
  • pkg/tnf/operator/job_controllers_test.go
  • pkg/tnf/operator/helpers_test.go
  • pkg/tnf/operator/status_collector.go
  • pkg/tnf/pkg/pacemaker/healthcheck.go
  • pkg/tnf/operator/status_collector_test.go
  • pkg/tnf/pkg/jobs/jobcontroller_test.go
  • pkg/tnf/operator/helpers.go
  • pkg/tnf/pkg/jobs/lifecycle_test.go
  • pkg/tnf/pkg/pacemaker/helpers.go
  • pkg/tnf/pkg/jobs/jobcontroller.go
  • pkg/tnf/operator/lifecycle_manager.go
  • pkg/tnf/pkg/tools/nodes.go
  • docs/tnf/job-controllers.md
  • pkg/tnf/operator/job_controllers.go

Comment thread pkg/tnf/pkg/jobs/lifecycle.go
@jaypoulz
jaypoulz force-pushed the jpoulin/commit-1-framework branch 4 times, most recently from 80043e1 to 9b3480a Compare July 23, 2026 22:26
@jaypoulz

Copy link
Copy Markdown
Contributor Author

/payload-job periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing-recovery periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing-recovery

@openshift-ci

openshift-ci Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@jaypoulz: trigger 8 job(s) for the /payload-(with-prs|job|aggregate|job-with-prs|aggregate-with-prs) command

  • periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing
  • periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing-recovery-1of3
  • periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing-recovery-2of3
  • periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing-recovery-3of3
  • periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing
  • periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing-recovery-1of3
  • periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing-recovery-2of3
  • periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing-recovery-3of3

See details on https://pr-payload-tests.ci.openshift.org/runs/ci/c3682090-86e5-11f1-8665-c2551a5d1a75-0

@jaypoulz

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

482-613: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the shared controller-startup boilerplate.

RunNodeJobController and RunClusterJobController duplicate the duplicate-start guard, NewJobController construction shape, and goroutine wrapper with cleanup — differing only in the hook body and log wording. The restart-side equivalent (restartJobOrRunController) already extracts this kind of shared logic; doing the same here would reduce upkeep risk when either startup path needs a fix.

🤖 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 482 - 613, Extract the shared
startup and cleanup logic from RunNodeJobController and RunClusterJobController
into a reusable helper that accepts the job name, log context, and hook
functions while retaining the existing NewJobController dependencies. Move the
duplicate-start guard, controller launch, and runningControllers cleanup into
that helper, then have both functions supply only their job-specific hook
configuration and messages.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pkg/tnf/pkg/jobs/lifecycle.go`:
- Around line 482-613: Extract the shared startup and cleanup logic from
RunNodeJobController and RunClusterJobController into a reusable helper that
accepts the job name, log context, and hook functions while retaining the
existing NewJobController dependencies. Move the duplicate-start guard,
controller launch, and runningControllers cleanup into that helper, then have
both functions supply only their job-specific hook configuration and messages.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d4bfe652-6e4c-4242-86bd-313e09e23292

📥 Commits

Reviewing files that changed from the base of the PR and between cdeeae6 and 9b3480a.

📒 Files selected for processing (29)
  • docs/tnf/job-controllers.md
  • docs/tnf/lifecycle-manager.md
  • pkg/tnf/auth/runner.go
  • pkg/tnf/operator/helpers.go
  • pkg/tnf/operator/helpers_test.go
  • pkg/tnf/operator/job_controllers.go
  • pkg/tnf/operator/job_controllers_test.go
  • pkg/tnf/operator/lifecycle_manager.go
  • pkg/tnf/operator/nodehandler.go
  • pkg/tnf/operator/nodehandler_test.go
  • pkg/tnf/operator/starter.go
  • pkg/tnf/operator/starter_test.go
  • pkg/tnf/operator/status_collector.go
  • pkg/tnf/operator/status_collector_test.go
  • pkg/tnf/pkg/jobs/batch.go
  • pkg/tnf/pkg/jobs/batch_test.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/pacemaker/healthcheck.go
  • pkg/tnf/pkg/pacemaker/helpers.go
  • pkg/tnf/pkg/pacemaker/statuscollector.go
  • pkg/tnf/pkg/pcs/auth.go
  • pkg/tnf/pkg/tools/jobs.go
  • pkg/tnf/pkg/tools/nodes.go
💤 Files with no reviewable changes (5)
  • pkg/tnf/operator/nodehandler_test.go
  • pkg/tnf/pkg/jobs/tnf.go
  • pkg/tnf/operator/nodehandler.go
  • pkg/tnf/operator/starter_test.go
  • pkg/tnf/pkg/jobs/tnf_test.go
🚧 Files skipped from review as they are similar to previous changes (21)
  • pkg/tnf/pkg/pacemaker/statuscollector.go
  • pkg/tnf/auth/runner.go
  • pkg/tnf/pkg/tools/jobs.go
  • pkg/tnf/pkg/pacemaker/healthcheck.go
  • pkg/tnf/operator/lifecycle_manager.go
  • pkg/tnf/pkg/jobs/batch.go
  • pkg/tnf/pkg/pacemaker/helpers.go
  • pkg/tnf/operator/helpers_test.go
  • pkg/tnf/pkg/jobs/batch_test.go
  • pkg/tnf/pkg/jobs/utils.go
  • pkg/tnf/operator/job_controllers_test.go
  • pkg/tnf/operator/status_collector_test.go
  • pkg/tnf/pkg/jobs/jobcontroller.go
  • pkg/tnf/operator/helpers.go
  • pkg/tnf/operator/status_collector.go
  • pkg/tnf/pkg/jobs/jobcontroller_test.go
  • pkg/tnf/pkg/tools/nodes.go
  • pkg/tnf/pkg/jobs/lifecycle_test.go
  • docs/tnf/job-controllers.md
  • pkg/tnf/operator/job_controllers.go
  • pkg/tnf/operator/starter.go

@jaypoulz
jaypoulz force-pushed the jpoulin/commit-1-framework branch 3 times, most recently from a3fbf23 to fea88ae Compare July 23, 2026 23:00
@jaypoulz

Copy link
Copy Markdown
Contributor Author

/payload-job periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing-recovery periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing-recovery

@openshift-ci

openshift-ci Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

@jaypoulz: trigger 8 job(s) for the /payload-(with-prs|job|aggregate|job-with-prs|aggregate-with-prs) command

  • periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing
  • periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing-recovery-1of3
  • periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing-recovery-2of3
  • periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing-recovery-3of3
  • periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing
  • periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing-recovery-1of3
  • periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing-recovery-2of3
  • periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing-recovery-3of3

See details on https://pr-payload-tests.ci.openshift.org/runs/ci/caf054d0-86fa-11f1-923d-dcd7b9b4794a-0

@jaypoulz
jaypoulz force-pushed the jpoulin/commit-1-framework branch from fea88ae to c674b1d Compare July 24, 2026 15:03
@openshift-ci

openshift-ci Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

@jaypoulz: The following test 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/unit c674b1d link true /test unit

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.

Comment thread pkg/tnf/operator/job_controllers.go
Comment thread pkg/tnf/operator/lifecycle_manager.go Outdated
@jaypoulz
jaypoulz force-pushed the jpoulin/commit-1-framework branch from c674b1d to 2e5d7e5 Compare July 24, 2026 19:49
@jaypoulz

Copy link
Copy Markdown
Contributor Author

/payload-job periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing-recovery periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing-recovery

@openshift-ci

openshift-ci Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

@jaypoulz: trigger 8 job(s) for the /payload-(with-prs|job|aggregate|job-with-prs|aggregate-with-prs) command

  • periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing
  • periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing-recovery-1of3
  • periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing-recovery-2of3
  • periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing-recovery-3of3
  • periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing
  • periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing-recovery-1of3
  • periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing-recovery-2of3
  • periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing-recovery-3of3

See details on https://pr-payload-tests.ci.openshift.org/runs/ci/49fe52f0-879f-11f1-8862-193f43b1c36e-0

Implements job controller framework for Two-Node Fencing (TNF) cluster management:

**Job Controller Framework (pkg/tnf/pkg/jobs/):**
- Multi-node job pattern with round-robin retry across schedulable nodes
- Single-node job pattern for node-specific operations
- Drift detection: resets retry state when node list or config changes
- Degraded condition for both blocked jobs (>10min timeout) and failed jobs
- Reason field distinguishes: SyncError (blocked) vs MaxRetriesExceeded (failed)
- Blocked jobs visible at ClusterOperator level (propagate via Degraded)
- JobRetryState tracks AttemptNumber, NodeIndex, TargetNodes
- RunNodeJobController/RunClusterJobController entry points
- ApplyJob drift handling: ignores retry field changes for non-failed cluster jobs (prevents recreation on operator restart)
- PascalCase condition names: TNF{Job}{Condition} format (e.g., TNFSetupJobAvailable)
- Legacy condition cleanup on upgrade: removes old tnf-xxx-job{Condition} format
- Log verbosity: steady-state checks use V(4), state changes use Info

**Lifecycle Manager (pkg/tnf/operator/lifecycle_manager.go):**
- PacemakerLifecycleManager: owns all TNF job lifecycle logic
- Event handlers: node Ready transitions (triggers bootstrap)
- Bootstrap mode: exponential backoff retry, requires 2 ready nodes
- Runtime mode: idempotent restart, handles 1-2 nodes
- Node selection: K8s ∩ Pacemaker intersection with fallback
- Fixed closure capture bugs: all affected nodes functions query informers dynamically (prevents false Degraded during node replacement)
- Fixed lifecycle context: event handler goroutines now use controller context (receive cancellation on shutdown)

**Job Implementations:**
- auth: sets hacluster password (per-node, prerequisite for Pacemaker)
- setup: creates Pacemaker cluster (one-time job, controller runs in both bootstrap and runtime modes to maintain conditions)
- after-setup: disables kubelet service (per-node, Pacemaker owns it)
- fencing: configures STONITH (cluster-wide, drift detection via ResourceVersion)
- update-setup: handles node replacements (cluster-wide, removes offline node, adds new node, updates IPs)

**Drift Detection:**
- Secret changes detected via ResourceVersion tracking in jobConfigFunc
- Syncs every minute to detect secret updates, deletions, and additions
- Eliminates need for separate secret event handlers
- Triggers job restart when fencing secret ResourceVersion changes

**Status Collector (pkg/tnf/operator/status_collector.go):**
- CronJob running every minute on active Pacemaker nodes
- Sticky-on-success, rotate-on-failure strategy
- Resets to first node when node list changes
- Log verbosity: steady-state pinning uses V(4), state changes use Info

**Node Management (pkg/tnf/operator/helpers.go):**
- getActivePacemakerNodes(): returns K8s ∩ Pacemaker intersection
- Staleness detection: 5-minute threshold for PacemakerCluster CR
- Sorted output for deterministic round-robin behavior

**Documentation:**
- docs/tnf/job-controllers.md: multi-node retry, drift detection, condition model
- docs/tnf/lifecycle-manager.md: startup sequence, node selection, status collector

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@jaypoulz
jaypoulz force-pushed the jpoulin/commit-1-framework branch from 2e5d7e5 to d1e0b65 Compare July 24, 2026 20:47
@jaypoulz

Copy link
Copy Markdown
Contributor Author

/payload-job periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing-recovery periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing-recovery

@openshift-ci

openshift-ci Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

@jaypoulz: trigger 8 job(s) for the /payload-(with-prs|job|aggregate|job-with-prs|aggregate-with-prs) command

  • periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing
  • periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing-recovery-1of3
  • periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing-recovery-2of3
  • periodic-ci-openshift-release-main-nightly-4.22-e2e-metal-ovn-two-node-fencing-recovery-3of3
  • periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing
  • periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing-recovery-1of3
  • periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing-recovery-2of3
  • periodic-ci-openshift-release-main-nightly-5.0-e2e-metal-ovn-two-node-fencing-recovery-3of3

See details on https://pr-payload-tests.ci.openshift.org/runs/ci/6ffbe9d0-87a5-11f1-96d0-e02ea1efaa82-0

@jaypoulz

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

52-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Doc comment narrower than the implemented behavior. The retry-field exemption applies to any non-failed cluster job (Line 87 !IsFailed(*existing)), including running ones, not just completed ones.

✏️ Suggested wording
-// spec, the existing job will be deleted and recreated. For completed cluster jobs
+// spec, the existing job will be deleted and recreated. For non-failed cluster jobs
 // (labeled with job-type=cluster), drift in retry-related fields (NodeName, node-index,
 // attempt labels) is ignored to prevent recreation on operator restart when retry state
 // is reinitialized.
🤖 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/batch.go` around lines 52 - 56, Update the doc comment
describing retry-related drift in cluster jobs to state that the exemption
applies to all non-failed cluster jobs, including running jobs, rather than only
completed jobs. Keep the implementation condition around !IsFailed unchanged.

86-97: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the TNF label keys into shared constants. tnf.etcd.openshift.io/job-type, .../node-index, and .../attempt are hard-coded here and again at Lines 130-133, and the writer side in lifecycle.go must use the identical strings for this drift check to work. A typo on either side silently disables retry-drift detection rather than failing to compile.

Based on learnings that configureMultiNodeJob must write state.AttemptNumber/state.NodeIndex into tnf.etcd.openshift.io/attempt and tnf.etcd.openshift.io/node-index and that ApplyJob must key off those same labels, centralizing the keys protects that contract.

#!/bin/bash
# Check whether constants for these label keys already exist, and where the keys are written/read.
rg -n --type=go 'tnf\.etcd\.openshift\.io/(job-type|node-index|attempt)' -g '!vendor/**'
🤖 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/batch.go` around lines 86 - 97, Define shared constants for
the TNF job-type, node-index, and attempt label keys, then replace every
hard-coded occurrence in the ApplyJob drift checks and related code with those
constants. Update the writer path in configureMultiNodeJob and lifecycle.go to
use the same constants while preserving the existing label values and
retry-drift behavior.

Source: Learnings

pkg/tnf/pkg/jobs/lifecycle_test.go (3)

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

Coverage summary contradicts the assertion. Line 20 says "state reset" on success, but Step 5 asserts the state is preserved (Line 377). Align the summary with the actual contract.

🤖 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 20, Update the lifecycle test
coverage summary entry for “Job succeeds” to state that the state is preserved,
matching the assertion in Step 5 and the established contract; retain the
“degraded cleared” portion unchanged.

71-72: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

expectJobDeleted and expectWaitForStopped are never asserted. Both fields are declared and populated in every case but no verification reads them, so the "deletes and runs controller" case only actually proves the controller started. Either assert the job is gone (Get returns NotFound) and that the wait path executed, or drop the fields.

🤖 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 71 - 72, Use
expectJobDeleted and expectWaitForStopped in the lifecycle test assertions:
verify the job Get operation returns NotFound when expectJobDeleted is true, and
verify the stopped-job wait path executed when expectWaitForStopped is true. If
these expectations cannot be asserted, remove both fields and their case values
instead of leaving them unused.

667-676: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Vacuous assertion when the condition is missing. If no condition matches expectedConditionType, the loop body never runs and the test passes without verifying anything. Track a found flag and require it (same pattern at Lines 792-801).

💚 Proposed hardening
 			if !tt.expectError && tt.blockedDuration > 0 {
 				_, status, _, _ := fakeOperatorClient.GetStaticPodOperatorState()
 				expectedConditionType := tools.ToPascalCase(jobName) + "Degraded"
+				found := false
 				for _, cond := range status.Conditions {
 					if cond.Type == expectedConditionType {
+						found = true
 						require.Equal(t, operatorv1.ConditionFalse, cond.Status, "Degraded should be cleared when unblocked")
 						break
 					}
 				}
+				require.True(t, found, "expected condition %s to be present", expectedConditionType)
 			}
🤖 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 667 - 676, Harden the
condition check in the blocked-duration validation by adding a found flag
alongside the loop over status.Conditions, setting it when expectedConditionType
matches, and requiring it after the loop before or alongside the existing
ConditionFalse assertion. Follow the established found-condition pattern used
around the referenced later test section.
pkg/tnf/operator/status_collector_test.go (2)

84-105: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Most-recent-job selection is untested. Every case creates a single Job, so the CreationTimestamp comparison loop in checkLastJobFailed (pkg/tnf/operator/status_collector.go Lines 141-147) never gets exercised — an inverted comparison there would go unnoticed. Add a case with an older failed Job plus a newer completed Job (and the reverse).

🤖 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/operator/status_collector_test.go` around lines 84 - 105, Extend the
tests around checkLastJobFailed to create multiple Jobs with distinct
CreationTimestamp values, covering both an older failed Job followed by a newer
completed Job and an older completed Job followed by a newer failed Job. Assert
that the result is determined by the most recently created Job, exercising the
timestamp comparison loop rather than the single-job path.

201-226: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This test re-implements the rotation logic instead of exercising it. Lines 201-226 are a copy of the reset/rotate block inside runPacemakerStatusCollectorCronJob (pkg/tnf/operator/status_collector.go Lines 82-119), so the test will keep passing if the production block changes or regresses. Extract the decision into a small pure helper and call it from both the CronJob config func and the test.

♻️ Sketch
// status_collector.go
func nextStatusCollectorNodeIndex(state *jobs.JobRetryState, currentNodeNames []string, lastJobFailed bool) int {
	state.Mu.Lock()
	defer state.Mu.Unlock()
	if !tools.StringSlicesEqual(state.TargetNodes, currentNodeNames) {
		state.NodeIndex = 0
		state.TargetNodes = currentNodeNames
	}
	if lastJobFailed && len(currentNodeNames) > 0 {
		state.NodeIndex = (state.NodeIndex + 1) % len(currentNodeNames)
	}
	return state.NodeIndex
}
🤖 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/operator/status_collector_test.go` around lines 201 - 226, Extract
the reset-and-rotation decision from runPacemakerStatusCollectorCronJob into a
pure helper such as nextStatusCollectorNodeIndex that updates JobRetryState,
handles changed or empty node lists safely, and returns the selected index.
Replace the CronJob’s inline logic and the duplicated block in the test with
calls to this helper, while leaving lastJobFailed retrieval and node selection
flow unchanged.
pkg/tnf/operator/starter.go (1)

146-188: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Surface Pacemaker startup failures instead of dropping the goroutine
newPacemakerLifecycleManager only does local setup here, so if it returns an error the Pacemaker controllers never start and this goroutine just exits after logging. Surface that failure through operator status or fail startup instead of leaving TNF partially enabled.

🤖 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/operator/starter.go` around lines 146 - 188, Update the Pacemaker
startup flow around newPacemakerLifecycleManager so its initialization error is
propagated through operator startup/status rather than only logged before
returning from the goroutine. Ensure a failed lifecycle-manager setup causes TNF
startup to fail or records an appropriate degraded/error condition, while
preserving normal controller startup when initialization succeeds.
🤖 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 148-150: Update the “Create job spec” documentation to describe
the actual single-node pinning mechanism as NodeName/fixed node assignment,
replacing the incorrect nodeSelector wording. Keep the backoffLimit=retries
guidance unchanged.
- Around line 180-188: The execution flow diagram should match the
implementation order in syncMultiNodeJobState: move the affectedNodesFunc
readiness check before the schedulableNodesFunc availability check, preserving
each branch’s existing error, timeout, and Degraded-condition behavior.

In `@pkg/tnf/pkg/jobs/lifecycle_test.go`:
- Around line 322-352: Check every fake-client setup error in
pkg/tnf/pkg/jobs/lifecycle_test.go#L322-L352 by wrapping each Jobs Create/Delete
call with require.NoError and using failedJob.DeepCopy() for recreated jobs. In
pkg/tnf/pkg/jobs/lifecycle_test.go#L625-L625, assert the Nodes Create result
inside the affected-nodes loop. In pkg/tnf/pkg/jobs/lifecycle_test.go#L744-L748,
assert both Nodes Create results for readyNode and the schedulable-node loop, so
syncMultiNodeJobState tests operate on the intended cluster state.

---

Nitpick comments:
In `@pkg/tnf/operator/starter.go`:
- Around line 146-188: Update the Pacemaker startup flow around
newPacemakerLifecycleManager so its initialization error is propagated through
operator startup/status rather than only logged before returning from the
goroutine. Ensure a failed lifecycle-manager setup causes TNF startup to fail or
records an appropriate degraded/error condition, while preserving normal
controller startup when initialization succeeds.

In `@pkg/tnf/operator/status_collector_test.go`:
- Around line 84-105: Extend the tests around checkLastJobFailed to create
multiple Jobs with distinct CreationTimestamp values, covering both an older
failed Job followed by a newer completed Job and an older completed Job followed
by a newer failed Job. Assert that the result is determined by the most recently
created Job, exercising the timestamp comparison loop rather than the single-job
path.
- Around line 201-226: Extract the reset-and-rotation decision from
runPacemakerStatusCollectorCronJob into a pure helper such as
nextStatusCollectorNodeIndex that updates JobRetryState, handles changed or
empty node lists safely, and returns the selected index. Replace the CronJob’s
inline logic and the duplicated block in the test with calls to this helper,
while leaving lastJobFailed retrieval and node selection flow unchanged.

In `@pkg/tnf/pkg/jobs/batch.go`:
- Around line 52-56: Update the doc comment describing retry-related drift in
cluster jobs to state that the exemption applies to all non-failed cluster jobs,
including running jobs, rather than only completed jobs. Keep the implementation
condition around !IsFailed unchanged.
- Around line 86-97: Define shared constants for the TNF job-type, node-index,
and attempt label keys, then replace every hard-coded occurrence in the ApplyJob
drift checks and related code with those constants. Update the writer path in
configureMultiNodeJob and lifecycle.go to use the same constants while
preserving the existing label values and retry-drift behavior.

In `@pkg/tnf/pkg/jobs/lifecycle_test.go`:
- Line 20: Update the lifecycle test coverage summary entry for “Job succeeds”
to state that the state is preserved, matching the assertion in Step 5 and the
established contract; retain the “degraded cleared” portion unchanged.
- Around line 71-72: Use expectJobDeleted and expectWaitForStopped in the
lifecycle test assertions: verify the job Get operation returns NotFound when
expectJobDeleted is true, and verify the stopped-job wait path executed when
expectWaitForStopped is true. If these expectations cannot be asserted, remove
both fields and their case values instead of leaving them unused.
- Around line 667-676: Harden the condition check in the blocked-duration
validation by adding a found flag alongside the loop over status.Conditions,
setting it when expectedConditionType matches, and requiring it after the loop
before or alongside the existing ConditionFalse assertion. Follow the
established found-condition pattern used around the referenced later test
section.
🪄 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: 2cfd3857-d35a-4e45-8be2-f36321446e70

📥 Commits

Reviewing files that changed from the base of the PR and between 9b3480a and d1e0b65.

📒 Files selected for processing (29)
  • docs/tnf/job-controllers.md
  • docs/tnf/lifecycle-manager.md
  • pkg/tnf/auth/runner.go
  • pkg/tnf/operator/helpers.go
  • pkg/tnf/operator/helpers_test.go
  • pkg/tnf/operator/job_controllers.go
  • pkg/tnf/operator/job_controllers_test.go
  • pkg/tnf/operator/lifecycle_manager.go
  • pkg/tnf/operator/nodehandler.go
  • pkg/tnf/operator/nodehandler_test.go
  • pkg/tnf/operator/starter.go
  • pkg/tnf/operator/starter_test.go
  • pkg/tnf/operator/status_collector.go
  • pkg/tnf/operator/status_collector_test.go
  • pkg/tnf/pkg/jobs/batch.go
  • pkg/tnf/pkg/jobs/batch_test.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/pacemaker/healthcheck.go
  • pkg/tnf/pkg/pacemaker/helpers.go
  • pkg/tnf/pkg/pacemaker/statuscollector.go
  • pkg/tnf/pkg/pcs/auth.go
  • pkg/tnf/pkg/tools/jobs.go
  • pkg/tnf/pkg/tools/nodes.go
💤 Files with no reviewable changes (5)
  • pkg/tnf/operator/nodehandler.go
  • pkg/tnf/pkg/jobs/tnf_test.go
  • pkg/tnf/pkg/jobs/tnf.go
  • pkg/tnf/operator/nodehandler_test.go
  • pkg/tnf/operator/starter_test.go
🚧 Files skipped from review as they are similar to previous changes (16)
  • pkg/tnf/auth/runner.go
  • pkg/tnf/pkg/pcs/auth.go
  • pkg/tnf/pkg/pacemaker/healthcheck.go
  • pkg/tnf/pkg/tools/jobs.go
  • pkg/tnf/pkg/pacemaker/statuscollector.go
  • pkg/tnf/pkg/pacemaker/helpers.go
  • pkg/tnf/operator/helpers.go
  • pkg/tnf/operator/lifecycle_manager.go
  • pkg/tnf/pkg/jobs/jobcontroller_test.go
  • pkg/tnf/operator/helpers_test.go
  • pkg/tnf/operator/status_collector.go
  • pkg/tnf/pkg/jobs/utils.go
  • pkg/tnf/pkg/tools/nodes.go
  • pkg/tnf/pkg/jobs/jobcontroller.go
  • pkg/tnf/pkg/jobs/batch_test.go
  • pkg/tnf/operator/job_controllers_test.go

Comment on lines +148 to +150
│ └─ Create job spec:
│ - Schedule on target node (nodeSelector)
│ - Set backoffLimit=retries (Kubernetes handles retries)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document the actual node-pinning mechanism.

The single-node implementation assigns job.Spec.Template.Spec.NodeName = node.Name (pkg/tnf/pkg/jobs/lifecycle.go, Lines 470-531), not a nodeSelector. Since nodeSelector is label-based and may match another node, this wording misstates the scheduling guarantee. Replace it with NodeName/fixed node assignment.

🤖 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 `@docs/tnf/job-controllers.md` around lines 148 - 150, Update the “Create job
spec” documentation to describe the actual single-node pinning mechanism as
NodeName/fixed node assignment, replacing the incorrect nodeSelector wording.
Keep the backoffLimit=retries guidance unchanged.

Comment on lines +180 to +188
│ ├─ Call syncMultiNodeJobState (manages retry state):
│ │ │
│ │ ├─ Check schedulableNodesFunc() → any nodes available?
│ │ │ ├─ NO → Return error after 10min (triggers Degraded via WithSyncDegradedOnError)
│ │ │ └─ YES → Clear Degraded condition (if was blocked), continue
│ │ │
│ │ ├─ Check affectedNodesFunc() → any nodes not ready?
│ │ │ ├─ YES → Return error after 10min (triggers Degraded via WithSyncDegradedOnError)
│ │ │ └─ NO → Clear Degraded condition (if was blocked), continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align the execution flow with the implementation order.

syncMultiNodeJobState checks affectedNodesFunc readiness before evaluating schedulable nodes (pkg/tnf/pkg/jobs/lifecycle.go, Lines 210-233), but this diagram documents the reverse order. Reordering these steps will keep the documented blocking and condition behavior accurate.

🤖 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 `@docs/tnf/job-controllers.md` around lines 180 - 188, The execution flow
diagram should match the implementation order in syncMultiNodeJobState: move the
affectedNodesFunc readiness check before the schedulableNodesFunc availability
check, preserving each branch’s existing error, timeout, and Degraded-condition
behavior.

Comment on lines +322 to +352
fakeKubeClient.BatchV1().Jobs(operatorclient.TargetNamespace).Create(ctx, failedJob, metav1.CreateOptions{})

err = syncMultiNodeJobState(ctx, jobName, targetNodesFunc, nil, nil, maxRetries, fakeKubeClient, fakeOperatorClient)
require.NoError(t, err)
state = getState()
require.Equal(t, 1, state.AttemptNumber, "Should still be attempt 1")
require.Equal(t, 1, state.NodeIndex, "Should advance to node index 1")

// Delete job to simulate ApplyJob detecting drift (NodeName changed) and recreating
fakeKubeClient.BatchV1().Jobs(operatorclient.TargetNamespace).Delete(ctx, jobName, metav1.DeleteOptions{})

// Step 3: Job fails on node 1 -> should start attempt 2, back to node 0
fakeKubeClient.BatchV1().Jobs(operatorclient.TargetNamespace).Create(ctx, failedJob, metav1.CreateOptions{})
err = syncMultiNodeJobState(ctx, jobName, targetNodesFunc, nil, nil, maxRetries, fakeKubeClient, fakeOperatorClient)
require.NoError(t, err)
state = getState()
require.Equal(t, 2, state.AttemptNumber, "Should advance to attempt 2")
require.Equal(t, 0, state.NodeIndex, "Should reset to node index 0")
require.False(t, isDegraded(), "Should not be degraded yet")

// Delete job to simulate ApplyJob detecting drift
fakeKubeClient.BatchV1().Jobs(operatorclient.TargetNamespace).Delete(ctx, jobName, metav1.DeleteOptions{})

// Step 4: Exhaust attempt 2 (fail on both nodes) -> should set degraded and reset to attempt 1
// Fail on node 0
fakeKubeClient.BatchV1().Jobs(operatorclient.TargetNamespace).Create(ctx, failedJob, metav1.CreateOptions{})
err = syncMultiNodeJobState(ctx, jobName, targetNodesFunc, nil, nil, maxRetries, fakeKubeClient, fakeOperatorClient)
require.NoError(t, err)
// Delete and recreate on node 1
fakeKubeClient.BatchV1().Jobs(operatorclient.TargetNamespace).Delete(ctx, jobName, metav1.DeleteOptions{})
fakeKubeClient.BatchV1().Jobs(operatorclient.TargetNamespace).Create(ctx, failedJob, metav1.CreateOptions{})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fake-client setup calls throughout this file discard their error returns. The shared root cause is unchecked Create/Delete results during test setup: if a setup call fails (e.g., a re-Create of the same failedJob pointer conflicting on ResourceVersion, or a duplicate node create), the subsequent syncMultiNodeJobState observes different cluster state and the assertions pass for the wrong reason.

  • pkg/tnf/pkg/jobs/lifecycle_test.go#L322-L352: wrap each Jobs(...).Create/Delete in require.NoError and pass failedJob.DeepCopy() on re-creation.
  • pkg/tnf/pkg/jobs/lifecycle_test.go#L625-L625: check the error from Nodes().Create inside the affected-nodes loop with require.NoError.
  • pkg/tnf/pkg/jobs/lifecycle_test.go#L744-L748: check the errors from both Nodes().Create calls (readyNode and the schedulable-node loop) with require.NoError.

As per path instructions for **/*.go: "Never ignore error returns".

📍 Affects 1 file
  • pkg/tnf/pkg/jobs/lifecycle_test.go#L322-L352 (this comment)
  • pkg/tnf/pkg/jobs/lifecycle_test.go#L625-L625
  • pkg/tnf/pkg/jobs/lifecycle_test.go#L744-L748
🤖 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 322 - 352, Check every
fake-client setup error in pkg/tnf/pkg/jobs/lifecycle_test.go#L322-L352 by
wrapping each Jobs Create/Delete call with require.NoError and using
failedJob.DeepCopy() for recreated jobs. In
pkg/tnf/pkg/jobs/lifecycle_test.go#L625-L625, assert the Nodes Create result
inside the affected-nodes loop. In pkg/tnf/pkg/jobs/lifecycle_test.go#L744-L748,
assert both Nodes Create results for readyNode and the schedulable-node loop, so
syncMultiNodeJobState tests operate on the intended cluster state.

Source: Path instructions

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.

3 participants