diff --git a/cmd/atelet/main.go b/cmd/atelet/main.go index 1d49f6123..155da1b32 100644 --- a/cmd/atelet/main.go +++ b/cmd/atelet/main.go @@ -571,6 +571,7 @@ func (s *AteomHerder) Checkpoint(ctx context.Context, req *ateletpb.CheckpointRe sandboxRec.ActorUID = req.GetActorUid() sandboxRec.ActorTemplateNamespace = req.GetActorTemplateNamespace() sandboxRec.ActorTemplateName = req.GetActorTemplateName() + sandboxRec.Scope = ateattr.SnapshotScopeValue(req.GetScope()) // No earlier pause snapshot can ever be restored again, so remove them // all: the actor's current state was just captured by CheckpointWorkload, diff --git a/cmd/atelet/main_test.go b/cmd/atelet/main_test.go index 0e135cee4..beb5be673 100644 --- a/cmd/atelet/main_test.go +++ b/cmd/atelet/main_test.go @@ -30,6 +30,7 @@ import ( "testing" "time" + "github.com/agent-substrate/substrate/internal/ateattr" "github.com/agent-substrate/substrate/internal/ateerrors" "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/proto/ateletpb" @@ -52,18 +53,41 @@ func TestSnapshotManifestActorMetadata(t *testing.T) { ActorUID: "actor-uid", ActorTemplateNamespace: "templates", ActorTemplateName: "agent", + Scope: ateattr.SnapshotScopeFull, } got, err := json.Marshal(rec) if err != nil { t.Fatal(err) } - for _, want := range []string{`"atespace":"team-a"`, `"actorName":"actor-1"`, `"actorUid":"actor-uid"`, `"actorTemplateNamespace":"templates"`, `"actorTemplateName":"agent"`} { + for _, want := range []string{`"atespace":"team-a"`, `"actorName":"actor-1"`, `"actorUid":"actor-uid"`, `"actorTemplateNamespace":"templates"`, `"actorTemplateName":"agent"`, `"scope":"full"`} { if !bytes.Contains(got, []byte(want)) { t.Errorf("manifest %s missing %s", got, want) } } } +// TestSnapshotManifestScopeAbsent pins backward compatibility: manifests +// written before the scope field existed must still parse, reporting an empty +// scope, and a scope-less record must not serialize a scope key at all. +func TestSnapshotManifestScopeAbsent(t *testing.T) { + legacy := []byte(`{"sandboxClass":"gvisor","snapshotFiles":["checkpoint.img"]}`) + rec, err := unmarshalSandboxRecord(legacy) + if err != nil { + t.Fatalf("unmarshalSandboxRecord(legacy manifest): %v", err) + } + if rec.Scope != "" { + t.Errorf("legacy manifest scope = %q, want empty", rec.Scope) + } + + got, err := json.Marshal(sandboxAssetsRecord{SandboxClass: "gvisor"}) + if err != nil { + t.Fatal(err) + } + if bytes.Contains(got, []byte(`"scope"`)) { + t.Errorf("scope-less record serialized a scope key: %s", got) + } +} + func TestWriteFileAtomic(t *testing.T) { dir := t.TempDir() target := filepath.Join(dir, "actor-id") diff --git a/cmd/atelet/sandbox_assets.go b/cmd/atelet/sandbox_assets.go index 11dac9e4a..ba62d6b0c 100644 --- a/cmd/atelet/sandbox_assets.go +++ b/cmd/atelet/sandbox_assets.go @@ -89,6 +89,11 @@ type sandboxAssetsRecord struct { // (gVisor's image files, cloud-hypervisor's snapshot set, ...). Empty in the // on-node record written at Run/Restore; populated at Checkpoint. SnapshotFiles []string `json:"snapshotFiles,omitempty"` + // Scope is the snapshot scope the checkpoint captured, as the shared + // ateattr label ("full" or "data"), so a snapshot's content is knowable + // from the manifest alone. Empty in the on-node record written at + // Run/Restore and in snapshot manifests written before this field existed. + Scope string `json:"scope,omitempty"` } // recordFromRequest projects a request's per-architecture SandboxAssets onto the