Skip to content

Commit 9a0f406

Browse files
authored
Merge pull request #573 from ValgulNecron/fix/018-backup-job-limits-values
feat(operator,chart): make backup/restore Job retry and deadline limits configurable
2 parents a5fc16e + 14d0a07 commit 9a0f406

9 files changed

Lines changed: 129 additions & 16 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ reaches `1.0.0`. Pre-1.0 minor versions may contain breaking changes.
4949

5050
### Added
5151

52+
- **Configurable backup/restore Job limits:** the restic backup and restore Jobs' retry limit and deadline are now Helm values (`operator.backupJobBackoffLimit`, default `2`; `operator.backupJobActiveDeadlineSeconds`, default `86400`), passed to the operator as `--backup-job-backoff-limit` / `--backup-job-active-deadline-seconds`.
5253
- **Multicluster gateways and unified server management:** an optional
5354
private mTLS agent gateway (with its own Helm deployment) lets one dashboard
5455
and API manage authorized game servers across independent clusters.

‎charts/gameplane/templates/operator.yaml‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,31 @@ spec:
282282
{{- if .Values.operator.resticImage }}
283283
- --restic-image={{ .Values.operator.resticImage }}
284284
{{- end }}
285+
{{- /* Omit the flags when the keys are absent/null (e.g. --reuse-values
286+
from a release predating them) so the operator's defaults
287+
(2 / 86400) apply. Not `| default`: an explicit 0 backoff is
288+
valid and must be honoured; a 0 deadline falls back. */}}
289+
{{- /* Validate before `| int64`: it turns any non-integer (e.g. "abc",
290+
1.5) into 0 instead of failing. YAML numbers arrive as
291+
float64 (1000000 prints as 1e+06), so check by kind. */}}
292+
{{- $backoff := .Values.operator.backupJobBackoffLimit }}
293+
{{- if not (kindIs "invalid" $backoff) }}
294+
{{- $whole := or (and (kindIs "float64" $backoff) (eq ($backoff | int64 | float64) $backoff)) (kindIs "int" $backoff) (kindIs "int64" $backoff) (and (kindIs "string" $backoff) (regexMatch "^[0-9]+$" $backoff)) }}
295+
{{- if or (not $whole) (lt ($backoff | int64) 0) }}
296+
{{- fail "operator.backupJobBackoffLimit must be an integer >= 0" }}
297+
{{- end }}
298+
- --backup-job-backoff-limit={{ $backoff | int64 }}
299+
{{- end }}
300+
{{- $deadline := .Values.operator.backupJobActiveDeadlineSeconds }}
301+
{{- if $deadline }}
302+
{{- $whole := or (and (kindIs "float64" $deadline) (eq ($deadline | int64 | float64) $deadline)) (kindIs "int" $deadline) (kindIs "int64" $deadline) (and (kindIs "string" $deadline) (regexMatch "^[0-9]+$" $deadline)) }}
303+
{{- if or (not $whole) (lt ($deadline | int64) 0) }}
304+
{{- fail "operator.backupJobActiveDeadlineSeconds must be an integer > 0" }}
305+
{{- end }}
306+
{{- if gt ($deadline | int64) 0 }}
307+
- --backup-job-active-deadline-seconds={{ $deadline | int64 }}
308+
{{- end }}
309+
{{- end }}
285310
- --agent-ca-bundle=/etc/gameplane/agent-ca/ca.crt
286311
- --agent-client-cert=/etc/gameplane/agent-client/tls.crt
287312
- --agent-client-key=/etc/gameplane/agent-client/tls.key

‎charts/gameplane/values.yaml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ operator:
9494
# private registry mirror for air-gapped installs (Docker Hub blocked).
9595
configInitImage: busybox:1.38.0
9696
resticImage: restic/restic:0.19.1
97+
# Bounds for the restic backup and restore Jobs the operator creates.
98+
# backupJobBackoffLimit: retries before a Job is marked Failed (>= 0).
99+
# backupJobActiveDeadlineSeconds: wall-clock cap per Job (> 0), so a stuck
100+
# pod cannot hold a Backup/Restore in flight forever. Defaults: 2 and 86400 (24h).
101+
backupJobBackoffLimit: 2
102+
backupJobActiveDeadlineSeconds: 86400
97103
# Mount a directory of module bundles into the operator so local-type
98104
# ModuleSources can index it (homelab: drop module dirs on disk and
99105
# they appear in the catalog). Provide exactly one of existingClaim

‎docs/install.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ Top-level knobs (see `values.yaml` for the full list):
113113
upstream pins; retag them to a private registry mirror for air-gapped clusters
114114
where Docker Hub is unreachable. They map to the operator's
115115
`--config-init-image` / `--restic-image` flags, mirroring `operator.agentImage`
116+
- `operator.backupJobBackoffLimit` / `operator.backupJobActiveDeadlineSeconds` — retry limit (default `2`, must be >= 0) and wall-clock deadline in seconds (default `86400`, must be > 0) for the restic backup/restore Jobs. They map to the operator's `--backup-job-backoff-limit` / `--backup-job-active-deadline-seconds` flags. Negative or non-integer values fail the render; an unset key (e.g. `helm upgrade --reuse-values` from an older release) or a `0` deadline uses the default, while an explicit `0` backoff is honoured.
116117
- `operator.gameDataStorage.storageClassName` — install-time default storage class
117118
for game server data volumes (unreleased; ships in the next release) (default `""`). Empty string uses the cluster's
118119
default StorageClass. Applies to all GameServers where neither the GameTemplate

‎operator/cmd/main.go‎

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ func main() {
148148
captureSidecarImage string
149149
captureDefaultMaxDurationSeconds int64
150150
captureDefaultMaxSizeBytes int64
151+
backupJobBackoffLimit int64
152+
backupJobActiveDeadlineSeconds int64
151153
)
152154
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "Address the metrics endpoint binds to.")
153155
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "Address the probe endpoint binds to.")
@@ -167,6 +169,10 @@ func main() {
167169
flag.StringVar(&resticImage, "restic-image", controller.DefaultResticImage,
168170
"Image for the restic backup/restore Jobs. "+
169171
"Point at a private registry mirror for air-gapped installs.")
172+
flag.Int64Var(&backupJobBackoffLimit, "backup-job-backoff-limit", int64(controller.DefaultBackupJobBackoffLimit),
173+
"BackoffLimit for the restic backup and restore Jobs (retries before the Job is marked Failed). Must be >= 0. Defaults to 2.")
174+
flag.Int64Var(&backupJobActiveDeadlineSeconds, "backup-job-active-deadline-seconds", controller.DefaultBackupJobActiveDeadlineSeconds,
175+
"ActiveDeadlineSeconds for the restic backup and restore Jobs. Must be > 0. Defaults to 86400 (24 hours).")
170176
flag.StringVar(&sentinelImage, "sentinel-image", controller.DefaultSentinelImage,
171177
"Image for the wake sentinel pod that holds advertised ports while a server is asleep. "+
172178
"Point at a private registry mirror for air-gapped installs.")
@@ -257,6 +263,16 @@ func main() {
257263
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
258264
setupLog := ctrl.Log.WithName("setup")
259265

266+
backupJobBackoff32, err := boundedInt32(backupJobBackoffLimit) // rejects <0 and >MaxInt32
267+
if err != nil {
268+
setupLog.Error(err, "invalid --backup-job-backoff-limit value (must be >= 0)", "value", backupJobBackoffLimit)
269+
os.Exit(1)
270+
}
271+
if backupJobActiveDeadlineSeconds <= 0 {
272+
setupLog.Error(nil, "invalid --backup-job-active-deadline-seconds value (must be > 0)", "value", backupJobActiveDeadlineSeconds)
273+
os.Exit(1)
274+
}
275+
260276
// Validate capture retention flags while they are still the int64 type
261277
// flag.Int64Var naturally produces. The minimum of 60 seconds aligns with
262278
// the CRD's own validation. The maximum of 604800 (7 days) is the
@@ -398,12 +414,14 @@ func main() {
398414
}
399415

400416
if err := (&controller.BackupReconciler{
401-
Client: mgr.GetClient(),
402-
Scheme: mgr.GetScheme(),
403-
Clientset: kubernetes.NewForConfigOrDie(mgr.GetConfig()),
404-
AgentClient: agentClient,
405-
ResticImage: resticImage,
406-
EventRecorder: mgr.GetEventRecorder("backup-controller"),
417+
Client: mgr.GetClient(),
418+
Scheme: mgr.GetScheme(),
419+
Clientset: kubernetes.NewForConfigOrDie(mgr.GetConfig()),
420+
AgentClient: agentClient,
421+
ResticImage: resticImage,
422+
JobBackoffLimit: &backupJobBackoff32,
423+
JobActiveDeadlineSeconds: backupJobActiveDeadlineSeconds,
424+
EventRecorder: mgr.GetEventRecorder("backup-controller"),
407425
}).SetupWithManager(mgr); err != nil {
408426
setupLog.Error(err, "unable to set up controller", "controller", "Backup")
409427
os.Exit(1)
@@ -416,10 +434,12 @@ func main() {
416434
os.Exit(1)
417435
}
418436
if err := (&controller.RestoreReconciler{
419-
Client: mgr.GetClient(),
420-
APIReader: mgr.GetAPIReader(),
421-
Scheme: mgr.GetScheme(),
422-
ResticImage: resticImage,
437+
Client: mgr.GetClient(),
438+
APIReader: mgr.GetAPIReader(),
439+
Scheme: mgr.GetScheme(),
440+
ResticImage: resticImage,
441+
JobBackoffLimit: &backupJobBackoff32,
442+
JobActiveDeadlineSeconds: backupJobActiveDeadlineSeconds,
423443
}).SetupWithManager(mgr); err != nil {
424444
setupLog.Error(err, "unable to set up controller", "controller", "Restore")
425445
os.Exit(1)

‎operator/internal/controller/backup_controller.go‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,29 @@ const (
5050
// or the pod is restarted.
5151
const maxUnquiesceFinalizeRetry = 10 * time.Minute
5252

53+
// Default bounds for the restic backup and restore Jobs (spec 018 OD-027).
54+
// Operators override them via --backup-job-backoff-limit /
55+
// --backup-job-active-deadline-seconds (Helm operator.backupJobBackoffLimit /
56+
// operator.backupJobActiveDeadlineSeconds).
57+
const (
58+
DefaultBackupJobBackoffLimit int32 = 2
59+
DefaultBackupJobActiveDeadlineSeconds int64 = 86400 // 24 hours
60+
)
61+
62+
// resolveJobLimits returns the Job BackoffLimit and ActiveDeadlineSeconds to
63+
// use: a nil backoff selects the default (0 is a valid explicit value, so
64+
// nil, not zero, means unset); a non-positive deadline selects the default.
65+
func resolveJobLimits(backoff *int32, deadline int64) (int32, int64) {
66+
b, d := DefaultBackupJobBackoffLimit, DefaultBackupJobActiveDeadlineSeconds
67+
if backoff != nil && *backoff >= 0 {
68+
b = *backoff
69+
}
70+
if deadline > 0 {
71+
d = deadline
72+
}
73+
return b, d
74+
}
75+
5376
// backupRestoreJobLabel/backupRestoreJobValue mark Backup and Restore Job
5477
// pods so the chart's allow-backup-restore-egress NetworkPolicy (F-215)
5578
// can select them. Without this label the Job pod carries only the Job
@@ -94,6 +117,11 @@ type BackupReconciler struct {
94117
// operator flag so air-gapped installs can point it at a private
95118
// registry mirror. Empty falls back to DefaultResticImage.
96119
ResticImage string
120+
// JobBackoffLimit / JobActiveDeadlineSeconds bound the restic backup Job.
121+
// Set from operator flags; nil / zero fall back to
122+
// DefaultBackupJobBackoffLimit / DefaultBackupJobActiveDeadlineSeconds.
123+
JobBackoffLimit *int32
124+
JobActiveDeadlineSeconds int64
97125
// SnapshotScrapeGracePeriod bounds how long after the restic Job
98126
// completes the reconciler keeps retrying to read the snapshot id from
99127
// the pod logs before failing the Backup. Zero uses
@@ -319,8 +347,7 @@ func (r *BackupReconciler) reconcile(ctx context.Context, req ctrl.Request) (ctr
319347
return ctrl.Result{}, err
320348
}
321349

322-
backoff := int32(2)
323-
deadline := int64(86400) // 24 hours
350+
backoff, deadline := resolveJobLimits(r.JobBackoffLimit, r.JobActiveDeadlineSeconds)
324351
job := &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: b.Name, Namespace: b.Namespace}}
325352
if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, job, func() error {
326353
if job.CreationTimestamp.IsZero() {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package controller
2+
3+
import "testing"
4+
5+
func TestResolveJobLimits(t *testing.T) {
6+
zero, five, neg := int32(0), int32(5), int32(-1)
7+
cases := []struct {
8+
name string
9+
b *int32
10+
d int64
11+
wantB int32
12+
wantD int64
13+
}{
14+
{"unset uses defaults", nil, 0, 2, 86400},
15+
{"override both", &five, 3600, 5, 3600},
16+
{"explicit zero backoff honoured", &zero, 0, 0, 86400},
17+
{"negative backoff and deadline fall back", &neg, -5, 2, 86400},
18+
}
19+
for _, tc := range cases {
20+
t.Run(tc.name, func(t *testing.T) {
21+
b, d := resolveJobLimits(tc.b, tc.d)
22+
if b != tc.wantB || d != tc.wantD {
23+
t.Errorf("got (%d,%d), want (%d,%d)", b, d, tc.wantB, tc.wantD)
24+
}
25+
})
26+
}
27+
}

‎operator/internal/controller/restore_controller.go‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ type RestoreReconciler struct {
3636
// operator flag so air-gapped installs can point it at a private
3737
// registry mirror. Empty falls back to DefaultResticImage.
3838
ResticImage string
39+
// JobBackoffLimit / JobActiveDeadlineSeconds bound the restic restore Job.
40+
// Set from operator flags; nil / zero fall back to
41+
// DefaultBackupJobBackoffLimit / DefaultBackupJobActiveDeadlineSeconds.
42+
JobBackoffLimit *int32
43+
JobActiveDeadlineSeconds int64
3944
}
4045

4146
// apiReader returns the uncached reader for live-consistency checks,
@@ -158,8 +163,7 @@ func (r *RestoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
158163

159164
// src (the source Backup, with RepoRef for the Job env) was resolved
160165
// above before the volume-snapshot branch.
161-
backoff := int32(2)
162-
deadline := int64(86400) // 24 hours
166+
backoff, deadline := resolveJobLimits(r.JobBackoffLimit, r.JobActiveDeadlineSeconds)
163167
job := &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: "restore-" + rs.Name, Namespace: rs.Namespace}}
164168
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, job, func() error {
165169
if job.CreationTimestamp.IsZero() {

0 commit comments

Comments
 (0)