Skip to content

feat: make StoreExec and StoreDebugInstance cleanup configurable per CR - #261

Merged
Patrick Derks (TrayserCassa) merged 5 commits into
mainfrom
fix/storeexec-job-lifecycle
Oct 8, 2026
Merged

Patrick Derks (TrayserCassa) merged 5 commits into
mainfrom
fix/storeexec-job-lifecycle

Conversation

@drzombey

@drzombey Tim Lange (drzombey) commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Problem

CR cleanup was driven by a single operator-wide env var (SUCCESSFUL_CR_CLEANUP_GRACE_PERIOD, default 1h) shared by StoreExec and StoreDebugInstance, and on the exec side it only ever applied to executions that finished successfully. That has three consequences:

  1. Every store in the cluster gets the same retention. A customer who wants failed migrations kept around for debugging and successful ones gone immediately has no way to say so.
  2. StoreExecs in error are never collected at all, so they accumulate in the namespace forever.
  3. The only opt-out was global. Setting successfulCRCleanupGracePeriod: "0" to keep debug CRs around also disabled StoreExec cleanup, and vice versa.

The fallback in storeExecFinishedAt also hid the first problem: if no condition carried a timestamp it returned time.Now() (or the CR creation time), so the delete window was computed from an arbitrary point rather than from when the execution actually finished.

Change

Retention is configured per CR, not per operator. SUCCESSFUL_CR_CLEANUP_GRACE_PERIOD is removed from internal/config, from both reconcilers, from cmd/main.go, and successfulCRCleanupGracePeriod is removed from the Helm chart. There is no operator-wide replacement.

StoreExec

StoreExecSpec gains two fields:

  • cleanupPeriodSuccessfulExec (default 5m)
  • cleanupPeriodErrorExec (default 1h)

reconcileSuccessfulStoreExecCleanup becomes reconcileStoreExecCleanup and picks the period from the CR's own state via cleanupPeriodFor: done → successful period, error → error period, anything else → no cleanup. A period of 0 disables cleanup for that case, so the old opt-out still exists, now per execution. Cron StoreExecs and CRs already being deleted are skipped as before.

Failed executions are collected too. error is now a terminal state for cleanup purposes, with its own (longer) default so there is time to look at it before it disappears.

No more guessed finish time. storeExecFinishedAt returns (time.Time, bool) and only answers for a terminal state, using the newest condition that actually has a LastTransitionTime. If nothing usable is recorded, cleanup does not run instead of deleting against a made-up timestamp.

StoreDebugInstance

spec.duration now drives both expiry and cleanup, replacing the removed grace period. spec.duration: 0 disables both, matching the 0 semantics of the two StoreExec fields.

spec.duration also changes from string to metav1.Duration. The CRD schema is unchanged (type: string, default: 1h) and the accepted grammar is identical, because metav1.Duration.UnmarshalJSON uses the same time.ParseDuration the four call sites called explicitly before. Those four manual parses, and the // validate duration block in Reconcile, are gone.

Duration validation

All three duration fields carry a CEL rule so the API server rejects malformed values at admission:

x-kubernetes-validations:
- message: must be a valid duration, e.g. 30s, 5m or 1h
  rule: self.matches('^(0|([0-9]+([.][0-9]+)?(ns|us|ms|s|m|h))+)$')

This is not cosmetic. metav1.Duration fails at JSON decode time, while the API server only validates type: string — so a single object with duration: "nonsense" is accepted into etcd and then breaks the typed client's LIST/WATCH for the whole type. Measured in a k3d cluster (see below): one such object took the entire StoreDebugInstance controller offline. +kubebuilder:validation:Pattern cannot be used here, controller-gen rejects it on non-string Go types.

Behaviour changes

before after
StoreExec done retention 1h, operator-wide spec.cleanupPeriodSuccessfulExec, default 5m
StoreExec error retention never collected spec.cleanupPeriodErrorExec, default 1h
StoreDebugInstance deletion creation + duration + grace creation + duration
StoreDebugInstance duration: 0 expires immediately, CR deleted after grace never expires, CR never collected
invalid duration string silently treated as 0 rejected by the API server

Two consequences worth calling out explicitly:

  • A completed debug CR is visible in done for about one requeue interval (10s) instead of the previous grace period. duration is the lifetime the caller asked for, and the CR no longer outliving it is the intended reading.
  • The combination "let the debug workload expire but retain its completed CR" is gone. It was only reachable through successfulCRCleanupGracePeriod: "0", which also disabled StoreExec cleanup. Removing that single global switch is the point of this PR.

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Existing Jobs can be recreated during upgrade, and cache latency can falsely mark newly created Jobs as failed.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Ties command Job lifetime to its StoreExec and adds cleanup for failed executions.

Changes:

  • Removes command Job TTL and tracks Job creation.
  • Detects missing previously started Jobs.
  • Adds configurable failed-StoreExec cleanup and tests.
File Description
internal/​job/​command.go Removes command Job TTL.
internal/​job/​command_test.go Tests TTL removal.
internal/​controller/​storeexec_status.go Detects disappeared Jobs.
internal/​controller/​storeexec_job_test.go Tests one-time creation and missing Jobs.
internal/​controller/​storeexec_controller.go Tracks creation and expands cleanup.
internal/​controller/​cleanup_test.go Tests failed and cron cleanup.
internal/​config/​config.go Adds failed-cleanup configuration.
helm/​values.yaml Exposes the cleanup grace period.
helm/​templates/​deployment.yaml Configures the environment variable.
cmd/​main.go Wires configuration into the reconciler.
api/​v1/​zz_generated.deepcopy.go Copies the new status field.
api/​v1/​execstatus.go Adds jobStartedAt status.
Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/controller/storeexec_controller.go
Comment thread internal/controller/storeexec_status.go Outdated
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Preview helm chart

helm upgrade --install shopware-operator \
  oci://ghcr.io/shopware/shopware-operator-preview/operator \
  --version 0.0.0-fix-storeexec-job-lifecycle.ga314be6
Chart version 0.0.0-fix-storeexec-job-lifecycle.ga314be6
Image tag fix-storeexec-job-lifecycle
Commit a314be6

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Legacy running executions whose Jobs were already collected can still be recreated once during rollout.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (2)
Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file

Comment thread internal/controller/storeexec_controller.go Outdated

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

Removing the shared TTL also unintentionally disables cleanup for CronJob-generated Jobs.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file
Previously missed (1)

In code that hasn't changed since last review

Medium severity CronJob templates lose TTL, retaining completed Jobs and pods indefinitely

internal/​job/​command.go:143

Removing the TTL from getJobSpec also removes it from CommandCronJob's JobTemplate, because that constructor uses this same helper. Cron-generated Jobs are owned by the CronJob rather than following the one-shot StoreExec lifecycle, and cron StoreExecs are explicitly excluded from the new cleanup path; suspended or no-longer-scheduled cron executions can therefore retain their completed Job and pods indefinitely instead of the previous 24 hours. Keep the TTL on the CronJob template while omitting it only for CommandJob.

@drzombey

Copy link
Copy Markdown
Contributor Author

Picking up the "previously missed" finding from the latest overview — CronJob templates lose TTL — it is correct and fixed in 3129184.

getJobSpec is shared by CommandJob and CommandCronJob, so dropping TTLSecondsAfterFinished took it off the cron job template too. Those jobs belong to the CronJob rather than to a one-shot StoreExec, and cron StoreExecs are excluded from the cleanup path, so nothing would have collected them. CommandCronJob now sets the TTL back on its own template; CommandJob keeps none.

One qualifier on the description: growth is not unbounded, since the CronJob history limits (defaults 3 successful / 1 failed) still apply. The real regression is a suspended or unscheduled cron — with no further runs to push them out, it would have kept those last jobs and their pods for good instead of 24h.

Covered by two tests now: the one-shot job carries no TTL, the cron template carries 86400.

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

The missing-Job deadline incorrectly marks healthy cron StoreExecs as failed after 24 hours.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file

Comment thread internal/controller/storeexec_status.go Outdated
@drzombey Tim Lange (drzombey) changed the title fix: tie the command job lifetime to its StoreExec fix: start a command job exactly once per StoreExec Sep 29, 2026
@drzombey
Tim Lange (drzombey) force-pushed the fix/storeexec-job-lifecycle branch 4 times, most recently from 3a46e89 to e59c52b Compare October 5, 2026 08:25
@drzombey Tim Lange (drzombey) changed the title fix: start a command job exactly once per StoreExec feat: make StoreExec cleanup configurable per CR Oct 5, 2026
@drzombey
Tim Lange (drzombey) force-pushed the fix/storeexec-job-lifecycle branch from 311f07e to 796e304 Compare October 5, 2026 08:38
@drzombey
Tim Lange (drzombey) requested a balanced review from Copilot October 5, 2026 08:41

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Unset duration fields from typed Go clients bypass the intended defaults and disable cleanup.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
Resolved since last review (1)
Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file

Comment thread api/v1/exec.go
Comment thread internal/controller/storeexec_controller.go

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

A terminal execution skipped by cleanup can be reset and potentially executed again when its referenced Store is absent.

Review effort: Balanced
Findings: 1 High severity · 2 Low severity

Open (3)
Resolved since last review (2)
Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file

Comment thread internal/controller/storeexec_controller.go
Comment thread cmd/main.go
Comment thread internal/controller/storeexec_controller.go Outdated
Comment thread internal/controller/storeexec_controller.go Outdated
Comment thread api/v1/exec.go Outdated
Comment thread api/v1/exec.go Outdated
Comment thread helm/values.yaml Outdated
@drzombey
Tim Lange (drzombey) force-pushed the fix/storeexec-job-lifecycle branch from e645f3e to 705a058 Compare October 7, 2026 13:31
StoreExec cleanup was driven by a single operator-wide grace period and
only applied to successful executions, so every store shared one retention
and failed executions were never collected.

StoreExecSpec gains cleanupPeriodSuccessfulExec (default 5m) and
cleanupPeriodErrorExec (default 1h). reconcileStoreExecCleanup picks the
period from the CR's own state; a period of zero disables cleanup for that
state, which keeps the previous opt-out per execution.

Both fields are pointers: omitempty does not drop a zero metav1.Duration,
so a value field would send an explicit "0s" from typed Go clients and
bypass the CRD defaults. An unset field falls back to the same default in
code, which also covers an older CRD that dropped the field.

storeExecFinishedAt no longer guesses. It answers only for a terminal state
and only from a condition that carries a LastTransitionTime, instead of
falling back to the creation time or time.Now().

CleanupGracePeriod is dropped from StoreExecReconciler; StoreDebugInstance
keeps using it.
@drzombey
Tim Lange (drzombey) force-pushed the fix/storeexec-job-lifecycle branch 2 times, most recently from 43d431c to eadd29d Compare October 7, 2026 13:35

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Skipped cleanup can reset terminal executions, and debug-instance retention changes exceed the described scope.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (3)
Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file

Comment thread internal/controller/storedebuginstance_controller.go
@drzombey Tim Lange (drzombey) changed the title feat: make StoreExec cleanup configurable per CR feat: make StoreExec and StoreDebugInstance cleanup configurable per CR Oct 7, 2026

Copilot AI 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.

🟡 Changes recommended

Legacy or overflowing duration values can still break informer decoding, and zero-duration labels incorrectly indicate expiration.

2 open findings
1 resolved since last review
Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file
Previously missed (1)

In code that hasn't changed since last review

Medium severity Zero duration incorrectly publishes an expired expiry label

internal/​util/​labels.go:58

A zero duration now means the debug instance never expires, but this still publishes its creation time as store.debug.validUntil, making the resource appear already expired to label consumers. Omit the expiry label when duration is zero so the generated Pod and Service labels reflect the new semantics.

	validUntil := storeDebugInstance.CreationTimestamp.Add(storeDebugInstance.Spec.Duration.Duration)

	labels[ShopwareKey("store.debug")] = "true"
	labels[ShopwareKey("store.debug.instance")] = storeDebugInstance.Name
	labels[ShopwareKey("store.debug.validUntil")] = fmt.Sprintf("%d", validUntil.UnixNano())

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread api/v1/exec.go
Comment thread api/v1/storedebuginstance_types.go
@TrayserCassa
Patrick Derks (TrayserCassa) merged commit 0be3597 into main Oct 8, 2026
7 checks passed
@TrayserCassa
Patrick Derks (TrayserCassa) deleted the fix/storeexec-job-lifecycle branch October 8, 2026 09:30
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.

3 participants