From 55390d81b23d91512ca242cf53ed74b29d070318 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Sun, 18 Oct 2026 09:52:54 -0700 Subject: [PATCH 1/4] feat(control): add pod restore calls ImportZone can restore a snapshot as the zone of a Kubernetes pod, leaving it paused until the pod's runtime claims it with ClaimRestoredZone, starts it with StartRestoredZone and binds each workload to its container with AdoptWorkload. --- protect/control/v1/control.proto | 69 ++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/protect/control/v1/control.proto b/protect/control/v1/control.proto index 04ccfc0..70c70b5 100644 --- a/protect/control/v1/control.proto +++ b/protect/control/v1/control.proto @@ -34,6 +34,9 @@ service ControlService { rpc ForkZone(ForkZoneRequest) returns (ForkZoneReply); rpc ExportZone(ExportZoneRequest) returns (stream ExportZoneReply); rpc ImportZone(stream ImportZoneRequest) returns (ImportZoneReply); + rpc ClaimRestoredZone(ClaimRestoredZoneRequest) returns (ClaimRestoredZoneReply); + rpc StartRestoredZone(StartRestoredZoneRequest) returns (StartRestoredZoneReply); + rpc AdoptWorkload(AdoptWorkloadRequest) returns (AdoptWorkloadReply); rpc ResolveZoneId(ResolveZoneIdRequest) returns (ResolveZoneIdReply); rpc ResolveZoneIds(ResolveZoneIdsRequest) returns (ResolveZoneIdsReply); @@ -220,12 +223,78 @@ message ImportZoneStart { // Import under this zone id instead of a freshly allocated one. Empty // allocates a new id, which is what lets one snapshot seed many zones. string zone_id = 2; + // Restore the snapshot as the zone of a Kubernetes pod. The zone is left + // paused, with its host mounts unbound, until the pod's runtime claims it. + PodRestore restore = 4; } message ImportZoneReply { string zone_id = 1; } +// The pod an imported zone is restored for. The three coordinates together are +// what a runtime must present to claim the zone, so a pod cannot claim a zone +// meant for another. +message PodRestore { + string uid = 1; + string namespace = 2; + string name = 3; + // Containers of the source pod that had already completed, which are + // reported to the kubelet as exited instead of being run again. + repeated string completed_containers = 4; +} + +// Claims the zone imported for a pod. Waits, up to the daemon's claim timeout, +// for the import to exist. A zone can be claimed once. +message ClaimRestoredZoneRequest { + string uid = 1; + string namespace = 2; + string name = 3; +} + +message ClaimRestoredZoneReply { + string zone_id = 1; + repeated RestoredWorkload workloads = 2; + repeated string completed_containers = 3; +} + +// A workload the restored zone was running, as the snapshot recorded it. +message RestoredWorkload { + string workload_id = 1; + string container_name = 2; + string image = 3; + repeated RestoredMount mounts = 4; +} + +// A host mount of a restored workload. The host path the snapshot recorded is +// not reported and is never used: `AdoptWorkload` supplies the new one. +message RestoredMount { + string target_path = 1; + bool read_only = 2; +} + +// Resumes a claimed zone and brings its network and workload ids up to date. +message StartRestoredZoneRequest { + string zone_id = 1; +} + +message StartRestoredZoneReply {} + +// Binds a restored workload to the container the kubelet created for it. +// `mounts` are the container's own mounts: each of the workload's recorded +// mounts must have exactly one with the same target path and read-only flag, +// and no other mount may be left over. Repeating the call for the same +// workload replaces the earlier one. +message AdoptWorkloadRequest { + string zone_id = 1; + string workload_id = 2; + string container_id = 3; + string sandbox_id = 4; + repeated MountSpec mounts = 5; +} + +message AdoptWorkloadReply {} + // Resolves zone "friendly name" to a singular zone UUID, if possible. Matching is exact. // See also `ResolveZoneIdsRequest` (plural). message ResolveZoneIdRequest { From d35662ac277a755a329ff688aa34bc76423b1596 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Sun, 18 Oct 2026 10:03:56 -0700 Subject: [PATCH 2/4] feat(control): carry the new container's annotations in AdoptWorkload --- protect/control/v1/control.proto | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/protect/control/v1/control.proto b/protect/control/v1/control.proto index 70c70b5..25e947c 100644 --- a/protect/control/v1/control.proto +++ b/protect/control/v1/control.proto @@ -283,7 +283,9 @@ message StartRestoredZoneReply {} // Binds a restored workload to the container the kubelet created for it. // `mounts` are the container's own mounts: each of the workload's recorded // mounts must have exactly one with the same target path and read-only flag, -// and no other mount may be left over. Repeating the call for the same +// and no other mount may be left over. `annotations` are set on the workload, +// replacing any with the same key, so it carries the new container's own +// identity instead of the source pod's. Repeating the call for the same // workload replaces the earlier one. message AdoptWorkloadRequest { string zone_id = 1; @@ -291,6 +293,7 @@ message AdoptWorkloadRequest { string container_id = 3; string sandbox_id = 4; repeated MountSpec mounts = 5; + repeated AnnotationSpec annotations = 6; } message AdoptWorkloadReply {} From 810f8f15de436aacae44a0850be5e71406c0887a Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Sun, 18 Oct 2026 10:34:13 -0700 Subject: [PATCH 3/4] feat(control): carry the new sandbox's annotations in StartRestoredZone --- protect/control/v1/control.proto | 3 +++ 1 file changed, 3 insertions(+) diff --git a/protect/control/v1/control.proto b/protect/control/v1/control.proto index 25e947c..29f7da5 100644 --- a/protect/control/v1/control.proto +++ b/protect/control/v1/control.proto @@ -276,6 +276,9 @@ message RestoredMount { // Resumes a claimed zone and brings its network and workload ids up to date. message StartRestoredZoneRequest { string zone_id = 1; + // Set on the zone's spec, replacing any with the same key, so the zone + // carries the new pod's sandbox identity instead of the source pod's. + repeated AnnotationSpec annotations = 2; } message StartRestoredZoneReply {} From 2f5d02a22001b73be208dee8e4592ef254de5ac7 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Sun, 18 Oct 2026 14:28:09 -0700 Subject: [PATCH 4/4] refactor(control): make the restore calls generic The restore claim is an opaque string with annotations the daemon does not read, and the runtime tells StartRestoredZone where each mount is bound, so the control API carries nothing about any orchestrator. --- protect/control/v1/control.proto | 79 +++++++++++++++++++------------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/protect/control/v1/control.proto b/protect/control/v1/control.proto index 29f7da5..0e69d07 100644 --- a/protect/control/v1/control.proto +++ b/protect/control/v1/control.proto @@ -223,72 +223,89 @@ message ImportZoneStart { // Import under this zone id instead of a freshly allocated one. Empty // allocates a new id, which is what lets one snapshot seed many zones. string zone_id = 2; - // Restore the snapshot as the zone of a Kubernetes pod. The zone is left - // paused, with its host mounts unbound, until the pod's runtime claims it. - PodRestore restore = 4; + // Hold the imported zone, paused and with its host mounts unbound, for a + // runtime that claims it with the same `claim`. + RestoreClaim restore = 4; } message ImportZoneReply { string zone_id = 1; } -// The pod an imported zone is restored for. The three coordinates together are -// what a runtime must present to claim the zone, so a pod cannot claim a zone -// meant for another. -message PodRestore { - string uid = 1; - string namespace = 2; - string name = 3; - // Containers of the source pod that had already completed, which are - // reported to the kubelet as exited instead of being run again. - repeated string completed_containers = 4; +// What an imported zone is held for. The zone waits, paused, until a runtime +// presents the same `claim`, so a runtime can only claim a zone that was +// imported for it. The daemon compares the string and nothing more. +message RestoreClaim { + string claim = 1; + // Kept with the zone and returned when it is claimed. The daemon does not + // read them. + repeated AnnotationSpec annotations = 2; } -// Claims the zone imported for a pod. Waits, up to the daemon's claim timeout, -// for the import to exist. A zone can be claimed once. +// Claims the zone imported for `claim`. Waits, up to the daemon's claim +// timeout, for the import to exist. A zone can be claimed once. message ClaimRestoredZoneRequest { - string uid = 1; - string namespace = 2; - string name = 3; + string claim = 1; } message ClaimRestoredZoneReply { string zone_id = 1; repeated RestoredWorkload workloads = 2; - repeated string completed_containers = 3; + // The annotations the import was given. + repeated AnnotationSpec annotations = 3; } // A workload the restored zone was running, as the snapshot recorded it. message RestoredWorkload { string workload_id = 1; - string container_name = 2; - string image = 3; + string image = 2; + repeated AnnotationSpec annotations = 3; repeated RestoredMount mounts = 4; } -// A host mount of a restored workload. The host path the snapshot recorded is -// not reported and is never used: `AdoptWorkload` supplies the new one. +// A host mount of a restored workload. `recorded_host_path` names a path on the +// host that took the snapshot and is only a hint: the daemon never binds it. message RestoredMount { string target_path = 1; bool read_only = 2; + string recorded_host_path = 3; +} + +// What a mount that has no host path yet is bound to until a container +// supplies it. +enum RestoredPlaceholder { + RESTORED_PLACEHOLDER_UNSPECIFIED = 0; + RESTORED_PLACEHOLDER_FILE = 1; + RESTORED_PLACEHOLDER_DIRECTORY = 2; } -// Resumes a claimed zone and brings its network and workload ids up to date. +// Where one mount of a restored workload is bound before its zone resumes. +message RestoredBinding { + string workload_id = 1; + string target_path = 2; + // The host path to bind. The daemon resolves it and refuses one outside its + // configured restore bind roots. Empty binds `placeholder` instead. + string host_path = 3; + RestoredPlaceholder placeholder = 4; +} + +// Binds every mount of a claimed zone, then resumes it. Each mount of each +// running workload needs exactly one binding. `annotations` are set on the +// zone's spec, replacing any with the same key. message StartRestoredZoneRequest { string zone_id = 1; - // Set on the zone's spec, replacing any with the same key, so the zone - // carries the new pod's sandbox identity instead of the source pod's. - repeated AnnotationSpec annotations = 2; + repeated RestoredBinding bindings = 2; + repeated AnnotationSpec annotations = 3; } message StartRestoredZoneReply {} -// Binds a restored workload to the container the kubelet created for it. +// Binds a restored workload to the container a runtime created for it. // `mounts` are the container's own mounts: each of the workload's recorded // mounts must have exactly one with the same target path and read-only flag, -// and no other mount may be left over. `annotations` are set on the workload, -// replacing any with the same key, so it carries the new container's own -// identity instead of the source pod's. Repeating the call for the same +// and no other mount may be left over. A mount whose resolved host path differs +// from the one it is bound to is rebound. `annotations` are set on the +// workload, replacing any with the same key. Repeating the call for the same // workload replaces the earlier one. message AdoptWorkloadRequest { string zone_id = 1;