Skip to content

Suspend standalone Job by default#141

Merged
aufi merged 2 commits into
migtools:mainfrom
aufi:job-reexec
Jun 18, 2026
Merged

Suspend standalone Job by default#141
aufi merged 2 commits into
migtools:mainfrom
aufi:job-reexec

Conversation

@aufi

@aufi aufi commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Implements a solution for issue #482 by automatically suspending standalone Kubernetes Jobs (those without ownerReferences) during migration to prevent them from re-executing on the target cluster.

Changes:

  • Added suspendStandaloneJob() function that sets spec.suspend: true on standalone Jobs
  • Introduced opt-out mechanism via crane.konveyor.io/job-idempotent: "true" annotation for Jobs that are safe to re-run
  • Jobs with ownerReferences (e.g., created by CronJobs) remain whited out as before
  • Updated existing Job tests to expect suspend patches

Behavior:

  • By default: standalone Jobs are suspended → safe migration, prevents accidental re-runs
  • With annotation: Jobs marked as idempotent run automatically → supports init Jobs, DB migrations
  • User can manually unsuspend Jobs post-migration: kubectl patch job -p '{"spec":{"suspend":false}}'

While Kubernetes expects Jobs to be idempotent, many real-world Jobs are not (e.g., one-shot data imports, billing operations). This conservative approach prevents data corruption or duplicate charges while allowing explicit opt-in for idempotent workloads.

Fixes: migtools/crane#482

Summary by CodeRabbit

  • New Features
    • Added automatic suspension of standalone Kubernetes Jobs during transformation. Standalone Jobs (without owner references) are now suspended unless explicitly marked idempotent.
    • Use the crane.konveyor.io/job-idempotent annotation to prevent suspension for specific jobs.
    • Jobs that are already suspended or that have owner references are left unchanged.
  • Tests
    • Expanded Job transformation test coverage to validate the updated suspension behavior and patch expectations.

Implements a solution for issue #482 by automatically suspending standalone Kubernetes Jobs
(those without ownerReferences) during migration to prevent them from re-executing on the target cluster.

Changes:
  - Added suspendStandaloneJob() function that sets spec.suspend: true on standalone Jobs
  - Introduced opt-out mechanism via crane.konveyor.io/job-idempotent: "true" annotation for Jobs that are safe to re-run
  - Jobs with ownerReferences (e.g., created by CronJobs) remain whited out as before
  - Updated existing Job tests to expect suspend patches

Behavior:
  - By default: standalone Jobs are suspended → safe migration, prevents accidental re-runs
  - With annotation: Jobs marked as idempotent run automatically → supports init Jobs, DB migrations
  - User can manually unsuspend Jobs post-migration: kubectl patch job <name> -p '{"spec":{"suspend":false}}'

While Kubernetes expects Jobs to be idempotent, many real-world Jobs are not (e.g., one-shot data imports, billing operations). This conservative approach prevents data corruption or duplicate charges while allowing explicit opt-in for idempotent workloads.

Fixes: migtools/crane#482

Signed-off-by: Marek Aufart <maufart@redhat.com>
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aff10a45-da2d-40bf-bb5a-4759611221cd

📥 Commits

Reviewing files that changed from the base of the PR and between 642ff52 and bd2c253.

📒 Files selected for processing (1)
  • transform/kubernetes/kubernetes.go

📝 Walkthrough

Walkthrough

Adds automatic suspension of standalone Kubernetes Jobs during transformation by introducing an exported annotation constant CraneJobIdempotentAnnotation (crane.konveyor.io/job-idempotent), a new suspendStandaloneJob function that emits a spec.suspend: true JSON Patch for Jobs without owner references, and wires this into the existing Job transform path, with test coverage updated accordingly.

Changes

Standalone Job suspension during transformation

Layer / File(s) Summary
Suspension constant, logic, and transform wiring
transform/kubernetes/kubernetes.go
Exports CraneJobIdempotentAnnotation; adds suspendStandaloneJob that returns a spec.suspend: true JSON Patch unless the Job has ownerReferences, carries the idempotent annotation set to "true", or already has spec.suspend: true; wires the call into the Job transform path after controller-UID removal.
Test expectations and new suspension cases
transform/kubernetes/kubernetes_test.go
Updates five existing controller-UID removal test expectations to include the /spec/suspend: true patch operation; adds four new test cases for standalone suspension, annotation opt-out, owned-Job skip, and already-suspended-Job skip.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

  • [BUG] Standalone Jobs re-execute on target cluster after migration crane#482 — [BUG] Standalone Jobs re-execute on target cluster after migration: This PR directly implements the proposed fix from that issue by suspending standalone Jobs (spec.suspend: true) during transformation and providing the crane.konveyor.io/job-idempotent annotation as an opt-out for critical init Jobs.

Poem

🐇 A bunny once hopped through the cluster's wide field,
Where Jobs kept re-running and refusing to yield.
So I patched in a suspend, set gently to true,
With an annotation escape hatch for init Jobs too.
Now standalone Jobs slumber until you say go —
The migration is tidy, the bunny says so! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change - automatically suspending standalone Kubernetes Jobs during migration, which is the primary objective of the PR.
Linked Issues check ✅ Passed The PR implements the suspend approach for standalone Jobs with annotation-based opt-out as described in issue #482, adding suspendStandaloneJob() logic and the idempotent annotation constant.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing Job suspension logic and tests for standalone Jobs, with no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
transform/kubernetes/kubernetes_test.go (1)

1048-1062: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

No-patch scenarios are currently unverified.

Line 1048 skips patch assertions when PatchResponseJson is empty, so cases like “DoNotSuspendStandaloneJobWithIdempotentAnnotation” and “SuspendStandaloneJobAlreadySuspended” do not actually verify that no suspend patch is emitted.

Proposed test harness fix
-			if len(c.PatchResponseJson) != 0 {
+			if len(c.PatchResponseJson) != 0 {
 				expectPatch, err := jsonpatch.DecodePatch([]byte(c.PatchResponseJson))
 				if err != nil {
 					t.Error(err)
 				}
 				if len(resp.Patches) != 0 {
 					ok, err := internaljsonpatch.Equal(resp.Patches, expectPatch)
 					if !ok || err != nil {
 						actual, _ := json.Marshal(resp.Patches)
 						t.Error(fmt.Sprintf("Invalid patches. Actual: %s, Expected: %v", actual, c.PatchResponseJson))
 					}
 				} else {
 					t.Error(fmt.Sprintf("Patches Expected: %#v, none found", expectPatch))
 				}
+			} else if len(resp.Patches) != 0 {
+				actual, _ := json.Marshal(resp.Patches)
+				t.Error(fmt.Sprintf("Unexpected patches. Actual: %s, Expected: none", actual))
 			}
🤖 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 `@transform/kubernetes/kubernetes_test.go` around lines 1048 - 1062, The test
currently skips patch verification when PatchResponseJson is empty, leaving
no-patch scenarios unverified. Add an else clause after the existing if
len(c.PatchResponseJson) != 0 block to verify that when no patches are expected,
resp.Patches is also empty. This ensures test cases like
"DoNotSuspendStandaloneJobWithIdempotentAnnotation" and
"SuspendStandaloneJobAlreadySuspended" actually verify that no suspend patch is
emitted.
🧹 Nitpick comments (1)
transform/kubernetes/kubernetes_test.go (1)

941-941: ⚡ Quick win

Use the exported annotation constant in test data.

Line 941 hardcodes crane.konveyor.io/job-idempotent; using kubernetes.CraneJobIdempotentAnnotation avoids drift if the key changes.

Proposed cleanup
-						"annotations": map[string]interface{}{
-							"crane.konveyor.io/job-idempotent": "true",
-						},
+						"annotations": map[string]interface{}{
+							kubernetes.CraneJobIdempotentAnnotation: "true",
+						},
🤖 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 `@transform/kubernetes/kubernetes_test.go` at line 941, The test data at the
hardcoded annotation key "crane.konveyor.io/job-idempotent" should use the
exported constant kubernetes.CraneJobIdempotentAnnotation instead. Replace the
hardcoded string literal with a reference to the constant to ensure the test
stays synchronized if the annotation key value is ever updated in the codebase.
🤖 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 `@transform/kubernetes/kubernetes.go`:
- Around line 1038-1039: The comment above the condition `if !found ||
!suspended` on line 1038 is inaccurate and misleading. The condition actually
patches when the suspend field is missing (not found) OR when suspend is false
(not suspended), but the comment incorrectly states it patches when suspend is
already true. Update the comment to accurately describe the actual logic: that
the patch is applied when the suspend field doesn't exist or when it's currently
set to false, ensuring the Job gets suspended by setting it to true.

---

Outside diff comments:
In `@transform/kubernetes/kubernetes_test.go`:
- Around line 1048-1062: The test currently skips patch verification when
PatchResponseJson is empty, leaving no-patch scenarios unverified. Add an else
clause after the existing if len(c.PatchResponseJson) != 0 block to verify that
when no patches are expected, resp.Patches is also empty. This ensures test
cases like "DoNotSuspendStandaloneJobWithIdempotentAnnotation" and
"SuspendStandaloneJobAlreadySuspended" actually verify that no suspend patch is
emitted.

---

Nitpick comments:
In `@transform/kubernetes/kubernetes_test.go`:
- Line 941: The test data at the hardcoded annotation key
"crane.konveyor.io/job-idempotent" should use the exported constant
kubernetes.CraneJobIdempotentAnnotation instead. Replace the hardcoded string
literal with a reference to the constant to ensure the test stays synchronized
if the annotation key value is ever updated in the codebase.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 871d59d8-3fb0-410e-af5c-a65336060585

📥 Commits

Reviewing files that changed from the base of the PR and between 71b323a and 642ff52.

📒 Files selected for processing (2)
  • transform/kubernetes/kubernetes.go
  • transform/kubernetes/kubernetes_test.go

Comment thread transform/kubernetes/kubernetes.go Outdated
Signed-off-by: Marek Aufart <maufart@redhat.com>

@stillalearner stillalearner 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.

lgtm

@aufi aufi merged commit 9791be7 into migtools:main Jun 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Standalone Jobs re-execute on target cluster after migration

2 participants