From 282de088f0c5880b419cd296591500f9ec44dc9d Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:44:17 +0200 Subject: [PATCH 1/2] feat: filesystem-only variant of sandbox snapshot endpoint (memory: false) Add a memory flag to POST /sandboxes/{sandboxID}/snapshots. When false, the snapshot template persists only the rootfs: sandboxes created from it cold-boot, and the source sandbox is rebooted in place (checkpoint cannot memory-resume without a memory snapshot) while keeping its identity and end time. Wired through SandboxCheckpointRequest.filesystem_only; the orchestrator branches to RebootSandbox and skips memory-prefetch work. Also tag linux-only files in cmd/create-build, cmd/resume-build, cmd/inspect-build, and pkg/server/prefetch_harvest.go with //go:build linux so make lint passes on macOS. Co-Authored-By: Claude Fable 5 --- packages/api/internal/api/api.gen.go | 385 +++++++++--------- .../handlers/snapshot_template_create.go | 7 +- .../orchestrator/snapshot_template.go | 22 +- .../cmd/create-build/proxyport_test.go | 2 + .../orchestrator/cmd/inspect-build/inspect.go | 2 + .../orchestrator/cmd/inspect-build/main.go | 2 + .../cmd/inspect-build/render_human.go | 2 + .../cmd/inspect-build/render_json.go | 2 + .../orchestrator/cmd/inspect-build/report.go | 2 + .../cmd/inspect-build/report_test.go | 2 + .../cmd/inspect-build/validate.go | 2 + .../cmd/inspect-build/validate_test.go | 2 + packages/orchestrator/cmd/resume-build/gdb.go | 2 + .../orchestrator/cmd/resume-build/gdb_test.go | 2 + packages/orchestrator/orchestrator.proto | 6 + .../pkg/server/prefetch_harvest.go | 2 + packages/orchestrator/pkg/server/sandboxes.go | 72 ++-- .../pkg/grpc/orchestrator/orchestrator.pb.go | 155 +++---- spec/openapi.yml | 11 + tests/integration/internal/api/generated.go | 3 + .../api/sandboxes/filesystem_only_test.go | 73 ++++ 21 files changed, 462 insertions(+), 296 deletions(-) diff --git a/packages/api/internal/api/api.gen.go b/packages/api/internal/api/api.gen.go index 4fec097373..f8f4968259 100644 --- a/packages/api/internal/api/api.gen.go +++ b/packages/api/internal/api/api.gen.go @@ -988,6 +988,9 @@ type SandboxRefreshRequest struct { // SandboxSnapshotRequest defines model for SandboxSnapshotRequest. type SandboxSnapshotRequest struct { + // Memory Whether the snapshot captures a full memory snapshot. When false, only the filesystem is persisted (a filesystem-only snapshot): sandboxes created from it cold-boot (reboot) from disk instead of restoring in-memory state. Taking a filesystem-only snapshot also reboots the source sandbox — its in-memory state, running processes, and open connections are lost while the filesystem and sandbox identity are preserved. Defaults to true. + Memory *bool `json:"memory,omitempty"` + // Name Optional name for the snapshot template. If a snapshot template with this name already exists, a new build will be assigned to the existing template instead of creating a new one. Name *string `json:"name,omitempty"` } @@ -13765,196 +13768,198 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "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", - "AwAA//8=", + "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/packages/api/internal/handlers/snapshot_template_create.go b/packages/api/internal/handlers/snapshot_template_create.go index 7908b5c9c0..65a7ea96d0 100644 --- a/packages/api/internal/handlers/snapshot_template_create.go +++ b/packages/api/internal/handlers/snapshot_template_create.go @@ -55,9 +55,12 @@ func (a *APIStore) PostSandboxesSandboxIDSnapshots(c *gin.Context, sandboxID api return } - // Build opts from the optional name + // Build opts from the optional name. memory defaults to true (full memory + // snapshot); memory:false requests a filesystem-only snapshot, which also + // reboots the source sandbox (resume-in-place needs a cold boot). opts := orchestrator.SnapshotTemplateOpts{ - Tag: id.DefaultTag, + Tag: id.DefaultTag, + FilesystemOnly: body.Memory != nil && !*body.Memory, } if body.Name != nil { diff --git a/packages/api/internal/orchestrator/snapshot_template.go b/packages/api/internal/orchestrator/snapshot_template.go index 7c648e2eb4..1587c758b0 100644 --- a/packages/api/internal/orchestrator/snapshot_template.go +++ b/packages/api/internal/orchestrator/snapshot_template.go @@ -32,6 +32,10 @@ type SnapshotTemplateOpts struct { Namespace *string // Tag is the build tag parsed from the name, defaults to "default". Tag string + // FilesystemOnly makes the snapshot persist only the filesystem (no memory); + // sandboxes created from it cold-boot, and the source sandbox is rebooted + // in place instead of memory-resumed. + FilesystemOnly bool } // CreateSnapshotTemplate creates a persistent snapshot template from a running sandbox and immediately resumes it. @@ -69,9 +73,7 @@ func (o *Orchestrator) CreateSnapshotTemplate(ctx context.Context, teamID uuid.U return SnapshotTemplateResult{}, fmt.Errorf("node '%s' not found", sbx.NodeID) } - // Snapshot templates are always memory snapshots; filesystem-only checkpoint - // (resume-in-place would need a reboot) is not supported yet. - upsertResult, err := o.throttledUpsertSnapshot(ctx, buildUpsertSnapshotParams(sbx, node, false)) + upsertResult, err := o.throttledUpsertSnapshot(ctx, buildUpsertSnapshotParams(sbx, node, opts.FilesystemOnly)) if err != nil { return SnapshotTemplateResult{}, fmt.Errorf("error upserting snapshot: %w", err) } @@ -84,14 +86,16 @@ func (o *Orchestrator) CreateSnapshotTemplate(ctx context.Context, teamID uuid.U } // Checkpoint pauses the sandbox, snapshots it, and resumes it on the - // orchestrator with the same ExecutionID. On error the orchestrator - // kills the sandbox itself; RemoveSandbox is still needed to clean up - // API-side state (store, routing, analytics). + // orchestrator with the same ExecutionID (a filesystem-only checkpoint + // reboots it instead). On error the orchestrator kills the sandbox itself; + // RemoveSandbox is still needed to clean up API-side state (store, routing, + // analytics). client, childCtx := node.GetClient(ctx) _, err = client.Sandbox.Checkpoint(childCtx, &orchestrator.SandboxCheckpointRequest{ - SandboxId: sbx.SandboxID, - BuildId: upsertResult.BuildID.String(), - Metadata: map[string]string{storageopts.ObjectMetadataTemplateID: snapshotTemplateEnvID}, + SandboxId: sbx.SandboxID, + BuildId: upsertResult.BuildID.String(), + Metadata: map[string]string{storageopts.ObjectMetadataTemplateID: snapshotTemplateEnvID}, + FilesystemOnly: opts.FilesystemOnly, }) if err != nil { o.failSnapshotBuild(ctx, upsertResult.BuildID, err) diff --git a/packages/orchestrator/cmd/create-build/proxyport_test.go b/packages/orchestrator/cmd/create-build/proxyport_test.go index 5f9452664a..eb897a4994 100644 --- a/packages/orchestrator/cmd/create-build/proxyport_test.go +++ b/packages/orchestrator/cmd/create-build/proxyport_test.go @@ -1,3 +1,5 @@ +//go:build linux + package main import "testing" diff --git a/packages/orchestrator/cmd/inspect-build/inspect.go b/packages/orchestrator/cmd/inspect-build/inspect.go index 10f3608cee..bfabbc9202 100644 --- a/packages/orchestrator/cmd/inspect-build/inspect.go +++ b/packages/orchestrator/cmd/inspect-build/inspect.go @@ -1,3 +1,5 @@ +//go:build linux + package main import ( diff --git a/packages/orchestrator/cmd/inspect-build/main.go b/packages/orchestrator/cmd/inspect-build/main.go index d65c0a061e..78eec2bda6 100644 --- a/packages/orchestrator/cmd/inspect-build/main.go +++ b/packages/orchestrator/cmd/inspect-build/main.go @@ -1,3 +1,5 @@ +//go:build linux + package main import ( diff --git a/packages/orchestrator/cmd/inspect-build/render_human.go b/packages/orchestrator/cmd/inspect-build/render_human.go index 2c29d9d5ff..ea5769eba1 100644 --- a/packages/orchestrator/cmd/inspect-build/render_human.go +++ b/packages/orchestrator/cmd/inspect-build/render_human.go @@ -1,3 +1,5 @@ +//go:build linux + package main import ( diff --git a/packages/orchestrator/cmd/inspect-build/render_json.go b/packages/orchestrator/cmd/inspect-build/render_json.go index b696228b26..1ceabf4e90 100644 --- a/packages/orchestrator/cmd/inspect-build/render_json.go +++ b/packages/orchestrator/cmd/inspect-build/render_json.go @@ -1,3 +1,5 @@ +//go:build linux + package main import ( diff --git a/packages/orchestrator/cmd/inspect-build/report.go b/packages/orchestrator/cmd/inspect-build/report.go index 4b8fd34307..1f276740fb 100644 --- a/packages/orchestrator/cmd/inspect-build/report.go +++ b/packages/orchestrator/cmd/inspect-build/report.go @@ -1,3 +1,5 @@ +//go:build linux + package main import ( diff --git a/packages/orchestrator/cmd/inspect-build/report_test.go b/packages/orchestrator/cmd/inspect-build/report_test.go index 40ddb56be7..7a2677aa97 100644 --- a/packages/orchestrator/cmd/inspect-build/report_test.go +++ b/packages/orchestrator/cmd/inspect-build/report_test.go @@ -1,3 +1,5 @@ +//go:build linux + package main import ( diff --git a/packages/orchestrator/cmd/inspect-build/validate.go b/packages/orchestrator/cmd/inspect-build/validate.go index 0772ac45fb..ee286a8345 100644 --- a/packages/orchestrator/cmd/inspect-build/validate.go +++ b/packages/orchestrator/cmd/inspect-build/validate.go @@ -1,3 +1,5 @@ +//go:build linux + package main import ( diff --git a/packages/orchestrator/cmd/inspect-build/validate_test.go b/packages/orchestrator/cmd/inspect-build/validate_test.go index 23f2fd2cb7..640f64c94e 100644 --- a/packages/orchestrator/cmd/inspect-build/validate_test.go +++ b/packages/orchestrator/cmd/inspect-build/validate_test.go @@ -1,3 +1,5 @@ +//go:build linux + package main import ( diff --git a/packages/orchestrator/cmd/resume-build/gdb.go b/packages/orchestrator/cmd/resume-build/gdb.go index f6bcca1f24..35d1009c89 100644 --- a/packages/orchestrator/cmd/resume-build/gdb.go +++ b/packages/orchestrator/cmd/resume-build/gdb.go @@ -1,3 +1,5 @@ +//go:build linux + package main import ( diff --git a/packages/orchestrator/cmd/resume-build/gdb_test.go b/packages/orchestrator/cmd/resume-build/gdb_test.go index d39735d97b..30d65cbe3d 100644 --- a/packages/orchestrator/cmd/resume-build/gdb_test.go +++ b/packages/orchestrator/cmd/resume-build/gdb_test.go @@ -1,3 +1,5 @@ +//go:build linux + package main import ( diff --git a/packages/orchestrator/orchestrator.proto b/packages/orchestrator/orchestrator.proto index 64935b1d40..e146d49804 100644 --- a/packages/orchestrator/orchestrator.proto +++ b/packages/orchestrator/orchestrator.proto @@ -179,6 +179,12 @@ message SandboxCheckpointRequest { // index (e.g. template_id). Opaque to the orchestrator, which just forwards // it to object metadata. map metadata = 4; + + // When true, persist only the filesystem (no memory snapshot); sandboxes + // created from such a snapshot cold-boot (reboot) from the rootfs, and the + // source sandbox is rebooted in place instead of memory-resumed. Default + // false = full memory snapshot, so existing callers are unaffected. + bool filesystem_only = 5; } message SandboxCheckpointResponse { diff --git a/packages/orchestrator/pkg/server/prefetch_harvest.go b/packages/orchestrator/pkg/server/prefetch_harvest.go index a739e49da6..e269639f6e 100644 --- a/packages/orchestrator/pkg/server/prefetch_harvest.go +++ b/packages/orchestrator/pkg/server/prefetch_harvest.go @@ -1,3 +1,5 @@ +//go:build linux + package server import ( diff --git a/packages/orchestrator/pkg/server/sandboxes.go b/packages/orchestrator/pkg/server/sandboxes.go index f01d3d0b8b..fc01b3f78e 100644 --- a/packages/orchestrator/pkg/server/sandboxes.go +++ b/packages/orchestrator/pkg/server/sandboxes.go @@ -25,6 +25,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox" + "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/block" "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/fc" sbxtemplate "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/template" "github.com/e2b-dev/infra/packages/orchestrator/pkg/template/metadata" @@ -756,9 +757,7 @@ func (s *Server) Checkpoint(ctx context.Context, in *orchestrator.SandboxCheckpo sbxlogger.E(sbx).Info(ctx, "Checkpointing sandbox") - // Checkpoint always takes a full memory snapshot; filesystem-only checkpoint - // (resume-in-place would need to reboot) is not supported yet. - res, err := s.snapshotAndCacheSandbox(ctx, sbx, in.GetBuildId(), in.GetMetadata(), storage.ObjectOriginSnapshotTemplate, false) + res, err := s.snapshotAndCacheSandbox(ctx, sbx, in.GetBuildId(), in.GetMetadata(), storage.ObjectOriginSnapshotTemplate, in.GetFilesystemOnly()) if err != nil { telemetry.ReportCriticalError(ctx, "error snapshotting sandbox for checkpoint", err, telemetry.WithSandboxID(in.GetSandboxId())) @@ -774,43 +773,66 @@ func (s *Server) Checkpoint(ctx context.Context, in *orchestrator.SandboxCheckpo return nil, status.Errorf(codes.Internal, "error getting template for resume: %s", err) } - // Resume the sandbox keeping the same ExecutionID (stable identity for + // Bring the sandbox back keeping the same ExecutionID (stable identity for // the API, routing catalog, and analytics) but with a fresh LifecycleID // so the old sandbox's cleanup goroutine won't // accidentally evict the resumed sandbox from the map. - resumedSbx, err := s.sandboxFactory.ResumeSandbox( - ctx, - template, - sbx.Config, - sandbox.RuntimeMetadata{ - TemplateID: sbx.Runtime.TemplateID, - SandboxID: sbx.Runtime.SandboxID, - ExecutionID: sbx.Runtime.ExecutionID, - TeamID: sbx.Runtime.TeamID, - BuildID: sbx.Runtime.BuildID, - SandboxType: sbx.Runtime.SandboxType, - }, - sbx.GetStartedAt(), - sbx.GetEndAt(), - sbx.APIStoredConfig, - ) + runtime := sandbox.RuntimeMetadata{ + TemplateID: sbx.Runtime.TemplateID, + SandboxID: sbx.Runtime.SandboxID, + ExecutionID: sbx.Runtime.ExecutionID, + TeamID: sbx.Runtime.TeamID, + BuildID: sbx.Runtime.BuildID, + SandboxType: sbx.Runtime.SandboxType, + } + + var resumedSbx *sandbox.Sandbox + if in.GetFilesystemOnly() { + // A filesystem-only snapshot has no memory to restore, so the only way + // to bring the sandbox back is a cold boot (reboot) from the sealed + // rootfs — guest RAM, processes, and connections are lost by design. + resumedSbx, err = s.sandboxFactory.RebootSandbox( + ctx, + template, + sbx.Config, + runtime, + sbx.GetEndAt(), + sbx.APIStoredConfig, + ) + } else { + resumedSbx, err = s.sandboxFactory.ResumeSandbox( + ctx, + template, + sbx.Config, + runtime, + sbx.GetStartedAt(), + sbx.GetEndAt(), + sbx.APIStoredConfig, + ) + } if err != nil { telemetry.ReportCriticalError(ctx, "error resuming sandbox after checkpoint", err, telemetry.WithSandboxID(in.GetSandboxId())) return nil, status.Errorf(codes.Internal, "error resuming sandbox after checkpoint: %s", err) } - // Collect prefetch data immediately after resume while it's most accurate - prefetchData, prefetchErr := resumedSbx.MemoryPrefetchData(ctx) - if prefetchErr != nil { - sbxlogger.I(resumedSbx).Warn(ctx, "failed to get prefetch data for checkpoint", zap.Error(prefetchErr)) + // Collect prefetch data immediately after resume while it's most accurate. + // A filesystem-only snapshot has no memfile, so there is no memory working + // set to prefetch on a cold boot. + var prefetchData block.PrefetchData + var prefetchErr error + if !in.GetFilesystemOnly() { + prefetchData, prefetchErr = resumedSbx.MemoryPrefetchData(ctx) + if prefetchErr != nil { + sbxlogger.I(resumedSbx).Warn(ctx, "failed to get prefetch data for checkpoint", zap.Error(prefetchErr)) + } } // Setup lifecycle for the resumed sandbox s.setupSandboxLifecycle(ctx, resumedSbx) // Embed prefetch data into the metadata so it's uploaded with the snapshot files in a single pass. - if prefetchErr == nil { + if !in.GetFilesystemOnly() && prefetchErr == nil { prefetchMapping := metadata.PrefetchEntriesToMapping(slices.Collect(maps.Values(prefetchData.BlockEntries)), prefetchData.BlockSize) if prefetchMapping != nil { res.meta = res.meta.WithPrefetch(&metadata.Prefetch{ diff --git a/packages/shared/pkg/grpc/orchestrator/orchestrator.pb.go b/packages/shared/pkg/grpc/orchestrator/orchestrator.pb.go index 06b039d1f2..1a3a4edb36 100644 --- a/packages/shared/pkg/grpc/orchestrator/orchestrator.pb.go +++ b/packages/shared/pkg/grpc/orchestrator/orchestrator.pb.go @@ -1246,6 +1246,11 @@ type SandboxCheckpointRequest struct { // index (e.g. template_id). Opaque to the orchestrator, which just forwards // it to object metadata. Metadata map[string]string `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + // When true, persist only the filesystem (no memory snapshot); sandboxes + // created from such a snapshot cold-boot (reboot) from the rootfs, and the + // source sandbox is rebooted in place instead of memory-resumed. Default + // false = full memory snapshot, so existing callers are unaffected. + FilesystemOnly bool `protobuf:"varint,5,opt,name=filesystem_only,json=filesystemOnly,proto3" json:"filesystem_only,omitempty"` } func (x *SandboxCheckpointRequest) Reset() { @@ -1301,6 +1306,13 @@ func (x *SandboxCheckpointRequest) GetMetadata() map[string]string { return nil } +func (x *SandboxCheckpointRequest) GetFilesystemOnly() bool { + if x != nil { + return x.FilesystemOnly + } + return false +} + type SandboxCheckpointResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1819,7 +1831,7 @@ var file_orchestrator_proto_rawDesc = []byte{ 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x12, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x69, 0x6e, 0x67, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xd6, 0x01, 0x0a, 0x18, 0x53, 0x61, 0x6e, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xff, 0x01, 0x0a, 0x18, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x61, 0x6e, 0x64, 0x62, @@ -1829,77 +1841,80 @@ var file_orchestrator_proto_rawDesc = []byte{ 0x0b, 0x32, 0x27, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x22, 0x61, 0x0a, 0x19, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, - 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, - 0x0a, 0x13, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x53, 0x63, - 0x68, 0x65, 0x64, 0x75, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x52, 0x12, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x22, 0xc7, 0x01, 0x0a, 0x0e, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, - 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x12, 0x26, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, - 0x78, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x39, 0x0a, 0x0a, - 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, - 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, - 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x64, 0x61, 0x74, 0x61, 0x12, 0x27, 0x0a, 0x0f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, + 0x65, 0x6d, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x66, + 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4f, 0x6e, 0x6c, 0x79, 0x1a, 0x3b, 0x0a, + 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x61, 0x0a, 0x19, 0x53, 0x61, + 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x13, 0x73, 0x63, 0x68, 0x65, 0x64, + 0x75, 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x69, 0x6e, + 0x67, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x12, 0x73, 0x63, 0x68, 0x65, 0x64, + 0x75, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xc7, 0x01, + 0x0a, 0x0e, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, + 0x12, 0x26, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0e, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, + 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x44, - 0x0a, 0x13, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x09, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x52, 0x75, 0x6e, 0x6e, 0x69, - 0x6e, 0x67, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x52, 0x09, 0x73, 0x61, 0x6e, 0x64, 0x62, - 0x6f, 0x78, 0x65, 0x73, 0x22, 0x71, 0x0a, 0x0f, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, - 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, - 0x49, 0x64, 0x12, 0x43, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x4b, 0x0a, 0x1f, 0x53, 0x61, 0x6e, 0x64, 0x62, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, + 0x12, 0x35, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, + 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x44, 0x0a, 0x13, 0x53, 0x61, 0x6e, 0x64, 0x62, + 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, + 0x0a, 0x09, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x0f, 0x2e, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x61, 0x6e, 0x64, 0x62, + 0x6f, 0x78, 0x52, 0x09, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x65, 0x73, 0x22, 0x71, 0x0a, + 0x0f, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, + 0x12, 0x19, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x43, 0x0a, 0x0f, 0x65, + 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x0e, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, + 0x22, 0x4b, 0x0a, 0x1f, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x43, + 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x06, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, + 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x06, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x32, 0xba, 0x03, + 0x0a, 0x0e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x12, 0x37, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x53, 0x61, 0x6e, + 0x64, 0x62, 0x6f, 0x78, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x16, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x12, 0x34, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x1a, 0x14, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x12, 0x15, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x12, 0x34, 0x0a, 0x05, 0x50, 0x61, 0x75, 0x73, 0x65, 0x12, 0x14, 0x2e, 0x53, 0x61, 0x6e, + 0x64, 0x62, 0x6f, 0x78, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x15, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x0a, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x19, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, + 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1a, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x10, + 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x73, + 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x20, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, - 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x06, 0x62, 0x75, - 0x69, 0x6c, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x43, 0x61, 0x63, - 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x06, 0x62, 0x75, - 0x69, 0x6c, 0x64, 0x73, 0x32, 0xba, 0x03, 0x0a, 0x0e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x12, 0x15, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, - 0x6f, 0x78, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x37, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x53, 0x61, 0x6e, - 0x64, 0x62, 0x6f, 0x78, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x34, 0x0a, 0x04, 0x4c, 0x69, 0x73, - 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x14, 0x2e, 0x53, 0x61, 0x6e, 0x64, - 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x37, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x53, 0x61, 0x6e, 0x64, - 0x62, 0x6f, 0x78, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x34, 0x0a, 0x05, 0x50, 0x61, 0x75, 0x73, - 0x65, 0x12, 0x14, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x50, 0x61, 0x75, 0x73, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, - 0x78, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, - 0x0a, 0x0a, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x19, 0x2e, 0x53, - 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, - 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x63, 0x68, 0x65, - 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, - 0x20, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x63, - 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x42, 0x2f, 0x5a, 0x2d, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x32, 0x62, 0x2d, 0x64, 0x65, 0x76, 0x2f, - 0x69, 0x6e, 0x66, 0x72, 0x61, 0x2f, 0x6f, 0x72, 0x63, 0x68, 0x65, 0x73, 0x74, 0x72, 0x61, 0x74, - 0x6f, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2f, 0x5a, 0x2d, 0x68, 0x74, + 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x65, 0x32, 0x62, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x69, 0x6e, 0x66, 0x72, 0x61, 0x2f, 0x6f, + 0x72, 0x63, 0x68, 0x65, 0x73, 0x74, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( diff --git a/spec/openapi.yml b/spec/openapi.yml index 18431862ae..79d06ae295 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -807,6 +807,17 @@ components: name: type: string description: Optional name for the snapshot template. If a snapshot template with this name already exists, a new build will be assigned to the existing template instead of creating a new one. + memory: + type: boolean + default: true + description: >- + Whether the snapshot captures a full memory snapshot. When false, + only the filesystem is persisted (a filesystem-only snapshot): + sandboxes created from it cold-boot (reboot) from disk instead of + restoring in-memory state. Taking a filesystem-only snapshot also + reboots the source sandbox — its in-memory state, running processes, + and open connections are lost while the filesystem and sandbox + identity are preserved. Defaults to true. SandboxPauseRequest: type: object diff --git a/tests/integration/internal/api/generated.go b/tests/integration/internal/api/generated.go index 3661a9b6a5..4b7fcb171b 100644 --- a/tests/integration/internal/api/generated.go +++ b/tests/integration/internal/api/generated.go @@ -983,6 +983,9 @@ type SandboxRefreshRequest struct { // SandboxSnapshotRequest defines model for SandboxSnapshotRequest. type SandboxSnapshotRequest struct { + // Memory Whether the snapshot captures a full memory snapshot. When false, only the filesystem is persisted (a filesystem-only snapshot): sandboxes created from it cold-boot (reboot) from disk instead of restoring in-memory state. Taking a filesystem-only snapshot also reboots the source sandbox — its in-memory state, running processes, and open connections are lost while the filesystem and sandbox identity are preserved. Defaults to true. + Memory *bool `json:"memory,omitempty"` + // Name Optional name for the snapshot template. If a snapshot template with this name already exists, a new build will be assigned to the existing template instead of creating a new one. Name *string `json:"name,omitempty"` } diff --git a/tests/integration/internal/tests/api/sandboxes/filesystem_only_test.go b/tests/integration/internal/tests/api/sandboxes/filesystem_only_test.go index 4822c6e850..53b4a150a7 100644 --- a/tests/integration/internal/tests/api/sandboxes/filesystem_only_test.go +++ b/tests/integration/internal/tests/api/sandboxes/filesystem_only_test.go @@ -168,6 +168,79 @@ func TestSandboxResume_FilesystemOnlyReboots(t *testing.T) { "default workdir after a filesystem-only reboot must be the template user's home") } +// TestSnapshotTemplate_FilesystemOnly verifies the filesystem-only variant of +// the snapshot endpoint (memory:false): the snapshot template persists only the +// rootfs, and taking it reboots the source sandbox in place — its rootfs and +// identity survive while a fresh kernel boot id proves the cold boot. A sandbox +// created from the template then cold-boots from the captured filesystem. +func TestSnapshotTemplate_FilesystemOnly(t *testing.T) { + t.Parallel() + c := setup.GetAPIClient() + ctx := t.Context() + + // Generous timeout: the checkpoint reboots the source sandbox in place, and + // RebootSandbox rejects an already-past end time; under parallel load a + // short timeout would make this flaky. + sbx := utils.SetupSandboxWithCleanup(t, c, utils.WithAutoPause(false), utils.WithTimeout(300)) + envdClient := setup.GetEnvdClient(t, ctx) + + bootBefore, err := utils.ExecCommandAsRootWithOutput(t, ctx, sbx, envdClient, + "cat", "/proc/sys/kernel/random/boot_id") + require.NoError(t, err) + bootBefore = strings.TrimSpace(bootBefore) + require.NotEmpty(t, bootBefore) + + const marker = "fs-only-snapshot-marker" + err = utils.ExecCommandAsRoot(t, ctx, sbx, envdClient, + "/bin/sh", "-c", "echo "+marker+" > /home/user/fs-only-snapshot-marker.txt") + require.NoError(t, err) + + memory := false + resp, err := c.PostSandboxesSandboxIDSnapshotsWithResponse(ctx, sbx.SandboxID, + api.PostSandboxesSandboxIDSnapshotsJSONRequestBody{Memory: &memory}, setup.WithAPIKey()) + require.NoError(t, err) + require.Equal(t, http.StatusCreated, resp.StatusCode(), + "filesystem-only snapshot should succeed, body: %s", string(resp.Body)) + require.NotNil(t, resp.JSON201) + snapshot := resp.JSON201 + t.Cleanup(func() { + c.DeleteTemplatesTemplateIDWithResponse(t.Context(), snapshot.SnapshotID, setup.WithAPIKey()) + }) + + // The source sandbox must come back running with its rootfs intact. + res, err := c.GetSandboxesSandboxIDWithResponse(ctx, sbx.SandboxID, setup.WithAPIKey()) + require.NoError(t, err) + require.NotNil(t, res.JSON200) + assert.Equal(t, api.Running, res.JSON200.State) + + got, err := utils.ExecCommandAsRootWithOutput(t, ctx, sbx, envdClient, + "cat", "/home/user/fs-only-snapshot-marker.txt") + require.NoError(t, err) + assert.Equal(t, marker, strings.TrimSpace(got), "rootfs marker must survive the in-place reboot") + + // A fresh boot id proves the source sandbox was cold-booted, not memory-resumed. + bootAfter, err := utils.ExecCommandAsRootWithOutput(t, ctx, sbx, envdClient, + "cat", "/proc/sys/kernel/random/boot_id") + require.NoError(t, err) + assert.NotEqual(t, bootBefore, strings.TrimSpace(bootAfter), + "boot id should change — a filesystem-only snapshot reboots the source sandbox") + + // A sandbox created from the filesystem-only template cold-boots from the + // captured rootfs, so the marker must be there. + newSbx := utils.SetupSandboxWithCleanup(t, c, + utils.WithTemplateID(snapshot.SnapshotID), + utils.WithAutoPause(false), + utils.WithTimeout(120), + ) + assert.NotEqual(t, sbx.SandboxID, newSbx.SandboxID) + + gotNew, err := utils.ExecCommandAsRootWithOutput(t, ctx, newSbx, envdClient, + "cat", "/home/user/fs-only-snapshot-marker.txt") + require.NoError(t, err) + assert.Equal(t, marker, strings.TrimSpace(gotNew), + "marker must be present in a sandbox created from the filesystem-only snapshot") +} + // TestSandboxAutoPause_FilesystemOnly verifies the auto-pause filesystem-only // path: a sandbox created with autoPauseMemory=false is auto-paused on timeout // as a filesystem-only snapshot (no memory), so resuming it cold-boots from the From 51e7be0203fbeeee4bb614e0f6a31294b88c556e Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:23:47 +0200 Subject: [PATCH 2/2] feat: derive filesystem-only snapshot from memory checkpoint Rework the memory:false snapshot flow so the source sandbox keeps its memory: the checkpoint takes a full memory snapshot (build A, the sandbox's own snapshot row) after a guest sync quiesce, derives a filesystem-only sibling build (B) whose rootfs header references A's data without duplicating it, memory-resumes the sandbox from A as a normal checkpoint, and returns B as the snapshot template. Sandboxes created from B cold-boot from the captured filesystem. The sibling upload is header+metadata only and runs after A's upload (its ancestor wait requires A's finalized header). Quiesce uses guest sync rather than fsfreeze: RAM captured with a frozen filesystem would memory-resume frozen and wedge all writes. Co-Authored-By: Claude Fable 5 --- packages/api/internal/api/api.gen.go | 386 +++++++++--------- .../handlers/snapshot_template_create.go | 4 +- .../orchestrator/snapshot_template.go | 120 ++++-- .../create_snapshot_template_build.sql.go | 87 ++++ .../create_snapshot_template_build.sql | 41 ++ packages/orchestrator/orchestrator.proto | 15 +- packages/orchestrator/pkg/sandbox/sandbox.go | 93 ++++- packages/orchestrator/pkg/sandbox/snapshot.go | 6 + packages/orchestrator/pkg/server/sandboxes.go | 189 ++++++--- .../pkg/grpc/orchestrator/orchestrator.pb.go | 254 ++++++------ spec/openapi.yml | 13 +- tests/integration/internal/api/generated.go | 2 +- .../api/sandboxes/filesystem_only_test.go | 30 +- 13 files changed, 819 insertions(+), 421 deletions(-) create mode 100644 packages/db/queries/create_snapshot_template_build.sql.go create mode 100644 packages/db/queries/snapshots/create_snapshot_template_build.sql diff --git a/packages/api/internal/api/api.gen.go b/packages/api/internal/api/api.gen.go index f8f4968259..6f96f1a333 100644 --- a/packages/api/internal/api/api.gen.go +++ b/packages/api/internal/api/api.gen.go @@ -988,7 +988,7 @@ type SandboxRefreshRequest struct { // SandboxSnapshotRequest defines model for SandboxSnapshotRequest. type SandboxSnapshotRequest struct { - // Memory Whether the snapshot captures a full memory snapshot. When false, only the filesystem is persisted (a filesystem-only snapshot): sandboxes created from it cold-boot (reboot) from disk instead of restoring in-memory state. Taking a filesystem-only snapshot also reboots the source sandbox — its in-memory state, running processes, and open connections are lost while the filesystem and sandbox identity are preserved. Defaults to true. + // Memory Whether the snapshot template captures the sandbox's memory. When false, the template persists only the filesystem (a filesystem-only snapshot): sandboxes created from it cold-boot (reboot) from disk instead of restoring in-memory state. The source sandbox itself is unaffected either way — it is checkpointed with memory and resumes with its processes and memory intact. Defaults to true. Memory *bool `json:"memory,omitempty"` // Name Optional name for the snapshot template. If a snapshot template with this name already exists, a new build will be assigned to the existing template instead of creating a new one. @@ -13768,198 +13768,198 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/packages/api/internal/handlers/snapshot_template_create.go b/packages/api/internal/handlers/snapshot_template_create.go index 65a7ea96d0..87915ce573 100644 --- a/packages/api/internal/handlers/snapshot_template_create.go +++ b/packages/api/internal/handlers/snapshot_template_create.go @@ -56,8 +56,8 @@ func (a *APIStore) PostSandboxesSandboxIDSnapshots(c *gin.Context, sandboxID api } // Build opts from the optional name. memory defaults to true (full memory - // snapshot); memory:false requests a filesystem-only snapshot, which also - // reboots the source sandbox (resume-in-place needs a cold boot). + // snapshot); memory:false requests a filesystem-only snapshot template, + // derived from the memory checkpoint the source sandbox resumes from. opts := orchestrator.SnapshotTemplateOpts{ Tag: id.DefaultTag, FilesystemOnly: body.Memory != nil && !*body.Memory, diff --git a/packages/api/internal/orchestrator/snapshot_template.go b/packages/api/internal/orchestrator/snapshot_template.go index 1587c758b0..f1d0f725e9 100644 --- a/packages/api/internal/orchestrator/snapshot_template.go +++ b/packages/api/internal/orchestrator/snapshot_template.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" + "github.com/e2b-dev/infra/packages/api/internal/orchestrator/nodemanager" "github.com/e2b-dev/infra/packages/api/internal/sandbox" "github.com/e2b-dev/infra/packages/db/pkg/types" "github.com/e2b-dev/infra/packages/db/queries" @@ -32,9 +33,10 @@ type SnapshotTemplateOpts struct { Namespace *string // Tag is the build tag parsed from the name, defaults to "default". Tag string - // FilesystemOnly makes the snapshot persist only the filesystem (no memory); - // sandboxes created from it cold-boot, and the source sandbox is rebooted - // in place instead of memory-resumed. + // FilesystemOnly makes the snapshot template persist only the filesystem: + // its build is derived from the memory checkpoint (sharing its rootfs data) + // and sandboxes created from it cold-boot. The source sandbox still gets a + // full memory checkpoint and resumes with its memory intact. FilesystemOnly bool } @@ -73,32 +75,58 @@ func (o *Orchestrator) CreateSnapshotTemplate(ctx context.Context, teamID uuid.U return SnapshotTemplateResult{}, fmt.Errorf("node '%s' not found", sbx.NodeID) } - upsertResult, err := o.throttledUpsertSnapshot(ctx, buildUpsertSnapshotParams(sbx, node, opts.FilesystemOnly)) + // The sandbox's own snapshot row is always a full memory snapshot, even for + // a filesystem-only template: the sandbox is memory-resumed from it and it + // can be memory-resumed again later (auto-resume included). + upsertResult, err := o.throttledUpsertSnapshot(ctx, buildUpsertSnapshotParams(sbx, node, false)) if err != nil { return SnapshotTemplateResult{}, fmt.Errorf("error upserting snapshot: %w", err) } + buildIDs := []uuid.UUID{upsertResult.BuildID} - snapshotTemplateEnvID, err := o.resolveOrCreateSnapshotTemplate(ctx, sandboxID, teamID, upsertResult.BuildID, sbx.NodeID, sbx.ClusterID, opts) + // A filesystem-only template gets its own derived build that shares the + // memory checkpoint's rootfs data via header references but carries no + // memory snapshot, so sandboxes created from it cold-boot. Otherwise the + // template reuses the memory build directly. + templateBuildID := upsertResult.BuildID + if opts.FilesystemOnly { + templateBuildID, err = o.sqlcDB.CreateSnapshotTemplateBuild(ctx, buildSnapshotTemplateBuildParams(sbx, node)) + if err != nil { + o.failSnapshotBuilds(ctx, buildIDs, err) + + return SnapshotTemplateResult{}, fmt.Errorf("error creating filesystem-only template build: %w", err) + } + buildIDs = append(buildIDs, templateBuildID) + } + + snapshotTemplateEnvID, err := o.resolveOrCreateSnapshotTemplate(ctx, sandboxID, teamID, templateBuildID, sbx.NodeID, sbx.ClusterID, opts) if err != nil { - o.failSnapshotBuild(ctx, upsertResult.BuildID, err) + o.failSnapshotBuilds(ctx, buildIDs, err) return SnapshotTemplateResult{}, err } // Checkpoint pauses the sandbox, snapshots it, and resumes it on the - // orchestrator with the same ExecutionID (a filesystem-only checkpoint - // reboots it instead). On error the orchestrator kills the sandbox itself; - // RemoveSandbox is still needed to clean up API-side state (store, routing, - // analytics). + // orchestrator with the same ExecutionID. On error the orchestrator + // kills the sandbox itself; RemoveSandbox is still needed to clean up + // API-side state (store, routing, analytics). + checkpointReq := &orchestrator.SandboxCheckpointRequest{ + SandboxId: sbx.SandboxID, + BuildId: upsertResult.BuildID.String(), + Metadata: map[string]string{storageopts.ObjectMetadataTemplateID: snapshotTemplateEnvID}, + } + if opts.FilesystemOnly { + // The memory build belongs to the sandbox's own snapshot env (like a + // pause build); the derived build is the one owned by the template env. + checkpointReq.Metadata = map[string]string{storageopts.ObjectMetadataTemplateID: upsertResult.TemplateID} + checkpointReq.FilesystemBuildId = templateBuildID.String() + checkpointReq.FilesystemMetadata = map[string]string{storageopts.ObjectMetadataTemplateID: snapshotTemplateEnvID} + } + client, childCtx := node.GetClient(ctx) - _, err = client.Sandbox.Checkpoint(childCtx, &orchestrator.SandboxCheckpointRequest{ - SandboxId: sbx.SandboxID, - BuildId: upsertResult.BuildID.String(), - Metadata: map[string]string{storageopts.ObjectMetadataTemplateID: snapshotTemplateEnvID}, - FilesystemOnly: opts.FilesystemOnly, - }) + _, err = client.Sandbox.Checkpoint(childCtx, checkpointReq) if err != nil { - o.failSnapshotBuild(ctx, upsertResult.BuildID, err) + o.failSnapshotBuilds(ctx, buildIDs, err) // Complete the snapshotting transition with error — leaves state as // Snapshotting (no restore to Running) and clears the transition key @@ -113,14 +141,16 @@ func (o *Orchestrator) CreateSnapshotTemplate(ctx context.Context, teamID uuid.U } now := time.Now() - err = o.sqlcDB.UpdateEnvBuildStatus(ctx, queries.UpdateEnvBuildStatusParams{ - Status: types.BuildStatusUploaded, - FinishedAt: &now, - Reason: types.BuildReason{}, - BuildID: upsertResult.BuildID, - }) - if err != nil { - return SnapshotTemplateResult{}, fmt.Errorf("error updating build status: %w", err) + for _, buildID := range buildIDs { + err = o.sqlcDB.UpdateEnvBuildStatus(ctx, queries.UpdateEnvBuildStatusParams{ + Status: types.BuildStatusUploaded, + FinishedAt: &now, + Reason: types.BuildReason{}, + BuildID: buildID, + }) + if err != nil { + return SnapshotTemplateResult{}, fmt.Errorf("error updating build status: %w", err) + } } o.snapshotCache.Invalidate(context.WithoutCancel(ctx), sandboxID) @@ -129,19 +159,39 @@ func (o *Orchestrator) CreateSnapshotTemplate(ctx context.Context, teamID uuid.U return SnapshotTemplateResult{ TemplateID: snapshotTemplateEnvID, - BuildID: upsertResult.BuildID, + BuildID: templateBuildID, }, nil } -func (o *Orchestrator) failSnapshotBuild(ctx context.Context, buildID uuid.UUID, cause error) { - err := o.sqlcDB.UpdateEnvBuildStatus(ctx, queries.UpdateEnvBuildStatusParams{ - Status: types.BuildStatusFailed, - FinishedAt: new(time.Now()), - Reason: types.BuildReason{Message: cause.Error()}, - BuildID: buildID, - }) - if err != nil { - telemetry.ReportError(ctx, "error failing build", err) +// buildSnapshotTemplateBuildParams mirrors buildUpsertSnapshotParams' build +// fields for the derived filesystem-only template build: same shape and CPU +// pinning to the source build, just not tied to the sandbox's snapshot env. +func buildSnapshotTemplateBuildParams(sbx sandbox.Sandbox, node *nodemanager.Node) queries.CreateSnapshotTemplateBuildParams { + return queries.CreateSnapshotTemplateBuildParams{ + Vcpu: sbx.VCpu, + RamMb: sbx.RamMB, + FreeDiskSizeMb: 0, + KernelVersion: sbx.KernelVersion, + FirecrackerVersion: sbx.FirecrackerVersion, + EnvdVersion: &sbx.EnvdVersion, + Status: types.BuildStatusSnapshotting, + OriginNodeID: &node.ID, + TotalDiskSizeMb: &sbx.TotalDiskSizeMB, + SourceBuildID: sbx.BuildID, + } +} + +func (o *Orchestrator) failSnapshotBuilds(ctx context.Context, buildIDs []uuid.UUID, cause error) { + for _, buildID := range buildIDs { + err := o.sqlcDB.UpdateEnvBuildStatus(ctx, queries.UpdateEnvBuildStatusParams{ + Status: types.BuildStatusFailed, + FinishedAt: new(time.Now()), + Reason: types.BuildReason{Message: cause.Error()}, + BuildID: buildID, + }) + if err != nil { + telemetry.ReportError(ctx, "error failing build", err) + } } } diff --git a/packages/db/queries/create_snapshot_template_build.sql.go b/packages/db/queries/create_snapshot_template_build.sql.go new file mode 100644 index 0000000000..cc90a23237 --- /dev/null +++ b/packages/db/queries/create_snapshot_template_build.sql.go @@ -0,0 +1,87 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: create_snapshot_template_build.sql + +package queries + +import ( + "context" + + "github.com/e2b-dev/infra/packages/db/pkg/types" + "github.com/google/uuid" +) + +const createSnapshotTemplateBuild = `-- name: CreateSnapshotTemplateBuild :one +INSERT INTO "public"."env_builds" ( + vcpu, + ram_mb, + free_disk_size_mb, + kernel_version, + firecracker_version, + envd_version, + status, + cluster_node_id, + total_disk_size_mb, + updated_at, + cpu_architecture, + cpu_family, + cpu_model, + cpu_model_name, + cpu_flags +) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + now(), + (SELECT eb.cpu_architecture FROM "public"."env_builds" eb WHERE eb.id = $10), + (SELECT eb.cpu_family FROM "public"."env_builds" eb WHERE eb.id = $10), + (SELECT eb.cpu_model FROM "public"."env_builds" eb WHERE eb.id = $10), + (SELECT eb.cpu_model_name FROM "public"."env_builds" eb WHERE eb.id = $10), + (SELECT eb.cpu_flags FROM "public"."env_builds" eb WHERE eb.id = $10) +) +RETURNING id as build_id +` + +type CreateSnapshotTemplateBuildParams struct { + Vcpu int64 + RamMb int64 + FreeDiskSizeMb int64 + KernelVersion string + FirecrackerVersion string + EnvdVersion *string + Status types.BuildStatus + OriginNodeID *string + TotalDiskSizeMb *int64 + SourceBuildID uuid.UUID +} + +// Creates a standalone env_builds row for a snapshot-template build that is +// derived from (and shares data with) another build of the same checkpoint, +// e.g. the filesystem-only sibling of a memory checkpoint. CPU info is copied +// from the source build so the derived build keeps the snapshot's CPU +// compatibility pinned to the original build, mirroring UpsertSnapshot. +func (q *Queries) CreateSnapshotTemplateBuild(ctx context.Context, arg CreateSnapshotTemplateBuildParams) (uuid.UUID, error) { + row := q.db.QueryRow(ctx, createSnapshotTemplateBuild, + arg.Vcpu, + arg.RamMb, + arg.FreeDiskSizeMb, + arg.KernelVersion, + arg.FirecrackerVersion, + arg.EnvdVersion, + arg.Status, + arg.OriginNodeID, + arg.TotalDiskSizeMb, + arg.SourceBuildID, + ) + var build_id uuid.UUID + err := row.Scan(&build_id) + return build_id, err +} diff --git a/packages/db/queries/snapshots/create_snapshot_template_build.sql b/packages/db/queries/snapshots/create_snapshot_template_build.sql new file mode 100644 index 0000000000..a4bf4905ae --- /dev/null +++ b/packages/db/queries/snapshots/create_snapshot_template_build.sql @@ -0,0 +1,41 @@ +-- name: CreateSnapshotTemplateBuild :one +-- Creates a standalone env_builds row for a snapshot-template build that is +-- derived from (and shares data with) another build of the same checkpoint, +-- e.g. the filesystem-only sibling of a memory checkpoint. CPU info is copied +-- from the source build so the derived build keeps the snapshot's CPU +-- compatibility pinned to the original build, mirroring UpsertSnapshot. +INSERT INTO "public"."env_builds" ( + vcpu, + ram_mb, + free_disk_size_mb, + kernel_version, + firecracker_version, + envd_version, + status, + cluster_node_id, + total_disk_size_mb, + updated_at, + cpu_architecture, + cpu_family, + cpu_model, + cpu_model_name, + cpu_flags +) +VALUES ( + @vcpu, + @ram_mb, + @free_disk_size_mb, + @kernel_version, + @firecracker_version, + @envd_version, + @status, + @origin_node_id, + @total_disk_size_mb, + now(), + (SELECT eb.cpu_architecture FROM "public"."env_builds" eb WHERE eb.id = @source_build_id), + (SELECT eb.cpu_family FROM "public"."env_builds" eb WHERE eb.id = @source_build_id), + (SELECT eb.cpu_model FROM "public"."env_builds" eb WHERE eb.id = @source_build_id), + (SELECT eb.cpu_model_name FROM "public"."env_builds" eb WHERE eb.id = @source_build_id), + (SELECT eb.cpu_flags FROM "public"."env_builds" eb WHERE eb.id = @source_build_id) +) +RETURNING id as build_id; diff --git a/packages/orchestrator/orchestrator.proto b/packages/orchestrator/orchestrator.proto index e146d49804..d7cb95be02 100644 --- a/packages/orchestrator/orchestrator.proto +++ b/packages/orchestrator/orchestrator.proto @@ -180,11 +180,16 @@ message SandboxCheckpointRequest { // it to object metadata. map metadata = 4; - // When true, persist only the filesystem (no memory snapshot); sandboxes - // created from such a snapshot cold-boot (reboot) from the rootfs, and the - // source sandbox is rebooted in place instead of memory-resumed. Default - // false = full memory snapshot, so existing callers are unaffected. - bool filesystem_only = 5; + // When set, the checkpoint additionally derives a filesystem-only sibling + // build under this ID: it shares the checkpoint's rootfs data via header + // references but carries no memory snapshot, so sandboxes created from it + // cold-boot (reboot) from the rootfs. The main build_id snapshot stays a + // full memory snapshot that the source sandbox is resumed from, exactly + // like a plain checkpoint. Unset = no derived build (existing behavior). + string filesystem_build_id = 5; + + // Provenance for the sibling build's storage objects, analogous to metadata. + map filesystem_metadata = 6; } message SandboxCheckpointResponse { diff --git a/packages/orchestrator/pkg/sandbox/sandbox.go b/packages/orchestrator/pkg/sandbox/sandbox.go index 07a78a9f52..d07d0a8cc5 100644 --- a/packages/orchestrator/pkg/sandbox/sandbox.go +++ b/packages/orchestrator/pkg/sandbox/sandbox.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "maps" "net/http" "sync" "time" @@ -1222,6 +1223,7 @@ func (s *Sandbox) Shutdown(ctx context.Context) error { type pauseOptions struct { filesystemSnapshot bool + filesystemSibling uuid.UUID } type PauseOption func(*pauseOptions) @@ -1234,6 +1236,15 @@ func WithFilesystemSnapshot() PauseOption { return func(o *pauseOptions) { o.filesystemSnapshot = true } } +// WithFilesystemSibling makes a memory pause additionally derive a +// filesystem-only sibling snapshot under the given build ID (see +// Snapshot.FilesystemSibling): a separate build that shares this pause's +// rootfs data through header references and carries no memory snapshot, so it +// is resumed by cold boot. Mutually exclusive with WithFilesystemSnapshot. +func WithFilesystemSibling(buildID uuid.UUID) PauseOption { + return func(o *pauseOptions) { o.filesystemSibling = buildID } +} + // Pause creates a snapshot of the sandbox. // // Currently the memory snapshotting works like this: @@ -1260,9 +1271,13 @@ func (s *Sandbox) Pause( for _, opt := range opts { opt(&pauseOpts) } + if pauseOpts.filesystemSnapshot && pauseOpts.filesystemSibling != uuid.Nil { + return nil, errors.New("filesystem-only snapshot cannot also derive a filesystem sibling") + } ctx, span := tracer.Start(ctx, "sandbox-snapshot", trace.WithAttributes( attribute.Bool("fs-only-snapshot", pauseOpts.filesystemSnapshot), + attribute.Bool("fs-sibling-snapshot", pauseOpts.filesystemSibling != uuid.Nil), )) defer span.End() @@ -1314,6 +1329,15 @@ func (s *Sandbox) Pause( // Memory prefetch refers to the memfile, which is not persisted. m.Prefetch = nil + } else if pauseOpts.filesystemSibling != uuid.Nil { + // The sibling is cold-booted from the rootfs alone, so the guest page + // cache must be flushed before pause or the sibling would miss + // acknowledged writes. Sync instead of fsfreeze: this memory snapshot + // IS resumed, and RAM captured with a frozen filesystem would resume + // frozen, wedging every write until an explicit thaw. + if err := s.guestSync(ctx, s.guestSyncTimeout(ctx)); err != nil { + return nil, fmt.Errorf("guest sync before filesystem sibling snapshot: %w", err) + } } // Record the snapshot kind in metadata so the resume path picks reboot vs @@ -1414,7 +1438,7 @@ func (s *Sandbox) Pause( return nil, err } - return &Snapshot{ + snapshot := &Snapshot{ Snapfile: snapfile, Metafile: metadataFileLink, MemorySnapshot: mem, @@ -1426,6 +1450,73 @@ func (s *Sandbox) Pause( BuildID: buildID, + cleanup: cleanup, + } + + if pauseOpts.filesystemSibling != uuid.Nil { + sibling, err := s.deriveFilesystemSibling(ctx, cleanup, m, rootfsHeader, pauseOpts.filesystemSibling) + if err != nil { + return nil, fmt.Errorf("derive filesystem sibling snapshot: %w", err) + } + snapshot.FilesystemSibling = sibling + } + + return snapshot, nil +} + +// deriveFilesystemSibling builds the filesystem-only sibling of a memory +// pause (see WithFilesystemSibling): a snapshot under its own build ID whose +// rootfs header maps every block to the memory build's data (next generation, +// no data of its own) and whose memory snapshot is empty, so it is resumed by +// cold boot. Its upload therefore waits on the memory build's upload future. +func (s *Sandbox) deriveFilesystemSibling( + ctx context.Context, + cleanup *Cleanup, + m metadata.Template, + rootfsHeader *header.Header, + buildID uuid.UUID, +) (*Snapshot, error) { + cachePaths, err := storage.Paths{BuildID: buildID.String()}.Cache(s.config.StorageConfig) + if err != nil { + return nil, fmt.Errorf("create sibling cache paths: %w", err) + } + cleanup.AddNoContext(ctx, cachePaths.Close) + + siblingHeader := &header.Header{ + Metadata: rootfsHeader.Metadata.NextGeneration(buildID), + Builds: maps.Clone(rootfsHeader.Builds), + Mapping: rootfsHeader.Mapping, + IncompletePendingUpload: true, + } + + // SameVersionTemplate drops the memory build's prefetch (refers to its + // memfile) and filesystem-only mark, which is then set for the sibling. + siblingTemplate := m.Template + siblingTemplate.BuildID = buildID.String() + siblingMeta := m.SameVersionTemplate(siblingTemplate).MarkFilesystemOnly(true) + + metafile := template.NewLocalFileLink(cachePaths.CacheMetadata()) + cleanup.AddNoContext(ctx, metafile.Close) + if err := siblingMeta.ToFile(metafile.Path()); err != nil { + return nil, fmt.Errorf("write sibling metadata: %w", err) + } + + return &Snapshot{ + // No snapfile: a filesystem-only build is resumed by reboot, and unlike a + // filesystem-only pause there is no FC snapfile of its own to cache. + Snapfile: nil, + Metafile: metafile, + MemorySnapshot: MemorySnapshot{Diff: &build.NoDiff{}, DiffHeader: NewResolvedDiffHeader(nil)}, + RootfsDiff: &build.NoDiff{}, + RootfsDiffHeader: NewResolvedDiffHeader(siblingHeader), + SchedulingMetadata: scheduling.FromHeaders(buildID, nil, siblingHeader, 0), + FilesystemSnapshot: true, + RootfsBlockSize: rootfsHeader.Metadata.BlockSize, + + BuildID: buildID, + + // The parent snapshot's cleanup owns the sibling's files: a failed pause + // runs it once for both, and on success ownership moves to the caches. cleanup: cleanup, }, nil } diff --git a/packages/orchestrator/pkg/sandbox/snapshot.go b/packages/orchestrator/pkg/sandbox/snapshot.go index 9b90397960..c2cca9d759 100644 --- a/packages/orchestrator/pkg/sandbox/snapshot.go +++ b/packages/orchestrator/pkg/sandbox/snapshot.go @@ -45,6 +45,12 @@ type Snapshot struct { // produces a NoDiff memfile but still needs its snapfile uploaded. FilesystemSnapshot bool + // FilesystemSibling is the filesystem-only sibling snapshot derived from + // this memory pause when requested via WithFilesystemSibling: a separate + // build sharing this snapshot's rootfs data via header references. Its + // upload must run after this snapshot's upload has finished. + FilesystemSibling *Snapshot + // RootfsBlockSize is captured sync at Pause time — needed sync by NewUpload's // compression validation. (The memfile block size lives in // MemorySnapshot.BlockSize.) diff --git a/packages/orchestrator/pkg/server/sandboxes.go b/packages/orchestrator/pkg/server/sandboxes.go index fc01b3f78e..b4f988c97c 100644 --- a/packages/orchestrator/pkg/server/sandboxes.go +++ b/packages/orchestrator/pkg/server/sandboxes.go @@ -25,7 +25,6 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox" - "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/block" "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/fc" sbxtemplate "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/template" "github.com/e2b-dev/infra/packages/orchestrator/pkg/template/metadata" @@ -645,7 +644,7 @@ func (s *Server) Pause(ctx context.Context, in *orchestrator.SandboxPauseRequest defer s.stopSandboxAsync(context.WithoutCancel(ctx), sbx) // Fire and forget - upload completes in the background - res, err := s.snapshotAndCacheSandbox(ctx, sbx, in.GetBuildId(), map[string]string{storage.ObjectMetadataTemplateID: in.GetTemplateId()}, storage.ObjectOriginPause, in.GetFilesystemOnly()) + res, err := s.snapshotAndCacheSandbox(ctx, sbx, in.GetBuildId(), map[string]string{storage.ObjectMetadataTemplateID: in.GetTemplateId()}, storage.ObjectOriginPause, in.GetFilesystemOnly(), nil) if err != nil { telemetry.ReportCriticalError(ctx, "error snapshotting sandbox", err, telemetry.WithSandboxID(in.GetSandboxId())) @@ -737,6 +736,17 @@ func (s *Server) Checkpoint(ctx context.Context, in *orchestrator.SandboxCheckpo return nil, status.Errorf(codes.FailedPrecondition, "%s", err.Error()) } + // A filesystem-only snapshot template is a derived sibling of the memory + // checkpoint build (see SandboxCheckpointRequest.filesystem_build_id). + var fsSibling *filesystemSiblingSpec + if fsID := in.GetFilesystemBuildId(); fsID != "" { + parsed, err := uuid.Parse(fsID) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid filesystem build id '%s': %s", fsID, err) + } + fsSibling = &filesystemSiblingSpec{buildID: parsed, provenance: in.GetFilesystemMetadata()} + } + // Acquire the starting semaphore before resuming, same as Create/Pause. if err := s.waitForAcquire(ctx); err != nil { return nil, err @@ -757,7 +767,7 @@ func (s *Server) Checkpoint(ctx context.Context, in *orchestrator.SandboxCheckpo sbxlogger.E(sbx).Info(ctx, "Checkpointing sandbox") - res, err := s.snapshotAndCacheSandbox(ctx, sbx, in.GetBuildId(), in.GetMetadata(), storage.ObjectOriginSnapshotTemplate, in.GetFilesystemOnly()) + res, err := s.snapshotAndCacheSandbox(ctx, sbx, in.GetBuildId(), in.GetMetadata(), storage.ObjectOriginSnapshotTemplate, false, fsSibling) if err != nil { telemetry.ReportCriticalError(ctx, "error snapshotting sandbox for checkpoint", err, telemetry.WithSandboxID(in.GetSandboxId())) @@ -773,66 +783,43 @@ func (s *Server) Checkpoint(ctx context.Context, in *orchestrator.SandboxCheckpo return nil, status.Errorf(codes.Internal, "error getting template for resume: %s", err) } - // Bring the sandbox back keeping the same ExecutionID (stable identity for + // Resume the sandbox keeping the same ExecutionID (stable identity for // the API, routing catalog, and analytics) but with a fresh LifecycleID // so the old sandbox's cleanup goroutine won't // accidentally evict the resumed sandbox from the map. - runtime := sandbox.RuntimeMetadata{ - TemplateID: sbx.Runtime.TemplateID, - SandboxID: sbx.Runtime.SandboxID, - ExecutionID: sbx.Runtime.ExecutionID, - TeamID: sbx.Runtime.TeamID, - BuildID: sbx.Runtime.BuildID, - SandboxType: sbx.Runtime.SandboxType, - } - - var resumedSbx *sandbox.Sandbox - if in.GetFilesystemOnly() { - // A filesystem-only snapshot has no memory to restore, so the only way - // to bring the sandbox back is a cold boot (reboot) from the sealed - // rootfs — guest RAM, processes, and connections are lost by design. - resumedSbx, err = s.sandboxFactory.RebootSandbox( - ctx, - template, - sbx.Config, - runtime, - sbx.GetEndAt(), - sbx.APIStoredConfig, - ) - } else { - resumedSbx, err = s.sandboxFactory.ResumeSandbox( - ctx, - template, - sbx.Config, - runtime, - sbx.GetStartedAt(), - sbx.GetEndAt(), - sbx.APIStoredConfig, - ) - } + resumedSbx, err := s.sandboxFactory.ResumeSandbox( + ctx, + template, + sbx.Config, + sandbox.RuntimeMetadata{ + TemplateID: sbx.Runtime.TemplateID, + SandboxID: sbx.Runtime.SandboxID, + ExecutionID: sbx.Runtime.ExecutionID, + TeamID: sbx.Runtime.TeamID, + BuildID: sbx.Runtime.BuildID, + SandboxType: sbx.Runtime.SandboxType, + }, + sbx.GetStartedAt(), + sbx.GetEndAt(), + sbx.APIStoredConfig, + ) if err != nil { telemetry.ReportCriticalError(ctx, "error resuming sandbox after checkpoint", err, telemetry.WithSandboxID(in.GetSandboxId())) return nil, status.Errorf(codes.Internal, "error resuming sandbox after checkpoint: %s", err) } - // Collect prefetch data immediately after resume while it's most accurate. - // A filesystem-only snapshot has no memfile, so there is no memory working - // set to prefetch on a cold boot. - var prefetchData block.PrefetchData - var prefetchErr error - if !in.GetFilesystemOnly() { - prefetchData, prefetchErr = resumedSbx.MemoryPrefetchData(ctx) - if prefetchErr != nil { - sbxlogger.I(resumedSbx).Warn(ctx, "failed to get prefetch data for checkpoint", zap.Error(prefetchErr)) - } + // Collect prefetch data immediately after resume while it's most accurate + prefetchData, prefetchErr := resumedSbx.MemoryPrefetchData(ctx) + if prefetchErr != nil { + sbxlogger.I(resumedSbx).Warn(ctx, "failed to get prefetch data for checkpoint", zap.Error(prefetchErr)) } // Setup lifecycle for the resumed sandbox s.setupSandboxLifecycle(ctx, resumedSbx) // Embed prefetch data into the metadata so it's uploaded with the snapshot files in a single pass. - if !in.GetFilesystemOnly() && prefetchErr == nil { + if prefetchErr == nil { prefetchMapping := metadata.PrefetchEntriesToMapping(slices.Collect(maps.Values(prefetchData.BlockEntries)), prefetchData.BlockSize) if prefetchMapping != nil { res.meta = res.meta.WithPrefetch(&metadata.Prefetch{ @@ -845,10 +832,37 @@ func (s *Server) Checkpoint(ctx context.Context, in *orchestrator.SandboxCheckpo } } - if s.featureFlags.BoolFlag(ctx, featureflags.PeerToPeerAsyncCheckpointFlag) { + switch { + case res.filesystemSibling != nil: + // The sibling's header references the memory build's data and its upload + // waits on that build's upload future, so run the two sequentially and + // synchronously — the sibling upload is header + metadata only, KB-scale. + // On failure tear down the resumed sandbox, same as the plain sync path. + uploadCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), uploadTimeout) + defer cancel() + + err := res.upload.Run(uploadCtx) + res.completeUpload(uploadCtx, err) + if err == nil { + err = res.filesystemSibling.upload.Run(uploadCtx) + res.filesystemSibling.completeUpload(uploadCtx, err) + } else { + // Finish the sibling's upload future so nothing waits on it forever. + res.filesystemSibling.completeUpload(uploadCtx, fmt.Errorf("memory build upload failed: %w", err)) + } + + if err != nil { + telemetry.ReportCriticalError(ctx, "error uploading snapshot for checkpoint", err, telemetry.WithSandboxID(in.GetSandboxId())) + + s.sandboxFactory.Sandboxes.MarkStopping(ctx, resumedSbx.Runtime.SandboxID, resumedSbx.LifecycleID) + s.stopSandboxAsync(context.WithoutCancel(ctx), resumedSbx) + + return nil, status.Errorf(codes.Internal, "error uploading snapshot for checkpoint '%s': %s", in.GetSandboxId(), err) + } + case s.featureFlags.BoolFlag(ctx, featureflags.PeerToPeerAsyncCheckpointFlag): // Async: return immediately; peer nodes can pull chunks from us during the upload window. s.uploadSnapshotAsync(ctx, resumedSbx, res) - } else { + default: // Sync: wait for upload before returning so a failed upload is surfaced to the caller. // On failure, tear down the resumed sandbox — without a persisted snapshot it cannot // be paused or resumed later. @@ -921,6 +935,20 @@ type snapshotResult struct { // with. The prefetch harvest reuses it verbatim when re-uploading the // metadata object, so the two can never drift. objectMetadata storage.ObjectMetadata + + // filesystemSibling is the derived filesystem-only sibling build's result + // (nil unless requested; its meta field is left zero). Its upload waits on + // this build's upload future, so Run it only after this build's upload has + // been completed with completeUpload. + filesystemSibling *snapshotResult +} + +// filesystemSiblingSpec asks snapshotAndCacheSandbox to additionally derive a +// filesystem-only sibling build of the memory snapshot — the snapshot-template +// build of a memory:false snapshot — and prepare its cache entry and upload. +type filesystemSiblingSpec struct { + buildID uuid.UUID + provenance map[string]string } // snapshotAndCacheSandbox creates a snapshot of a sandbox and adds it to the @@ -933,6 +961,7 @@ func (s *Server) snapshotAndCacheSandbox( provenance map[string]string, buildOrigin storage.ObjectOrigin, filesystemOnly bool, + fsSibling *filesystemSiblingSpec, ) (*snapshotResult, error) { meta, err := sbx.Template.Metadata() if err != nil { @@ -949,6 +978,9 @@ func (s *Server) snapshotAndCacheSandbox( if filesystemOnly { pauseOpts = append(pauseOpts, sandbox.WithFilesystemSnapshot()) } + if fsSibling != nil { + pauseOpts = append(pauseOpts, sandbox.WithFilesystemSibling(fsSibling.buildID)) + } snapshot, err := sbx.Pause(ctx, meta, sandbox.SnapshotUseCasePause, pauseOpts...) if err != nil { @@ -1014,12 +1046,67 @@ func (s *Server) snapshotAndCacheSandbox( } } - return &snapshotResult{ + res := &snapshotResult{ meta: meta, schedulingMetadata: snapshot.SchedulingMetadata, upload: upload, completeUpload: completeUpload, objectMetadata: objectMetadata, + } + + if sibling := snapshot.FilesystemSibling; sibling != nil { + res.filesystemSibling, err = s.registerFilesystemSibling(ctx, sbx, sibling, fsSibling.provenance, buildOrigin) + if err != nil { + return nil, fmt.Errorf("register filesystem sibling: %w", err) + } + } + + return res, nil +} + +// registerFilesystemSibling adds the derived filesystem-only sibling snapshot +// (see sandbox.WithFilesystemSibling) to the local template cache and registers +// its upload — a header + metadata upload only, since the sibling shares the +// memory build's rootfs data. No peer-to-peer chunk registration: the sibling +// has no data of its own to serve. +func (s *Server) registerFilesystemSibling( + ctx context.Context, + sbx *sandbox.Sandbox, + snap *sandbox.Snapshot, + provenance map[string]string, + buildOrigin storage.ObjectOrigin, +) (*snapshotResult, error) { + err := s.templateCache.AddSnapshot( + ctx, + snap.BuildID.String(), + snap.MemorySnapshot.DiffHeader, + snap.RootfsDiffHeader, + snap.Snapfile, + snap.Metafile, + snap.MemorySnapshot.Diff, + snap.RootfsDiff, + ) + if err != nil { + return nil, fmt.Errorf("error adding sibling snapshot to template cache: %w", err) + } + + objectMetadata := storage.ObjectMetadata{} + maps.Copy(objectMetadata, provenance) + objectMetadata[storage.ObjectMetadataTeamID] = sbx.Runtime.TeamID + objectMetadata[storage.ObjectMetadataBuildOrigin] = string(buildOrigin) + + upload, err := sandbox.NewUpload(ctx, s.uploads, snap, s.persistence, s.config.StorageConfig.CompressConfig, s.featureFlags, storage.UseCasePause, objectMetadata) + if err != nil { + return nil, fmt.Errorf("register sibling upload: %w", err) + } + + return &snapshotResult{ + schedulingMetadata: snap.SchedulingMetadata, + upload: upload, + completeUpload: func(ctx context.Context, uploadErr error) { + upload.Finish(ctx, uploadErr) + }, + objectMetadata: objectMetadata, }, nil } diff --git a/packages/shared/pkg/grpc/orchestrator/orchestrator.pb.go b/packages/shared/pkg/grpc/orchestrator/orchestrator.pb.go index 1a3a4edb36..4d3c2a16a7 100644 --- a/packages/shared/pkg/grpc/orchestrator/orchestrator.pb.go +++ b/packages/shared/pkg/grpc/orchestrator/orchestrator.pb.go @@ -1246,11 +1246,15 @@ type SandboxCheckpointRequest struct { // index (e.g. template_id). Opaque to the orchestrator, which just forwards // it to object metadata. Metadata map[string]string `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - // When true, persist only the filesystem (no memory snapshot); sandboxes - // created from such a snapshot cold-boot (reboot) from the rootfs, and the - // source sandbox is rebooted in place instead of memory-resumed. Default - // false = full memory snapshot, so existing callers are unaffected. - FilesystemOnly bool `protobuf:"varint,5,opt,name=filesystem_only,json=filesystemOnly,proto3" json:"filesystem_only,omitempty"` + // When set, the checkpoint additionally derives a filesystem-only sibling + // build under this ID: it shares the checkpoint's rootfs data via header + // references but carries no memory snapshot, so sandboxes created from it + // cold-boot (reboot) from the rootfs. The main build_id snapshot stays a + // full memory snapshot that the source sandbox is resumed from, exactly + // like a plain checkpoint. Unset = no derived build (existing behavior). + FilesystemBuildId string `protobuf:"bytes,5,opt,name=filesystem_build_id,json=filesystemBuildId,proto3" json:"filesystem_build_id,omitempty"` + // Provenance for the sibling build's storage objects, analogous to metadata. + FilesystemMetadata map[string]string `protobuf:"bytes,6,rep,name=filesystem_metadata,json=filesystemMetadata,proto3" json:"filesystem_metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } func (x *SandboxCheckpointRequest) Reset() { @@ -1306,11 +1310,18 @@ func (x *SandboxCheckpointRequest) GetMetadata() map[string]string { return nil } -func (x *SandboxCheckpointRequest) GetFilesystemOnly() bool { +func (x *SandboxCheckpointRequest) GetFilesystemBuildId() string { if x != nil { - return x.FilesystemOnly + return x.FilesystemBuildId } - return false + return "" +} + +func (x *SandboxCheckpointRequest) GetFilesystemMetadata() map[string]string { + if x != nil { + return x.FilesystemMetadata + } + return nil } type SandboxCheckpointResponse struct { @@ -1831,7 +1842,7 @@ var file_orchestrator_proto_rawDesc = []byte{ 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x12, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x69, 0x6e, 0x67, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xff, 0x01, 0x0a, 0x18, 0x53, 0x61, 0x6e, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xb1, 0x03, 0x0a, 0x18, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x61, 0x6e, 0x64, 0x62, @@ -1841,80 +1852,91 @@ var file_orchestrator_proto_rawDesc = []byte{ 0x0b, 0x32, 0x27, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x12, 0x27, 0x0a, 0x0f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, - 0x65, 0x6d, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x66, - 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4f, 0x6e, 0x6c, 0x79, 0x1a, 0x3b, 0x0a, - 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x61, 0x0a, 0x19, 0x53, 0x61, - 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x13, 0x73, 0x63, 0x68, 0x65, 0x64, - 0x75, 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x69, 0x6e, - 0x67, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x12, 0x73, 0x63, 0x68, 0x65, 0x64, - 0x75, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xc7, 0x01, - 0x0a, 0x0e, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, - 0x12, 0x26, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x0e, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, - 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, - 0x12, 0x35, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, - 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x44, 0x0a, 0x13, 0x53, 0x61, 0x6e, 0x64, 0x62, - 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, - 0x0a, 0x09, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x0f, 0x2e, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x61, 0x6e, 0x64, 0x62, - 0x6f, 0x78, 0x52, 0x09, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x65, 0x73, 0x22, 0x71, 0x0a, - 0x0f, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, - 0x12, 0x19, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x43, 0x0a, 0x0f, 0x65, - 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, + 0x64, 0x61, 0x74, 0x61, 0x12, 0x2e, 0x0a, 0x13, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, + 0x65, 0x6d, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x11, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x42, 0x75, 0x69, + 0x6c, 0x64, 0x49, 0x64, 0x12, 0x62, 0x0a, 0x13, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, + 0x65, 0x6d, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x31, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x46, 0x69, 0x6c, + 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x12, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x45, 0x0a, 0x17, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, + 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x61, 0x0a, 0x19, + 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x13, 0x73, 0x63, 0x68, + 0x65, 0x64, 0x75, 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, + 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x12, 0x73, 0x63, 0x68, + 0x65, 0x64, 0x75, 0x6c, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, + 0xc7, 0x01, 0x0a, 0x0e, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x61, 0x6e, 0x64, 0x62, + 0x6f, 0x78, 0x12, 0x26, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, + 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x0e, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, - 0x22, 0x4b, 0x0a, 0x1f, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x43, - 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x06, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, - 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x06, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x32, 0xba, 0x03, - 0x0a, 0x0e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x37, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x53, 0x61, 0x6e, - 0x64, 0x62, 0x6f, 0x78, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x12, 0x34, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x1a, 0x14, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x12, 0x15, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x12, 0x34, 0x0a, 0x05, 0x50, 0x61, 0x75, 0x73, 0x65, 0x12, 0x14, 0x2e, 0x53, 0x61, 0x6e, - 0x64, 0x62, 0x6f, 0x78, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x15, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x0a, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x19, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, - 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1a, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, - 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x10, - 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x73, - 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x20, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, - 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, - 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2f, 0x5a, 0x2d, 0x68, 0x74, - 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x65, 0x32, 0x62, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x69, 0x6e, 0x66, 0x72, 0x61, 0x2f, 0x6f, - 0x72, 0x63, 0x68, 0x65, 0x73, 0x74, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x44, 0x0a, 0x13, 0x53, 0x61, 0x6e, + 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x2d, 0x0a, 0x09, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x65, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x52, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x61, 0x6e, + 0x64, 0x62, 0x6f, 0x78, 0x52, 0x09, 0x73, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x65, 0x73, 0x22, + 0x71, 0x0a, 0x0f, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, + 0x66, 0x6f, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x43, 0x0a, + 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x0e, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, + 0x6d, 0x65, 0x22, 0x4b, 0x0a, 0x1f, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, + 0x74, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x06, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, + 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x06, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x32, + 0xba, 0x03, 0x0a, 0x0e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x53, + 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x12, 0x34, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x14, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, + 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x12, 0x15, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x12, 0x34, 0x0a, 0x05, 0x50, 0x61, 0x75, 0x73, 0x65, 0x12, 0x14, 0x2e, 0x53, + 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x50, 0x61, 0x75, 0x73, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x0a, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x19, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, + 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x53, 0x61, 0x6e, 0x64, 0x62, 0x6f, 0x78, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, + 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, 0x69, 0x6c, + 0x64, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x20, 0x2e, 0x53, 0x61, 0x6e, + 0x64, 0x62, 0x6f, 0x78, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x42, 0x75, + 0x69, 0x6c, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2f, 0x5a, 0x2d, + 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x65, 0x32, 0x62, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x69, 0x6e, 0x66, 0x72, 0x61, + 0x2f, 0x6f, 0x72, 0x63, 0x68, 0x65, 0x73, 0x74, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1929,7 +1951,7 @@ func file_orchestrator_proto_rawDescGZIP() []byte { return file_orchestrator_proto_rawDescData } -var file_orchestrator_proto_msgTypes = make([]protoimpl.MessageInfo, 27) +var file_orchestrator_proto_msgTypes = make([]protoimpl.MessageInfo, 28) var file_orchestrator_proto_goTypes = []interface{}{ (*SandboxConfig)(nil), // 0: SandboxConfig (*SandboxAutoResumeConfig)(nil), // 1: SandboxAutoResumeConfig @@ -1958,8 +1980,9 @@ var file_orchestrator_proto_goTypes = []interface{}{ nil, // 24: SandboxNetworkTransform.HeadersEntry nil, // 25: SandboxNetworkEgressConfig.RulesEntry nil, // 26: SandboxCheckpointRequest.MetadataEntry - (*timestamppb.Timestamp)(nil), // 27: google.protobuf.Timestamp - (*emptypb.Empty)(nil), // 28: google.protobuf.Empty + nil, // 27: SandboxCheckpointRequest.FilesystemMetadataEntry + (*timestamppb.Timestamp)(nil), // 28: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 29: google.protobuf.Empty } var file_orchestrator_proto_depIdxs = []int32{ 22, // 0: SandboxConfig.env_vars:type_name -> SandboxConfig.EnvVarsEntry @@ -1974,40 +1997,41 @@ var file_orchestrator_proto_depIdxs = []int32{ 5, // 9: SandboxNetworkDomainRules.rules:type_name -> SandboxNetworkRule 25, // 10: SandboxNetworkEgressConfig.rules:type_name -> SandboxNetworkEgressConfig.RulesEntry 0, // 11: SandboxCreateRequest.sandbox:type_name -> SandboxConfig - 27, // 12: SandboxCreateRequest.start_time:type_name -> google.protobuf.Timestamp - 27, // 13: SandboxCreateRequest.end_time:type_name -> google.protobuf.Timestamp + 28, // 12: SandboxCreateRequest.start_time:type_name -> google.protobuf.Timestamp + 28, // 13: SandboxCreateRequest.end_time:type_name -> google.protobuf.Timestamp 14, // 14: SandboxCreateResponse.scheduling_metadata:type_name -> SchedulingMetadata - 27, // 15: SandboxUpdateRequest.end_time:type_name -> google.protobuf.Timestamp + 28, // 15: SandboxUpdateRequest.end_time:type_name -> google.protobuf.Timestamp 7, // 16: SandboxUpdateRequest.egress:type_name -> SandboxNetworkEgressConfig 14, // 17: SandboxPauseResponse.scheduling_metadata:type_name -> SchedulingMetadata 26, // 18: SandboxCheckpointRequest.metadata:type_name -> SandboxCheckpointRequest.MetadataEntry - 14, // 19: SandboxCheckpointResponse.scheduling_metadata:type_name -> SchedulingMetadata - 0, // 20: RunningSandbox.config:type_name -> SandboxConfig - 27, // 21: RunningSandbox.start_time:type_name -> google.protobuf.Timestamp - 27, // 22: RunningSandbox.end_time:type_name -> google.protobuf.Timestamp - 18, // 23: SandboxListResponse.sandboxes:type_name -> RunningSandbox - 27, // 24: CachedBuildInfo.expiration_time:type_name -> google.protobuf.Timestamp - 20, // 25: SandboxListCachedBuildsResponse.builds:type_name -> CachedBuildInfo - 6, // 26: SandboxNetworkEgressConfig.RulesEntry.value:type_name -> SandboxNetworkDomainRules - 9, // 27: SandboxService.Create:input_type -> SandboxCreateRequest - 11, // 28: SandboxService.Update:input_type -> SandboxUpdateRequest - 28, // 29: SandboxService.List:input_type -> google.protobuf.Empty - 12, // 30: SandboxService.Delete:input_type -> SandboxDeleteRequest - 13, // 31: SandboxService.Pause:input_type -> SandboxPauseRequest - 16, // 32: SandboxService.Checkpoint:input_type -> SandboxCheckpointRequest - 28, // 33: SandboxService.ListCachedBuilds:input_type -> google.protobuf.Empty - 10, // 34: SandboxService.Create:output_type -> SandboxCreateResponse - 28, // 35: SandboxService.Update:output_type -> google.protobuf.Empty - 19, // 36: SandboxService.List:output_type -> SandboxListResponse - 28, // 37: SandboxService.Delete:output_type -> google.protobuf.Empty - 15, // 38: SandboxService.Pause:output_type -> SandboxPauseResponse - 17, // 39: SandboxService.Checkpoint:output_type -> SandboxCheckpointResponse - 21, // 40: SandboxService.ListCachedBuilds:output_type -> SandboxListCachedBuildsResponse - 34, // [34:41] is the sub-list for method output_type - 27, // [27:34] is the sub-list for method input_type - 27, // [27:27] is the sub-list for extension type_name - 27, // [27:27] is the sub-list for extension extendee - 0, // [0:27] is the sub-list for field type_name + 27, // 19: SandboxCheckpointRequest.filesystem_metadata:type_name -> SandboxCheckpointRequest.FilesystemMetadataEntry + 14, // 20: SandboxCheckpointResponse.scheduling_metadata:type_name -> SchedulingMetadata + 0, // 21: RunningSandbox.config:type_name -> SandboxConfig + 28, // 22: RunningSandbox.start_time:type_name -> google.protobuf.Timestamp + 28, // 23: RunningSandbox.end_time:type_name -> google.protobuf.Timestamp + 18, // 24: SandboxListResponse.sandboxes:type_name -> RunningSandbox + 28, // 25: CachedBuildInfo.expiration_time:type_name -> google.protobuf.Timestamp + 20, // 26: SandboxListCachedBuildsResponse.builds:type_name -> CachedBuildInfo + 6, // 27: SandboxNetworkEgressConfig.RulesEntry.value:type_name -> SandboxNetworkDomainRules + 9, // 28: SandboxService.Create:input_type -> SandboxCreateRequest + 11, // 29: SandboxService.Update:input_type -> SandboxUpdateRequest + 29, // 30: SandboxService.List:input_type -> google.protobuf.Empty + 12, // 31: SandboxService.Delete:input_type -> SandboxDeleteRequest + 13, // 32: SandboxService.Pause:input_type -> SandboxPauseRequest + 16, // 33: SandboxService.Checkpoint:input_type -> SandboxCheckpointRequest + 29, // 34: SandboxService.ListCachedBuilds:input_type -> google.protobuf.Empty + 10, // 35: SandboxService.Create:output_type -> SandboxCreateResponse + 29, // 36: SandboxService.Update:output_type -> google.protobuf.Empty + 19, // 37: SandboxService.List:output_type -> SandboxListResponse + 29, // 38: SandboxService.Delete:output_type -> google.protobuf.Empty + 15, // 39: SandboxService.Pause:output_type -> SandboxPauseResponse + 17, // 40: SandboxService.Checkpoint:output_type -> SandboxCheckpointResponse + 21, // 41: SandboxService.ListCachedBuilds:output_type -> SandboxListCachedBuildsResponse + 35, // [35:42] is the sub-list for method output_type + 28, // [28:35] is the sub-list for method input_type + 28, // [28:28] is the sub-list for extension type_name + 28, // [28:28] is the sub-list for extension extendee + 0, // [0:28] is the sub-list for field type_name } func init() { file_orchestrator_proto_init() } @@ -2293,7 +2317,7 @@ func file_orchestrator_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_orchestrator_proto_rawDesc, NumEnums: 0, - NumMessages: 27, + NumMessages: 28, NumExtensions: 0, NumServices: 1, }, diff --git a/spec/openapi.yml b/spec/openapi.yml index 79d06ae295..adb16aa8e3 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -811,13 +811,12 @@ components: type: boolean default: true description: >- - Whether the snapshot captures a full memory snapshot. When false, - only the filesystem is persisted (a filesystem-only snapshot): - sandboxes created from it cold-boot (reboot) from disk instead of - restoring in-memory state. Taking a filesystem-only snapshot also - reboots the source sandbox — its in-memory state, running processes, - and open connections are lost while the filesystem and sandbox - identity are preserved. Defaults to true. + Whether the snapshot template captures the sandbox's memory. When + false, the template persists only the filesystem (a filesystem-only + snapshot): sandboxes created from it cold-boot (reboot) from disk + instead of restoring in-memory state. The source sandbox itself is + unaffected either way — it is checkpointed with memory and resumes + with its processes and memory intact. Defaults to true. SandboxPauseRequest: type: object diff --git a/tests/integration/internal/api/generated.go b/tests/integration/internal/api/generated.go index 4b7fcb171b..1a703b96cd 100644 --- a/tests/integration/internal/api/generated.go +++ b/tests/integration/internal/api/generated.go @@ -983,7 +983,7 @@ type SandboxRefreshRequest struct { // SandboxSnapshotRequest defines model for SandboxSnapshotRequest. type SandboxSnapshotRequest struct { - // Memory Whether the snapshot captures a full memory snapshot. When false, only the filesystem is persisted (a filesystem-only snapshot): sandboxes created from it cold-boot (reboot) from disk instead of restoring in-memory state. Taking a filesystem-only snapshot also reboots the source sandbox — its in-memory state, running processes, and open connections are lost while the filesystem and sandbox identity are preserved. Defaults to true. + // Memory Whether the snapshot template captures the sandbox's memory. When false, the template persists only the filesystem (a filesystem-only snapshot): sandboxes created from it cold-boot (reboot) from disk instead of restoring in-memory state. The source sandbox itself is unaffected either way — it is checkpointed with memory and resumes with its processes and memory intact. Defaults to true. Memory *bool `json:"memory,omitempty"` // Name Optional name for the snapshot template. If a snapshot template with this name already exists, a new build will be assigned to the existing template instead of creating a new one. diff --git a/tests/integration/internal/tests/api/sandboxes/filesystem_only_test.go b/tests/integration/internal/tests/api/sandboxes/filesystem_only_test.go index 53b4a150a7..fec7d41d55 100644 --- a/tests/integration/internal/tests/api/sandboxes/filesystem_only_test.go +++ b/tests/integration/internal/tests/api/sandboxes/filesystem_only_test.go @@ -169,18 +169,17 @@ func TestSandboxResume_FilesystemOnlyReboots(t *testing.T) { } // TestSnapshotTemplate_FilesystemOnly verifies the filesystem-only variant of -// the snapshot endpoint (memory:false): the snapshot template persists only the -// rootfs, and taking it reboots the source sandbox in place — its rootfs and -// identity survive while a fresh kernel boot id proves the cold boot. A sandbox -// created from the template then cold-boots from the captured filesystem. +// the snapshot endpoint (memory:false): the returned template persists only the +// rootfs, so a sandbox created from it cold-boots from the captured filesystem, +// while the source sandbox is memory-checkpointed and resumes with its memory +// intact — an unchanged kernel boot id proves it was not rebooted. func TestSnapshotTemplate_FilesystemOnly(t *testing.T) { t.Parallel() c := setup.GetAPIClient() ctx := t.Context() - // Generous timeout: the checkpoint reboots the source sandbox in place, and - // RebootSandbox rejects an already-past end time; under parallel load a - // short timeout would make this flaky. + // Generous timeout: the checkpoint resume goes through placement, and under + // parallel load a short timeout could expire the end time before it runs. sbx := utils.SetupSandboxWithCleanup(t, c, utils.WithAutoPause(false), utils.WithTimeout(300)) envdClient := setup.GetEnvdClient(t, ctx) @@ -216,14 +215,15 @@ func TestSnapshotTemplate_FilesystemOnly(t *testing.T) { got, err := utils.ExecCommandAsRootWithOutput(t, ctx, sbx, envdClient, "cat", "/home/user/fs-only-snapshot-marker.txt") require.NoError(t, err) - assert.Equal(t, marker, strings.TrimSpace(got), "rootfs marker must survive the in-place reboot") + assert.Equal(t, marker, strings.TrimSpace(got), "rootfs marker must survive the checkpoint") - // A fresh boot id proves the source sandbox was cold-booted, not memory-resumed. + // An unchanged boot id proves the source sandbox was memory-resumed, not + // rebooted: only the returned template is filesystem-only. bootAfter, err := utils.ExecCommandAsRootWithOutput(t, ctx, sbx, envdClient, "cat", "/proc/sys/kernel/random/boot_id") require.NoError(t, err) - assert.NotEqual(t, bootBefore, strings.TrimSpace(bootAfter), - "boot id should change — a filesystem-only snapshot reboots the source sandbox") + assert.Equal(t, bootBefore, strings.TrimSpace(bootAfter), + "boot id must not change — a filesystem-only snapshot memory-resumes the source sandbox") // A sandbox created from the filesystem-only template cold-boots from the // captured rootfs, so the marker must be there. @@ -239,6 +239,14 @@ func TestSnapshotTemplate_FilesystemOnly(t *testing.T) { require.NoError(t, err) assert.Equal(t, marker, strings.TrimSpace(gotNew), "marker must be present in a sandbox created from the filesystem-only snapshot") + + // A fresh boot id in the new sandbox proves it cold-booted from disk rather + // than restoring the source's memory state. + newBoot, err := utils.ExecCommandAsRootWithOutput(t, ctx, newSbx, envdClient, + "cat", "/proc/sys/kernel/random/boot_id") + require.NoError(t, err) + assert.NotEqual(t, bootBefore, strings.TrimSpace(newBoot), + "a sandbox created from a filesystem-only snapshot must cold-boot") } // TestSandboxAutoPause_FilesystemOnly verifies the auto-pause filesystem-only