From 318b1790f2595dc87304d349d463f963ea509c01 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 21:27:49 +0000 Subject: [PATCH 1/2] feat(operator,chart): make backup/restore Job retry and deadline limits configurable Backup and restore restic Jobs took a hard-coded BackoffLimit 2 and ActiveDeadlineSeconds 86400. They now come from operator flags --backup-job-backoff-limit / --backup-job-active-deadline-seconds, set from Helm values operator.backupJobBackoffLimit / operator.backupJobActiveDeadlineSeconds (defaults 2 and 86400). Missing keys (helm upgrade --reuse-values from an older release) and a zero deadline fall back to the defaults; negative values fail the render and the operator start-up. Spec 018 OD-027. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UshpSigdLccuYZi7oFJX9y Signed-off-by: Claude --- CHANGELOG.md | 1 + charts/gameplane/templates/operator.yaml | 16 ++++++++ charts/gameplane/values.yaml | 6 +++ docs/install.md | 1 + operator/cmd/main.go | 40 ++++++++++++++----- .../internal/controller/backup_controller.go | 31 +++++++++++++- .../internal/controller/job_limits_test.go | 27 +++++++++++++ .../internal/controller/restore_controller.go | 8 +++- operator/specs.md | 6 ++- 9 files changed, 120 insertions(+), 16 deletions(-) create mode 100644 operator/internal/controller/job_limits_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index abe6c1bc6..ffefb94e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ reaches `1.0.0`. Pre-1.0 minor versions may contain breaking changes. ### Added +- **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`. - **Multicluster gateways and unified server management:** an optional private mTLS agent gateway (with its own Helm deployment) lets one dashboard and API manage authorized game servers across independent clusters. diff --git a/charts/gameplane/templates/operator.yaml b/charts/gameplane/templates/operator.yaml index 57afa3898..5666f66fb 100644 --- a/charts/gameplane/templates/operator.yaml +++ b/charts/gameplane/templates/operator.yaml @@ -282,6 +282,22 @@ spec: {{- if .Values.operator.resticImage }} - --restic-image={{ .Values.operator.resticImage }} {{- end }} + {{- /* Omit the flags when the keys are absent/null (e.g. --reuse-values + from a release predating them) so the operator's defaults + (2 / 86400) apply. Not `| default`: an explicit 0 backoff is + valid and must be honoured; a 0 deadline falls back. */}} + {{- if not (kindIs "invalid" .Values.operator.backupJobBackoffLimit) }} + {{- if lt (.Values.operator.backupJobBackoffLimit | int64) 0 }} + {{- fail "operator.backupJobBackoffLimit must be >= 0" }} + {{- end }} + - --backup-job-backoff-limit={{ .Values.operator.backupJobBackoffLimit | int64 }} + {{- end }} + {{- if .Values.operator.backupJobActiveDeadlineSeconds }} + {{- if lt (.Values.operator.backupJobActiveDeadlineSeconds | int64) 0 }} + {{- fail "operator.backupJobActiveDeadlineSeconds must be > 0" }} + {{- end }} + - --backup-job-active-deadline-seconds={{ .Values.operator.backupJobActiveDeadlineSeconds | int64 }} + {{- end }} - --agent-ca-bundle=/etc/gameplane/agent-ca/ca.crt - --agent-client-cert=/etc/gameplane/agent-client/tls.crt - --agent-client-key=/etc/gameplane/agent-client/tls.key diff --git a/charts/gameplane/values.yaml b/charts/gameplane/values.yaml index 9e18f21b9..17e7bf685 100644 --- a/charts/gameplane/values.yaml +++ b/charts/gameplane/values.yaml @@ -94,6 +94,12 @@ operator: # private registry mirror for air-gapped installs (Docker Hub blocked). configInitImage: busybox:1.38.0 resticImage: restic/restic:0.19.1 + # Bounds for the restic backup and restore Jobs the operator creates. + # backupJobBackoffLimit: retries before a Job is marked Failed (>= 0). + # backupJobActiveDeadlineSeconds: wall-clock cap per Job (> 0), so a stuck + # pod cannot hold a Backup/Restore in flight forever. Defaults: 2 and 86400 (24h). + backupJobBackoffLimit: 2 + backupJobActiveDeadlineSeconds: 86400 # Mount a directory of module bundles into the operator so local-type # ModuleSources can index it (homelab: drop module dirs on disk and # they appear in the catalog). Provide exactly one of existingClaim diff --git a/docs/install.md b/docs/install.md index 30a0fe6a4..690a698d1 100644 --- a/docs/install.md +++ b/docs/install.md @@ -113,6 +113,7 @@ Top-level knobs (see `values.yaml` for the full list): upstream pins; retag them to a private registry mirror for air-gapped clusters where Docker Hub is unreachable. They map to the operator's `--config-init-image` / `--restic-image` flags, mirroring `operator.agentImage` +- `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 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. - `operator.gameDataStorage.storageClassName` — install-time default storage class for game server data volumes (unreleased; ships in the next release) (default `""`). Empty string uses the cluster's default StorageClass. Applies to all GameServers where neither the GameTemplate diff --git a/operator/cmd/main.go b/operator/cmd/main.go index e15d7e0ba..dc85db9df 100644 --- a/operator/cmd/main.go +++ b/operator/cmd/main.go @@ -148,6 +148,8 @@ func main() { captureSidecarImage string captureDefaultMaxDurationSeconds int64 captureDefaultMaxSizeBytes int64 + backupJobBackoffLimit int64 + backupJobActiveDeadlineSeconds int64 ) flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "Address the metrics endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "Address the probe endpoint binds to.") @@ -167,6 +169,10 @@ func main() { flag.StringVar(&resticImage, "restic-image", controller.DefaultResticImage, "Image for the restic backup/restore Jobs. "+ "Point at a private registry mirror for air-gapped installs.") + flag.Int64Var(&backupJobBackoffLimit, "backup-job-backoff-limit", int64(controller.DefaultBackupJobBackoffLimit), + "BackoffLimit for the restic backup and restore Jobs (retries before the Job is marked Failed). Must be >= 0. Defaults to 2.") + flag.Int64Var(&backupJobActiveDeadlineSeconds, "backup-job-active-deadline-seconds", controller.DefaultBackupJobActiveDeadlineSeconds, + "ActiveDeadlineSeconds for the restic backup and restore Jobs. Must be > 0. Defaults to 86400 (24 hours).") flag.StringVar(&sentinelImage, "sentinel-image", controller.DefaultSentinelImage, "Image for the wake sentinel pod that holds advertised ports while a server is asleep. "+ "Point at a private registry mirror for air-gapped installs.") @@ -257,6 +263,16 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) setupLog := ctrl.Log.WithName("setup") + backupJobBackoff32, err := boundedInt32(backupJobBackoffLimit) // rejects <0 and >MaxInt32 + if err != nil { + setupLog.Error(err, "invalid --backup-job-backoff-limit value (must be >= 0)", "value", backupJobBackoffLimit) + os.Exit(1) + } + if backupJobActiveDeadlineSeconds <= 0 { + setupLog.Error(nil, "invalid --backup-job-active-deadline-seconds value (must be > 0)", "value", backupJobActiveDeadlineSeconds) + os.Exit(1) + } + // Validate capture retention flags while they are still the int64 type // flag.Int64Var naturally produces. The minimum of 60 seconds aligns with // the CRD's own validation. The maximum of 604800 (7 days) is the @@ -398,12 +414,14 @@ func main() { } if err := (&controller.BackupReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Clientset: kubernetes.NewForConfigOrDie(mgr.GetConfig()), - AgentClient: agentClient, - ResticImage: resticImage, - EventRecorder: mgr.GetEventRecorder("backup-controller"), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Clientset: kubernetes.NewForConfigOrDie(mgr.GetConfig()), + AgentClient: agentClient, + ResticImage: resticImage, + JobBackoffLimit: &backupJobBackoff32, + JobActiveDeadlineSeconds: backupJobActiveDeadlineSeconds, + EventRecorder: mgr.GetEventRecorder("backup-controller"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to set up controller", "controller", "Backup") os.Exit(1) @@ -416,10 +434,12 @@ func main() { os.Exit(1) } if err := (&controller.RestoreReconciler{ - Client: mgr.GetClient(), - APIReader: mgr.GetAPIReader(), - Scheme: mgr.GetScheme(), - ResticImage: resticImage, + Client: mgr.GetClient(), + APIReader: mgr.GetAPIReader(), + Scheme: mgr.GetScheme(), + ResticImage: resticImage, + JobBackoffLimit: &backupJobBackoff32, + JobActiveDeadlineSeconds: backupJobActiveDeadlineSeconds, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to set up controller", "controller", "Restore") os.Exit(1) diff --git a/operator/internal/controller/backup_controller.go b/operator/internal/controller/backup_controller.go index ec9e8c173..3a928b5fc 100644 --- a/operator/internal/controller/backup_controller.go +++ b/operator/internal/controller/backup_controller.go @@ -50,6 +50,29 @@ const ( // or the pod is restarted. const maxUnquiesceFinalizeRetry = 10 * time.Minute +// Default bounds for the restic backup and restore Jobs (spec 018 OD-027). +// Operators override them via --backup-job-backoff-limit / +// --backup-job-active-deadline-seconds (Helm operator.backupJobBackoffLimit / +// operator.backupJobActiveDeadlineSeconds). +const ( + DefaultBackupJobBackoffLimit int32 = 2 + DefaultBackupJobActiveDeadlineSeconds int64 = 86400 // 24 hours +) + +// resolveJobLimits returns the Job BackoffLimit and ActiveDeadlineSeconds to +// use: a nil backoff selects the default (0 is a valid explicit value, so +// nil, not zero, means unset); a non-positive deadline selects the default. +func resolveJobLimits(backoff *int32, deadline int64) (int32, int64) { + b, d := DefaultBackupJobBackoffLimit, DefaultBackupJobActiveDeadlineSeconds + if backoff != nil && *backoff >= 0 { + b = *backoff + } + if deadline > 0 { + d = deadline + } + return b, d +} + // backupRestoreJobLabel/backupRestoreJobValue mark Backup and Restore Job // pods so the chart's allow-backup-restore-egress NetworkPolicy (F-215) // can select them. Without this label the Job pod carries only the Job @@ -94,6 +117,11 @@ type BackupReconciler struct { // operator flag so air-gapped installs can point it at a private // registry mirror. Empty falls back to DefaultResticImage. ResticImage string + // JobBackoffLimit / JobActiveDeadlineSeconds bound the restic backup Job. + // Set from operator flags; nil / zero fall back to + // DefaultBackupJobBackoffLimit / DefaultBackupJobActiveDeadlineSeconds. + JobBackoffLimit *int32 + JobActiveDeadlineSeconds int64 // SnapshotScrapeGracePeriod bounds how long after the restic Job // completes the reconciler keeps retrying to read the snapshot id from // the pod logs before failing the Backup. Zero uses @@ -319,8 +347,7 @@ func (r *BackupReconciler) reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, err } - backoff := int32(2) - deadline := int64(86400) // 24 hours + backoff, deadline := resolveJobLimits(r.JobBackoffLimit, r.JobActiveDeadlineSeconds) job := &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: b.Name, Namespace: b.Namespace}} if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, job, func() error { if job.CreationTimestamp.IsZero() { diff --git a/operator/internal/controller/job_limits_test.go b/operator/internal/controller/job_limits_test.go new file mode 100644 index 000000000..d4d827bf3 --- /dev/null +++ b/operator/internal/controller/job_limits_test.go @@ -0,0 +1,27 @@ +package controller + +import "testing" + +func TestResolveJobLimits(t *testing.T) { + zero, five, neg := int32(0), int32(5), int32(-1) + cases := []struct { + name string + b *int32 + d int64 + wantB int32 + wantD int64 + }{ + {"unset uses defaults", nil, 0, 2, 86400}, + {"override both", &five, 3600, 5, 3600}, + {"explicit zero backoff honoured", &zero, 0, 0, 86400}, + {"negative backoff and deadline fall back", &neg, -5, 2, 86400}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + b, d := resolveJobLimits(tc.b, tc.d) + if b != tc.wantB || d != tc.wantD { + t.Errorf("got (%d,%d), want (%d,%d)", b, d, tc.wantB, tc.wantD) + } + }) + } +} diff --git a/operator/internal/controller/restore_controller.go b/operator/internal/controller/restore_controller.go index 839383d36..1ff7d44a2 100644 --- a/operator/internal/controller/restore_controller.go +++ b/operator/internal/controller/restore_controller.go @@ -36,6 +36,11 @@ type RestoreReconciler struct { // operator flag so air-gapped installs can point it at a private // registry mirror. Empty falls back to DefaultResticImage. ResticImage string + // JobBackoffLimit / JobActiveDeadlineSeconds bound the restic restore Job. + // Set from operator flags; nil / zero fall back to + // DefaultBackupJobBackoffLimit / DefaultBackupJobActiveDeadlineSeconds. + JobBackoffLimit *int32 + JobActiveDeadlineSeconds int64 } // apiReader returns the uncached reader for live-consistency checks, @@ -158,8 +163,7 @@ func (r *RestoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct // src (the source Backup, with RepoRef for the Job env) was resolved // above before the volume-snapshot branch. - backoff := int32(2) - deadline := int64(86400) // 24 hours + backoff, deadline := resolveJobLimits(r.JobBackoffLimit, r.JobActiveDeadlineSeconds) job := &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Name: "restore-" + rs.Name, Namespace: rs.Namespace}} _, err := controllerutil.CreateOrUpdate(ctx, r.Client, job, func() error { if job.CreationTimestamp.IsZero() { diff --git a/operator/specs.md b/operator/specs.md index 5337a8081..0024dc1fe 100644 --- a/operator/specs.md +++ b/operator/specs.md @@ -214,7 +214,7 @@ Primary reconcilers register with the manager in `cmd/main.go` and handle CRD li - **Responsibility:** Drive Backup to completion: coordinate quiesce (if agent available), spawn restic Job, track snapshot ID. - **Key functions:** - Quiesce orchestration: call agent Quiesce before Job, Unquiesce after completion. - - Restic Job: mount data PVC, run restic backup to configured destination (S3, B2, rest-server, local fs). Job carries BackoffLimit 2 and ActiveDeadlineSeconds 86400 (provisional, OD-005); only a Failed=True Job condition (set by jobPermanentlyFailed check) marks the Backup Failed. Pod SecurityContext takes FSGroup from the target GameTemplate's spec.security.fsGroup (fallback 65532 if template missing). + - Restic Job: mount data PVC, run restic backup to configured destination (S3, B2, rest-server, local fs). Job carries BackoffLimit and ActiveDeadlineSeconds from the reconciler's JobBackoffLimit / JobActiveDeadlineSeconds fields (operator flags --backup-job-backoff-limit / --backup-job-active-deadline-seconds, Helm operator.backupJobBackoffLimit / operator.backupJobActiveDeadlineSeconds; defaults 2 and 86400, applied when unset; spec 018 OD-027); only a Failed=True Job condition (set by jobPermanentlyFailed check) marks the Backup Failed. Pod SecurityContext takes FSGroup from the target GameTemplate's spec.security.fsGroup (fallback 65532 if template missing). - Snapshot tracking: parse restic JSON output from container logs to extract snapshot ID. - Snapshot cleanup on delete (`backup_forget.go`): restic-strategy Backups carry `BackupSnapshotFinalizer` (`gameplane.local/backup-snapshot-finalizer`, added on first reconcile, also for pre-existing non-Failed Backups; not added for volume-snapshot or Failed Backups). `finalizeDelete` runs the unquiesce stage (`BackupFinalizer`) first, then `finalizeSnapshot`: it deletes the Backup's own still-running Job (foreground, awaited), waits while a non-terminal Restore pins the Backup or its snapshot id, then runs Job `-forget` (`restic unlock || true; restic forget --prune --retry-lock 10m`, backoff 3, deadline 1800s, 50m/128Mi requests and 512Mi memory limit, no PVC; the snapshot id is passed through env `FORGET_SNAPSHOT_ID` and must match `^[0-9a-f]{8,64}$`). Success sets `SnapshotForgotten=True` and a Normal `SnapshotForgotten` event; a Failed Job, a missing/incomplete repo Secret, a terminating namespace, or `maxSnapshotForgetWait` (45m, from annotation `backup.gameplane.local/forget-started-at`) sets `SnapshotForgotten=False` with a Warning `SnapshotForgetAbandoned` event and releases the Backup, leaving the snapshot in the repository. A Backup with no recorded snapshot id is released with `SnapshotForgetSkipped` when it may have written one. Backup Jobs run `restic backup /data --json --retry-lock 10m ...`. RBAC markers on `backup_controller.go` grant `backups` update/patch, `backups/status`, `backups/finalizers` and `jobs` create/update/patch/delete. Tests: `backup_forget_test.go`, `backup_forget_envtest_test.go`, `retention_terminating_test.go`; e2e `TestBackup_DeleteForgetsSnapshot` and the extended `TestBackupSchedule_RetentionTrimsPast`. Spec: `specs/020-backup-snapshot-forget/`. - Status: report phase (Pending/Running/Succeeded/Failed), error details. @@ -233,7 +233,7 @@ Primary reconcilers register with the manager in `cmd/main.go` and handle CRD li - **Key functions:** - Pin snapshot ID at observation (immutable during restore). - Suspend: set spec.suspend=true on target GameServer, wait for pods to scale to 0. - - Restore Job: mount snapshot (from VolumeSnapshot or restic), run restic restore. Job carries BackoffLimit 2 and ActiveDeadlineSeconds 86400 (provisional, OD-005); only a Failed=True Job condition marks the Restore Failed. Pod SecurityContext takes FSGroup from the target GameTemplate's spec.security.fsGroup (fallback 65532 if template missing). + - Restore Job: mount snapshot (from VolumeSnapshot or restic), run restic restore. Job carries BackoffLimit and ActiveDeadlineSeconds from the reconciler's JobBackoffLimit / JobActiveDeadlineSeconds fields (operator flags --backup-job-backoff-limit / --backup-job-active-deadline-seconds, Helm operator.backupJobBackoffLimit / operator.backupJobActiveDeadlineSeconds; defaults 2 and 86400, applied when unset; spec 018 OD-027); only a Failed=True Job condition marks the Restore Failed. Pod SecurityContext takes FSGroup from the target GameTemplate's spec.security.fsGroup (fallback 65532 if template missing). - Resume: once the restore Job succeeds, clear the GameServer's suspend flag and set Restore's own phase to Succeeded in the same pass — it does not wait for the GameServer to actually reach Running again. - Status: report phase (Pending/Suspending/Running/Succeeded/Failed). `RestorePhaseResuming` is declared on the type but never assigned by this reconciler (F-260). - Volume-snapshot restore reference handling: When a volume-snapshot restore stands up a new GameServer from the original server's spec, any references in spec.env (SecretKeyRef, ConfigMapKeyRef) and spec.networking.tunnel.credentialsSecretRef that are owned by the original server must be copied to new objects owned by the restored server, with rewritten names, because the restored server does not own the originals. Ownership of a source object is an OwnerReference to the original server (Kind, Name, UID) and is re-verified against the source object immediately before each copy, not just once during planning. An object that already exists under a copy name is accepted only if the restored server controls it (a controller OwnerReference, `controller: true`); otherwise the restore fails. The restore fails before the new GameServer is created if the original server does not own every referenced object. The references are rewritten in the new GameServer's spec when it is created, and the copies are made right after creation and re-ensured on every reconcile pass. Only an ownership refusal fails the restore; transient errors (stale cache reads, write conflicts, a referenced object not yet visible in the cache) requeue it. A restore's 10-minute deadline starts when the restore first plans the new server (before it is created) and covers both creating the server with its reference copies and waiting for it to reach Running; it is enforced even while a transient error (such as a referenced object that stays missing) keeps recurring, so the restore cannot run past the limit, and once it expires the restore fails with a deadline message. @@ -372,6 +372,8 @@ Primary reconcilers register with the manager in `cmd/main.go` and handle CRD li | `--agent-image-pull-policy` | string | `` | ImagePullPolicy override (Always/IfNotPresent/Never); empty leaves unset. | | `--config-init-image` | string | `controller.DefaultConfigInitImage` | Init container for rendering config files onto data volume. | | `--restic-image` | string | `controller.DefaultResticImage` | restic backup/restore Job image. | +| `--backup-job-backoff-limit` | int64 | 2 | BackoffLimit of restic backup/restore Jobs; must be >= 0. | +| `--backup-job-active-deadline-seconds` | int64 | 86400 | ActiveDeadlineSeconds of restic backup/restore Jobs; must be > 0. | | `--sentinel-image` | string | `controller.DefaultSentinelImage` | Wake-on-connect sentinel container image. | | `--tunnel-frp-image` | string | `controller.DefaultTunnelFrpImage` | frp tunnel client container image. | | `--tunnel-tailscale-image` | string | `controller.DefaultTunnelTailscaleImage` | Tailscale tunnel client container image. | From 14d0a079265e730b2c54ca414bb1506caa95e0f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 21:32:34 +0000 Subject: [PATCH 2/2] fix(chart): reject non-integer backup Job limit values instead of rendering 0 `| int64` turned values like "abc" or 1.5 into 0, which disabled retries or gave the operator a 0 deadline it refuses at start-up. The template now checks for a whole number by kind (YAML numbers arrive as float64) before converting, and fails the render otherwise. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UshpSigdLccuYZi7oFJX9y Signed-off-by: Claude --- charts/gameplane/templates/operator.yaml | 25 ++++++++++++++++-------- docs/install.md | 2 +- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/charts/gameplane/templates/operator.yaml b/charts/gameplane/templates/operator.yaml index 5666f66fb..c81f91084 100644 --- a/charts/gameplane/templates/operator.yaml +++ b/charts/gameplane/templates/operator.yaml @@ -286,17 +286,26 @@ spec: from a release predating them) so the operator's defaults (2 / 86400) apply. Not `| default`: an explicit 0 backoff is valid and must be honoured; a 0 deadline falls back. */}} - {{- if not (kindIs "invalid" .Values.operator.backupJobBackoffLimit) }} - {{- if lt (.Values.operator.backupJobBackoffLimit | int64) 0 }} - {{- fail "operator.backupJobBackoffLimit must be >= 0" }} + {{- /* Validate before `| int64`: it turns any non-integer (e.g. "abc", + 1.5) into 0 instead of failing. YAML numbers arrive as + float64 (1000000 prints as 1e+06), so check by kind. */}} + {{- $backoff := .Values.operator.backupJobBackoffLimit }} + {{- if not (kindIs "invalid" $backoff) }} + {{- $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)) }} + {{- if or (not $whole) (lt ($backoff | int64) 0) }} + {{- fail "operator.backupJobBackoffLimit must be an integer >= 0" }} {{- end }} - - --backup-job-backoff-limit={{ .Values.operator.backupJobBackoffLimit | int64 }} + - --backup-job-backoff-limit={{ $backoff | int64 }} {{- end }} - {{- if .Values.operator.backupJobActiveDeadlineSeconds }} - {{- if lt (.Values.operator.backupJobActiveDeadlineSeconds | int64) 0 }} - {{- fail "operator.backupJobActiveDeadlineSeconds must be > 0" }} + {{- $deadline := .Values.operator.backupJobActiveDeadlineSeconds }} + {{- if $deadline }} + {{- $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)) }} + {{- if or (not $whole) (lt ($deadline | int64) 0) }} + {{- fail "operator.backupJobActiveDeadlineSeconds must be an integer > 0" }} + {{- end }} + {{- if gt ($deadline | int64) 0 }} + - --backup-job-active-deadline-seconds={{ $deadline | int64 }} {{- end }} - - --backup-job-active-deadline-seconds={{ .Values.operator.backupJobActiveDeadlineSeconds | int64 }} {{- end }} - --agent-ca-bundle=/etc/gameplane/agent-ca/ca.crt - --agent-client-cert=/etc/gameplane/agent-client/tls.crt diff --git a/docs/install.md b/docs/install.md index 690a698d1..68bf30122 100644 --- a/docs/install.md +++ b/docs/install.md @@ -113,7 +113,7 @@ Top-level knobs (see `values.yaml` for the full list): upstream pins; retag them to a private registry mirror for air-gapped clusters where Docker Hub is unreachable. They map to the operator's `--config-init-image` / `--restic-image` flags, mirroring `operator.agentImage` -- `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 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. +- `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. - `operator.gameDataStorage.storageClassName` — install-time default storage class for game server data volumes (unreleased; ships in the next release) (default `""`). Empty string uses the cluster's default StorageClass. Applies to all GameServers where neither the GameTemplate