diff --git a/agent/Dockerfile b/agent/Dockerfile index d6fdba85..33e9d5e7 100644 --- a/agent/Dockerfile +++ b/agent/Dockerfile @@ -341,7 +341,7 @@ ENTRYPOINT ["/usr/local/bin/snapshot-agent"] # PATH search — and so must this placeholder image (same BASE_IMAGE). # # Backend entrypoints run a restore standby path when the operator -# injects DYN_SNAPSHOT_RESTORE_STANDBY=1, then sleep while the +# injects SNAPSHOT_RESTORE_STANDBY=1, then sleep while the # snapshot-agent restores into the container. # ============================================================================= FROM ${BASE_IMAGE} AS placeholder diff --git a/api/v1alpha1/constants.go b/api/v1alpha1/constants.go index 2d2c32ac..c46c6d66 100644 --- a/api/v1alpha1/constants.go +++ b/api/v1alpha1/constants.go @@ -63,10 +63,25 @@ const ( // this name) keep working while new images can move to // SnapshotControlDirEnv. // - // Deprecated: use SnapshotControlDirEnv instead. Remove once no workload - // image depends on this name. + // Use SnapshotControlDirEnv for new workload images. Remove this legacy + // name once no workload image depends on it. LegacySnapshotControlDirEnv = "DYN_SNAPSHOT_CONTROL_DIR" + // RestoreStandbyModeEnv asks standby-aware workload entrypoints to capture + // restore context and sleep instead of cold-starting the workload. Generic + // images that do not honor this env must still provide their own inert + // restore command. + RestoreStandbyModeEnv = "SNAPSHOT_RESTORE_STANDBY" + + // LegacyRestoreStandbyModeEnv is the deprecated restore standby env var. + // Restore pod shaping injects both names during the migration window so + // existing workload images keep working while new images move to + // RestoreStandbyModeEnv. + // + // Use RestoreStandbyModeEnv for new workload images. Remove this legacy + // name once no workload image depends on it. + LegacyRestoreStandbyModeEnv = "DYN_SNAPSHOT_RESTORE_STANDBY" + // SnapshotCompleteFile named the sentinel the agent used to release a // checkpointed workload when leave-running dumps existed. A checkpoint now // always terminates the source process, so the agent no longer writes it; diff --git a/e2e/snapshot_e2e/workloads.py b/e2e/snapshot_e2e/workloads.py index cd40b798..200812f0 100644 --- a/e2e/snapshot_e2e/workloads.py +++ b/e2e/snapshot_e2e/workloads.py @@ -93,7 +93,7 @@ def restore_pod( } } spec["containers"][0]["env"] = [ - {"name": "DYN_SNAPSHOT_RESTORE_STANDBY", "value": "1"}, + {"name": "SNAPSHOT_RESTORE_STANDBY", "value": "1"}, {"name": "SNAPSHOT_CONTROL_DIR", "value": CONTROL_DIR}, {"name": RESTORE_TOKEN_ENV, "value": run.restore_token}, ] @@ -153,7 +153,7 @@ def multi_restore_pod( } ], "env": [ - {"name": "DYN_SNAPSHOT_RESTORE_STANDBY", "value": "1"}, + {"name": "SNAPSHOT_RESTORE_STANDBY", "value": "1"}, {"name": "SNAPSHOT_CONTROL_DIR", "value": CONTROL_DIR}, {"name": RESTORE_TOKEN_ENV, "value": restore_tokens[destination]}, ], diff --git a/operator/internal/protocol/restore.go b/operator/internal/protocol/restore.go index fa917158..5e4f63a9 100644 --- a/operator/internal/protocol/restore.go +++ b/operator/internal/protocol/restore.go @@ -19,14 +19,7 @@ type PodOptions struct { SeccompProfile string } -const ( - // RestoreStandbyModeEnv asks standby-aware workload entrypoints to capture - // restore context and sleep instead of cold-starting the workload. Generic - // images that do not honor this env must still provide their own inert - // restore command. - RestoreStandbyModeEnv = "DYN_SNAPSHOT_RESTORE_STANDBY" - restoreStartupFailureThreshold = 1800 // 30 minutes at 1s cadence. -) +const restoreStartupFailureThreshold = 1800 // 30 minutes at 1s cadence. // NewRestorePod shapes every annotated target container for restore. func NewRestorePod(pod *corev1.Pod, opts PodOptions) (*corev1.Pod, error) { @@ -55,7 +48,7 @@ func NewRestorePod(pod *corev1.Pod, opts PodOptions) (*corev1.Pod, error) { // PrepareRestorePodSpec applies restore shaping to annotated target containers. // It does not change container command/args. Once the checkpoint is ready, it -// sets DYN_SNAPSHOT_RESTORE_STANDBY=1 so standby-aware workload entrypoints +// sets SNAPSHOT_RESTORE_STANDBY=1 so standby-aware workload entrypoints // sleep before CRIU restore; generic images that do not honor the env must // still provide their own inert restore command. func PrepareRestorePodSpec( @@ -92,27 +85,25 @@ func PrepareRestorePodSpec( // Standby-aware entrypoints honor this env by writing restore // context and sleeping. Keep command/args intact so generic images // can provide their own inert restore entrypoint when needed. - foundRestoreStandbyModeEnv := false - for i := range container.Env { - if container.Env[i].Name == RestoreStandbyModeEnv { - container.Env[i].Value = "1" - container.Env[i].ValueFrom = nil - foundRestoreStandbyModeEnv = true - break - } - } - if !foundRestoreStandbyModeEnv { - container.Env = append(container.Env, corev1.EnvVar{ - Name: RestoreStandbyModeEnv, - Value: "1", - }) - } + ensureEnvValue(container, snapshotv1alpha1.RestoreStandbyModeEnv, "1") + ensureEnvValue(container, snapshotv1alpha1.LegacyRestoreStandbyModeEnv, "1") ensureRestoreStartupProbe(container) } } return nil } +func ensureEnvValue(container *corev1.Container, name, value string) { + for i := range container.Env { + if container.Env[i].Name == name { + container.Env[i].Value = value + container.Env[i].ValueFrom = nil + return + } + } + container.Env = append(container.Env, corev1.EnvVar{Name: name, Value: value}) +} + // ensureRestoreStartupProbe installs a StartupProbe that gates Ready until // CRIU restore completes. It prefers the workload's existing Startup/Liveness/ // Readiness probe (deep-copied with tightened cadence and infinite retries), diff --git a/operator/internal/protocol/restore_test.go b/operator/internal/protocol/restore_test.go index bf1db580..cfb3d0d1 100644 --- a/operator/internal/protocol/restore_test.go +++ b/operator/internal/protocol/restore_test.go @@ -52,7 +52,8 @@ func TestNewRestorePodSetsRestoreFromAnnotation(t *testing.T) { main := &pod.Spec.Containers[0] assert.Equal(t, []string{"python3"}, main.Command) assert.Equal(t, []string{"serve.py"}, main.Args) - assert.Equal(t, "1", envValue(main.Env, RestoreStandbyModeEnv)) + assert.Equal(t, "1", envValue(main.Env, snapshotv1alpha1.RestoreStandbyModeEnv)) + assert.Equal(t, "1", envValue(main.Env, snapshotv1alpha1.LegacyRestoreStandbyModeEnv)) assert.Equal(t, snapshotv1alpha1.SnapshotControlMountPath, main.VolumeMounts[0].MountPath) assert.Equal(t, "main", main.VolumeMounts[0].SubPath) require.NotNil(t, main.StartupProbe) @@ -80,7 +81,33 @@ func TestPrepareRestorePodSpecIsIdempotent(t *testing.T) { main := &spec.Containers[0] assert.Len(t, spec.Volumes, 1) assert.Len(t, main.VolumeMounts, 1) - assert.Equal(t, "1", envValue(main.Env, RestoreStandbyModeEnv)) + assert.Equal(t, "1", envValue(main.Env, snapshotv1alpha1.RestoreStandbyModeEnv)) + assert.Equal(t, "1", envValue(main.Env, snapshotv1alpha1.LegacyRestoreStandbyModeEnv)) + assert.Equal(t, 1, envCount(main.Env, snapshotv1alpha1.RestoreStandbyModeEnv)) + assert.Equal(t, 1, envCount(main.Env, snapshotv1alpha1.LegacyRestoreStandbyModeEnv)) +} + +func TestPrepareRestorePodSpecAddsCanonicalStandbyEnvWhenLegacyExists(t *testing.T) { + spec := restorePodFixture().Spec + spec.Containers[0].Env = []corev1.EnvVar{{ + Name: snapshotv1alpha1.LegacyRestoreStandbyModeEnv, + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{FieldPath: "metadata.name"}, + }, + }} + + require.NoError(t, PrepareRestorePodSpec(&spec, restoreMappings("main"), "", true)) + + main := &spec.Containers[0] + assert.Equal(t, "1", envValue(main.Env, snapshotv1alpha1.RestoreStandbyModeEnv)) + assert.Equal(t, "1", envValue(main.Env, snapshotv1alpha1.LegacyRestoreStandbyModeEnv)) + assert.Equal(t, 1, envCount(main.Env, snapshotv1alpha1.RestoreStandbyModeEnv)) + assert.Equal(t, 1, envCount(main.Env, snapshotv1alpha1.LegacyRestoreStandbyModeEnv)) + for _, item := range main.Env { + if item.Name == snapshotv1alpha1.LegacyRestoreStandbyModeEnv { + assert.Nil(t, item.ValueFrom) + } + } } func TestPrepareRestorePodSpecReusesExistingProbe(t *testing.T) { @@ -140,7 +167,8 @@ func TestNewRestorePodShapesMappedDestinations(t *testing.T) { require.Equal(t, name, container.Name) require.Len(t, container.VolumeMounts, 1) assert.Equal(t, name, container.VolumeMounts[0].SubPath) - assert.Equal(t, "1", envValue(container.Env, RestoreStandbyModeEnv)) + assert.Equal(t, "1", envValue(container.Env, snapshotv1alpha1.RestoreStandbyModeEnv)) + assert.Equal(t, "1", envValue(container.Env, snapshotv1alpha1.LegacyRestoreStandbyModeEnv)) require.NotNil(t, container.StartupProbe) } } @@ -170,3 +198,13 @@ func envValue(env []corev1.EnvVar, name string) string { } return "" } + +func envCount(env []corev1.EnvVar, name string) int { + count := 0 + for _, item := range env { + if item.Name == name { + count++ + } + } + return count +}