From a78f8e2284255127f9c9ad3a11025d1ada2f5ccc Mon Sep 17 00:00:00 2001 From: cloudonly Date: Tue, 13 Oct 2026 16:52:20 +0800 Subject: [PATCH] Require mounted snapshot-capable volumes for VOLUMES templates --- .../internal/apivalidation/actor_template.go | 19 +++++++-- .../apivalidation/actor_template_test.go | 41 ++++++++++++++++++- .../controlapi/functionaltest/actor_test.go | 8 ++-- docs/api-guide.md | 4 ++ 4 files changed, 65 insertions(+), 7 deletions(-) diff --git a/cmd/ateapi/internal/apivalidation/actor_template.go b/cmd/ateapi/internal/apivalidation/actor_template.go index 4df9c84a41..869fb1a5e1 100644 --- a/cmd/ateapi/internal/apivalidation/actor_template.go +++ b/cmd/ateapi/internal/apivalidation/actor_template.go @@ -75,23 +75,36 @@ func ValidateCustom_CreateActorTemplateRequest_ActorTemplate(_ context.Context, value.GetSnapshotConfig().GetPreferredFidelity().String(), "ROOTFS fidelity requires sandbox_config.sandbox_class SANDBOX_CLASS_MICROVM")) } - declared := make(map[string]bool, len(value.GetVolumes())) + declared := make(map[string]*ateapipb.Volume, len(value.GetVolumes())) for _, vol := range value.GetVolumes() { - declared[vol.GetName()] = true + declared[vol.GetName()] = vol } + canCaptureVolumes := false for i, ctr := range value.GetContainers() { for j, mount := range ctr.GetVolumeMounts() { name := mount.GetName() if name == "" { continue // required is enforced by tags } - if !declared[name] { + volume := declared[name] + if volume == nil { errs = append(errs, field.Invalid( fldPath.Child("containers").Index(i).Child("volume_mounts").Index(j).Child("name"), name, "must reference a volume declared in the template")) + continue + } + if volume.GetDurableDir() != nil || + (value.GetSandboxConfig().GetSandboxClass() == ateapipb.SandboxClass_SANDBOX_CLASS_MICROVM && volume.GetExternalVolumeTemplate() != nil) { + canCaptureVolumes = true } } } + if value.GetSnapshotConfig().GetPreferredFidelity() == ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES && !canCaptureVolumes { + errs = append(errs, field.Invalid( + fldPath.Child("snapshot_config", "preferred_fidelity"), + value.GetSnapshotConfig().GetPreferredFidelity().String(), + "VOLUMES fidelity requires a mounted durable-dir volume, or a mounted external volume on MICROVM")) + } return errs } diff --git a/cmd/ateapi/internal/apivalidation/actor_template_test.go b/cmd/ateapi/internal/apivalidation/actor_template_test.go index 12d979784d..6311e33fc1 100644 --- a/cmd/ateapi/internal/apivalidation/actor_template_test.go +++ b/cmd/ateapi/internal/apivalidation/actor_template_test.go @@ -63,11 +63,50 @@ func TestValidateCreateActorTemplateRequest(t *testing.T) { })}, field.ErrorList{field.Invalid(field.NewPath("actor_template", "metadata", "name"), "Tmpl_A", "").WithOrigin("format=k8s-short-name")}, }, { - "valid volumes fidelity", + "valid volumes fidelity with a mounted durable dir", &ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(tmpl *ateapipb.ActorTemplate) { tmpl.SnapshotConfig.PreferredFidelity = ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES + tmpl.Volumes = []*ateapipb.Volume{{Name: "data", DurableDir: &ateapipb.DurableDirVolumeSource{}}} + tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{{Name: "data", MountPath: "/data"}} })}, nil, + }, { + "volumes fidelity without volumes rejected", + &ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(tmpl *ateapipb.ActorTemplate) { + tmpl.SnapshotConfig.PreferredFidelity = ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES + })}, + field.ErrorList{field.Invalid(field.NewPath("actor_template", "snapshot_config", "preferred_fidelity"), "SNAPSHOT_FIDELITY_VOLUMES", "")}, + }, { + "volumes fidelity with an unmounted durable dir rejected", + &ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(tmpl *ateapipb.ActorTemplate) { + tmpl.SnapshotConfig.PreferredFidelity = ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES + tmpl.Volumes = []*ateapipb.Volume{{Name: "data", DurableDir: &ateapipb.DurableDirVolumeSource{}}} + })}, + field.ErrorList{field.Invalid(field.NewPath("actor_template", "snapshot_config", "preferred_fidelity"), "SNAPSHOT_FIDELITY_VOLUMES", "")}, + }, { + "volumes fidelity with CSI only on gvisor rejected", + &ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(tmpl *ateapipb.ActorTemplate) { + tmpl.SnapshotConfig.PreferredFidelity = ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES + tmpl.Volumes = []*ateapipb.Volume{{Name: "data", ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{Capacity: "10Gi", StorageClassName: "fast-ssd"}}} + tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{{Name: "data", MountPath: "/data"}} + })}, + field.ErrorList{field.Invalid(field.NewPath("actor_template", "snapshot_config", "preferred_fidelity"), "SNAPSHOT_FIDELITY_VOLUMES", "")}, + }, { + "volumes fidelity with CSI only on microvm accepted", + &ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(tmpl *ateapipb.ActorTemplate) { + tmpl.SnapshotConfig.PreferredFidelity = ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES + tmpl.SandboxConfig = &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_MICROVM, ConfigName: "microvm-default"} + tmpl.Volumes = []*ateapipb.Volume{{Name: "data", ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{Capacity: "10Gi", StorageClassName: "fast-ssd"}}} + tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{{Name: "data", MountPath: "/data"}} + })}, + nil, + }, { + "volumes fidelity without volumes on microvm rejected", + &ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(tmpl *ateapipb.ActorTemplate) { + tmpl.SnapshotConfig.PreferredFidelity = ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES + tmpl.SandboxConfig = &ateapipb.SandboxConfig{SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_MICROVM, ConfigName: "microvm-default"} + })}, + field.ErrorList{field.Invalid(field.NewPath("actor_template", "snapshot_config", "preferred_fidelity"), "SNAPSHOT_FIDELITY_VOLUMES", "")}, }, { // Only the micro-VM runtime captures rootfs changes without memory; // the fixture is a gVisor template. diff --git a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go index 49f4f64b4d..95a6f0f138 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go @@ -3046,10 +3046,12 @@ func createDataFidelityTemplate(t *testing.T, tc *testContext, ns string) *ateap SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR, ConfigName: "gvisor-default", }, + Volumes: []*ateapipb.Volume{{Name: "data", DurableDir: &ateapipb.DurableDirVolumeSource{}}}, Containers: []*ateapipb.Container{{ - Name: "main", - Image: "main@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - Command: []string{"/main"}, + Name: "main", + Image: "main@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + Command: []string{"/main"}, + VolumeMounts: []*ateapipb.VolumeMount{{Name: "data", MountPath: "/data"}}, }}, WorkerSelector: &ateapipb.Selector{ MatchLabels: map[string]string{poolLabelKey: ns}, diff --git a/docs/api-guide.md b/docs/api-guide.md index d87e300aa6..42eed98b99 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -538,6 +538,10 @@ When an `ActorTemplate` is created with golden snapshots enabled: `CreateActor` uses an explicit `sourceTag` when supplied; otherwise it resolves the template's golden tag and records that snapshot on the new actor. If the golden tag is not ready yet, the actor starts without a snapshot and cold-boots even if the tag becomes ready before its first resume. The default does not populate the caller-owned `sourceTag` field. Deleting the template collects its golden tag and any unfinished golden actor. +VOLUMES templates require a volume mounted by a container. gVisor requires a +durable-dir mount; micro-VM also supports mounted external volumes. A declared +but unmounted volume does not satisfy this requirement. + ### Resumption Lifecycle Once a template is `Ready`, creating an actor logically (via `kubectl ate create actor`) allows it to be resumed instantly on any free worker in the referenced `WorkerPool`. Substrate bypasses the standard container boot and restores the process directly from its last saved state.