diff --git a/cmd/atecontroller/internal/controllers/workerpool_apply.go b/cmd/atecontroller/internal/controllers/workerpool_apply.go index 594754fa76..10a3e4e50e 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_apply.go +++ b/cmd/atecontroller/internal/controllers/workerpool_apply.go @@ -26,9 +26,9 @@ import ( metav1ac "k8s.io/client-go/applyconfigurations/meta/v1" "github.com/agent-substrate/substrate/internal/ateomcapacity" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/deviceplugin" "github.com/agent-substrate/substrate/internal/installdefaults" + "github.com/agent-substrate/substrate/internal/nodepath" atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" ) @@ -167,7 +167,7 @@ func buildDeploymentApplyConfig(wp *atev1alpha1.WorkerPool, otel ateomOTelSettin WithReadOnly(true), corev1ac.VolumeMount(). WithName("run-ateom"). - WithMountPath(ateompath.BasePath). + WithMountPath(nodepath.BasePath). WithMountPropagation(corev1.MountPropagationHostToContainer), corev1ac.VolumeMount(). WithName(atunnelIdentityVolume). @@ -194,7 +194,7 @@ func buildDeploymentApplyConfig(wp *atev1alpha1.WorkerPool, otel ateomOTelSettin corev1ac.Volume(). WithName("run-ateom"). WithHostPath(corev1ac.HostPathVolumeSource(). - WithPath(ateompath.BasePath). + WithPath(nodepath.BasePath). WithType(corev1.HostPathDirectoryOrCreate)), corev1ac.Volume(). WithName(atunnelIdentityVolume). diff --git a/cmd/atecontroller/internal/controllers/workerpool_apply_test.go b/cmd/atecontroller/internal/controllers/workerpool_apply_test.go index 5ec80357a8..483957b7ff 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_apply_test.go +++ b/cmd/atecontroller/internal/controllers/workerpool_apply_test.go @@ -30,9 +30,9 @@ import ( metav1ac "k8s.io/client-go/applyconfigurations/meta/v1" "github.com/agent-substrate/substrate/internal/ateomcapacity" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/deviceplugin" "github.com/agent-substrate/substrate/internal/installdefaults" + "github.com/agent-substrate/substrate/internal/nodepath" atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" ) @@ -713,7 +713,7 @@ func expectedDeploymentApplyConfig(mutatePodSpec func(*corev1ac.PodSpecApplyConf corev1ac.Volume(). WithName("run-ateom"). WithHostPath(corev1ac.HostPathVolumeSource(). - WithPath(ateompath.BasePath). + WithPath(nodepath.BasePath). WithType(corev1.HostPathDirectoryOrCreate)), corev1ac.Volume(). WithName(atunnelIdentityVolume). @@ -799,7 +799,7 @@ func expectedDeploymentApplyConfig(mutatePodSpec func(*corev1ac.PodSpecApplyConf WithReadOnly(true), corev1ac.VolumeMount(). WithName("run-ateom"). - WithMountPath(ateompath.BasePath). + WithMountPath(nodepath.BasePath). WithMountPropagation(corev1.MountPropagationHostToContainer), corev1ac.VolumeMount(). WithName(atunnelIdentityVolume). diff --git a/cmd/atelet/imagegc.go b/cmd/atelet/imagegc.go index 4eb8b63f3d..d0e4b7de90 100644 --- a/cmd/atelet/imagegc.go +++ b/cmd/atelet/imagegc.go @@ -35,8 +35,9 @@ import ( "strings" "time" - "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" "github.com/agent-substrate/substrate/internal/imagecache" + "github.com/agent-substrate/substrate/internal/nodepath" "github.com/spf13/pflag" "golang.org/x/sys/unix" ) @@ -79,7 +80,7 @@ func validateImageCacheGCFlags() error { if imageCacheDirOutsideBasePath(*imageCacheDir) { slog.Warn("Image cache dir is outside the ateom base path; its volume watermarks are measured separately from actor state", slog.String("image_cache_dir", *imageCacheDir), - slog.String("actors_dir", ateompath.ActorsDir)) + slog.String("actors_dir", ateletpath.ActorsDir)) } return nil } @@ -93,7 +94,7 @@ func imageCacheDirOutsideBasePath(dir string) bool { if err != nil { abs = filepath.Clean(dir) } - return !strings.HasPrefix(abs, ateompath.BasePath+string(os.PathSeparator)) + return !strings.HasPrefix(abs, nodepath.BasePath+string(os.PathSeparator)) } // imageCacheGCTarget computes the bytes a pass should free: the larger diff --git a/cmd/atelet/imagegc_test.go b/cmd/atelet/imagegc_test.go index 99df8d3a33..07ac664ec8 100644 --- a/cmd/atelet/imagegc_test.go +++ b/cmd/atelet/imagegc_test.go @@ -25,8 +25,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/imagecache" + "github.com/agent-substrate/substrate/internal/nodepath" ) func TestImageCacheGCTarget(t *testing.T) { @@ -160,13 +160,13 @@ func TestImageCacheDirOutsideBasePath(t *testing.T) { dir string want bool }{ - {"inside", filepath.Join(ateompath.BasePath, "image-cache"), false}, - {"inside with doubled separator", ateompath.BasePath + "//image-cache", false}, - {"inside via dot-dot", ateompath.BasePath + "/x/../image-cache", false}, - {"base path itself is not inside", ateompath.BasePath, true}, - {"sibling with the base path as name prefix", ateompath.BasePath + "-other/image-cache", true}, + {"inside", filepath.Join(nodepath.BasePath, "image-cache"), false}, + {"inside with doubled separator", nodepath.BasePath + "//image-cache", false}, + {"inside via dot-dot", nodepath.BasePath + "/x/../image-cache", false}, + {"base path itself is not inside", nodepath.BasePath, true}, + {"sibling with the base path as name prefix", nodepath.BasePath + "-other/image-cache", true}, {"outside", "/var/lib/elsewhere/image-cache", true}, - {"dot-dot escaping the base path", ateompath.BasePath + "/../elsewhere/image-cache", true}, + {"dot-dot escaping the base path", nodepath.BasePath + "/../elsewhere/image-cache", true}, {"relative resolves against the cwd, not the base path", "image-cache", true}, } for _, tc := range cases { diff --git a/internal/ateompath/ateompath.go b/cmd/atelet/internal/ateletpath/ateletpath.go similarity index 57% rename from internal/ateompath/ateompath.go rename to cmd/atelet/internal/ateletpath/ateletpath.go index ea5ed4a50d..a0340cba6f 100644 --- a/internal/ateompath/ateompath.go +++ b/cmd/atelet/internal/ateletpath/ateletpath.go @@ -12,37 +12,31 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Ateom and atelet need to agree on many filesystem paths. They are defined in this package. -package ateompath +// Package ateletpath is atelet's on-node layout: the per-actor directories +// it passes to ateom as ActorDirs, and the directories only atelet uses. +package ateletpath import ( "path/filepath" -) -const ( - // The base path. This is both the path of the root shared folder on the - // host filesystem, and when it is mounted into ateom and atelet containers. - BasePath = "/var/lib/ateom-gvisor" + "github.com/agent-substrate/substrate/internal/nodepath" + "github.com/agent-substrate/substrate/internal/proto/ateompb" ) var ( // StaticFilesDir holds things like downloaded runsc binaries. - StaticFilesDir = filepath.Join(BasePath, "static-files") + StaticFilesDir = filepath.Join(nodepath.BasePath, "static-files") // ImageCacheDir is the node-local OCI image layer cache (see // internal/imagecache). It lives under BasePath so the cached layer // directories are visible at the same path in atelet (which writes them) // and in every ateom pod (which mounts them as overlay lowerdirs). - ImageCacheDir = filepath.Join(BasePath, "image-cache") + ImageCacheDir = filepath.Join(nodepath.BasePath, "image-cache") // ActorsDir holds the per-actor state directories (see ActorPath). The // image cache's eviction root-set scan reads the bundle overlay specs // under it. - ActorsDir = filepath.Join(BasePath, "actors") - - // AteomSupportSocket is the node-local atelet socket used by atunnel - // to request credentials for the worker's current actor assignment. - AteomSupportSocket = filepath.Join(BasePath, "ateom-support.sock") + ActorsDir = filepath.Join(nodepath.BasePath, "actors") ) func RunSCBinaryPath(sha256 string) string { @@ -58,61 +52,6 @@ func GVisorReleaseDir(sha256 string) string { return filepath.Join(StaticFilesDir, "gvisor-"+sha256) } -// AteletOTLPSocketPath is the node-scoped unix socket atelet serves the OTLP -// relay on (see internal/otlprelay). It is node-scoped rather than per-pod -// because every ateom on the node pushes into the same relay: atelet is a -// DaemonSet, so one socket collapses N per-pod collector connections into one -// per-node connection. -// -// It sits directly under BasePath, which is the host directory already mounted -// at the same path into atelet and into every ateom pod, so no new volume is -// needed for ateom to reach it. Note that BasePath is mounted writable -// (workerpool_apply.go) and shared with CredentialBrokerSocket and the image -// cache, so a worker pod can unlink or replace this socket. Confining -// atelet-owned sockets to a subdirectory mounted read-only would be an -// improvement, but it is a property of the whole BasePath mount rather than of -// this socket — a read-only subdir needs its own volume and mount, and the pod -// keeps CAP_SYS_ADMIN. Tracked separately rather than solved here. -func AteletOTLPSocketPath() string { - return filepath.Join( - BasePath, - "atelet-otlp.sock", - ) -} - -// AteomsDir is the parent of every per-ateom directory. Each ateom creates -// AteomPath(podUID) under it when it boots, so listing this directory is how a -// scraper with no prior knowledge discovers the node's ateoms. -func AteomsDir() string { - return filepath.Join(BasePath, "ateoms") -} - -func AteomPath(podUID string) string { - return filepath.Join(AteomsDir(), podUID) -} - -func AteomSocketPath(podUID string) string { - return filepath.Join( - AteomPath(podUID), - "ateom.sock", - ) -} - -// ActorNetNSName names an actor's sandbox network namespace. -func ActorNetNSName(actorUID string) string { - return "ateom-actor:" + actorUID -} - -// ActorNetNSPath is the mount path of the actor's named namespace. -func ActorNetNSPath(actorUID string) string { - return filepath.Join("/run/netns", ActorNetNSName(actorUID)) -} - -// ActorResolvConfPath is the resolver bind source outside the actor's rootfs. -func ActorResolvConfPath(actorUID string) string { - return filepath.Join(ActorPath(actorUID), "resolv.conf") -} - func ActorPath(actorUID string) string { return filepath.Join( ActorsDir, @@ -154,27 +93,6 @@ func OCIBundlePath(actorUID, containerName string) string { ) } -// ImageVolumeMountPath returns where ateom composes one image volume for a -// container. The path is per-container: containers of one actor may mount the -// same volume, and each needs its own mount point inside its own bundle. -func ImageVolumeMountPath(actorUID, containerName, volumeName string) string { - return ImageVolumeMountPathInBundle(OCIBundlePath(actorUID, containerName), volumeName) -} - -// ImageVolumeMountPathInBundle returns the image volume mount path inside a -// bundle path. -func ImageVolumeMountPathInBundle(bundlePath, volumeName string) string { - return filepath.Join(bundlePath, "volumes", volumeName) -} - -func RunscDebugLogDir(actorUID, containerName string) string { - return filepath.Join( - ActorPath(actorUID), - "runsc-debug-logs", - containerName, - ) -} - func CheckpointStateDir(actorUID string) string { return filepath.Join( ActorPath(actorUID), @@ -195,13 +113,6 @@ func LocalSnapshotDir(actorUID, snapshotName string) string { return filepath.Join(LocalCheckpointsDir(actorUID), snapshotName) } -// DurableDirTarFile is the snapshot file holding the tar of an -// actor's durable-dir volumes (entries are /... relative to -// DurableDirVolumeMountsDir). Written by ateom-microvm at checkpoint; a DATA -// snapshot consists of this file alone, so atelet uses the name to carve the -// durable data out of a FULL snapshot's file set. -const DurableDirTarFile = "durable-dir.tar" - // DurableDirVolumeMountsDir is the directory where individual durable-dir // volumes are mounted. func DurableDirVolumeMountsDir(actorUID string) string { @@ -275,13 +186,6 @@ func PIDFileDir(actorUID string) string { ) } -func PIDFilePath(actorUID, containerName string) string { - return filepath.Join( - PIDFileDir(actorUID), - containerName+".pid", - ) -} - func VolumesDir(actorUID string) string { return filepath.Join( ActorPath(actorUID), @@ -296,12 +200,18 @@ func VolumeHostPath(actorUID, volumeName string) string { ) } -// StagingDirPrefix returns the prefix directory for staging CSI volumes. -func StagingDirPrefix() string { - return filepath.Join(BasePath, "staging") -} - -// KubeletPluginSocketPath returns the path to the CSI driver socket in kubelet plugins directory. -func KubeletPluginSocketPath(driverName string) string { - return filepath.Join("/var/lib/kubelet/plugins", driverName, "csi.sock") +// ActorDirs is the directory set atelet passes to ateom for an actor. ateom +// takes these from the request rather than deriving them from the actor UID. +func ActorDirs(actorUID string) *ateompb.ActorDirs { + return &ateompb.ActorDirs{ + RootDir: ActorPath(actorUID), + OciBundleDir: OCIBundleDir(actorUID), + RunscStateDir: RunSCStateDir(actorUID), + PidFileDir: PIDFileDir(actorUID), + CheckpointDir: CheckpointStateDir(actorUID), + RestoreDir: RestoreStateDir(actorUID), + DurableDirVolumeMountsDir: DurableDirVolumeMountsDir(actorUID), + SystemInfoVolumeRootsDir: SystemInfoVolumeRootsDir(actorUID), + VolumesDir: VolumesDir(actorUID), + } } diff --git a/cmd/atelet/internal/ateletpath/ateletpath_test.go b/cmd/atelet/internal/ateletpath/ateletpath_test.go new file mode 100644 index 0000000000..d834345680 --- /dev/null +++ b/cmd/atelet/internal/ateletpath/ateletpath_test.go @@ -0,0 +1,50 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ateletpath + +import ( + "path/filepath" + "strings" + "testing" +) + +func TestActorDirs(t *testing.T) { + const actorUID = "actor-uid-1" + actorDirs := ActorDirs(actorUID) + if actorDirs.GetRootDir() != ActorPath(actorUID) { + t.Fatalf("root_dir = %q, want %q", actorDirs.GetRootDir(), ActorPath(actorUID)) + } + under := map[string]string{ + "oci_bundle_dir": actorDirs.GetOciBundleDir(), + "runsc_state_dir": actorDirs.GetRunscStateDir(), + "pid_file_dir": actorDirs.GetPidFileDir(), + "checkpoint_dir": actorDirs.GetCheckpointDir(), + "restore_dir": actorDirs.GetRestoreDir(), + "durable_dir_volume_mounts_dir": actorDirs.GetDurableDirVolumeMountsDir(), + "system_info_volume_roots_dir": actorDirs.GetSystemInfoVolumeRootsDir(), + "volumes_dir": actorDirs.GetVolumesDir(), + } + seen := map[string]string{} + for field, dir := range under { + rel, err := filepath.Rel(actorDirs.GetRootDir(), dir) + if err != nil || rel == "." || strings.HasPrefix(rel, "..") { + t.Errorf("%s = %q is not a proper subdirectory of root_dir %q", field, dir, actorDirs.GetRootDir()) + } + if other, dup := seen[dir]; dup { + t.Errorf("%s and %s share %q", field, other, dir) + } + seen[dir] = field + } +} diff --git a/cmd/atelet/lifecycle_test.go b/cmd/atelet/lifecycle_test.go index ed27fa9eb0..a960a9a9b6 100644 --- a/cmd/atelet/lifecycle_test.go +++ b/cmd/atelet/lifecycle_test.go @@ -24,10 +24,11 @@ import ( "runtime" "testing" - "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" "github.com/agent-substrate/substrate/internal/proto/ateletpb" "github.com/agent-substrate/substrate/internal/proto/ateompb" "google.golang.org/grpc" + "google.golang.org/protobuf/proto" ) // useTempNodeDirs roots atelet's on-node state in temp directories so a test @@ -36,33 +37,45 @@ import ( func useTempNodeDirs(t *testing.T) { t.Helper() root := t.TempDir() - origActors, origStatic := ateompath.ActorsDir, ateompath.StaticFilesDir - ateompath.ActorsDir = filepath.Join(root, "actors") - ateompath.StaticFilesDir = filepath.Join(root, "static-files") + origActors, origStatic := ateletpath.ActorsDir, ateletpath.StaticFilesDir + ateletpath.ActorsDir = filepath.Join(root, "actors") + ateletpath.StaticFilesDir = filepath.Join(root, "static-files") t.Cleanup(func() { - ateompath.ActorsDir, ateompath.StaticFilesDir = origActors, origStatic + ateletpath.ActorsDir, ateletpath.StaticFilesDir = origActors, origStatic }) } // fakeAteom is a fake ateom in a worker pod. It writes the files a -// real checkpoint would leave in the checkpoint-state dir, and reads back -// what a restore was handed. +// real checkpoint would leave in the checkpoint dir, and reads back what a +// restore was handed. Like a real ateom it takes every actor directory from +// the request, never derived from the actor UID. type fakeAteom struct { ateompb.UnimplementedAteomServer // snapshotFiles are written at checkpoint and reported back to atelet as // the exact set the snapshot consists of. snapshotFiles map[string]string - // restored holds the file contents staged into the restore-state dir by - // the most recent RestoreWorkload. + // restored holds the file contents staged into the restore dir by the + // most recent RestoreWorkload. restored map[string]string + // actorDirs records the ActorDirs each RPC arrived with, by RPC name. + actorDirs map[string]*ateompb.ActorDirs } -func (f *fakeAteom) RunWorkload(context.Context, *ateompb.RunWorkloadRequest) (*ateompb.RunWorkloadResponse, error) { +func (f *fakeAteom) recordActorDirs(rpc string, actorDirs *ateompb.ActorDirs) { + if f.actorDirs == nil { + f.actorDirs = map[string]*ateompb.ActorDirs{} + } + f.actorDirs[rpc] = actorDirs +} + +func (f *fakeAteom) RunWorkload(_ context.Context, req *ateompb.RunWorkloadRequest) (*ateompb.RunWorkloadResponse, error) { + f.recordActorDirs("RunWorkload", req.GetActorDirs()) return &ateompb.RunWorkloadResponse{}, nil } func (f *fakeAteom) CheckpointWorkload(_ context.Context, req *ateompb.CheckpointWorkloadRequest) (*ateompb.CheckpointWorkloadResponse, error) { - dir := ateompath.CheckpointStateDir(req.GetActorUid()) + f.recordActorDirs("CheckpointWorkload", req.GetActorDirs()) + dir := req.GetActorDirs().GetCheckpointDir() names := make([]string, 0, len(f.snapshotFiles)) for name, body := range f.snapshotFiles { if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o600); err != nil { @@ -74,7 +87,8 @@ func (f *fakeAteom) CheckpointWorkload(_ context.Context, req *ateompb.Checkpoin } func (f *fakeAteom) RestoreWorkload(_ context.Context, req *ateompb.RestoreWorkloadRequest) (*ateompb.RestoreWorkloadResponse, error) { - dir := ateompath.RestoreStateDir(req.GetActorUid()) + f.recordActorDirs("RestoreWorkload", req.GetActorDirs()) + dir := req.GetActorDirs().GetRestoreDir() f.restored = map[string]string{} for name := range f.snapshotFiles { body, err := os.ReadFile(filepath.Join(dir, name)) @@ -86,7 +100,8 @@ func (f *fakeAteom) RestoreWorkload(_ context.Context, req *ateompb.RestoreWorkl return &ateompb.RestoreWorkloadResponse{}, nil } -func (f *fakeAteom) TerminateWorkload(context.Context, *ateompb.TerminateWorkloadRequest) (*ateompb.TerminateWorkloadResponse, error) { +func (f *fakeAteom) TerminateWorkload(_ context.Context, req *ateompb.TerminateWorkloadRequest) (*ateompb.TerminateWorkloadResponse, error) { + f.recordActorDirs("TerminateWorkload", req.GetActorDirs()) return &ateompb.TerminateWorkloadResponse{}, nil } @@ -192,7 +207,7 @@ func TestLocalSnapshotGC(t *testing.T) { }); err != nil { t.Fatalf("Checkpoint: %v", err) } - snapshotFile := filepath.Join(ateompath.LocalSnapshotDir(actorUID, snapshotName), "checkpoint.img") + snapshotFile := filepath.Join(ateletpath.LocalSnapshotDir(actorUID, snapshotName), "checkpoint.img") if _, err := os.Stat(snapshotFile); err != nil { t.Fatalf("pause did not write the local snapshot: %v", err) } @@ -231,7 +246,16 @@ func TestLocalSnapshotGC(t *testing.T) { t.Fatalf("Terminate: %v", err) } - localDir := ateompath.LocalCheckpointsDir(actorUID) + // Every RPC hands ateom the same directory set; the fake already relied + // on checkpoint_dir and restore_dir above to place and find the snapshot. + want := ateletpath.ActorDirs(actorUID) + for _, rpc := range []string{"RunWorkload", "CheckpointWorkload", "RestoreWorkload", "TerminateWorkload"} { + if got := ateom.actorDirs[rpc]; !proto.Equal(got, want) { + t.Errorf("%s carried actor actorDirs %v, want %v", rpc, got, want) + } + } + + localDir := ateletpath.LocalCheckpointsDir(actorUID) if _, err := os.Stat(localDir); !os.IsNotExist(err) { leaked, _ := filepath.Glob(filepath.Join(localDir, "*", "*")) t.Errorf("local checkpoint dir survived terminate (stat err = %v), leaked files: %v", err, leaked) @@ -239,7 +263,7 @@ func TestLocalSnapshotGC(t *testing.T) { // Terminate is the only chance to reclaim the actor's directory: nothing // else on the node deletes it. - actorDir := ateompath.ActorPath(actorUID) + actorDir := ateletpath.ActorPath(actorUID) if entries, err := os.ReadDir(actorDir); err == nil { left := make([]string, 0, len(entries)) for _, e := range entries { diff --git a/cmd/atelet/local_checkpoints.go b/cmd/atelet/local_checkpoints.go index ca7b963825..1e2ff89b25 100644 --- a/cmd/atelet/local_checkpoints.go +++ b/cmd/atelet/local_checkpoints.go @@ -22,13 +22,13 @@ import ( "os" "path/filepath" - "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" ) // pruneLocalCheckpoints removes every local snapshot of the actor. A missing // directory is not an error, so retries are safe. func pruneLocalCheckpoints(ctx context.Context, actorUID string) error { - return pruneLocalCheckpointDir(ctx, ateompath.LocalCheckpointsDir(actorUID)) + return pruneLocalCheckpointDir(ctx, ateletpath.LocalCheckpointsDir(actorUID)) } func pruneLocalCheckpointDir(ctx context.Context, dir string) error { diff --git a/cmd/atelet/main.go b/cmd/atelet/main.go index c4a996e841..b938d5439f 100644 --- a/cmd/atelet/main.go +++ b/cmd/atelet/main.go @@ -34,15 +34,16 @@ import ( "sync" "github.com/agent-substrate/substrate/cmd/atelet/internal/ategcs" + "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" "github.com/agent-substrate/substrate/cmd/atelet/internal/sparsefile" "github.com/agent-substrate/substrate/internal/actorlog" "github.com/agent-substrate/substrate/internal/ateapiauth" "github.com/agent-substrate/substrate/internal/ateattr" "github.com/agent-substrate/substrate/internal/ateinterceptors" "github.com/agent-substrate/substrate/internal/atelet" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/credbundle" "github.com/agent-substrate/substrate/internal/imagecache" + "github.com/agent-substrate/substrate/internal/nodepath" "github.com/agent-substrate/substrate/internal/ocispec" "github.com/agent-substrate/substrate/internal/otlprelay" "github.com/agent-substrate/substrate/internal/proto/ateletpb" @@ -95,12 +96,12 @@ var ( gcpAuthForImagePulls = pflag.Bool("gcp-auth-for-image-pulls", true, "Use GCP application default credentials mechanism.") localhostRegistryReplacement = pflag.String("localhost-registry-replacement", "", "The replacement registry endpoint for localhost and/or loopback IP addresses, useful for local development. for example kind-registry:5000") - imageCacheDir = pflag.String("image-cache-dir", ateompath.ImageCacheDir, "Directory for the node-local OCI image layer cache. Must be on the volume shared with the ateom pods (the cached layers are their overlay lowerdirs), and on a disk sized for both capacity and IOPS: unpack throughput is gated by the volume's IOPS.") + imageCacheDir = pflag.String("image-cache-dir", ateletpath.ImageCacheDir, "Directory for the node-local OCI image layer cache. Must be on the volume shared with the ateom pods (the cached layers are their overlay lowerdirs), and on a disk sized for both capacity and IOPS: unpack throughput is gated by the volume's IOPS.") showVersion = pflag.Bool("version", false, "Print version and exit.") logLevelFlag = pflag.String("log-level", "info", "Minimum log level: debug, info, warn, or error.") - otlpRelaySocket = pflag.String("otlp-relay-socket", ateompath.AteletOTLPSocketPath(), "Unix socket to serve the OTLP relay on, which forwards the node's ateom telemetry to OTEL_EXPORTER_OTLP_ENDPOINT so worker pods need no network path to the collector. Empty disables the relay.") + otlpRelaySocket = pflag.String("otlp-relay-socket", nodepath.AteletOTLPSocketPath(), "Unix socket to serve the OTLP relay on, which forwards the node's ateom telemetry to OTEL_EXPORTER_OTLP_ENDPOINT so worker pods need no network path to the collector. Empty disables the relay.") actorStatsPollInterval = pflag.Duration("actor-stats-poll-interval", time.Minute, fmt.Sprintf("Actor resource utilization sampling frequency. 0 disables the sampling entirely; minimum accepted value is %v.", minActorStatsPollInterval)) @@ -205,7 +206,7 @@ func main() { imageCache, err := imagecache.New(*imageCacheDir, imagecache.WithAuthenticator(gcpRegistryAuthn), imagecache.WithLocalhostRegistryReplacement(*localhostRegistryReplacement), - imagecache.WithActorsDir(ateompath.ActorsDir), + imagecache.WithActorsDir(ateletpath.ActorsDir), imagecache.WithMinAge(*imageCacheMinAge), imagecache.WithMeter(otel.Meter("atelet")), ) @@ -353,15 +354,15 @@ func main() { ateomFacingTLS := tlsCfg.Clone() ateomFacingTLS.VerifyConnection = verifyClientOnSameNode(ateletIdentity) - if err := os.Remove(ateompath.AteomSupportSocket); err != nil && !errors.Is(err, os.ErrNotExist) { + if err := os.Remove(nodepath.AteomSupportSocket); err != nil && !errors.Is(err, os.ErrNotExist) { serverboot.Fatal(ctx, "Failed to remove stale credential broker socket", err) } - ateomFacingLis, err := net.Listen("unix", ateompath.AteomSupportSocket) + ateomFacingLis, err := net.Listen("unix", nodepath.AteomSupportSocket) if err != nil { serverboot.Fatal(ctx, "Failed to listen for credential broker", err) } defer ateomFacingLis.Close() - if err := os.Chmod(ateompath.AteomSupportSocket, 0o600); err != nil { + if err := os.Chmod(nodepath.AteomSupportSocket, 0o600); err != nil { serverboot.Fatal(ctx, "Failed to restrict credential broker socket", err) } @@ -538,6 +539,7 @@ func (s *AteomHerder) Run(ctx context.Context, req *ateletpb.RunRequest) (resp * RuntimeAssetPaths: assetPaths, Spec: spec, ActorUid: actorUID, + ActorDirs: ateletpath.ActorDirs(actorUID), EgressGateway: toAteomEgressGateway(req.GetEgressGateway()), CpuMilli: req.GetCpuMilli(), MemoryBytes: req.GetMemoryBytes(), @@ -630,7 +632,7 @@ func (s *AteomHerder) Checkpoint(ctx context.Context, req *ateletpb.CheckpointRe return nil, err } - checkpointDir := ateompath.CheckpointStateDir(actorUID) + checkpointDir := ateletpath.CheckpointStateDir(actorUID) client, err := s.dialAteom(ctx, req.GetTargetAteomUid()) if err != nil { @@ -656,6 +658,7 @@ func (s *AteomHerder) Checkpoint(ctx context.Context, req *ateletpb.CheckpointRe Spec: spec, Scope: toAteomSnapshotScope(req.GetScope()), ActorUid: actorUID, + ActorDirs: ateletpath.ActorDirs(actorUID), }) dAteom = time.Since(tAteom) if err != nil { @@ -733,7 +736,7 @@ func toAteomSnapshotScope(scope ateletpb.SnapshotScope) ateompb.SnapshotScope { } func (s *AteomHerder) moveLocalCheckpoint(ctx context.Context, req *ateletpb.CheckpointRequest, checkpointDir string, rec *sandboxAssetsRecord) error { - localCheckpointPath := ateompath.LocalSnapshotDir(req.GetActorUid(), req.GetLocalConfig().GetSnapshotName()) + localCheckpointPath := ateletpath.LocalSnapshotDir(req.GetActorUid(), req.GetLocalConfig().GetSnapshotName()) if err := os.MkdirAll(localCheckpointPath, 0o700); err != nil { return fmt.Errorf("while creating local checkpoint directory: %w", err) } @@ -852,7 +855,7 @@ func (s *AteomHerder) UploadPausedCheckpoint(ctx context.Context, req *ateletpb. if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } - localDir := ateompath.LocalSnapshotDir(req.GetActorUid(), req.GetLocalSnapshotName()) + localDir := ateletpath.LocalSnapshotDir(req.GetActorUid(), req.GetLocalSnapshotName()) tPersist := time.Now() sandboxClass, err := s.uploadLocalCheckpointDir(ctx, req, localDir, uri) @@ -936,12 +939,12 @@ func (s *AteomHerder) uploadLocalCheckpointDir(ctx context.Context, req *ateletp func narrowFullCaptureToData(rec *sandboxAssetsRecord) error { switch atev1alpha1.SandboxClass(rec.SandboxClass) { case atev1alpha1.SandboxClassMicroVM, atev1alpha1.SandboxClassGvisor: - if !slices.Contains(rec.SnapshotFiles, ateompath.DurableDirTarFile) { + if !slices.Contains(rec.SnapshotFiles, resources.DurableDirTarFile) { // No durable-dir volumes were attached at pause: this snapshot // holds no data, and never will — not retryable. - return status.Errorf(codes.FailedPrecondition, "full %s capture has no %s; the actor has no durable data to upload as %s", rec.SandboxClass, ateompath.DurableDirTarFile, ateattr.SnapshotScopeData) + return status.Errorf(codes.FailedPrecondition, "full %s capture has no %s; the actor has no durable data to upload as %s", rec.SandboxClass, resources.DurableDirTarFile, ateattr.SnapshotScopeData) } - rec.SnapshotFiles = []string{ateompath.DurableDirTarFile} + rec.SnapshotFiles = []string{resources.DurableDirTarFile} rec.Scope = ateattr.SnapshotScopeData return nil @@ -1006,7 +1009,7 @@ func (s *AteomHerder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) return nil, mountErr } - checkpointDir := ateompath.RestoreStateDir(actorUID) + checkpointDir := ateletpath.RestoreStateDir(actorUID) // The snapshot is self-describing: recover the sandbox binaries that created // it from the manifest stored beside the checkpoint images (the Restore @@ -1038,7 +1041,7 @@ func (s *AteomHerder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) return nil, fmt.Errorf("while unmarshalling sandbox record: %w", err) } case ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL: - manifest, err := os.ReadFile(filepath.Join(ateompath.LocalSnapshotDir(actorUID, req.GetLocalConfig().GetSnapshotName()), sandboxManifestName)) + manifest, err := os.ReadFile(filepath.Join(ateletpath.LocalSnapshotDir(actorUID, req.GetLocalConfig().GetSnapshotName()), sandboxManifestName)) if err != nil { return nil, wrapFileSystemErr("while reading local snapshot manifest", err) } @@ -1142,7 +1145,7 @@ func (s *AteomHerder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) // the golden's from object storage, concurrently. gLocal, gLocalCtx := errgroup.WithContext(gctx) gLocal.Go(func() error { - if err := s.copyLocalCheckpoint(gLocalCtx, req.GetLocalConfig().GetSnapshotName(), ateompath.LocalCheckpointsDir(actorUID), checkpointDir, sandboxRec.SnapshotFiles); err != nil { + if err := s.copyLocalCheckpoint(gLocalCtx, req.GetLocalConfig().GetSnapshotName(), ateletpath.LocalCheckpointsDir(actorUID), checkpointDir, sandboxRec.SnapshotFiles); err != nil { return err } return nil @@ -1218,6 +1221,7 @@ func (s *AteomHerder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) Spec: spec, Scope: toAteomSnapshotScope(req.GetScope()), ActorUid: req.GetActorUid(), + ActorDirs: ateletpath.ActorDirs(actorUID), EgressGateway: toAteomEgressGateway(req.GetEgressGateway()), CpuMilli: req.GetCpuMilli(), MemoryBytes: req.GetMemoryBytes(), @@ -1283,6 +1287,7 @@ func (s *AteomHerder) Terminate(ctx context.Context, req *ateletpb.TerminateRequ ActorTemplateName: req.GetActorTemplateName(), RunscPath: runscPathFor(assetPaths), Spec: spec, + ActorDirs: ateletpath.ActorDirs(actorUID), }); err != nil { if status.Code(err) == codes.NotFound { slog.InfoContext(ctx, "workload not found on ateom during terminate", slog.Any("actor", actorRef), slog.String("actorUID", actorUID)) @@ -1408,7 +1413,7 @@ func (s *AteomHerder) prepareOCIBundles( for _, vol := range spec.GetVolumes() { switch vol.GetSource().(type) { case *ateletpb.Volume_DurableDir: - volPath := ateompath.DurableDirVolumeMountPoint(actorUID, vol.GetName()) + volPath := ateletpath.DurableDirVolumeMountPoint(actorUID, vol.GetName()) if err := os.MkdirAll(volPath, 0o700); err != nil { return fmt.Errorf("while creating %q: %w", volPath, err) } @@ -1428,7 +1433,7 @@ func (s *AteomHerder) prepareOCIBundles( []string{"/pause"}, nil, nil, - ateompath.ActorNetNSPath(actorUID), + nodepath.ActorNetNSPath(actorUID), nil, // pause is sandbox infra; it mounts no volumes. nil, nil, // pause only reaps; it needs no capabilities. @@ -1456,7 +1461,7 @@ func (s *AteomHerder) prepareOCIBundles( ctr.GetCommand(), ctr.GetArgs(), envs, - ateompath.ActorNetNSPath(actorUID), + nodepath.ActorNetNSPath(actorUID), spec.GetVolumes(), ctr.GetVolumeMounts(), resolveCapabilities(ctr.GetSecurityContext().GetCapabilities()), @@ -1595,7 +1600,7 @@ func newAteomDialer(size int) *AteomDialer { // ateomSocketPath resolves a pod UID to the ateom socket atelet dials. A // variable because the real path is rooted at the node's BasePath, which a // test cannot serve on. -var ateomSocketPath = ateompath.AteomSocketPath +var ateomSocketPath = nodepath.AteomSocketPath func (d *AteomDialer) DialAteomPod(ctx context.Context, podUID string) (*grpc.ClientConn, error) { key := podUID @@ -1848,7 +1853,7 @@ func resetActorDirs(actorUID string) error { // making them writable. (The rootfs itself is just an empty mountpoint // here: the overlay is mounted in the ateom pod's mount namespace, not // atelet's, and is detached by ateom at teardown.) - bundleDir := ateompath.OCIBundleDir(actorUID) + bundleDir := ateletpath.OCIBundleDir(actorUID) if err := imagecache.RemoveAllWritable(bundleDir); err != nil { return wrapFileSystemErr("while deleting bundle dir: %w", err) } @@ -1856,7 +1861,7 @@ func resetActorDirs(actorUID string) error { return wrapFileSystemErr("while creating bundle dir: %w", err) } - runscDir := ateompath.RunSCStateDir(actorUID) + runscDir := ateletpath.RunSCStateDir(actorUID) if err := os.RemoveAll(runscDir); err != nil { return wrapFileSystemErr("while deleting runsc state dir: %w", err) } @@ -1864,7 +1869,7 @@ func resetActorDirs(actorUID string) error { return wrapFileSystemErr("while creating runsc state dir: %w", err) } - pidFileDir := ateompath.PIDFileDir(actorUID) + pidFileDir := ateletpath.PIDFileDir(actorUID) if err := os.RemoveAll(pidFileDir); err != nil { return wrapFileSystemErr("while deleting PID file dir: %w", err) } @@ -1872,7 +1877,7 @@ func resetActorDirs(actorUID string) error { return wrapFileSystemErr("while creating PID file dir: %w", err) } - checkpointDir := ateompath.CheckpointStateDir(actorUID) + checkpointDir := ateletpath.CheckpointStateDir(actorUID) if err := os.RemoveAll(checkpointDir); err != nil { return wrapFileSystemErr("while deleting checkpoint-state dir: %w", err) } @@ -1880,7 +1885,7 @@ func resetActorDirs(actorUID string) error { return wrapFileSystemErr("while creating checkpoint-state dir: %w", err) } - restoreStateDir := ateompath.RestoreStateDir(actorUID) + restoreStateDir := ateletpath.RestoreStateDir(actorUID) if err := os.RemoveAll(restoreStateDir); err != nil { return wrapFileSystemErr("while deleting restore-state dir: %w", err) } @@ -1888,7 +1893,7 @@ func resetActorDirs(actorUID string) error { return wrapFileSystemErr("while creating restore-state dir: %w", err) } - durableDirVolumesMountDir := ateompath.DurableDirVolumeMountsDir(actorUID) + durableDirVolumesMountDir := ateletpath.DurableDirVolumeMountsDir(actorUID) if err := os.RemoveAll(durableDirVolumesMountDir); err != nil { return wrapFileSystemErr("while deleting durable-dir volumes mount dir: %w", err) } @@ -1898,7 +1903,7 @@ func resetActorDirs(actorUID string) error { // World-readable (0o755): bind-mounted read-only into the actor, whose // workload reads it through the gofer. - systemInfoVolumeRootsDir := ateompath.SystemInfoVolumeRootsDir(actorUID) + systemInfoVolumeRootsDir := ateletpath.SystemInfoVolumeRootsDir(actorUID) if err := os.RemoveAll(systemInfoVolumeRootsDir); err != nil { return wrapFileSystemErr("while deleting system-info volume roots dir: %w", err) } @@ -1908,7 +1913,7 @@ func resetActorDirs(actorUID string) error { // Do not call RemoveAll on volume directories in case the unmount failed. // We do not want to delete mount content. - volumesDir := ateompath.VolumesDir(actorUID) + volumesDir := ateletpath.VolumesDir(actorUID) entries, err := os.ReadDir(volumesDir) if err != nil && !os.IsNotExist(err) { return wrapFileSystemErr("while reading volumes dir: %w", err) @@ -1937,7 +1942,7 @@ func removeActorDirs(actorUID string) error { if err := resetActorDirs(actorUID); err != nil { return err } - if err := os.RemoveAll(ateompath.ActorPath(actorUID)); err != nil { + if err := os.RemoveAll(ateletpath.ActorPath(actorUID)); err != nil { return wrapFileSystemErr("while deleting actor dir: %w", err) } return nil diff --git a/cmd/atelet/main_test.go b/cmd/atelet/main_test.go index 52e8ed5808..fb90bb3e45 100644 --- a/cmd/atelet/main_test.go +++ b/cmd/atelet/main_test.go @@ -33,9 +33,9 @@ import ( "time" "github.com/agent-substrate/substrate/cmd/atelet/internal/ategcs" + "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" "github.com/agent-substrate/substrate/internal/ateattr" "github.com/agent-substrate/substrate/internal/atelet" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/proto/ateletpb" "github.com/agent-substrate/substrate/internal/proto/ateompb" "github.com/agent-substrate/substrate/internal/resources" @@ -430,14 +430,14 @@ func TestToAteomSnapshotScope(t *testing.T) { // with a nil error, failing the test. StaticFilesDir is redirected to a temp // dir so the planted path is writable and isolated. func TestFetchAssetRejectsBadHash(t *testing.T) { - orig := ateompath.StaticFilesDir - ateompath.StaticFilesDir = t.TempDir() - t.Cleanup(func() { ateompath.StaticFilesDir = orig }) + orig := ateletpath.StaticFilesDir + ateletpath.StaticFilesDir = t.TempDir() + t.Cleanup(func() { ateletpath.StaticFilesDir = orig }) // Invalid (8 chars, not 64) but separator-free, so it resolves to a normal // filename inside the temp StaticFilesDir. const badHash = "deadbeef" - if err := os.WriteFile(ateompath.RunSCBinaryPath(badHash), []byte("planted"), 0o755); err != nil { + if err := os.WriteFile(ateletpath.RunSCBinaryPath(badHash), []byte("planted"), 0o755); err != nil { t.Fatalf("planting cache file: %v", err) } @@ -471,15 +471,15 @@ func (fakeObjectStorage) PutObject(_ context.Context, _, _ string, _ io.Reader) // TestFetchAssetStreaming covers the streamed download: good asset cached, // over-cap rejected, hash mismatch rejected (failures leave no cache file). func TestFetchAssetStreaming(t *testing.T) { - origDir, origCap := ateompath.StaticFilesDir, maxAssetBytes - t.Cleanup(func() { ateompath.StaticFilesDir, maxAssetBytes = origDir, origCap }) + origDir, origCap := ateletpath.StaticFilesDir, maxAssetBytes + t.Cleanup(func() { ateletpath.StaticFilesDir, maxAssetBytes = origDir, origCap }) content := []byte("micro-vm kernel bytes") goodHash := fmt.Sprintf("%x", sha256.Sum256(content)) const url = "gs://test-bucket/asset" t.Run("good asset is cached", func(t *testing.T) { - ateompath.StaticFilesDir = t.TempDir() + ateletpath.StaticFilesDir = t.TempDir() s := &AteomHerder{anonGCSClient: fakeObjectStorage{data: content}} path, err := s.fetchAsset(context.Background(), assetEntry{URL: url, SHA256: goodHash}) if err != nil { @@ -495,20 +495,20 @@ func TestFetchAssetStreaming(t *testing.T) { }) t.Run("over-cap asset rejected, cache not written", func(t *testing.T) { - ateompath.StaticFilesDir = t.TempDir() + ateletpath.StaticFilesDir = t.TempDir() maxAssetBytes = 4 // content is longer than this s := &AteomHerder{anonGCSClient: fakeObjectStorage{data: content}} _, err := s.fetchAsset(context.Background(), assetEntry{URL: url, SHA256: goodHash}) if err == nil { t.Fatal("fetchAsset accepted an over-cap asset") } - if _, err := os.Stat(ateompath.RunSCBinaryPath(goodHash)); !errors.Is(err, os.ErrNotExist) { + if _, err := os.Stat(ateletpath.RunSCBinaryPath(goodHash)); !errors.Is(err, os.ErrNotExist) { t.Errorf("over-cap download left a file at the cache path (stat err = %v)", err) } }) t.Run("hash mismatch rejected, cache not written", func(t *testing.T) { - ateompath.StaticFilesDir = t.TempDir() + ateletpath.StaticFilesDir = t.TempDir() maxAssetBytes = origCap wrongHash := strings.Repeat("a", 64) // valid 64-hex format, wrong value s := &AteomHerder{anonGCSClient: fakeObjectStorage{data: content}} @@ -516,13 +516,13 @@ func TestFetchAssetStreaming(t *testing.T) { if err == nil { t.Fatal("fetchAsset accepted a hash mismatch") } - if _, err := os.Stat(ateompath.RunSCBinaryPath(wrongHash)); !errors.Is(err, os.ErrNotExist) { + if _, err := os.Stat(ateletpath.RunSCBinaryPath(wrongHash)); !errors.Is(err, os.ErrNotExist) { t.Errorf("mismatched download left a file at the cache path (stat err = %v)", err) } }) t.Run("missing object keeps the client's sentinel", func(t *testing.T) { - ateompath.StaticFilesDir = t.TempDir() + ateletpath.StaticFilesDir = t.TempDir() maxAssetBytes = origCap // The ategcs clients tag a missing object with ErrObjectNotFound. notFound := fmt.Errorf("%w: no such object", ategcs.ErrObjectNotFound) @@ -534,7 +534,7 @@ func TestFetchAssetStreaming(t *testing.T) { }) t.Run("malformed url is rejected", func(t *testing.T) { - ateompath.StaticFilesDir = t.TempDir() + ateletpath.StaticFilesDir = t.TempDir() maxAssetBytes = origCap s := &AteomHerder{anonGCSClient: fakeObjectStorage{data: content}} // Invalid percent-escape: url.Parse rejects it inside ategcs.Open. @@ -545,7 +545,7 @@ func TestFetchAssetStreaming(t *testing.T) { }) t.Run("network error keeps its context wrap", func(t *testing.T) { - ateompath.StaticFilesDir = t.TempDir() + ateletpath.StaticFilesDir = t.TempDir() maxAssetBytes = origCap s := &AteomHerder{anonGCSClient: fakeObjectStorage{err: errors.New("connection refused")}} _, err := s.fetchAsset(context.Background(), assetEntry{URL: url, SHA256: goodHash}) @@ -852,7 +852,7 @@ func TestRemoveActorDirsReclaimsTheRoot(t *testing.T) { t.Fatalf("resetActorDirs: %v", err) } // Written at Run and deliberately kept out of the directories reset wipes. - if err := os.WriteFile(ateompath.ActorSandboxAssetsFile(actorUID), []byte("{}"), 0o600); err != nil { + if err := os.WriteFile(ateletpath.ActorSandboxAssetsFile(actorUID), []byte("{}"), 0o600); err != nil { t.Fatalf("writing sandbox assets file: %v", err) } @@ -860,7 +860,7 @@ func TestRemoveActorDirsReclaimsTheRoot(t *testing.T) { t.Fatalf("removeActorDirs: %v", err) } - actorDir := ateompath.ActorPath(actorUID) + actorDir := ateletpath.ActorPath(actorUID) if entries, err := os.ReadDir(actorDir); err == nil { left := make([]string, 0, len(entries)) for _, e := range entries { @@ -880,7 +880,7 @@ func TestRemoveActorDirsKeepsPopulatedVolume(t *testing.T) { useTempNodeDirs(t) const actorUID = "actor-uid-1" - stillMounted := filepath.Join(ateompath.VolumeHostPath(actorUID, "data"), "payload") + stillMounted := filepath.Join(ateletpath.VolumeHostPath(actorUID, "data"), "payload") if err := os.MkdirAll(filepath.Dir(stillMounted), 0o755); err != nil { t.Fatalf("creating volume dir: %v", err) } @@ -1243,7 +1243,7 @@ func TestUploadLocalCheckpointDir(t *testing.T) { return sandboxAssetsRecord{ SandboxClass: class, PauseImage: testPauseImage, - SnapshotFiles: []string{"config.json", "memory-ranges", ateompath.DurableDirTarFile}, + SnapshotFiles: []string{"config.json", "memory-ranges", resources.DurableDirTarFile}, Scope: ateattr.SnapshotScopeFull, } } @@ -1266,7 +1266,7 @@ func TestUploadLocalCheckpointDir(t *testing.T) { s := &AteomHerder{gcsClient: store} dir := filepath.Join(t.TempDir(), "pause-snap-1") writeLocalSnapshot(t, dir, fullRec("microvm"), map[string]string{ - "config.json": "cfg", "memory-ranges": "mem", ateompath.DurableDirTarFile: "data", + "config.json": "cfg", "memory-ranges": "mem", resources.DurableDirTarFile: "data", }) if _, err := s.uploadLocalCheckpointDir(ctx, validUploadPausedCheckpointRequest(), dir, uri); err != nil { @@ -1291,7 +1291,7 @@ func TestUploadLocalCheckpointDir(t *testing.T) { s := &AteomHerder{gcsClient: store} dir := filepath.Join(t.TempDir(), "pause-snap-1") writeLocalSnapshot(t, dir, fullRec("microvm"), map[string]string{ - "config.json": "cfg", "memory-ranges": "mem", ateompath.DurableDirTarFile: "data", + "config.json": "cfg", "memory-ranges": "mem", resources.DurableDirTarFile: "data", }) req := validUploadPausedCheckpointRequest() @@ -1310,7 +1310,7 @@ func TestUploadLocalCheckpointDir(t *testing.T) { if rec.Scope != ateattr.SnapshotScopeData { t.Errorf("uploaded manifest scope = %q, want %q", rec.Scope, ateattr.SnapshotScopeData) } - if want := []string{ateompath.DurableDirTarFile}; !slices.Equal(rec.SnapshotFiles, want) { + if want := []string{resources.DurableDirTarFile}; !slices.Equal(rec.SnapshotFiles, want) { t.Errorf("uploaded manifest files = %v, want %v", rec.SnapshotFiles, want) } }) @@ -1357,9 +1357,9 @@ func TestUploadLocalCheckpointDir(t *testing.T) { writeLocalSnapshot(t, dir, sandboxAssetsRecord{ SandboxClass: "mystery", PauseImage: testPauseImage, - SnapshotFiles: []string{ateompath.DurableDirTarFile}, + SnapshotFiles: []string{resources.DurableDirTarFile}, Scope: ateattr.SnapshotScopeFull, - }, map[string]string{ateompath.DurableDirTarFile: "data"}) + }, map[string]string{resources.DurableDirTarFile: "data"}) req := validUploadPausedCheckpointRequest() req.DesiredScope = ateletpb.SnapshotScope_SNAPSHOT_SCOPE_DATA @@ -1375,9 +1375,9 @@ func TestUploadLocalCheckpointDir(t *testing.T) { writeLocalSnapshot(t, dir, sandboxAssetsRecord{ SandboxClass: "microvm", PauseImage: testPauseImage, - SnapshotFiles: []string{ateompath.DurableDirTarFile}, + SnapshotFiles: []string{resources.DurableDirTarFile}, Scope: ateattr.SnapshotScopeData, - }, map[string]string{ateompath.DurableDirTarFile: "data"}) + }, map[string]string{resources.DurableDirTarFile: "data"}) _, err := s.uploadLocalCheckpointDir(ctx, validUploadPausedCheckpointRequest(), dir, uri) if got := status.Code(err); got != codes.FailedPrecondition { @@ -1392,8 +1392,8 @@ func TestUploadLocalCheckpointDir(t *testing.T) { writeLocalSnapshot(t, dir, sandboxAssetsRecord{ SandboxClass: "microvm", PauseImage: testPauseImage, - SnapshotFiles: []string{ateompath.DurableDirTarFile}, - }, map[string]string{ateompath.DurableDirTarFile: "data"}) + SnapshotFiles: []string{resources.DurableDirTarFile}, + }, map[string]string{resources.DurableDirTarFile: "data"}) req := validUploadPausedCheckpointRequest() req.DesiredScope = ateletpb.SnapshotScope_SNAPSHOT_SCOPE_DATA @@ -1433,7 +1433,7 @@ func TestUploadLocalCheckpointDir(t *testing.T) { s := &AteomHerder{gcsClient: &recordingObjectStorage{putErr: errors.New("boom")}} dir := filepath.Join(t.TempDir(), "pause-snap-1") writeLocalSnapshot(t, dir, fullRec("microvm"), map[string]string{ - "config.json": "cfg", "memory-ranges": "mem", ateompath.DurableDirTarFile: "data", + "config.json": "cfg", "memory-ranges": "mem", resources.DurableDirTarFile: "data", }) _, err := s.uploadLocalCheckpointDir(ctx, validUploadPausedCheckpointRequest(), dir, uri) diff --git a/cmd/atelet/oci.go b/cmd/atelet/oci.go index 879907c3af..f82c279d7c 100644 --- a/cmd/atelet/oci.go +++ b/cmd/atelet/oci.go @@ -22,7 +22,7 @@ import ( "sort" "strings" - "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" "github.com/agent-substrate/substrate/internal/imagecache" "github.com/agent-substrate/substrate/internal/ocispec" "github.com/agent-substrate/substrate/internal/proto/ateletpb" @@ -82,7 +82,7 @@ func prepareOCIDirectory(ctx context.Context, imageCache *imagecache.Store, acto span.SetAttributes(attribute.String("image", ref)) defer span.End() - bundlePath := ateompath.OCIBundlePath(actorUID, containerName) + bundlePath := ateletpath.OCIBundlePath(actorUID, containerName) // Clear any previous bundle contents (belt and suspenders: resetActorDirs // already wiped the bundle dir on the Run/Restore path). @@ -149,15 +149,17 @@ func prepareOCIDirectory(ctx context.Context, imageCache *imagecache.Store, acto // Write the runtime-neutral OCI spec to config.json. if err := ocispec.Save(bundlePath, ocispec.Build(ocispec.Options{ - ActorUID: actorUID, - ContainerName: containerName, - Args: resolvedArgs, - Env: resolvedEnv, - NetNSPath: netns, - Volumes: volumes, - VolumeMounts: volumeMounts, - Capabilities: capabilities, - Resources: resources, + Args: resolvedArgs, + Env: resolvedEnv, + NetNSPath: netns, + Volumes: volumes, + VolumeMounts: volumeMounts, + Capabilities: capabilities, + Resources: resources, + DurableDirVolumeMountsDir: ateletpath.DurableDirVolumeMountsDir(actorUID), + VolumesDir: ateletpath.VolumesDir(actorUID), + SystemInfoVolumeRootsDir: ateletpath.SystemInfoVolumeRootsDir(actorUID), + BundlePath: bundlePath, })); err != nil { return fmt.Errorf("while writing OCI spec: %w", err) } diff --git a/cmd/atelet/sandbox_assets.go b/cmd/atelet/sandbox_assets.go index 46c2324095..f1c741bed1 100644 --- a/cmd/atelet/sandbox_assets.go +++ b/cmd/atelet/sandbox_assets.go @@ -38,7 +38,7 @@ import ( "time" "github.com/agent-substrate/substrate/cmd/atelet/internal/ategcs" - "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" "github.com/agent-substrate/substrate/internal/proto/ateletpb" "github.com/agent-substrate/substrate/internal/resources" ) @@ -136,7 +136,7 @@ func recordFromRequest(sa *ateletpb.SandboxAssets) (*sandboxAssetsRecord, error) // Assets are cached, so re-fetching at Checkpoint/Restore is a no-op once // present. func (s *AteomHerder) ensureSandboxAssets(ctx context.Context, rec *sandboxAssetsRecord) (map[string]string, error) { - if err := os.MkdirAll(ateompath.StaticFilesDir, 0o700); err != nil { + if err := os.MkdirAll(ateletpath.StaticFilesDir, 0o700); err != nil { return nil, fmt.Errorf("while creating static files dir: %w", err) } paths := make(map[string]string, len(rec.Assets)) @@ -173,7 +173,7 @@ func (s *AteomHerder) fetchAsset(ctx context.Context, entry assetEntry) (string, return "", wrapFileSystemErr("while validating asset hash", err) } - localPath := ateompath.RunSCBinaryPath(entry.SHA256) + localPath := ateletpath.RunSCBinaryPath(entry.SHA256) _, err := os.Stat(localPath) if err == nil { slog.DebugContext(ctx, "Sandbox asset cache hit", slog.String("path", localPath)) @@ -210,7 +210,7 @@ func (s *AteomHerder) fetchGVisorRelease(ctx context.Context, entry assetEntry) return "", wrapFileSystemErr("while validating asset hash", err) } - releaseDir := ateompath.GVisorReleaseDir(entry.SHA256) + releaseDir := ateletpath.GVisorReleaseDir(entry.SHA256) runscPath := filepath.Join(releaseDir, "runsc") _, err := os.Stat(releaseDir) if err == nil { @@ -229,7 +229,7 @@ func (s *AteomHerder) fetchGVisorRelease(ctx context.Context, entry assetEntry) defer os.Remove(tarball) slog.InfoContext(ctx, "gVisor release download complete", slog.String("url", entry.URL), slog.Duration("duration", time.Since(tDownload))) - tmpDir, err := os.MkdirTemp(ateompath.StaticFilesDir, filepath.Base(releaseDir)+"-extract-") + tmpDir, err := os.MkdirTemp(ateletpath.StaticFilesDir, filepath.Base(releaseDir)+"-extract-") if err != nil { return "", wrapFileSystemErr("while creating extraction dir", err) } @@ -281,7 +281,7 @@ func (s *AteomHerder) downloadVerified(ctx context.Context, entry assetEntry, tm return "", fmt.Errorf("while parsing sha256 hash: %w", err) } - tmpFile, err := os.CreateTemp(ateompath.StaticFilesDir, tmpPrefix) + tmpFile, err := os.CreateTemp(ateletpath.StaticFilesDir, tmpPrefix) if err != nil { return "", wrapFileSystemErr("while creating temp file", err) } @@ -457,7 +457,7 @@ func writeSandboxRecord(actorUID string, rec *sandboxAssetsRecord) error { if err != nil { return wrapFileSystemErr("while marshaling sandbox record", err) } - path := ateompath.ActorSandboxAssetsFile(actorUID) + path := ateletpath.ActorSandboxAssetsFile(actorUID) if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return wrapFileSystemErr("while creating actor dir", err) } @@ -470,7 +470,7 @@ func writeSandboxRecord(actorUID string, rec *sandboxAssetsRecord) error { // readSandboxRecord loads the actor's on-node sandbox record written at // Run/Restore. func readSandboxRecord(actorUID string) (*sandboxAssetsRecord, error) { - path := ateompath.ActorSandboxAssetsFile(actorUID) + path := ateletpath.ActorSandboxAssetsFile(actorUID) data, err := os.ReadFile(path) if err != nil { return nil, wrapFileSystemErr("while reading sandbox record", err) diff --git a/cmd/atelet/sandbox_prewarm_test.go b/cmd/atelet/sandbox_prewarm_test.go index 71030d34d3..d3c5db0d03 100644 --- a/cmd/atelet/sandbox_prewarm_test.go +++ b/cmd/atelet/sandbox_prewarm_test.go @@ -31,7 +31,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/cache" - "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" "github.com/agent-substrate/substrate/internal/imagecache" "github.com/agent-substrate/substrate/pkg/api/v1alpha1" "github.com/agent-substrate/substrate/pkg/client/clientset/versioned/fake" @@ -162,9 +162,9 @@ func TestPrewarmEnqueueFilters(t *testing.T) { // prewarm is requeued with backoff, and a config deleted between enqueue and // processing is forgotten without retries. func TestPrewarmProcessRetries(t *testing.T) { - origDir := ateompath.StaticFilesDir - ateompath.StaticFilesDir = t.TempDir() - t.Cleanup(func() { ateompath.StaticFilesDir = origDir }) + origDir := ateletpath.StaticFilesDir + ateletpath.StaticFilesDir = t.TempDir() + t.Cleanup(func() { ateletpath.StaticFilesDir = origDir }) ctx := context.Background() cfg := gvisorConfig("gvisor-default", "gs://bucket/runsc", fmt.Sprintf("%x", sha256.Sum256([]byte("runsc")))) @@ -196,9 +196,9 @@ func TestPrewarmProcessRetries(t *testing.T) { // filter as gVisor may be micro-VM by the time the worker resolves it, and a // node without KVM must skip it rather than download guest assets. func TestPrewarmProcessReappliesClassGate(t *testing.T) { - origDir := ateompath.StaticFilesDir - ateompath.StaticFilesDir = t.TempDir() - t.Cleanup(func() { ateompath.StaticFilesDir = origDir }) + origDir := ateletpath.StaticFilesDir + ateletpath.StaticFilesDir = t.TempDir() + t.Cleanup(func() { ateletpath.StaticFilesDir = origDir }) ctx := context.Background() // Enqueued while gVisor, edited to micro-VM before the worker ran. @@ -242,9 +242,9 @@ func TestMicrovmNodeCapable(t *testing.T) { // lands in the image cache, and an asset fetch failure does not stop the // pull (the two live in different backends). func TestPrewarmPauseImage(t *testing.T) { - origDir := ateompath.StaticFilesDir - ateompath.StaticFilesDir = t.TempDir() - t.Cleanup(func() { ateompath.StaticFilesDir = origDir }) + origDir := ateletpath.StaticFilesDir + ateletpath.StaticFilesDir = t.TempDir() + t.Cleanup(func() { ateletpath.StaticFilesDir = origDir }) srv := httptest.NewServer(registry.New(registry.Logger(log.New(io.Discard, "", 0)))) defer srv.Close() @@ -328,10 +328,10 @@ func (hangingObjectStorage) PutObject(_ context.Context, _, _ string, _ io.Reade // prewarmTimeout: the queue has one worker, so an attempt that never returned // would block every other config's prewarm. func TestPrewarmTimeout(t *testing.T) { - origDir, origTimeout := ateompath.StaticFilesDir, prewarmTimeout - ateompath.StaticFilesDir = t.TempDir() + origDir, origTimeout := ateletpath.StaticFilesDir, prewarmTimeout + ateletpath.StaticFilesDir = t.TempDir() prewarmTimeout = 50 * time.Millisecond - t.Cleanup(func() { ateompath.StaticFilesDir, prewarmTimeout = origDir, origTimeout }) + t.Cleanup(func() { ateletpath.StaticFilesDir, prewarmTimeout = origDir, origTimeout }) cfg := gvisorConfig("gvisor-default", "gs://bucket/runsc", fmt.Sprintf("%x", sha256.Sum256([]byte("hung runsc")))) cfg.Spec.PauseImage = "" @@ -353,10 +353,10 @@ func TestPrewarmTimeout(t *testing.T) { // fake clientset flows through the informer into the prewarm worker, which // lands the asset in the static-files cache without any Run/Restore request. func TestSandboxAssetPrewarmDownloads(t *testing.T) { - origDir, origJitter := ateompath.StaticFilesDir, prewarmMaxJitter - ateompath.StaticFilesDir = t.TempDir() + origDir, origJitter := ateletpath.StaticFilesDir, prewarmMaxJitter + ateletpath.StaticFilesDir = t.TempDir() prewarmMaxJitter = 0 - t.Cleanup(func() { ateompath.StaticFilesDir, prewarmMaxJitter = origDir, origJitter }) + t.Cleanup(func() { ateletpath.StaticFilesDir, prewarmMaxJitter = origDir, origJitter }) host := imageVolumeTestRegistry(t) pauseRef := host + "/pause:3.10" @@ -387,7 +387,7 @@ func TestSandboxAssetPrewarmDownloads(t *testing.T) { defer close(stopCh) factory.Start(stopCh) - wantPath := ateompath.RunSCBinaryPath(sha) + wantPath := ateletpath.RunSCBinaryPath(sha) deadline := time.Now().Add(10 * time.Second) for { if _, err := os.Stat(wantPath); err == nil { diff --git a/cmd/atelet/statspoller.go b/cmd/atelet/statspoller.go index cfee8efd83..cfb4be00cd 100644 --- a/cmd/atelet/statspoller.go +++ b/cmd/atelet/statspoller.go @@ -37,7 +37,7 @@ import ( "k8s.io/client-go/kubernetes" "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/internal/nodepath" "github.com/agent-substrate/substrate/internal/proto/ateompb" ) @@ -107,7 +107,7 @@ type statsPoller struct { interval time.Duration // ateomsDir is the directory whose entries are worker pod UIDs - // (ateompath.AteomsDir on a real node; a fixture in tests). + // (nodepath.AteomsDir on a real node; a fixture in tests). ateomsDir string // dial returns a stats client for one ateom plus the closer that releases @@ -598,7 +598,7 @@ func startStatsPoller(ctx context.Context, interval time.Duration, inst *statsIn poller := &statsPoller{ interval: interval, - ateomsDir: ateompath.AteomsDir(), + ateomsDir: nodepath.AteomsDir(), dial: func(_ context.Context, podUID string) (activeStatsClient, io.Closer, error) { conn, closer, err := dialAteomStats(podUID) if err != nil { @@ -625,7 +625,7 @@ func startStatsPoller(ctx context.Context, interval time.Duration, inst *statsIn // the actual connect inside the RPC. func dialAteomStats(podUID string) (*grpc.ClientConn, io.Closer, error) { conn, err := grpc.NewClient( - "unix://"+ateompath.AteomSocketPath(podUID), + "unix://"+nodepath.AteomSocketPath(podUID), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithStatsHandler(otelgrpc.NewClientHandler()), ) diff --git a/cmd/atelet/systeminfovolume.go b/cmd/atelet/systeminfovolume.go index 08516d6a83..317fe403ac 100644 --- a/cmd/atelet/systeminfovolume.go +++ b/cmd/atelet/systeminfovolume.go @@ -26,7 +26,7 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" "github.com/agent-substrate/substrate/internal/pemutil" "github.com/agent-substrate/substrate/internal/proto/ateletpb" "github.com/agent-substrate/substrate/internal/resources" @@ -56,7 +56,7 @@ func systemInfoVolumesFor(actorUID string, spec *ateletpb.WorkloadSpec) []*syste if si := vol.GetSystemInfo(); si != nil { volumes = append(volumes, &systemInfoVolume{ Name: vol.GetName(), - Root: ateompath.SystemInfoVolumeRoot(actorUID, vol.GetName()), + Root: ateletpath.SystemInfoVolumeRoot(actorUID, vol.GetName()), Spec: si, }) } diff --git a/cmd/atelet/systeminfovolume_test.go b/cmd/atelet/systeminfovolume_test.go index 3ff2953a2f..e45a2d788c 100644 --- a/cmd/atelet/systeminfovolume_test.go +++ b/cmd/atelet/systeminfovolume_test.go @@ -25,7 +25,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" "github.com/agent-substrate/substrate/internal/proto/ateletpb" "github.com/agent-substrate/substrate/internal/resources" certsv1beta1 "k8s.io/api/certificates/v1beta1" @@ -574,7 +574,7 @@ func TestSystemInfoVolumesFor(t *testing.T) { t.Fatalf("systemInfoVolumesFor = %+v, want the two system-info volumes in spec order", got) } for _, v := range got { - if want := ateompath.SystemInfoVolumeRoot("uid-1", v.Name); v.Root != want || v.Spec == nil { + if want := ateletpath.SystemInfoVolumeRoot("uid-1", v.Name); v.Root != want || v.Spec == nil { t.Errorf("volume %q: root %q spec %v, want root %q and a spec", v.Name, v.Root, v.Spec, want) } } diff --git a/cmd/atelet/volumes.go b/cmd/atelet/volumes.go index 2a1e54cefb..b2698d7004 100644 --- a/cmd/atelet/volumes.go +++ b/cmd/atelet/volumes.go @@ -21,7 +21,7 @@ import ( "log/slog" "os" - "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" "github.com/agent-substrate/substrate/internal/proto/ateletpb" "github.com/agent-substrate/substrate/internal/volume" "github.com/agent-substrate/substrate/internal/volume/csi" @@ -35,7 +35,7 @@ func (s *AteomHerder) mountExternalVolumes(ctx context.Context, actorUID string, if ext == nil { continue } - hostPath := ateompath.VolumeHostPath(actorUID, vol.GetName()) + hostPath := ateletpath.VolumeHostPath(actorUID, vol.GetName()) if err := os.MkdirAll(hostPath, 0o750); err != nil { return fmt.Errorf("failed to create mount point %q: %w", hostPath, err) } @@ -58,7 +58,7 @@ func (s *AteomHerder) unmountExternalVolumes(ctx context.Context, actorUID strin if ext == nil { continue } - hostPath := ateompath.VolumeHostPath(actorUID, vol.GetName()) + hostPath := ateletpath.VolumeHostPath(actorUID, vol.GetName()) slog.InfoContext(ctx, "Unmounting volume", slog.String("volume_id", ext.GetStorageVolumeId()), slog.String("host_path", hostPath), slog.String("volume_type", ext.GetVolumeType())) // TODO: Standardize volume plugin lookup and error handling across control plane // and worker plane (e.g. via a shared helper). diff --git a/cmd/ateom-gvisor/durable.go b/cmd/ateom-gvisor/durable.go index 7e713c0c89..b58ab29169 100644 --- a/cmd/ateom-gvisor/durable.go +++ b/cmd/ateom-gvisor/durable.go @@ -23,17 +23,17 @@ import ( "path/filepath" "strings" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/proto/ateompb" + "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/internal/tarutil" ) // durableTarFile is the snapshot file holding the tar of the actor's durable-dir // volumes. Its entries are /... relative to -// ateompath.DurableDirVolumeMountsDir, so extraction restores the same layout. +// ActorDirs.durable_dir_volume_mounts_dir, so extraction restores the same layout. // The name is shared with atelet, which uses it to carve durable data out of a // FULL snapshot's file set when uploading a paused checkpoint as DATA. -const durableTarFile = ateompath.DurableDirTarFile +const durableTarFile = resources.DurableDirTarFile // hasDurableVolumes reports whether any container mounts a durable-dir volume. func hasDurableVolumes(containers []*ateompb.Container) bool { diff --git a/cmd/ateom-gvisor/main.go b/cmd/ateom-gvisor/main.go index a44424f5c2..e33bc63383 100644 --- a/cmd/ateom-gvisor/main.go +++ b/cmd/ateom-gvisor/main.go @@ -39,13 +39,13 @@ import ( "github.com/agent-substrate/substrate/internal/ateinterceptors" "github.com/agent-substrate/substrate/internal/ateomcapacity" "github.com/agent-substrate/substrate/internal/ateomnet" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/ateomstats" "github.com/agent-substrate/substrate/internal/atunnel" "github.com/agent-substrate/substrate/internal/childreap" "github.com/agent-substrate/substrate/internal/contextlogging" "github.com/agent-substrate/substrate/internal/imagecache" "github.com/agent-substrate/substrate/internal/installdefaults" + "github.com/agent-substrate/substrate/internal/nodepath" "github.com/agent-substrate/substrate/internal/ocispec" "github.com/agent-substrate/substrate/internal/otlprelay" "github.com/agent-substrate/substrate/internal/proto/ateompb" @@ -61,6 +61,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/reflection" "google.golang.org/grpc/status" + "k8s.io/apimachinery/pkg/util/validation/field" ) var ( @@ -83,7 +84,7 @@ var ( showVersion = pflag.Bool("version", false, "Print version and exit.") logLevelFlag = pflag.String("log-level", "info", "Minimum log level: debug, info, warn, or error.") - otlpRelaySocket = pflag.String("otlp-relay-socket", ateompath.AteletOTLPSocketPath(), + otlpRelaySocket = pflag.String("otlp-relay-socket", nodepath.AteletOTLPSocketPath(), "Unix socket of atelet's OTLP relay to export telemetry through, keeping it off the pod network. Empty, or absent at startup, exports directly to OTEL_EXPORTER_OTLP_ENDPOINT instead.") // reaper collects children orphaned in the pod PID namespace. @@ -171,7 +172,7 @@ func do(ctx context.Context) error { defer serverboot.ShutdownProvider("MeterProvider", mp.Shutdown) // Create ateom dir - ateomDir := ateompath.AteomPath(*podUID) + ateomDir := nodepath.AteomPath(*podUID) if err := resources.ValidateAteomUID(*podUID); err != nil { return fmt.Errorf("in resources.ValidateAteomUID: %w", err) } @@ -195,7 +196,7 @@ func do(ctx context.Context) error { slog.InfoContext(ctx, "Child process reaper launched") // Clean up any old socket. - sockPath := ateompath.AteomSocketPath(*podUID) + sockPath := nodepath.AteomSocketPath(*podUID) if err := os.RemoveAll(sockPath); err != nil { return fmt.Errorf("while removing %q: %w", sockPath, err) } @@ -261,7 +262,7 @@ func do(ctx context.Context) error { // that reaches here is a misconfiguration no restart-in-place will fix. go func() { err := ateomcapacity.Report(ctx, ateomcapacity.ReportConfig{ - SocketPath: ateompath.AteomSupportSocket, + SocketPath: nodepath.AteomSupportSocket, CredentialBundlePath: *workerCredentialBundle, TrustBundlePath: *podIdentityTrustBundle, AteletSPIFFEID: *ateletIdentity, @@ -380,6 +381,9 @@ type AteomService struct { // sandbox is the network of the actor this worker is serving. sandbox ateomnet.SessionHolder + // resolvConf is that actor's resolv.conf, removed with its network. + // Guarded by lock. + resolvConf string actorLogger *actorlog.ActorLogger atunnelIngress *atunnel.Server @@ -644,7 +648,18 @@ func containerNames(containers []*ateompb.Container) []string { return names } +// validateActorDirs rejects a request whose actor directories are unusable. +func validateActorDirs(actorDirs *ateompb.ActorDirs) error { + if errs := resources.ValidateActorDirs(actorDirs, field.NewPath("actor_dirs")); len(errs) > 0 { + return status.Error(codes.InvalidArgument, errs.ToAggregate().Error()) + } + return nil +} + func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkloadRequest) (resp *ateompb.RunWorkloadResponse, retErr error) { + if err := validateActorDirs(req.GetActorDirs()); err != nil { + return nil, err + } s.lock.Lock() defer s.lock.Unlock() if err := s.rejectIfDraining(); err != nil { @@ -677,7 +692,7 @@ func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkload if err != nil { return nil, err } - if err := s.prepareSandboxNetwork(ctx, req.GetActorUid()); err != nil { + if err := s.prepareSandboxNetwork(ctx, req.GetActorUid(), req.GetActorDirs()); err != nil { // Cleared here as well as in the deferred cleanup below, because that // defer is not registered until after this check. s.activeActor.Store(nil) @@ -686,6 +701,7 @@ func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkload rcmd := &runsc{ path: req.GetRunscPath(), actorUID: req.GetActorUid(), + actorDirs: req.GetActorDirs(), size: sizing.FromLimits(req.GetCpuMilli(), req.GetMemoryBytes()), durableVolumes: durableVolumeNames(req.GetSpec()), } @@ -703,7 +719,7 @@ func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkload // mounted, mirroring the post-checkpoint cleanup — otherwise they // linger in this namespace until atelet wipes the bundle dirs. // Run before the network cleanup. - if err := imagecache.UnmountAllUnder(ateompath.OCIBundleDir(req.GetActorUid())); err != nil { + if err := imagecache.UnmountAllUnder(req.GetActorDirs().GetOciBundleDir()); err != nil { slog.WarnContext(ctx, "Failed to unmount bundle rootfs overlays after Run failure", "actorUID", req.GetActorUid(), "err", err) } @@ -717,7 +733,7 @@ func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkload // upper — because mounting is ateom's job (atelet runs with no // capabilities); runsc's gofer resolves the mount in this pod's mount // namespace. - if err := imagecache.SetupBundleRootfs(ateompath.OCIBundlePath(req.GetActorUid(), ocispec.PauseContainer)); err != nil { + if err := imagecache.SetupBundleRootfs(rcmd.bundle(ocispec.PauseContainer)); err != nil { return nil, fmt.Errorf("while composing pause rootfs: %w", err) } containersToDelete = append(containersToDelete, ocispec.PauseContainer) @@ -736,7 +752,7 @@ func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkload return nil, fmt.Errorf("while starting json log pipe for %q: %w", ac.GetName(), err) } defer pw.Close() - if err := imagecache.SetupBundleRootfs(ateompath.OCIBundlePath(req.GetActorUid(), ac.GetName())); err != nil { + if err := imagecache.SetupBundleRootfs(rcmd.bundle(ac.GetName())); err != nil { return nil, fmt.Errorf("while composing %q rootfs: %w", ac.GetName(), err) } containersToDelete = append(containersToDelete, ac.GetName()) @@ -765,6 +781,9 @@ func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkload // Allow checkpointing even if the pod is shutting down. This will allow actors // (or the harness) to suspend on shutdown. func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.CheckpointWorkloadRequest) (*ateompb.CheckpointWorkloadResponse, error) { + if err := validateActorDirs(req.GetActorDirs()); err != nil { + return nil, err + } s.lock.Lock() defer s.lock.Unlock() @@ -787,11 +806,12 @@ func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.Chec // Checkpoint only saves state; no sizing is applied, so size is left zero. rcmd := &runsc{ - path: req.GetRunscPath(), - actorUID: req.GetActorUid(), + path: req.GetRunscPath(), + actorUID: req.GetActorUid(), + actorDirs: req.GetActorDirs(), } - checkpointPath := ateompath.CheckpointStateDir(req.GetActorUid()) + checkpointPath := req.GetActorDirs().GetCheckpointDir() if err := os.MkdirAll(checkpointPath, 0o700); err != nil { return nil, fmt.Errorf("while creating checkpoint directory: %w", err) } @@ -806,7 +826,7 @@ func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.Chec if err := rcmd.cmdPause(ctx, ocispec.PauseContainer); err != nil { return nil, fmt.Errorf("while pausing pause container: %w", err) } - tarErr := tarDurableVolumes(ctx, ateompath.DurableDirVolumeMountsDir(req.GetActorUid()), checkpointPath) + tarErr := tarDurableVolumes(ctx, req.GetActorDirs().GetDurableDirVolumeMountsDir(), checkpointPath) // Undoing our own pause must not depend on the caller's context: // tarutil does not check ctx, so a deadline expiring mid-tar would // fail the resume instantly and leave the sandbox paused forever. @@ -825,7 +845,7 @@ func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.Chec return nil, fmt.Errorf("while checkpointing pause: %w", err) } if hasDurableVolumes(req.GetSpec().GetContainers()) { - if err := tarDurableVolumes(ctx, ateompath.DurableDirVolumeMountsDir(req.GetActorUid()), checkpointPath); err != nil { + if err := tarDurableVolumes(ctx, req.GetActorDirs().GetDurableDirVolumeMountsDir(), checkpointPath); err != nil { return nil, fmt.Errorf("while archiving durable-dir volumes: %w", err) } } @@ -848,7 +868,7 @@ func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.Chec // Cleanup the containers after checkpointing. // This is best-effort cleanup for actor containers that may have been left behind after checkpointing. - if err := s.terminateWorkload(ctx, attribution.Ref, attribution.UID, req.GetRunscPath(), req.GetSpec().GetContainers()); err != nil { + if err := s.terminateWorkload(ctx, attribution.Ref, attribution.UID, req.GetRunscPath(), req.GetActorDirs(), req.GetSpec().GetContainers()); err != nil { slog.WarnContext(ctx, "failed to terminate workload after checkpoint", slog.String("actor", attribution.Ref.String()), slog.String("actorUID", attribution.UID), @@ -941,6 +961,9 @@ func isContainerAlreadyGone(ctx context.Context, rcmd containerRuntime, name str } func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.RestoreWorkloadRequest) (resp *ateompb.RestoreWorkloadResponse, retErr error) { + if err := validateActorDirs(req.GetActorDirs()); err != nil { + return nil, err + } s.lock.Lock() defer s.lock.Unlock() if err := s.rejectIfDraining(); err != nil { @@ -972,7 +995,7 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore if err != nil { return nil, err } - if err := s.prepareSandboxNetwork(ctx, req.GetActorUid()); err != nil { + if err := s.prepareSandboxNetwork(ctx, req.GetActorUid(), req.GetActorDirs()); err != nil { // Same as the Run path: the defer below is not registered yet. s.activeActor.Store(nil) return nil, err @@ -980,6 +1003,7 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore rcmd := &runsc{ path: req.GetRunscPath(), actorUID: req.GetActorUid(), + actorDirs: req.GetActorDirs(), size: sizing.FromLimits(req.GetCpuMilli(), req.GetMemoryBytes()), durableVolumes: durableVolumeNames(req.GetSpec()), } @@ -994,7 +1018,7 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore } deleteContainers(cleanupCtx, rcmd, containersToDelete, "Restore") // Same overlay detach as the Run-failure path above. - if err := imagecache.UnmountAllUnder(ateompath.OCIBundleDir(req.GetActorUid())); err != nil { + if err := imagecache.UnmountAllUnder(req.GetActorDirs().GetOciBundleDir()); err != nil { slog.WarnContext(ctx, "Failed to unmount bundle rootfs overlays after Restore failure", "actorUID", req.GetActorUid(), "err", err) } @@ -1003,17 +1027,17 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore } } }() - checkpointDir := ateompath.RestoreStateDir(req.GetActorUid()) + checkpointDir := req.GetActorDirs().GetRestoreDir() if hasDurableVolumes(req.GetSpec().GetContainers()) { - if err := untarDurableVolumes(ateompath.DurableDirVolumeMountsDir(req.GetActorUid()), checkpointDir); err != nil { + if err := untarDurableVolumes(req.GetActorDirs().GetDurableDirVolumeMountsDir(), checkpointDir); err != nil { return nil, fmt.Errorf("while restoring durable-dir volumes: %w", err) } } // Compose the pause rootfs before create (see RunWorkload). runsc restore // only needs the rootfs to hold the correct content; whether it came from // an untar or an overlay of cached layers is transparent to it. - if err := imagecache.SetupBundleRootfs(ateompath.OCIBundlePath(req.GetActorUid(), ocispec.PauseContainer)); err != nil { + if err := imagecache.SetupBundleRootfs(rcmd.bundle(ocispec.PauseContainer)); err != nil { return nil, fmt.Errorf("while composing pause rootfs: %w", err) } @@ -1048,7 +1072,7 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore return nil, fmt.Errorf("while starting json log pipe for %q: %w", ac.GetName(), err) } defer pw.Close() - if err := imagecache.SetupBundleRootfs(ateompath.OCIBundlePath(req.GetActorUid(), ac.GetName())); err != nil { + if err := imagecache.SetupBundleRootfs(rcmd.bundle(ac.GetName())); err != nil { return nil, fmt.Errorf("while composing %q rootfs: %w", ac.GetName(), err) } switch req.GetScope() { @@ -1107,7 +1131,7 @@ func (s *AteomService) prepareActorEgress(ctx context.Context, actorAtespace, ac return nil, fmt.Errorf("invalid egress gateway address %q: %w", gateway.GetAddress(), err) } certificateSource, err := atunnel.NewBrokerCertificateSource(atunnel.BrokerConfig{ - SocketPath: ateompath.AteomSupportSocket, + SocketPath: nodepath.AteomSupportSocket, CredentialBundlePath: s.workerCredentialBundlePath, TrustBundlePath: s.podIdentityTrustBundlePath, ActorAtespace: actorAtespace, @@ -1137,6 +1161,9 @@ func (s *AteomService) prepareActorEgress(ctx context.Context, actorAtespace, ac } func (s *AteomService) TerminateWorkload(ctx context.Context, req *ateompb.TerminateWorkloadRequest) (*ateompb.TerminateWorkloadResponse, error) { + if err := validateActorDirs(req.GetActorDirs()); err != nil { + return nil, err + } s.lock.Lock() defer s.lock.Unlock() @@ -1144,7 +1171,7 @@ func (s *AteomService) TerminateWorkload(ctx context.Context, req *ateompb.Termi attribution := ateomstats.ActorAttributionFromRequest(req) - if err := s.terminateWorkload(ctx, attribution.Ref, attribution.UID, req.GetRunscPath(), req.GetSpec().GetContainers()); err != nil { + if err := s.terminateWorkload(ctx, attribution.Ref, attribution.UID, req.GetRunscPath(), req.GetActorDirs(), req.GetSpec().GetContainers()); err != nil { return nil, fmt.Errorf("failed to terminate workload: %w", err) } @@ -1154,15 +1181,16 @@ func (s *AteomService) TerminateWorkload(ctx context.Context, req *ateompb.Termi return &ateompb.TerminateWorkloadResponse{}, nil } -func (s *AteomService) terminateWorkload(ctx context.Context, actorRef resources.ActorRef, actorUID, runscPath string, containers []*ateompb.Container) error { +func (s *AteomService) terminateWorkload(ctx context.Context, actorRef resources.ActorRef, actorUID, runscPath string, actorDirs *ateompb.ActorDirs, containers []*ateompb.Container) error { var errs []error if err := s.deactivateActorNetworking(ctx); err != nil { errs = append(errs, fmt.Errorf("while deactivating actor networking: %w", err)) } rcmd := &runsc{ - path: runscPath, - actorUID: actorUID, + path: runscPath, + actorUID: actorUID, + actorDirs: actorDirs, } // Detached from the caller: a deadline mid-`runsc delete` would leave the @@ -1182,7 +1210,7 @@ func (s *AteomService) terminateWorkload(ctx context.Context, actorRef resources // (deleting a bundle out from under a live mount in this namespace would // leave the mount orphaned until the pod restarts). Best-effort, same as // the container cleanup above. - if err := imagecache.UnmountAllUnder(ateompath.OCIBundleDir(actorUID)); err != nil { + if err := imagecache.UnmountAllUnder(actorDirs.GetOciBundleDir()); err != nil { errs = append(errs, fmt.Errorf("while unmounting bundle rootfs overlays: %w", err)) } diff --git a/cmd/ateom-gvisor/runsc.go b/cmd/ateom-gvisor/runsc.go index bbf9e13ed5..116ecb6aee 100644 --- a/cmd/ateom-gvisor/runsc.go +++ b/cmd/ateom-gvisor/runsc.go @@ -25,11 +25,11 @@ import ( "log/slog" "os" "os/exec" + "path/filepath" "slices" "strings" "syscall" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/ocispec" "github.com/agent-substrate/substrate/internal/proto/ateompb" "github.com/agent-substrate/substrate/internal/sizing" @@ -38,6 +38,8 @@ import ( type runsc struct { path string actorUID string + // actorDirs are the actor's directories, from the request. + actorDirs *ateompb.ActorDirs // size is the actor's declared limits. size sizing.SandboxSize // durableVolumes are the durable-dir volume names declared to the sandbox. @@ -57,9 +59,19 @@ func durableVolumeNames(spec *ateompb.WorkloadSpec) []string { return slices.Compact(names) } +// bundle is the container's OCI bundle. +func (r *runsc) bundle(containerName string) string { + return filepath.Join(r.actorDirs.GetOciBundleDir(), containerName) +} + +// pidFile is where runsc writes the container's pid. +func (r *runsc) pidFile(containerName string) string { + return filepath.Join(r.actorDirs.GetPidFileDir(), containerName+".pid") +} + // shapeSpec loads, shapes for gVisor, and saves the container's OCI spec. func (r *runsc) shapeSpec(containerName string) error { - bundle := ateompath.OCIBundlePath(r.actorUID, containerName) + bundle := r.bundle(containerName) spec, err := ocispec.Load(bundle) if err != nil { return err @@ -69,7 +81,7 @@ func (r *runsc) shapeSpec(containerName string) error { ContainerName: containerName, DurableVolumes: r.durableVolumes, Size: r.size, - ResolvConf: ateompath.ActorResolvConfPath(r.actorUID), + ResolvConf: resolvConfPath(r.actorDirs), }) return ocispec.Save(bundle, spec) } @@ -85,11 +97,11 @@ func (r *runsc) cmdCreate(ctx context.Context, out io.Writer, containerName stri "-log-format", "json", "--alsologtostderr", // "-debug", - // "-debug-log", ateompath.RunscDebugLogDir(r.actorUID, containerName) + "/", + // "-debug-log", filepath.Join(r.actorDirs.GetRootDir(), "runsc-debug-logs", containerName) + "/", // "-debug-to-user-log", // "-log-packets", // "-strace", - "-root", ateompath.RunSCStateDir(r.actorUID), + "-root", r.actorDirs.GetRunscStateDir(), // Provision the sentry's vCPU count from the cgroup CPU quota written by // sizing.ApplyToOCISpec, so the sandbox is sized to the pod's limit (runsc // otherwise sizes to all host CPUs). Global flag: before the subcommand. @@ -97,8 +109,8 @@ func (r *runsc) cmdCreate(ctx context.Context, out io.Writer, containerName stri } args = append(args, "create", - "-bundle", ateompath.OCIBundlePath(r.actorUID, containerName), - "-pid-file", ateompath.PIDFilePath(r.actorUID, containerName), + "-bundle", r.bundle(containerName), + "-pid-file", r.pidFile(containerName), ) args = append(args, additionalArgs...) @@ -126,12 +138,12 @@ func (r *runsc) cmdStart(ctx context.Context, out io.Writer, containerName strin "-log-format", "json", "--alsologtostderr", // "-debug", - // "-debug-log", ateompath.RunscDebugLogDir(r.actorUID, containerName)+"/", + // "-debug-log", filepath.Join(r.actorDirs.GetRootDir(), "runsc-debug-logs", containerName) + "/", // "-debug-to-user-log", // "-log-packets", // "-strace", "-allow-connected-on-save", - "-root", ateompath.RunSCStateDir(r.actorUID), + "-root", r.actorDirs.GetRunscStateDir(), } startArgs = append(startArgs, "start", containerName) cmd := exec.CommandContext(ctx, r.path, startArgs...) @@ -155,11 +167,11 @@ func (r *runsc) cmdCheckpoint(ctx context.Context, containerName, checkpointPath "-log-format", "json", "--alsologtostderr", // "-debug", - // "-debug-log", ateompath.RunscDebugLogDir(r.actorUID, containerName)+"/", + // "-debug-log", filepath.Join(r.actorDirs.GetRootDir(), "runsc-debug-logs", containerName) + "/", // "-debug-to-user-log", // "-log-packets", // "-strace", - "-root", ateompath.RunSCStateDir(r.actorUID), + "-root", r.actorDirs.GetRunscStateDir(), "checkpoint", "-image-path", checkpointPath, containerName, // Name of the container @@ -181,11 +193,11 @@ func (r *runsc) cmdFsCheckpoint(ctx context.Context, containerName, checkpointPa "-log-format", "json", "--alsologtostderr", // "-debug", - // "-debug-log", ateompath.RunscDebugLogDir(r.actorUID, containerName)+"/", + // "-debug-log", filepath.Join(r.actorDirs.GetRootDir(), "runsc-debug-logs", containerName) + "/", // "-debug-to-user-log", // "-log-packets", // "-strace", - "-root", ateompath.RunSCStateDir(r.actorUID), + "-root", r.actorDirs.GetRunscStateDir(), "fscheckpoint", "-image-path", checkpointPath, } @@ -216,7 +228,7 @@ func (r *runsc) pauseArgs(containerName string) []string { return []string{ "-log-format", "json", "--alsologtostderr", - "-root", ateompath.RunSCStateDir(r.actorUID), + "-root", r.actorDirs.GetRunscStateDir(), "pause", containerName, } @@ -241,7 +253,7 @@ func (r *runsc) resumeArgs(containerName string) []string { return []string{ "-log-format", "json", "--alsologtostderr", - "-root", ateompath.RunSCStateDir(r.actorUID), + "-root", r.actorDirs.GetRunscStateDir(), "resume", containerName, } @@ -273,19 +285,19 @@ func (r *runsc) cmdRestore(ctx context.Context, out io.Writer, containerName, ch "-log-format", "json", "--alsologtostderr", // "-debug", - // "-debug-log", ateompath.RunscDebugLogDir(r.actorUID, containerName)+"/", + // "-debug-log", filepath.Join(r.actorDirs.GetRootDir(), "runsc-debug-logs", containerName) + "/", // "-debug-to-user-log", // "-log-packets", // "-strace", - "-root", ateompath.RunSCStateDir(r.actorUID), + "-root", r.actorDirs.GetRunscStateDir(), // Match cmdCreate: size the restored sentry from the cgroup CPU quota. "--cpu-num-from-quota", } restoreArgs = append(restoreArgs, "restore", - "-bundle", ateompath.OCIBundlePath(r.actorUID, containerName), + "-bundle", r.bundle(containerName), "-image-path", checkpointPath, - "-pid-file", ateompath.PIDFilePath(r.actorUID, containerName), + "-pid-file", r.pidFile(containerName), "-background", "-detach", containerName, @@ -306,7 +318,7 @@ func (r *runsc) cmdDelete(ctx context.Context, containerName string) error { "-log-format", "json", "--alsologtostderr", // "-debug", - "-root", ateompath.RunSCStateDir(r.actorUID), + "-root", r.actorDirs.GetRunscStateDir(), "delete", "-force", containerName, @@ -326,7 +338,7 @@ func (r *runsc) cmdState(ctx context.Context, containerName string) error { r.path, "-log-format", "json", "--alsologtostderr", - "-root", ateompath.RunSCStateDir(r.actorUID), + "-root", r.actorDirs.GetRunscStateDir(), "state", containerName, ) @@ -345,7 +357,7 @@ func (r *runsc) cmdList(ctx context.Context) ([]string, error) { r.path, "-log-format", "json", "--alsologtostderr", - "-root", ateompath.RunSCStateDir(r.actorUID), + "-root", r.actorDirs.GetRunscStateDir(), "list", "-quiet", ) @@ -364,7 +376,7 @@ func (r *runsc) killArgs(containerName, signal string) []string { return []string{ "-log-format", "json", "--alsologtostderr", - "-root", ateompath.RunSCStateDir(r.actorUID), + "-root", r.actorDirs.GetRunscStateDir(), "kill", containerName, signal, @@ -391,7 +403,7 @@ func (r *runsc) waitArgs(containerName string) []string { return []string{ "-log-format", "json", "--alsologtostderr", - "-root", ateompath.RunSCStateDir(r.actorUID), + "-root", r.actorDirs.GetRunscStateDir(), "wait", containerName, } diff --git a/cmd/ateom-gvisor/runsc_test.go b/cmd/ateom-gvisor/runsc_test.go index c46373cd70..f222f2b119 100644 --- a/cmd/ateom-gvisor/runsc_test.go +++ b/cmd/ateom-gvisor/runsc_test.go @@ -20,21 +20,24 @@ import ( "reflect" "testing" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/ocispec" + "github.com/agent-substrate/substrate/internal/proto/ateompb" ) +var testActorDirs = &ateompb.ActorDirs{RunscStateDir: "/node/actors/test-actor-123/runsc-state"} + func TestKillArgs(t *testing.T) { r := &runsc{ - path: "/usr/bin/runsc", - actorUID: "test-actor-123", + path: "/usr/bin/runsc", + actorUID: "test-actor-123", + actorDirs: testActorDirs, } got := r.killArgs("my-container", "SIGTERM") want := []string{ "-log-format", "json", "--alsologtostderr", - "-root", ateompath.RunSCStateDir("test-actor-123"), + "-root", testActorDirs.GetRunscStateDir(), "kill", "my-container", "SIGTERM", @@ -47,15 +50,16 @@ func TestKillArgs(t *testing.T) { func TestWaitArgs(t *testing.T) { r := &runsc{ - path: "/usr/bin/runsc", - actorUID: "test-actor-123", + path: "/usr/bin/runsc", + actorUID: "test-actor-123", + actorDirs: testActorDirs, } got := r.waitArgs("my-container") want := []string{ "-log-format", "json", "--alsologtostderr", - "-root", ateompath.RunSCStateDir("test-actor-123"), + "-root", testActorDirs.GetRunscStateDir(), "wait", "my-container", } @@ -67,15 +71,16 @@ func TestWaitArgs(t *testing.T) { func TestPauseArgs(t *testing.T) { r := &runsc{ - path: "/usr/bin/runsc", - actorUID: "test-actor-123", + path: "/usr/bin/runsc", + actorUID: "test-actor-123", + actorDirs: testActorDirs, } got := r.pauseArgs(ocispec.PauseContainer) want := []string{ "-log-format", "json", "--alsologtostderr", - "-root", ateompath.RunSCStateDir("test-actor-123"), + "-root", testActorDirs.GetRunscStateDir(), "pause", ocispec.PauseContainer, } @@ -87,15 +92,16 @@ func TestPauseArgs(t *testing.T) { func TestResumeArgs(t *testing.T) { r := &runsc{ - path: "/usr/bin/runsc", - actorUID: "test-actor-123", + path: "/usr/bin/runsc", + actorUID: "test-actor-123", + actorDirs: testActorDirs, } got := r.resumeArgs(ocispec.PauseContainer) want := []string{ "-log-format", "json", "--alsologtostderr", - "-root", ateompath.RunSCStateDir("test-actor-123"), + "-root", testActorDirs.GetRunscStateDir(), "resume", ocispec.PauseContainer, } diff --git a/cmd/ateom-gvisor/sandboxnet.go b/cmd/ateom-gvisor/sandboxnet.go index 00b4a8ee36..1290e98347 100644 --- a/cmd/ateom-gvisor/sandboxnet.go +++ b/cmd/ateom-gvisor/sandboxnet.go @@ -26,12 +26,12 @@ import ( "path/filepath" "github.com/agent-substrate/substrate/internal/ateomnet" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/atunnel" + "github.com/agent-substrate/substrate/internal/proto/ateompb" ) // prepareSandboxNetwork builds the actor's network and starts serving it. -func (s *AteomService) prepareSandboxNetwork(ctx context.Context, actorUID string) error { +func (s *AteomService) prepareSandboxNetwork(ctx context.Context, actorUID string, actorDirs *ateompb.ActorDirs) error { if err := s.releaseSandboxNetwork(ctx); err != nil { return err } @@ -46,10 +46,12 @@ func (s *AteomService) prepareSandboxNetwork(ctx context.Context, actorUID strin } // Point the sandbox resolver at its gateway. - if _, err := actorResolvConf(actorUID); err != nil { + path, err := actorResolvConf(actorDirs) + if err != nil { _ = session.Close(ctx) return err } + s.resolvConf = path return s.sandbox.Replace(ctx, session) } @@ -57,21 +59,27 @@ func (s *AteomService) prepareSandboxNetwork(ctx context.Context, actorUID strin // releaseSandboxNetwork stops serving the actor and takes its network down, // along with the resolv.conf atelet's per-activation reset leaves behind. func (s *AteomService) releaseSandboxNetwork(ctx context.Context) error { - if session := s.sandbox.Session(); session != nil { - if err := os.Remove(ateompath.ActorResolvConfPath(session.Network.ActorUID)); err != nil && !errors.Is(err, fs.ErrNotExist) { + if s.resolvConf != "" { + if err := os.Remove(s.resolvConf); err != nil && !errors.Is(err, fs.ErrNotExist) { slog.WarnContext(ctx, "Failed to remove the actor resolv.conf", slog.Any("err", err)) } + s.resolvConf = "" } return s.sandbox.Close(ctx) } +// resolvConfPath is the resolver bind source outside the actor's rootfs. +func resolvConfPath(actorDirs *ateompb.ActorDirs) string { + return filepath.Join(actorDirs.GetRootDir(), "resolv.conf") +} + // actorResolvConf writes the resolver bind source outside the actor's rootfs. -func actorResolvConf(actorUID string) (string, error) { +func actorResolvConf(actorDirs *ateompb.ActorDirs) (string, error) { pod, err := os.ReadFile("/etc/resolv.conf") if err != nil { return "", fmt.Errorf("reading the worker pod resolv.conf: %w", err) } - path := ateompath.ActorResolvConfPath(actorUID) + path := resolvConfPath(actorDirs) if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return "", fmt.Errorf("creating the actor directory: %w", err) } diff --git a/cmd/ateom-microvm/checkpoint.go b/cmd/ateom-microvm/checkpoint.go index fa8c4c8e80..a9277df4e9 100644 --- a/cmd/ateom-microvm/checkpoint.go +++ b/cmd/ateom-microvm/checkpoint.go @@ -27,7 +27,6 @@ import ( "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/ch" "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/kata" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/ateomstats" "github.com/agent-substrate/substrate/internal/imagecache" "github.com/agent-substrate/substrate/internal/proto/ateompb" @@ -60,6 +59,9 @@ import ( // Allow checkpointing even if the pod is shutting down. This will allow actors // (or the harness) to suspend on shutdown. func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.CheckpointWorkloadRequest) (*ateompb.CheckpointWorkloadResponse, error) { + if err := validateActorDirs(req.GetActorDirs()); err != nil { + return nil, err + } s.lock.Lock() defer s.lock.Unlock() @@ -74,6 +76,7 @@ func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.Chec attribution := ateomstats.ActorAttributionFromRequest(req) actorUID := req.GetActorUid() + actorDirs := req.GetActorDirs() s.actorLogger.EmitLifecycleLog(ctx, "Actor checkpointing", attribution) @@ -118,7 +121,7 @@ func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.Chec } dPause := time.Since(tPause) - checkpointDir := ateompath.CheckpointStateDir(actorUID) + checkpointDir := actorDirs.GetCheckpointDir() // Start from a clean dir so CH's snapshot files are the only contents. if err := os.RemoveAll(checkpointDir); err != nil { return nil, fmt.Errorf("while clearing checkpoint dir %q: %w", checkpointDir, err) @@ -155,7 +158,7 @@ func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.Chec if durable { g.Go(func() error { t := time.Now() - if err := tarDurableVolumes(gctx, ateompath.DurableDirVolumeMountsDir(actorUID), checkpointDir); err != nil { + if err := tarDurableVolumes(gctx, actorDirs.GetDurableDirVolumeMountsDir(), checkpointDir); err != nil { return err } dDurable = time.Since(t) @@ -165,7 +168,7 @@ func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.Chec if scope == ateompb.SnapshotScope_SNAPSHOT_SCOPE_FULL { g.Go(func() error { t := time.Now() - if err := tarRootfsUpper(gctx, rootfsUpperDir(actorUID), checkpointDir); err != nil { + if err := tarRootfsUpper(gctx, rootfsUpperDir(actorDirs), checkpointDir); err != nil { return err } dUpper = time.Since(t) @@ -187,7 +190,7 @@ func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.Chec // Tear down: the actor returns to "available". Best-effort; the snapshot is // already on disk for atelet to ship. tTeardown := time.Now() - if err := s.terminateWorkload(ctx, actorUID); err != nil { + if err := s.terminateWorkload(ctx, actorUID, actorDirs); err != nil { slog.WarnContext(ctx, "failed to terminate workload after checkpoint", slog.String("actor", attribution.Ref.String()), slog.String("actorUID", actorUID), @@ -281,7 +284,7 @@ func listFiles(dir string) ([]string, error) { // teardownActor stops the ateom-owned CH VMM for an actor. ra may be // nil (e.g. ateom restarted and lost in-memory state). -func (s *AteomService) teardownActor(ctx context.Context, id string, ra *runningActor, client *ch.Client) error { +func (s *AteomService) teardownActor(ctx context.Context, id string, actorDirs *ateompb.ActorDirs, ra *runningActor, client *ch.Client) error { // Stop offering the guest to GetWorkloadStats first, before anything below // makes it stop answering. Clearing it here rather than alongside the // attribution is what keeps a poll that lands mid-teardown on the @@ -334,13 +337,13 @@ func (s *AteomService) teardownActor(ctx context.Context, id string, ra *running // Remove the rootfs upper dir: ateom owns it — atelet's actor-dir reset // doesn't know it — and its absence is what marks a worker as holding no // disk-backed upper. Runs after the checkpoint tar, which is already on disk. - if err := os.RemoveAll(rootfsUpperDir(id)); err != nil { + if err := os.RemoveAll(rootfsUpperDir(actorDirs)); err != nil { slog.WarnContext(ctx, "Failed to remove rootfs upper dir", slog.String("actorUID", id), slog.Any("err", err)) } // Detach the bundle rootfs overlays composed in buildActorContainers, so // atelet's bundle wipe doesn't strand live mounts in this namespace. - if err := imagecache.UnmountAllUnder(ateompath.OCIBundleDir(id)); err != nil { + if err := imagecache.UnmountAllUnder(actorDirs.GetOciBundleDir()); err != nil { if !errors.Is(err, os.ErrNotExist) { errs = append(errs, fmt.Errorf("while unmounting bundle rootfs overlays: %w", err)) } @@ -351,12 +354,15 @@ func (s *AteomService) teardownActor(ctx context.Context, id string, ra *running // TerminateWorkload stops the running actor, tears down its VMM, and cleans up // networking and overlays. func (s *AteomService) TerminateWorkload(ctx context.Context, req *ateompb.TerminateWorkloadRequest) (*ateompb.TerminateWorkloadResponse, error) { + if err := validateActorDirs(req.GetActorDirs()); err != nil { + return nil, err + } s.lock.Lock() defer s.lock.Unlock() attribution := ateomstats.ActorAttributionFromRequest(req) - if err := s.terminateWorkload(ctx, attribution.UID); err != nil { + if err := s.terminateWorkload(ctx, attribution.UID, req.GetActorDirs()); err != nil { return nil, fmt.Errorf("failed to terminate workload: %w", err) } @@ -365,7 +371,7 @@ func (s *AteomService) TerminateWorkload(ctx context.Context, req *ateompb.Termi return &ateompb.TerminateWorkloadResponse{}, nil } -func (s *AteomService) terminateWorkload(ctx context.Context, actorUID string) error { +func (s *AteomService) terminateWorkload(ctx context.Context, actorUID string, actorDirs *ateompb.ActorDirs) error { var errs []error if err := s.deactivateActorNetworking(ctx); err != nil { errs = append(errs, fmt.Errorf("while deactivating actor networking: %w", err)) @@ -378,7 +384,7 @@ func (s *AteomService) terminateWorkload(ctx context.Context, actorUID string) e } client := ch.NewClient(chSocket) - if err := s.teardownActor(ctx, actorUID, ra, client); err != nil { + if err := s.teardownActor(ctx, actorUID, actorDirs, ra, client); err != nil { errs = append(errs, fmt.Errorf("while tearing down actor: %w", err)) } delete(s.running, actorUID) diff --git a/cmd/ateom-microvm/csi.go b/cmd/ateom-microvm/csi.go index ce535fefe8..c9fed86d86 100644 --- a/cmd/ateom-microvm/csi.go +++ b/cmd/ateom-microvm/csi.go @@ -22,7 +22,6 @@ import ( "os" "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/kata" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/ocispec" "github.com/agent-substrate/substrate/internal/proto/ateompb" ) @@ -39,8 +38,7 @@ func hasCsiVolumes(containers []*ateompb.Container) bool { // stageCsiVolumes bind-mounts the actor's host CSI volumes directory // into the sandbox's shared virtio-fs tree at SharedDir(actorUID)/csi. -func (s *AteomService) stageCsiVolumes(ctx context.Context, actorUID string) error { - src := ateompath.VolumesDir(actorUID) +func (s *AteomService) stageCsiVolumes(ctx context.Context, actorUID, src string) error { if _, err := os.Stat(src); err != nil { return fmt.Errorf("while checking CSI volumes dir %q: %w", src, err) } diff --git a/cmd/ateom-microvm/durable.go b/cmd/ateom-microvm/durable.go index 6287dec92c..21073c0ea1 100644 --- a/cmd/ateom-microvm/durable.go +++ b/cmd/ateom-microvm/durable.go @@ -22,7 +22,7 @@ package main // state: it survives suspend/resume and, under the Data snapshot scope, is the // ONLY thing captured (the workload cold-starts on restore). The host side is // owned by atelet, which creates one directory per volume under -// ateompath.DurableDirVolumeMountsDir(actorUID) and wipes them when the actor's +// ActorDirs.durable_dir_volume_mounts_dir and wipes them when the actor's // directories are reset. // // ateom exposes that host directory to the guest under the single kataShared @@ -42,18 +42,18 @@ import ( "path/filepath" "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/kata" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/ocispec" "github.com/agent-substrate/substrate/internal/proto/ateompb" + "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/internal/tarutil" ) // durableTarFile is the snapshot file holding the tar of the actor's durable-dir // volumes. Its entries are /... relative to -// ateompath.DurableDirVolumeMountsDir, so extraction restores the same layout. +// ActorDirs.durable_dir_volume_mounts_dir, so extraction restores the same layout. // The name is shared with atelet, which uses it to carve durable data out of a // FULL snapshot's file set when uploading a paused checkpoint as DATA. -const durableTarFile = ateompath.DurableDirTarFile +const durableTarFile = resources.DurableDirTarFile // hasDurableVolumes reports whether any container mounts a durable-dir volume. func hasDurableVolumes(containers []*ateompb.Container) bool { @@ -67,8 +67,7 @@ func hasDurableVolumes(containers []*ateompb.Container) bool { // stageDurableVolumes bind-mounts the actor's host durable-dir directory // into the sandbox's shared virtio-fs tree at SharedDir(actorUID)/durable. -func (s *AteomService) stageDurableVolumes(ctx context.Context, actorUID string) error { - src := ateompath.DurableDirVolumeMountsDir(actorUID) +func (s *AteomService) stageDurableVolumes(ctx context.Context, actorUID, src string) error { if _, err := os.Stat(src); err != nil { return fmt.Errorf("while checking durable-dir volumes dir %q: %w", src, err) } diff --git a/cmd/ateom-microvm/main.go b/cmd/ateom-microvm/main.go index b5029da6ae..5d1263855e 100644 --- a/cmd/ateom-microvm/main.go +++ b/cmd/ateom-microvm/main.go @@ -44,9 +44,9 @@ import ( "github.com/agent-substrate/substrate/internal/ateinterceptors" "github.com/agent-substrate/substrate/internal/ateomcapacity" "github.com/agent-substrate/substrate/internal/ateomnet" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/atunnel" "github.com/agent-substrate/substrate/internal/installdefaults" + "github.com/agent-substrate/substrate/internal/nodepath" "github.com/agent-substrate/substrate/internal/otlprelay" "github.com/agent-substrate/substrate/internal/proto/ateompb" "github.com/agent-substrate/substrate/internal/resources" @@ -58,6 +58,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/reflection" "google.golang.org/grpc/status" + "k8s.io/apimachinery/pkg/util/validation/field" ) var ( @@ -68,7 +69,7 @@ var ( showVersion = flag.Bool("version", false, "Print version and exit.") logLevelFlag = flag.String("log-level", "info", "Minimum log level: debug, info, warn, or error.") - otlpRelaySocket = flag.String("otlp-relay-socket", ateompath.AteletOTLPSocketPath(), + otlpRelaySocket = flag.String("otlp-relay-socket", nodepath.AteletOTLPSocketPath(), "Unix socket of atelet's OTLP relay to export telemetry through, keeping it off the pod network. Empty, or absent at startup, exports directly to OTEL_EXPORTER_OTLP_ENDPOINT instead.") // Every listen address here is an unspecified wildcard, which Go binds as a @@ -156,7 +157,7 @@ func do(ctx context.Context) error { defer serverboot.ShutdownProvider("MeterProvider", mp.Shutdown) // Create ateom dir. - ateomDir := ateompath.AteomPath(*podUID) + ateomDir := nodepath.AteomPath(*podUID) if err := resources.ValidateAteomUID(*podUID); err != nil { return fmt.Errorf("in resources.ValidateAteomUID: %w", err) } @@ -190,7 +191,7 @@ func do(ctx context.Context) error { } // Clean up any old socket. - sockPath := ateompath.AteomSocketPath(*podUID) + sockPath := nodepath.AteomSocketPath(*podUID) if err := os.RemoveAll(sockPath); err != nil { return fmt.Errorf("while removing %q: %w", sockPath, err) } @@ -302,7 +303,7 @@ func do(ctx context.Context) error { // that reaches here is a misconfiguration no restart-in-place will fix. go func() { err := ateomcapacity.Report(ctx, ateomcapacity.ReportConfig{ - SocketPath: ateompath.AteomSupportSocket, + SocketPath: nodepath.AteomSupportSocket, CredentialBundlePath: *workerCredentialBundle, TrustBundlePath: *podIdentityTrustBundle, AteletSPIFFEID: *ateletIdentity, @@ -544,7 +545,7 @@ func (s *AteomService) prepareActorEgress(ctx context.Context, actorAtespace, ac return nil, fmt.Errorf("invalid egress gateway address %q: %w", gateway.GetAddress(), err) } certificateSource, err := atunnel.NewBrokerCertificateSource(atunnel.BrokerConfig{ - SocketPath: ateompath.AteomSupportSocket, + SocketPath: nodepath.AteomSupportSocket, CredentialBundlePath: s.workerCredentialBundlePath, TrustBundlePath: s.podIdentityTrustBundlePath, @@ -597,6 +598,14 @@ func (s *AteomService) deactivateActorNetworking(ctx context.Context) error { return nil } +// validateActorDirs rejects a request whose actor directories are unusable. +func validateActorDirs(actorDirs *ateompb.ActorDirs) error { + if errs := resources.ValidateActorDirs(actorDirs, field.NewPath("actor_dirs")); len(errs) > 0 { + return status.Error(codes.InvalidArgument, errs.ToAggregate().Error()) + } + return nil +} + // rejectIfDraining returns a codes.Unavailable error if ateom has begun graceful // shutdown, so the control plane reschedules the actor onto a live worker. func (s *AteomService) rejectIfDraining() error { diff --git a/cmd/ateom-microvm/net_linux_test.go b/cmd/ateom-microvm/net_linux_test.go index b54184781f..344bade052 100644 --- a/cmd/ateom-microvm/net_linux_test.go +++ b/cmd/ateom-microvm/net_linux_test.go @@ -24,8 +24,8 @@ import ( "github.com/vishvananda/netns" "github.com/agent-substrate/substrate/internal/ateomnet" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/atunnel" + "github.com/agent-substrate/substrate/internal/nodepath" "github.com/agent-substrate/substrate/internal/roottest" ) @@ -52,7 +52,7 @@ func TestPrepareSandboxNetworkReplacesSameActor(t *testing.T) { t.Fatal(err) } } - named, err := netns.GetFromName(ateompath.ActorNetNSName(actorUID)) + named, err := netns.GetFromName(nodepath.ActorNetNSName(actorUID)) if err != nil { t.Fatalf("opening replacement namespace by name: %v", err) } diff --git a/cmd/ateom-microvm/restore.go b/cmd/ateom-microvm/restore.go index 7a530b79af..ffa44e8d52 100644 --- a/cmd/ateom-microvm/restore.go +++ b/cmd/ateom-microvm/restore.go @@ -30,7 +30,6 @@ import ( "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/ch" "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/kata" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/imagecache" "github.com/agent-substrate/substrate/internal/proto/ateompb" "github.com/agent-substrate/substrate/internal/resources" @@ -79,6 +78,9 @@ func restoreMemMode(ctx context.Context, info ch.VMMInfo) string { // Contract with atelet: the snapshot's files have been downloaded to RestoreStateDir, // and the durable-dir volume directories re-created (empty). func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.RestoreWorkloadRequest) (resp *ateompb.RestoreWorkloadResponse, retErr error) { + if err := validateActorDirs(req.GetActorDirs()); err != nil { + return nil, err + } s.lock.Lock() defer s.lock.Unlock() @@ -98,6 +100,7 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore p := actorBootParams{ actorRef: resources.ActorRef{Atespace: req.GetAtespace(), Name: req.GetActorName()}, actorUID: req.GetActorUid(), + actorDirs: req.GetActorDirs(), templateAtespace: req.GetActorTemplateAtespace(), templateName: req.GetActorTemplateName(), containers: req.GetSpec().GetContainers(), @@ -105,8 +108,8 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore egressGateway: req.GetEgressGateway(), size: sizing.FromLimits(req.GetCpuMilli(), req.GetMemoryBytes()), } - restoreDir := ateompath.RestoreStateDir(p.actorUID) - durableDir := ateompath.DurableDirVolumeMountsDir(p.actorUID) + restoreDir := p.actorDirs.GetRestoreDir() + durableDir := p.actorDirs.GetDurableDirVolumeMountsDir() tStart := time.Now() attribution := p.actorAttribution() @@ -212,7 +215,7 @@ func (s *AteomService) restoreFullScope(ctx context.Context, p actorBootParams, untarDone := make(chan error, 1) untarJoined := false go func() { - untarDone <- untarRootfsUpper(rootfsUpperDir(actorUID), restoreDir) + untarDone <- untarRootfsUpper(rootfsUpperDir(p.actorDirs), restoreDir) }() defer func() { if !untarJoined { @@ -234,7 +237,7 @@ func (s *AteomService) restoreFullScope(ctx context.Context, p actorBootParams, if len(containers) > maxActorContainers { return status.Errorf(codes.Unimplemented, "ateom-microvm supports at most %d containers, got %d", maxActorContainers, len(containers)) } - ctrs, err := s.buildActorContainers(actorUID, containers) + ctrs, err := s.buildActorContainers(p.actorDirs, containers) if err != nil { return err } @@ -248,7 +251,7 @@ func (s *AteomService) restoreFullScope(ctx context.Context, p actorBootParams, return untarErr } tUpper := time.Now() - vfsdCmd, err := s.stageMergedRootfs(ctx, rr, actorUID, ctrs, containers) + vfsdCmd, err := s.stageMergedRootfs(ctx, rr, actorUID, p.actorDirs, ctrs, containers) if err != nil { return err } @@ -279,7 +282,7 @@ func (s *AteomService) restoreFullScope(ctx context.Context, p actorBootParams, } // Detach any bundle rootfs overlays mounted by buildActorContainers // before the failure, mirroring teardownActor's cleanup. - if err := imagecache.UnmountAllUnder(ateompath.OCIBundleDir(actorUID)); err != nil { + if err := imagecache.UnmountAllUnder(p.actorDirs.GetOciBundleDir()); err != nil { slog.WarnContext(ctx, "Failed to unmount bundle rootfs overlays after Restore failure", slog.Any("err", err)) } } diff --git a/cmd/ateom-microvm/rootfsupper.go b/cmd/ateom-microvm/rootfsupper.go index e3dbb9c826..3637a2f626 100644 --- a/cmd/ateom-microvm/rootfsupper.go +++ b/cmd/ateom-microvm/rootfsupper.go @@ -50,7 +50,7 @@ import ( "path/filepath" "strings" - "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/internal/proto/ateompb" "github.com/agent-substrate/substrate/internal/tarutil" ) @@ -64,9 +64,9 @@ const rootfsUpperTarFile = "rootfs-upper.tar" // rootfsUpperDir is the host directory backing the actor's rootfs overlay // uppers: one subdirectory per container (see kata.UpperWorkDirs). Local to -// this binary — no other component touches it — hence not in ateompath. -func rootfsUpperDir(actorUID string) string { - return filepath.Join(ateompath.ActorPath(actorUID), "rootfs-upper") +// this binary: atelet never touches it, so it is not one of the ActorDirs. +func rootfsUpperDir(actorDirs *ateompb.ActorDirs) string { + return filepath.Join(actorDirs.GetRootDir(), "rootfs-upper") } // resetRootfsUpperDir gives a cold boot a pristine upper directory: a cold @@ -74,8 +74,8 @@ func rootfsUpperDir(actorUID string) string { // know about this directory, so ateom wipes any previous activation's contents // itself. The per-container fs/work subdirectories are created by the overlay // staging (kata.StageMergedRootfs). -func resetRootfsUpperDir(actorUID string) error { - dir := rootfsUpperDir(actorUID) +func resetRootfsUpperDir(actorDirs *ateompb.ActorDirs) error { + dir := rootfsUpperDir(actorDirs) if err := os.RemoveAll(dir); err != nil { return fmt.Errorf("while clearing rootfs upper dir %q: %w", dir, err) } diff --git a/cmd/ateom-microvm/run.go b/cmd/ateom-microvm/run.go index 6b263349cc..85e37f9ca1 100644 --- a/cmd/ateom-microvm/run.go +++ b/cmd/ateom-microvm/run.go @@ -33,7 +33,6 @@ import ( "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/ch" "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/kata" "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/third_party/kata/agentpb" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/imagecache" "github.com/agent-substrate/substrate/internal/ocispec" "github.com/agent-substrate/substrate/internal/proto/ateompb" @@ -224,6 +223,9 @@ func (s *AteomService) resolveRuntime(paths map[string]string) resolvedRuntime { // are on disk and passed as runtime asset paths. // - The OCI bundle (config.json + populated rootfs/) is prepared per container. func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkloadRequest) (resp *ateompb.RunWorkloadResponse, retErr error) { + if err := validateActorDirs(req.GetActorDirs()); err != nil { + return nil, err + } s.lock.Lock() defer s.lock.Unlock() if err := s.rejectIfDraining(); err != nil { @@ -244,6 +246,7 @@ func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkload p := actorBootParams{ actorRef: resources.ActorRef{Atespace: req.GetAtespace(), Name: req.GetActorName()}, actorUID: req.GetActorUid(), + actorDirs: req.GetActorDirs(), templateAtespace: req.GetActorTemplateAtespace(), templateName: req.GetActorTemplateName(), containers: req.GetSpec().GetContainers(), @@ -280,8 +283,10 @@ func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkload // request, or from a Restore request whose snapshot scope covers only the // durable-dir volumes (the workload itself cold-starts). type actorBootParams struct { - actorRef resources.ActorRef - actorUID string + actorRef resources.ActorRef + actorUID string + // actorDirs are the actor's directories, from the request. + actorDirs *ateompb.ActorDirs templateAtespace string templateName string containers []*ateompb.Container @@ -382,7 +387,7 @@ func (s *AteomService) coldBootActor(ctx context.Context, p actorBootParams) (re } // Detach any bundle rootfs overlays mounted by buildActorContainers // before the failure, mirroring teardownActor's cleanup. - if err := imagecache.UnmountAllUnder(ateompath.OCIBundleDir(actorUID)); err != nil { + if err := imagecache.UnmountAllUnder(p.actorDirs.GetOciBundleDir()); err != nil { slog.WarnContext(ctx, "Failed to unmount bundle rootfs overlays after Run failure", slog.Any("err", err)) } } @@ -410,7 +415,7 @@ func (s *AteomService) coldBootActor(ctx context.Context, p actorBootParams) (re // Prepare each container's OCI spec + record its bundle rootfs (the overlay // lower the host merges under the container's writable upper). - ctrs, err := s.buildActorContainers(actorUID, containers) + ctrs, err := s.buildActorContainers(p.actorDirs, containers) if err != nil { return err } @@ -436,7 +441,7 @@ func (s *AteomService) coldBootActor(ctx context.Context, p actorBootParams) (re // A cold boot starts from the bare image: give it a pristine host upper dir // (atelet's actor-dir reset does not know this directory; see rootfsupper.go). - if err := resetRootfsUpperDir(actorUID); err != nil { + if err := resetRootfsUpperDir(p.actorDirs); err != nil { return err } @@ -444,7 +449,7 @@ func (s *AteomService) coldBootActor(ctx context.Context, p actorBootParams) (re // host upper, mounted into the shared dir) + durable-dir and CSI volumes (if any), // and start the ONE virtiofsd that serves them all. CH connects to it at vm.create // and demand-pages for the actor's lifetime, so ateom owns the process (killed in teardownActor). - vfsdCmd, err := s.stageMergedRootfs(ctx, rr, actorUID, ctrs, containers) + vfsdCmd, err := s.stageMergedRootfs(ctx, rr, actorUID, p.actorDirs, ctrs, containers) if err != nil { return err } @@ -587,16 +592,16 @@ func (s *AteomService) coldBootActor(ctx context.Context, p actorBootParams) (re // and records the bundle rootfs that backs the overlay's RO lower. No host disk is // mounted here — the merged overlays are assembled in stageMergedRootfs after the // sandbox state is clean. Both RunWorkload and RestoreWorkload go through here. -func (s *AteomService) buildActorContainers(actorUID string, containers []*ateompb.Container) ([]actorContainer, error) { +func (s *AteomService) buildActorContainers(actorDirs *ateompb.ActorDirs, containers []*ateompb.Container) ([]actorContainer, error) { ctrs := make([]actorContainer, len(containers)) for i, c := range containers { cn := c.GetName() - bundle := ateompath.OCIBundlePath(actorUID, cn) + bundle := filepath.Join(actorDirs.GetOciBundleDir(), cn) spec, err := ocispec.Load(bundle) if err != nil { return nil, fmt.Errorf("while reading the OCI spec for %q: %w", cn, err) } - if err := ocispec.ShapeMicroVM(spec, ocispec.MicroVMOptions{ActorUID: actorUID, ContainerID: cn}); err != nil { + if err := ocispec.ShapeMicroVM(spec, ocispec.MicroVMOptions{ActorDirs: actorDirs, ContainerID: cn}); err != nil { return nil, fmt.Errorf("while shaping the OCI spec for %q: %w", cn, err) } // Compose the bundle rootfs from the node's cached image layers (an @@ -633,31 +638,31 @@ func (s *AteomService) buildActorContainers(actorUID string, containers []*ateom // upper contents). The returned virtiofsd cmd outlives this call (CH // demand-pages from it); the caller owns it (tracked on runningActor, killed // in teardownActor). -func (s *AteomService) stageMergedRootfs(ctx context.Context, rr resolvedRuntime, id string, ctrs []actorContainer, containers []*ateompb.Container) (*exec.Cmd, error) { - upperBase := rootfsUpperDir(id) +func (s *AteomService) stageMergedRootfs(ctx context.Context, rr resolvedRuntime, id string, actorDirs *ateompb.ActorDirs, ctrs []actorContainer, containers []*ateompb.Container) (*exec.Cmd, error) { + upperBase := rootfsUpperDir(actorDirs) for _, c := range ctrs { if err := kata.StageMergedRootfs(ctx, c.bundleRootfs, upperBase, id, c.name); err != nil { return nil, fmt.Errorf("while staging merged rootfs for %q: %w", c.name, err) } for _, vm := range c.imageMounts { - src := ateompath.ImageVolumeMountPath(id, c.name, vm.GetVolumeName()) + src := imagecache.ImageVolumeMountPath(filepath.Join(actorDirs.GetOciBundleDir(), c.name), vm.GetVolumeName()) if err := kata.StageImageVolume(ctx, src, id, c.name, vm.GetVolumeName()); err != nil { return nil, fmt.Errorf("while staging image volume %q for %q: %w", vm.GetVolumeName(), c.name, err) } } } if hasDurableVolumes(containers) { - if err := s.stageDurableVolumes(ctx, id); err != nil { + if err := s.stageDurableVolumes(ctx, id, actorDirs.GetDurableDirVolumeMountsDir()); err != nil { return nil, fmt.Errorf("while staging durable-dir volumes: %w", err) } } if hasCsiVolumes(containers) { - if err := s.stageCsiVolumes(ctx, id); err != nil { + if err := s.stageCsiVolumes(ctx, id, actorDirs.GetVolumesDir()); err != nil { return nil, fmt.Errorf("while staging CSI volumes: %w", err) } } if hasSystemInfoVolumes(containers) { - if err := s.stageSystemInfoVolumes(ctx, id); err != nil { + if err := s.stageSystemInfoVolumes(ctx, id, actorDirs.GetSystemInfoVolumeRootsDir()); err != nil { return nil, fmt.Errorf("while staging system-info volumes: %w", err) } } diff --git a/cmd/ateom-microvm/systeminfo.go b/cmd/ateom-microvm/systeminfo.go index 95a14ff37b..580becbd22 100644 --- a/cmd/ateom-microvm/systeminfo.go +++ b/cmd/ateom-microvm/systeminfo.go @@ -21,7 +21,7 @@ // so its contents always describe the actor actually being started, whatever // checkpointed state it boots from. The host side is owned by atelet, which // creates one directory per volume under -// ateompath.SystemInfoVolumeRootsDir(actorUID) and wipes/rebuilds them when +// ActorDirs.system_info_volume_roots_dir and wipes/rebuilds them when // the actor's directories are reset. // // ateom exposes that host directory to the guest under the single kataShared @@ -51,7 +51,6 @@ import ( "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/kata" "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/reaper" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/ocispec" "github.com/agent-substrate/substrate/internal/proto/ateompb" ) @@ -71,8 +70,7 @@ func hasSystemInfoVolumes(containers []*ateompb.Container) bool { // into the sandbox's shared virtio-fs tree at SharedDir(actorUID)/system-info, // then remounts the bind read-only: atelet is the only writer, and it writes // the host source directly, never through the share. -func (s *AteomService) stageSystemInfoVolumes(ctx context.Context, actorUID string) error { - src := ateompath.SystemInfoVolumeRootsDir(actorUID) +func (s *AteomService) stageSystemInfoVolumes(ctx context.Context, actorUID, src string) error { if _, err := os.Stat(src); err != nil { return fmt.Errorf("while checking system-info volumes dir %q: %w", src, err) } diff --git a/internal/ateomnet/sandbox.go b/internal/ateomnet/sandbox.go index d235c5c2ac..0785bdb5db 100644 --- a/internal/ateomnet/sandbox.go +++ b/internal/ateomnet/sandbox.go @@ -28,7 +28,7 @@ import ( "sync" "syscall" - "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/internal/nodepath" "github.com/google/nftables" "github.com/google/nftables/binaryutil" @@ -87,7 +87,7 @@ func SetupSandboxNetwork(ctx context.Context, cfg SandboxNetworkConfig) (_ *Sand return nil, fmt.Errorf("actornet: actor UID is required") } - actorNSName := ateompath.ActorNetNSName(actorUID) + actorNSName := nodepath.ActorNetNSName(actorUID) actorNS, err := CreateNetNSWithoutSwitching(actorNSName) if err != nil { return nil, fmt.Errorf("while creating the actor netns %s: %w", actorNSName, err) @@ -275,7 +275,7 @@ const gatewayVethName = "atside" // SandboxGatewayNetNSName names the namespace holding the veth peer and atunnel's // sockets for one actor. func SandboxGatewayNetNSName(actorUID string) string { - return ateompath.ActorNetNSName(actorUID) + "-at" + return nodepath.ActorNetNSName(actorUID) + "-at" } // CleanupSandboxNetwork closes namespace handles and removes their names. @@ -297,7 +297,7 @@ func CleanupSandboxNetwork(network *SandboxNetwork) error { } } // Deleting the namespaces takes any veth pair with them. - for _, name := range []string{ateompath.ActorNetNSName(network.ActorUID), SandboxGatewayNetNSName(network.ActorUID)} { + for _, name := range []string{nodepath.ActorNetNSName(network.ActorUID), SandboxGatewayNetNSName(network.ActorUID)} { if err := removeNamedNetNS(name); err != nil { errs = errors.Join(errs, fmt.Errorf("while deleting netns %s: %w", name, err)) } diff --git a/internal/ateomnet/sandbox_linux_test.go b/internal/ateomnet/sandbox_linux_test.go index b6dac378eb..5e403c2d97 100644 --- a/internal/ateomnet/sandbox_linux_test.go +++ b/internal/ateomnet/sandbox_linux_test.go @@ -29,7 +29,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/internal/nodepath" "github.com/vishvananda/netlink" "github.com/agent-substrate/substrate/internal/roottest" @@ -320,7 +320,7 @@ func TestSetupSucceedsOverALeftoverNamespace(t *testing.T) { if first.GatewayNetNS != first.RuntimeNetNS { first.GatewayNetNS.Close() } - for _, name := range []string{ateompath.ActorNetNSName(uid), SandboxGatewayNetNSName(uid)} { + for _, name := range []string{nodepath.ActorNetNSName(uid), SandboxGatewayNetNSName(uid)} { if _, err := os.Stat("/var/run/netns/" + name); err != nil { t.Fatalf("expected leftover netns %s: %v", name, err) } @@ -506,7 +506,7 @@ func TestSessionCloseReportsAWaitItCouldNotFinish(t *testing.T) { if err := session.Close(ctx); !errors.Is(err, context.DeadlineExceeded) { t.Errorf("Close = %v, want the deadline reported", err) } - if _, err := netns.GetFromName(ateompath.ActorNetNSName("close-deadline")); err == nil { + if _, err := netns.GetFromName(nodepath.ActorNetNSName("close-deadline")); err == nil { t.Error("the namespace survived a Close whose wait timed out") } } diff --git a/internal/imagecache/bundle_linux.go b/internal/imagecache/bundle_linux.go index 55fd4f3577..f4edfd4052 100644 --- a/internal/imagecache/bundle_linux.go +++ b/internal/imagecache/bundle_linux.go @@ -28,8 +28,6 @@ import ( "strings" "golang.org/x/sys/unix" - - "github.com/agent-substrate/substrate/internal/ateompath" ) // SetupBundleRootfs composes the bundle's rootfs from cached layers per the @@ -113,7 +111,7 @@ func setupImageVolumes(bundlePath string, volumes []ImageVolumeOverlay) error { } } - mountpoint := ateompath.ImageVolumeMountPathInBundle(bundlePath, vol.Name) + mountpoint := ImageVolumeMountPath(bundlePath, vol.Name) if err := os.MkdirAll(mountpoint, 0o700); err != nil { return fmt.Errorf("while creating image volume mount point %q: %w", mountpoint, err) } diff --git a/internal/imagecache/imagecache.go b/internal/imagecache/imagecache.go index e0a0302347..f53ce7a18d 100644 --- a/internal/imagecache/imagecache.go +++ b/internal/imagecache/imagecache.go @@ -248,7 +248,7 @@ func WithPlatform(p v1.Platform) Option { } // WithActorsDir points the eviction root-set scan at the node's actors -// directory (the per-actor state dirs under ateompath.BasePath). Each +// directory (the per-actor state dirs under nodepath.BasePath). Each // //bundles//rootfs-overlay.json roots its // image and layers against eviction. Empty disables the scan. func WithActorsDir(dir string) Option { diff --git a/internal/imagecache/spec.go b/internal/imagecache/spec.go index f8670bd709..47c2f5db13 100644 --- a/internal/imagecache/spec.go +++ b/internal/imagecache/spec.go @@ -24,6 +24,13 @@ import ( "strings" ) +// ImageVolumeMountPath is where one image volume is composed inside a +// bundle. atelet writes it into config.json as the bind source; the ateom +// mounts the volume there (see setupImageVolumes). +func ImageVolumeMountPath(bundlePath, volumeName string) string { + return filepath.Join(bundlePath, "volumes", volumeName) +} + // OverlaySpecFileName is the file atelet writes into each container bundle, // next to config.json, describing how to compose the bundle's rootfs from // cached layers. Its absence means the bundle's rootfs is a plain directory diff --git a/internal/nodepath/nodepath.go b/internal/nodepath/nodepath.go new file mode 100644 index 0000000000..1e318fcd28 --- /dev/null +++ b/internal/nodepath/nodepath.go @@ -0,0 +1,77 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package nodepath holds the on-node paths atelet and the ateoms agree on +// before any RPC: the host directory both mount and the sockets and netns. +package nodepath + +import "path/filepath" + +// BasePath is the root shared folder on the host filesystem, mounted at the +// same path into the atelet and ateom containers. +const BasePath = "/var/lib/ateom-gvisor" + +// AteomSupportSocket is the node-local atelet socket used by atunnel +// to request credentials for the worker's current actor assignment. +var AteomSupportSocket = filepath.Join(BasePath, "ateom-support.sock") + +// AteletOTLPSocketPath is the node-scoped unix socket atelet serves the OTLP +// relay on (see internal/otlprelay). It is node-scoped rather than per-pod +// because every ateom on the node pushes into the same relay: atelet is a +// DaemonSet, so one socket collapses N per-pod collector connections into one +// per-node connection. +// +// It sits directly under BasePath, which is the host directory already mounted +// at the same path into atelet and into every ateom pod, so no new volume is +// needed for ateom to reach it. Note that BasePath is mounted writable +// (workerpool_apply.go) and shared with AteomSupportSocket and the image +// cache, so a worker pod can unlink or replace this socket. Confining +// atelet-owned sockets to a subdirectory mounted read-only would be an +// improvement, but it is a property of the whole BasePath mount rather than of +// this socket — a read-only subdir needs its own volume and mount, and the pod +// keeps CAP_SYS_ADMIN. Tracked separately rather than solved here. +func AteletOTLPSocketPath() string { + return filepath.Join( + BasePath, + "atelet-otlp.sock", + ) +} + +// AteomsDir is the parent of every per-ateom directory. Each ateom creates +// AteomPath(podUID) under it when it boots, so listing this directory is how a +// scraper with no prior knowledge discovers the node's ateoms. +func AteomsDir() string { + return filepath.Join(BasePath, "ateoms") +} + +func AteomPath(podUID string) string { + return filepath.Join(AteomsDir(), podUID) +} + +func AteomSocketPath(podUID string) string { + return filepath.Join( + AteomPath(podUID), + "ateom.sock", + ) +} + +// ActorNetNSName names an actor's sandbox network namespace. +func ActorNetNSName(actorUID string) string { + return "ateom-actor:" + actorUID +} + +// ActorNetNSPath is the mount path of the actor's named namespace. +func ActorNetNSPath(actorUID string) string { + return filepath.Join("/run/netns", ActorNetNSName(actorUID)) +} diff --git a/internal/ateompath/ateompath_test.go b/internal/nodepath/nodepath_test.go similarity index 86% rename from internal/ateompath/ateompath_test.go rename to internal/nodepath/nodepath_test.go index d4b372ba01..caf5073716 100644 --- a/internal/ateompath/ateompath_test.go +++ b/internal/nodepath/nodepath_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ateompath +package nodepath import ( "strings" @@ -80,17 +80,3 @@ func TestAteomPathUniqueness(t *testing.T) { t.Errorf("expected different paths for different pod UIDs, got %q", path1) } } - -func TestActorPathUsesUID(t *testing.T) { - uid1 := "123e4567-e89b-12d3-a456-426614174000" - uid2 := "987f6543-e21b-32d1-b654-246614174111" - - path1 := ActorPath(uid1) - path2 := ActorPath(uid2) - if path1 == path2 { - t.Fatalf("different actor UIDs produced the same path %q", path1) - } - if want := "/actors/" + uid1; !strings.HasSuffix(path1, want) { - t.Errorf("ActorPath(%q) = %q, want suffix %q", uid1, path1, want) - } -} diff --git a/internal/ocispec/microvm.go b/internal/ocispec/microvm.go index e5c7d5d6ee..9b93a1f8b5 100644 --- a/internal/ocispec/microvm.go +++ b/internal/ocispec/microvm.go @@ -17,10 +17,12 @@ package ocispec import ( "fmt" "path" + "path/filepath" "slices" "strings" - "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/internal/imagecache" + "github.com/agent-substrate/substrate/internal/proto/ateompb" "github.com/opencontainers/runtime-spec/specs-go" ) @@ -39,7 +41,9 @@ const ( // MicroVMOptions describes the micro-VM context of one actor container. type MicroVMOptions struct { - ActorUID string + // ActorDirs are the actor's directories; volume bind sources under them are + // staged into the share. + ActorDirs *ateompb.ActorDirs ContainerID string } @@ -54,7 +58,7 @@ func ShapeMicroVM(spec *specs.Spec, o MicroVMOptions) error { if m.Type != "bind" { continue } - src, err := guestVolumeSource(m.Source, o.ActorUID, o.ContainerID) + src, err := guestVolumeSource(m.Source, o.ActorDirs, o.ContainerID) if err != nil { return fmt.Errorf("mount %q: %w", m.Destination, err) } @@ -108,13 +112,17 @@ func mergeKataResources(from *specs.LinuxResources) *specs.LinuxResources { } // guestVolumeSource maps a volume's host directory to its guest path. -func guestVolumeSource(hostPath, actorUID, containerID string) (string, error) { +func guestVolumeSource(hostPath string, actorDirs *ateompb.ActorDirs, containerID string) (string, error) { for _, staged := range []struct{ host, guest string }{ - {ateompath.DurableDirVolumeMountsDir(actorUID), path.Join(GuestSharedDir, ShareDurable)}, - {ateompath.VolumesDir(actorUID), path.Join(GuestSharedDir, ShareCSI)}, - {ateompath.SystemInfoVolumeRootsDir(actorUID), path.Join(GuestSharedDir, ShareSystemInfo)}, - {ateompath.ImageVolumeMountPath(actorUID, containerID, ""), path.Join(GuestSharedDir, containerID, ShareVolumes)}, + {actorDirs.GetDurableDirVolumeMountsDir(), path.Join(GuestSharedDir, ShareDurable)}, + {actorDirs.GetVolumesDir(), path.Join(GuestSharedDir, ShareCSI)}, + {actorDirs.GetSystemInfoVolumeRootsDir(), path.Join(GuestSharedDir, ShareSystemInfo)}, + {imagecache.ImageVolumeMountPath(filepath.Join(actorDirs.GetOciBundleDir(), containerID), ""), path.Join(GuestSharedDir, containerID, ShareVolumes)}, } { + // An unset directory must not turn into a prefix every path matches. + if !filepath.IsAbs(staged.host) { + continue + } if rel, ok := strings.CutPrefix(hostPath, staged.host+"/"); ok { return path.Join(staged.guest, rel), nil } diff --git a/internal/ocispec/microvm_test.go b/internal/ocispec/microvm_test.go index cb7ce5da08..fb3f808fa5 100644 --- a/internal/ocispec/microvm_test.go +++ b/internal/ocispec/microvm_test.go @@ -122,10 +122,10 @@ func eqInt64Ptr(a, b *int64) bool { func TestShapeMicroVM_KeepsDeclaredContainerLimits(t *testing.T) { const declared = 64 * 1024 * 1024 spec := Build(Options{ - ActorUID: testActorUID, ContainerName: "app", Args: []string{"/app"}, + Args: []string{"/app"}, Resources: &ateletpb.ResourceLimits{MemoryBytes: declared}, }) - if err := ShapeMicroVM(spec, MicroVMOptions{ActorUID: testActorUID, ContainerID: "app"}); err != nil { + if err := ShapeMicroVM(spec, MicroVMOptions{ActorDirs: parityActorDirs, ContainerID: "app"}); err != nil { t.Fatalf("ShapeMicroVM() = %v", err) } @@ -140,8 +140,8 @@ func TestShapeMicroVM_KeepsDeclaredContainerLimits(t *testing.T) { // A container that declares nothing must stay unbounded inside the guest: guest // RAM is the real ceiling, and a cap equal to the whole guest can never bind. func TestShapeMicroVM_LeavesUndeclaredContainerUnlimited(t *testing.T) { - spec := Build(Options{ActorUID: testActorUID, ContainerName: "app", Args: []string{"/app"}}) - if err := ShapeMicroVM(spec, MicroVMOptions{ActorUID: testActorUID, ContainerID: "app"}); err != nil { + spec := Build(Options{Args: []string{"/app"}}) + if err := ShapeMicroVM(spec, MicroVMOptions{ActorDirs: parityActorDirs, ContainerID: "app"}); err != nil { t.Fatalf("ShapeMicroVM() = %v", err) } diff --git a/internal/ocispec/ocispec.go b/internal/ocispec/ocispec.go index 1e398c8379..5c59a4045a 100644 --- a/internal/ocispec/ocispec.go +++ b/internal/ocispec/ocispec.go @@ -21,8 +21,9 @@ import ( "fmt" "os" "path" + "path/filepath" - "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/internal/imagecache" "github.com/agent-substrate/substrate/internal/proto/ateletpb" "github.com/opencontainers/runtime-spec/specs-go" ) @@ -36,10 +37,8 @@ const hostname = "actor" // Options describes one actor container. Args, Env and Capabilities arrive // already resolved. type Options struct { - ActorUID string - ContainerName string - Args []string - Env []string + Args []string + Env []string // NetNSPath is the network namespace the ateom runs the actor in. NetNSPath string Volumes []*ateletpb.Volume @@ -47,6 +46,15 @@ type Options struct { Capabilities []string // Resources are the container's own declared limits, or nil for none. Resources *ateletpb.ResourceLimits + + // The actor's directories the bind mount sources are joined from, one + // subdirectory per volume name. + DurableDirVolumeMountsDir string + VolumesDir string + SystemInfoVolumeRootsDir string + // BundlePath is this container's bundle, where its image volumes are + // composed. + BundlePath string } const ( @@ -167,14 +175,14 @@ func Build(o Options) *specs.Spec { options := []string{"bind", "rw"} switch volumesByName[vm.GetName()].GetSource().(type) { case *ateletpb.Volume_DurableDir: - srcPath = ateompath.DurableDirVolumeMountPoint(o.ActorUID, vm.GetName()) + srcPath = filepath.Join(o.DurableDirVolumeMountsDir, vm.GetName()) case *ateletpb.Volume_External: - srcPath = ateompath.VolumeHostPath(o.ActorUID, vm.GetName()) + srcPath = filepath.Join(o.VolumesDir, vm.GetName()) case *ateletpb.Volume_SystemInfo: - srcPath = ateompath.SystemInfoVolumeRoot(o.ActorUID, vm.GetName()) + srcPath = filepath.Join(o.SystemInfoVolumeRootsDir, vm.GetName()) options = []string{"bind", "ro"} case *ateletpb.Volume_Image: - srcPath = ateompath.ImageVolumeMountPath(o.ActorUID, o.ContainerName, vm.GetName()) + srcPath = imagecache.ImageVolumeMountPath(o.BundlePath, vm.GetName()) options = []string{"bind", "ro"} default: continue diff --git a/internal/ocispec/ocispec_test.go b/internal/ocispec/ocispec_test.go index 31c70d1993..d19d4ae80a 100644 --- a/internal/ocispec/ocispec_test.go +++ b/internal/ocispec/ocispec_test.go @@ -18,13 +18,11 @@ import ( "slices" "testing" - "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/internal/imagecache" "github.com/agent-substrate/substrate/internal/proto/ateletpb" "github.com/opencontainers/runtime-spec/specs-go" ) -const testActorUID = "actor_uid" - // mountFor returns the spec's mount at destination dest, or fails. func mountFor(t *testing.T, spec *specs.Spec, dest string) specs.Mount { t.Helper() @@ -49,11 +47,15 @@ func TestBuild_VolumeMounts(t *testing.T) { {Name: "csi", Source: &ateletpb.Volume_External{External: &ateletpb.ExternalVolumeSource{}}}, {Name: "agent", Source: &ateletpb.Volume_Image{Image: &ateletpb.ImageVolumeSource{}}}, } + const ( + durableDir = "/node/actors/a/durable-dir" + volumesDir = "/node/actors/a/volumes" + sysInfoDir = "/node/actors/a/system-info" + bundle = "/node/actors/a/bundles/app" + ) spec := Build(Options{ - ActorUID: testActorUID, - ContainerName: "app", - Args: []string{"/app"}, - Volumes: volumes, + Args: []string{"/app"}, + Volumes: volumes, VolumeMounts: []*ateletpb.VolumeMount{ {Name: "data", MountPath: "/var/data"}, {Name: "data", MountPath: "/home/counter"}, @@ -61,6 +63,10 @@ func TestBuild_VolumeMounts(t *testing.T) { {Name: "csi", MountPath: "/mnt/csi"}, {Name: "agent", MountPath: "/ate"}, }, + DurableDirVolumeMountsDir: durableDir, + VolumesDir: volumesDir, + SystemInfoVolumeRootsDir: sysInfoDir, + BundlePath: bundle, }) for _, tc := range []struct { @@ -68,11 +74,11 @@ func TestBuild_VolumeMounts(t *testing.T) { wantSource string wantOpts []string }{ - {"/var/data", ateompath.DurableDirVolumeMountPoint(testActorUID, "data"), []string{"bind", "rw"}}, - {"/home/counter", ateompath.DurableDirVolumeMountPoint(testActorUID, "data"), []string{"bind", "rw"}}, - {"/run/ate", ateompath.SystemInfoVolumeRoot(testActorUID, "sysinfo"), []string{"bind", "ro"}}, - {"/mnt/csi", ateompath.VolumeHostPath(testActorUID, "csi"), []string{"bind", "rw"}}, - {"/ate", ateompath.ImageVolumeMountPath(testActorUID, "app", "agent"), []string{"bind", "ro"}}, + {"/var/data", durableDir + "/data", []string{"bind", "rw"}}, + {"/home/counter", durableDir + "/data", []string{"bind", "rw"}}, + {"/run/ate", sysInfoDir + "/sysinfo", []string{"bind", "ro"}}, + {"/mnt/csi", volumesDir + "/csi", []string{"bind", "rw"}}, + {"/ate", imagecache.ImageVolumeMountPath(bundle, "agent"), []string{"bind", "ro"}}, } { m := mountFor(t, spec, tc.dest) if m.Type != "bind" { @@ -90,9 +96,7 @@ func TestBuild_VolumeMounts(t *testing.T) { // A mount naming an undeclared volume is skipped. func TestBuild_UnknownVolumeMountSkipped(t *testing.T) { spec := Build(Options{ - ActorUID: testActorUID, - ContainerName: "app", - VolumeMounts: []*ateletpb.VolumeMount{{Name: "missing", MountPath: "/mnt/missing"}}, + VolumeMounts: []*ateletpb.VolumeMount{{Name: "missing", MountPath: "/mnt/missing"}}, }) for _, m := range spec.Mounts { if m.Destination == "/mnt/missing" { @@ -104,7 +108,7 @@ func TestBuild_UnknownVolumeMountSkipped(t *testing.T) { // The resolved set lands in bounding, effective and permitted only. func TestBuild_Capabilities(t *testing.T) { want := []string{"CAP_CHOWN", "CAP_KILL"} - spec := Build(Options{ActorUID: testActorUID, ContainerName: "app", Args: []string{"/app"}, Capabilities: want}) + spec := Build(Options{Args: []string{"/app"}, Capabilities: want}) caps := spec.Process.Capabilities if caps == nil { @@ -137,7 +141,7 @@ func TestBuild_Capabilities(t *testing.T) { // The pause container gets no capabilities. func TestBuild_NoCapabilitiesForPause(t *testing.T) { - spec := Build(Options{ActorUID: testActorUID, ContainerName: PauseContainer, Args: []string{"/pause"}}) + spec := Build(Options{Args: []string{"/pause"}}) caps := spec.Process.Capabilities if caps == nil { @@ -161,7 +165,7 @@ func TestBuild_NoCapabilitiesForPause(t *testing.T) { func TestSaveLoadRoundTrip(t *testing.T) { bundle := t.TempDir() - want := Build(Options{ActorUID: testActorUID, ContainerName: "app", Args: []string{"/app"}, NetNSPath: "/run/netns/x"}) + want := Build(Options{Args: []string{"/app"}, NetNSPath: "/run/netns/x"}) if err := Save(bundle, want); err != nil { t.Fatalf("Save() = %v", err) } @@ -235,7 +239,7 @@ func TestOCIResources_NegativeIsUnset(t *testing.T) { // A container without limits carries no linux.resources at all. func TestBuild_NoResourcesLeavesLinuxUntouched(t *testing.T) { - spec := Build(Options{ActorUID: testActorUID, ContainerName: PauseContainer, Args: []string{"/pause"}}) + spec := Build(Options{Args: []string{"/pause"}}) if spec.Linux.Resources != nil { t.Errorf("Linux.Resources = %v, want nil when no limits are declared", spec.Linux.Resources) } @@ -243,7 +247,7 @@ func TestBuild_NoResourcesLeavesLinuxUntouched(t *testing.T) { func TestBuild_ResourcesApplied(t *testing.T) { spec := Build(Options{ - ActorUID: testActorUID, ContainerName: "app", Args: []string{"/app"}, + Args: []string{"/app"}, Resources: &ateletpb.ResourceLimits{MemoryBytes: 67108864}, }) if spec.Linux.Resources == nil || spec.Linux.Resources.Memory == nil { diff --git a/internal/ocispec/parity_test.go b/internal/ocispec/parity_test.go index 3e0b9de6c4..10b62e8def 100644 --- a/internal/ocispec/parity_test.go +++ b/internal/ocispec/parity_test.go @@ -21,15 +21,28 @@ import ( "testing" "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "github.com/agent-substrate/substrate/internal/proto/ateompb" "github.com/agent-substrate/substrate/internal/sizing" "github.com/opencontainers/runtime-spec/specs-go" ) // parityOptions mounts one volume of every kind. +const testActorUID = "actor_uid" + +var parityActorDirs = &ateompb.ActorDirs{ + RootDir: "/node/actors/a", + OciBundleDir: "/node/actors/a/bundles", + DurableDirVolumeMountsDir: "/node/actors/a/durable-dir", + SystemInfoVolumeRootsDir: "/node/actors/a/system-info", + VolumesDir: "/node/actors/a/volumes", +} + var parityOptions = Options{ - ActorUID: testActorUID, - ContainerName: "app", - Args: []string{"/app"}, + Args: []string{"/app"}, + DurableDirVolumeMountsDir: parityActorDirs.GetDurableDirVolumeMountsDir(), + VolumesDir: parityActorDirs.GetVolumesDir(), + SystemInfoVolumeRootsDir: parityActorDirs.GetSystemInfoVolumeRootsDir(), + BundlePath: parityActorDirs.GetOciBundleDir() + "/app", Volumes: []*ateletpb.Volume{ durableVolume("data"), {Name: "sysinfo", Source: &ateletpb.Volume_SystemInfo{SystemInfo: &ateletpb.SystemInfoVolume{}}}, @@ -74,7 +87,7 @@ func TestShapers_PreserveEveryVolumeMount(t *testing.T) { }, { runtime: "microvm", shape: func(s *specs.Spec) error { - return ShapeMicroVM(s, MicroVMOptions{ActorUID: testActorUID, ContainerID: "app"}) + return ShapeMicroVM(s, MicroVMOptions{ActorDirs: parityActorDirs, ContainerID: "app"}) }, }} { t.Run(tc.runtime, func(t *testing.T) { @@ -104,7 +117,7 @@ func TestShapers_PreserveEveryVolumeMount(t *testing.T) { // ShapeMicroVM rewrites bind sources to their guest share paths. func TestShapeMicroVM_TranslatesSourcesIntoTheShare(t *testing.T) { spec := Build(parityOptions) - if err := ShapeMicroVM(spec, MicroVMOptions{ActorUID: testActorUID, ContainerID: "app"}); err != nil { + if err := ShapeMicroVM(spec, MicroVMOptions{ActorDirs: parityActorDirs, ContainerID: "app"}); err != nil { t.Fatalf("ShapeMicroVM() = %v", err) } for _, tc := range []struct{ dest, wantSource string }{ @@ -125,13 +138,21 @@ func TestShapeMicroVM_TranslatesSourcesIntoTheShare(t *testing.T) { // ShapeMicroVM errors on a bind that is not staged into the share. func TestShapeMicroVM_UnstagedSourceIsAnError(t *testing.T) { - spec := Build(Options{ActorUID: testActorUID, ContainerName: "app", Args: []string{"/app"}}) + spec := Build(Options{Args: []string{"/app"}}) spec.Mounts = append(spec.Mounts, specs.Mount{Destination: "/mnt/new", Type: "bind", Source: "/var/lib/ate/new-kind/x"}) - if err := ShapeMicroVM(spec, MicroVMOptions{ActorUID: testActorUID, ContainerID: "app"}); err == nil { + if err := ShapeMicroVM(spec, MicroVMOptions{ActorDirs: parityActorDirs, ContainerID: "app"}); err == nil { t.Fatal("ShapeMicroVM() = nil, want an error for a bind that is not staged into the share") } } +// With no actor directories nothing is staged, so every volume bind errors. +func TestShapeMicroVM_UnsetDirsStageNothing(t *testing.T) { + spec := Build(parityOptions) + if err := ShapeMicroVM(spec, MicroVMOptions{ActorDirs: &ateompb.ActorDirs{}, ContainerID: "app"}); err == nil { + t.Fatal("ShapeMicroVM() = nil, want an error when no directory is staged") + } +} + // ShapeGVisor inserts resolv.conf ahead of the volume bind mounts. func TestShapeGVisor_ResolvConfPrecedesVolumes(t *testing.T) { spec := Build(parityOptions) diff --git a/internal/proto/ateompb/ateom.pb.go b/internal/proto/ateompb/ateom.pb.go index 376dfef7f5..3f95707678 100644 --- a/internal/proto/ateompb/ateom.pb.go +++ b/internal/proto/ateompb/ateom.pb.go @@ -212,6 +212,127 @@ func (StatsSource) EnumDescriptor() ([]byte, []int) { return file_ateom_proto_rawDescGZIP(), []int{2} } +// ActorDirs is the set of per-actor directories atelet prepares on the volume +// it shares with ateom. ateom takes them from the RPC instead of deriving +// them from the actor UID. +// Every directory is under root_dir. +type ActorDirs struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The actor's own directory. + RootDir string `protobuf:"bytes,1,opt,name=root_dir,json=rootDir,proto3" json:"root_dir,omitempty"` + // Holds one OCI bundle per container, named after the container. + OciBundleDir string `protobuf:"bytes,2,opt,name=oci_bundle_dir,json=ociBundleDir,proto3" json:"oci_bundle_dir,omitempty"` + // runsc --root for the actor's sandbox. gVisor only. + RunscStateDir string `protobuf:"bytes,3,opt,name=runsc_state_dir,json=runscStateDir,proto3" json:"runsc_state_dir,omitempty"` + // Where runsc writes .pid. gVisor only. + PidFileDir string `protobuf:"bytes,4,opt,name=pid_file_dir,json=pidFileDir,proto3" json:"pid_file_dir,omitempty"` + // Where CheckpointWorkload writes the snapshot files. + CheckpointDir string `protobuf:"bytes,5,opt,name=checkpoint_dir,json=checkpointDir,proto3" json:"checkpoint_dir,omitempty"` + // Where the snapshot files RestoreWorkload reads have been downloaded to. + RestoreDir string `protobuf:"bytes,6,opt,name=restore_dir,json=restoreDir,proto3" json:"restore_dir,omitempty"` + // Holds one subdirectory per durable-dir volume. + DurableDirVolumeMountsDir string `protobuf:"bytes,7,opt,name=durable_dir_volume_mounts_dir,json=durableDirVolumeMountsDir,proto3" json:"durable_dir_volume_mounts_dir,omitempty"` + // Holds one subdirectory per system-info volume. + SystemInfoVolumeRootsDir string `protobuf:"bytes,8,opt,name=system_info_volume_roots_dir,json=systemInfoVolumeRootsDir,proto3" json:"system_info_volume_roots_dir,omitempty"` + // Holds one mount point per CSI volume. + VolumesDir string `protobuf:"bytes,9,opt,name=volumes_dir,json=volumesDir,proto3" json:"volumes_dir,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ActorDirs) Reset() { + *x = ActorDirs{} + mi := &file_ateom_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ActorDirs) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ActorDirs) ProtoMessage() {} + +func (x *ActorDirs) ProtoReflect() protoreflect.Message { + mi := &file_ateom_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ActorDirs.ProtoReflect.Descriptor instead. +func (*ActorDirs) Descriptor() ([]byte, []int) { + return file_ateom_proto_rawDescGZIP(), []int{0} +} + +func (x *ActorDirs) GetRootDir() string { + if x != nil { + return x.RootDir + } + return "" +} + +func (x *ActorDirs) GetOciBundleDir() string { + if x != nil { + return x.OciBundleDir + } + return "" +} + +func (x *ActorDirs) GetRunscStateDir() string { + if x != nil { + return x.RunscStateDir + } + return "" +} + +func (x *ActorDirs) GetPidFileDir() string { + if x != nil { + return x.PidFileDir + } + return "" +} + +func (x *ActorDirs) GetCheckpointDir() string { + if x != nil { + return x.CheckpointDir + } + return "" +} + +func (x *ActorDirs) GetRestoreDir() string { + if x != nil { + return x.RestoreDir + } + return "" +} + +func (x *ActorDirs) GetDurableDirVolumeMountsDir() string { + if x != nil { + return x.DurableDirVolumeMountsDir + } + return "" +} + +func (x *ActorDirs) GetSystemInfoVolumeRootsDir() string { + if x != nil { + return x.SystemInfoVolumeRootsDir + } + return "" +} + +func (x *ActorDirs) GetVolumesDir() string { + if x != nil { + return x.VolumesDir + } + return "" +} + type TerminateWorkloadRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Atespace string `protobuf:"bytes,1,opt,name=atespace,proto3" json:"atespace,omitempty"` @@ -221,13 +342,14 @@ type TerminateWorkloadRequest struct { ActorTemplateName string `protobuf:"bytes,5,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` RunscPath string `protobuf:"bytes,6,opt,name=runsc_path,json=runscPath,proto3" json:"runsc_path,omitempty"` Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` + ActorDirs *ActorDirs `protobuf:"bytes,8,opt,name=actor_dirs,json=actorDirs,proto3" json:"actor_dirs,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TerminateWorkloadRequest) Reset() { *x = TerminateWorkloadRequest{} - mi := &file_ateom_proto_msgTypes[0] + mi := &file_ateom_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -239,7 +361,7 @@ func (x *TerminateWorkloadRequest) String() string { func (*TerminateWorkloadRequest) ProtoMessage() {} func (x *TerminateWorkloadRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[0] + mi := &file_ateom_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -252,7 +374,7 @@ func (x *TerminateWorkloadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TerminateWorkloadRequest.ProtoReflect.Descriptor instead. func (*TerminateWorkloadRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{0} + return file_ateom_proto_rawDescGZIP(), []int{1} } func (x *TerminateWorkloadRequest) GetAtespace() string { @@ -304,6 +426,13 @@ func (x *TerminateWorkloadRequest) GetSpec() *WorkloadSpec { return nil } +func (x *TerminateWorkloadRequest) GetActorDirs() *ActorDirs { + if x != nil { + return x.ActorDirs + } + return nil +} + type TerminateWorkloadResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -312,7 +441,7 @@ type TerminateWorkloadResponse struct { func (x *TerminateWorkloadResponse) Reset() { *x = TerminateWorkloadResponse{} - mi := &file_ateom_proto_msgTypes[1] + mi := &file_ateom_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -324,7 +453,7 @@ func (x *TerminateWorkloadResponse) String() string { func (*TerminateWorkloadResponse) ProtoMessage() {} func (x *TerminateWorkloadResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[1] + mi := &file_ateom_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -337,7 +466,7 @@ func (x *TerminateWorkloadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TerminateWorkloadResponse.ProtoReflect.Descriptor instead. func (*TerminateWorkloadResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{1} + return file_ateom_proto_rawDescGZIP(), []int{2} } type RunWorkloadRequest struct { @@ -360,15 +489,16 @@ type RunWorkloadRequest struct { // sizes the sandbox to these (cgroup caps via the OCI spec, and for the // micro-VM the VM's vCPU count and memory). Zero means "unset": keep the // runtime default (unlimited for gVisor, ateom's own default for the micro-VM). - CpuMilli int64 `protobuf:"varint,11,opt,name=cpu_milli,json=cpuMilli,proto3" json:"cpu_milli,omitempty"` // CPU limit in millicores (1000 = one core). - MemoryBytes int64 `protobuf:"varint,12,opt,name=memory_bytes,json=memoryBytes,proto3" json:"memory_bytes,omitempty"` // Memory limit in bytes. + CpuMilli int64 `protobuf:"varint,11,opt,name=cpu_milli,json=cpuMilli,proto3" json:"cpu_milli,omitempty"` // CPU limit in millicores (1000 = one core). + MemoryBytes int64 `protobuf:"varint,12,opt,name=memory_bytes,json=memoryBytes,proto3" json:"memory_bytes,omitempty"` // Memory limit in bytes. + ActorDirs *ActorDirs `protobuf:"bytes,13,opt,name=actor_dirs,json=actorDirs,proto3" json:"actor_dirs,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RunWorkloadRequest) Reset() { *x = RunWorkloadRequest{} - mi := &file_ateom_proto_msgTypes[2] + mi := &file_ateom_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -380,7 +510,7 @@ func (x *RunWorkloadRequest) String() string { func (*RunWorkloadRequest) ProtoMessage() {} func (x *RunWorkloadRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[2] + mi := &file_ateom_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -393,7 +523,7 @@ func (x *RunWorkloadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RunWorkloadRequest.ProtoReflect.Descriptor instead. func (*RunWorkloadRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{2} + return file_ateom_proto_rawDescGZIP(), []int{3} } func (x *RunWorkloadRequest) GetAtespace() string { @@ -473,6 +603,13 @@ func (x *RunWorkloadRequest) GetMemoryBytes() int64 { return 0 } +func (x *RunWorkloadRequest) GetActorDirs() *ActorDirs { + if x != nil { + return x.ActorDirs + } + return nil +} + // EgressGateway configures tunneled egress for one actor activation. type EgressGateway struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -485,7 +622,7 @@ type EgressGateway struct { func (x *EgressGateway) Reset() { *x = EgressGateway{} - mi := &file_ateom_proto_msgTypes[3] + mi := &file_ateom_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -497,7 +634,7 @@ func (x *EgressGateway) String() string { func (*EgressGateway) ProtoMessage() {} func (x *EgressGateway) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[3] + mi := &file_ateom_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -510,7 +647,7 @@ func (x *EgressGateway) ProtoReflect() protoreflect.Message { // Deprecated: Use EgressGateway.ProtoReflect.Descriptor instead. func (*EgressGateway) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{3} + return file_ateom_proto_rawDescGZIP(), []int{4} } func (x *EgressGateway) GetAddress() string { @@ -530,7 +667,7 @@ type WorkloadSpec struct { func (x *WorkloadSpec) Reset() { *x = WorkloadSpec{} - mi := &file_ateom_proto_msgTypes[4] + mi := &file_ateom_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -542,7 +679,7 @@ func (x *WorkloadSpec) String() string { func (*WorkloadSpec) ProtoMessage() {} func (x *WorkloadSpec) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[4] + mi := &file_ateom_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -555,7 +692,7 @@ func (x *WorkloadSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkloadSpec.ProtoReflect.Descriptor instead. func (*WorkloadSpec) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{4} + return file_ateom_proto_rawDescGZIP(), []int{5} } func (x *WorkloadSpec) GetContainers() []*Container { @@ -586,7 +723,7 @@ type Container struct { func (x *Container) Reset() { *x = Container{} - mi := &file_ateom_proto_msgTypes[5] + mi := &file_ateom_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -598,7 +735,7 @@ func (x *Container) String() string { func (*Container) ProtoMessage() {} func (x *Container) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[5] + mi := &file_ateom_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -611,7 +748,7 @@ func (x *Container) ProtoReflect() protoreflect.Message { // Deprecated: Use Container.ProtoReflect.Descriptor instead. func (*Container) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{5} + return file_ateom_proto_rawDescGZIP(), []int{6} } func (x *Container) GetName() string { @@ -667,7 +804,7 @@ type VolumeMount struct { func (x *VolumeMount) Reset() { *x = VolumeMount{} - mi := &file_ateom_proto_msgTypes[6] + mi := &file_ateom_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -679,7 +816,7 @@ func (x *VolumeMount) String() string { func (*VolumeMount) ProtoMessage() {} func (x *VolumeMount) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[6] + mi := &file_ateom_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -692,7 +829,7 @@ func (x *VolumeMount) ProtoReflect() protoreflect.Message { // Deprecated: Use VolumeMount.ProtoReflect.Descriptor instead. func (*VolumeMount) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{6} + return file_ateom_proto_rawDescGZIP(), []int{7} } func (x *VolumeMount) GetVolumeName() string { @@ -723,7 +860,7 @@ type DurableDirVolumeMount struct { func (x *DurableDirVolumeMount) Reset() { *x = DurableDirVolumeMount{} - mi := &file_ateom_proto_msgTypes[7] + mi := &file_ateom_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -735,7 +872,7 @@ func (x *DurableDirVolumeMount) String() string { func (*DurableDirVolumeMount) ProtoMessage() {} func (x *DurableDirVolumeMount) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[7] + mi := &file_ateom_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -748,7 +885,7 @@ func (x *DurableDirVolumeMount) ProtoReflect() protoreflect.Message { // Deprecated: Use DurableDirVolumeMount.ProtoReflect.Descriptor instead. func (*DurableDirVolumeMount) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{7} + return file_ateom_proto_rawDescGZIP(), []int{8} } func (x *DurableDirVolumeMount) GetVolumeName() string { @@ -781,7 +918,7 @@ type SystemInfoVolumeMount struct { func (x *SystemInfoVolumeMount) Reset() { *x = SystemInfoVolumeMount{} - mi := &file_ateom_proto_msgTypes[8] + mi := &file_ateom_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -793,7 +930,7 @@ func (x *SystemInfoVolumeMount) String() string { func (*SystemInfoVolumeMount) ProtoMessage() {} func (x *SystemInfoVolumeMount) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[8] + mi := &file_ateom_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -806,7 +943,7 @@ func (x *SystemInfoVolumeMount) ProtoReflect() protoreflect.Message { // Deprecated: Use SystemInfoVolumeMount.ProtoReflect.Descriptor instead. func (*SystemInfoVolumeMount) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{8} + return file_ateom_proto_rawDescGZIP(), []int{9} } func (x *SystemInfoVolumeMount) GetVolumeName() string { @@ -838,7 +975,7 @@ type ImageVolumeMount struct { func (x *ImageVolumeMount) Reset() { *x = ImageVolumeMount{} - mi := &file_ateom_proto_msgTypes[9] + mi := &file_ateom_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -850,7 +987,7 @@ func (x *ImageVolumeMount) String() string { func (*ImageVolumeMount) ProtoMessage() {} func (x *ImageVolumeMount) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[9] + mi := &file_ateom_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -863,7 +1000,7 @@ func (x *ImageVolumeMount) ProtoReflect() protoreflect.Message { // Deprecated: Use ImageVolumeMount.ProtoReflect.Descriptor instead. func (*ImageVolumeMount) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{9} + return file_ateom_proto_rawDescGZIP(), []int{10} } func (x *ImageVolumeMount) GetVolumeName() string { @@ -893,7 +1030,7 @@ type WakeupProbe struct { func (x *WakeupProbe) Reset() { *x = WakeupProbe{} - mi := &file_ateom_proto_msgTypes[10] + mi := &file_ateom_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -905,7 +1042,7 @@ func (x *WakeupProbe) String() string { func (*WakeupProbe) ProtoMessage() {} func (x *WakeupProbe) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[10] + mi := &file_ateom_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -918,7 +1055,7 @@ func (x *WakeupProbe) ProtoReflect() protoreflect.Message { // Deprecated: Use WakeupProbe.ProtoReflect.Descriptor instead. func (*WakeupProbe) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{10} + return file_ateom_proto_rawDescGZIP(), []int{11} } func (x *WakeupProbe) GetHttpGet() *HTTPGetAction { @@ -948,7 +1085,7 @@ type HTTPGetAction struct { func (x *HTTPGetAction) Reset() { *x = HTTPGetAction{} - mi := &file_ateom_proto_msgTypes[11] + mi := &file_ateom_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -960,7 +1097,7 @@ func (x *HTTPGetAction) String() string { func (*HTTPGetAction) ProtoMessage() {} func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[11] + mi := &file_ateom_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -973,7 +1110,7 @@ func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { // Deprecated: Use HTTPGetAction.ProtoReflect.Descriptor instead. func (*HTTPGetAction) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{11} + return file_ateom_proto_rawDescGZIP(), []int{12} } func (x *HTTPGetAction) GetPath() string { @@ -998,7 +1135,7 @@ type RunWorkloadResponse struct { func (x *RunWorkloadResponse) Reset() { *x = RunWorkloadResponse{} - mi := &file_ateom_proto_msgTypes[12] + mi := &file_ateom_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1010,7 +1147,7 @@ func (x *RunWorkloadResponse) String() string { func (*RunWorkloadResponse) ProtoMessage() {} func (x *RunWorkloadResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[12] + mi := &file_ateom_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1023,7 +1160,7 @@ func (x *RunWorkloadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RunWorkloadResponse.ProtoReflect.Descriptor instead. func (*RunWorkloadResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{12} + return file_ateom_proto_rawDescGZIP(), []int{13} } type CheckpointWorkloadRequest struct { @@ -1051,13 +1188,14 @@ type CheckpointWorkloadRequest struct { RuntimeAssetPaths map[string]string `protobuf:"bytes,9,rep,name=runtime_asset_paths,json=runtimeAssetPaths,proto3" json:"runtime_asset_paths,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // What content to include in the checkpoint. Scope SnapshotScope `protobuf:"varint,10,opt,name=scope,proto3,enum=ateom.SnapshotScope" json:"scope,omitempty"` + ActorDirs *ActorDirs `protobuf:"bytes,11,opt,name=actor_dirs,json=actorDirs,proto3" json:"actor_dirs,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CheckpointWorkloadRequest) Reset() { *x = CheckpointWorkloadRequest{} - mi := &file_ateom_proto_msgTypes[13] + mi := &file_ateom_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1069,7 +1207,7 @@ func (x *CheckpointWorkloadRequest) String() string { func (*CheckpointWorkloadRequest) ProtoMessage() {} func (x *CheckpointWorkloadRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[13] + mi := &file_ateom_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1082,7 +1220,7 @@ func (x *CheckpointWorkloadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointWorkloadRequest.ProtoReflect.Descriptor instead. func (*CheckpointWorkloadRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{13} + return file_ateom_proto_rawDescGZIP(), []int{14} } func (x *CheckpointWorkloadRequest) GetAtespace() string { @@ -1155,6 +1293,13 @@ func (x *CheckpointWorkloadRequest) GetScope() SnapshotScope { return SnapshotScope_SNAPSHOT_SCOPE_UNSPECIFIED } +func (x *CheckpointWorkloadRequest) GetActorDirs() *ActorDirs { + if x != nil { + return x.ActorDirs + } + return nil +} + type CheckpointWorkloadResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // snapshot_files lists the files ateom wrote into the checkpoint directory @@ -1167,7 +1312,7 @@ type CheckpointWorkloadResponse struct { func (x *CheckpointWorkloadResponse) Reset() { *x = CheckpointWorkloadResponse{} - mi := &file_ateom_proto_msgTypes[14] + mi := &file_ateom_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1179,7 +1324,7 @@ func (x *CheckpointWorkloadResponse) String() string { func (*CheckpointWorkloadResponse) ProtoMessage() {} func (x *CheckpointWorkloadResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[14] + mi := &file_ateom_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1192,7 +1337,7 @@ func (x *CheckpointWorkloadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointWorkloadResponse.ProtoReflect.Descriptor instead. func (*CheckpointWorkloadResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{14} + return file_ateom_proto_rawDescGZIP(), []int{15} } func (x *CheckpointWorkloadResponse) GetSnapshotFiles() []string { @@ -1229,15 +1374,16 @@ type RestoreWorkloadRequest struct { // (re)size the sandbox on a DATA-scope restore (fresh guest container). On a // FULL micro-VM restore the size baked into the snapshot is authoritative and // these are ignored. Zero means "unset": keep the runtime default. - CpuMilli int64 `protobuf:"varint,14,opt,name=cpu_milli,json=cpuMilli,proto3" json:"cpu_milli,omitempty"` // CPU limit in millicores (1000 = one core). - MemoryBytes int64 `protobuf:"varint,15,opt,name=memory_bytes,json=memoryBytes,proto3" json:"memory_bytes,omitempty"` // Memory limit in bytes. + CpuMilli int64 `protobuf:"varint,14,opt,name=cpu_milli,json=cpuMilli,proto3" json:"cpu_milli,omitempty"` // CPU limit in millicores (1000 = one core). + MemoryBytes int64 `protobuf:"varint,15,opt,name=memory_bytes,json=memoryBytes,proto3" json:"memory_bytes,omitempty"` // Memory limit in bytes. + ActorDirs *ActorDirs `protobuf:"bytes,16,opt,name=actor_dirs,json=actorDirs,proto3" json:"actor_dirs,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RestoreWorkloadRequest) Reset() { *x = RestoreWorkloadRequest{} - mi := &file_ateom_proto_msgTypes[15] + mi := &file_ateom_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1249,7 +1395,7 @@ func (x *RestoreWorkloadRequest) String() string { func (*RestoreWorkloadRequest) ProtoMessage() {} func (x *RestoreWorkloadRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[15] + mi := &file_ateom_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1262,7 +1408,7 @@ func (x *RestoreWorkloadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreWorkloadRequest.ProtoReflect.Descriptor instead. func (*RestoreWorkloadRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{15} + return file_ateom_proto_rawDescGZIP(), []int{16} } func (x *RestoreWorkloadRequest) GetAtespace() string { @@ -1363,6 +1509,13 @@ func (x *RestoreWorkloadRequest) GetMemoryBytes() int64 { return 0 } +func (x *RestoreWorkloadRequest) GetActorDirs() *ActorDirs { + if x != nil { + return x.ActorDirs + } + return nil +} + type RestoreWorkloadResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -1371,7 +1524,7 @@ type RestoreWorkloadResponse struct { func (x *RestoreWorkloadResponse) Reset() { *x = RestoreWorkloadResponse{} - mi := &file_ateom_proto_msgTypes[16] + mi := &file_ateom_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1383,7 +1536,7 @@ func (x *RestoreWorkloadResponse) String() string { func (*RestoreWorkloadResponse) ProtoMessage() {} func (x *RestoreWorkloadResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[16] + mi := &file_ateom_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1396,7 +1549,7 @@ func (x *RestoreWorkloadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreWorkloadResponse.ProtoReflect.Descriptor instead. func (*RestoreWorkloadResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{16} + return file_ateom_proto_rawDescGZIP(), []int{17} } type GetWorkloadStatsRequest struct { @@ -1412,7 +1565,7 @@ type GetWorkloadStatsRequest struct { func (x *GetWorkloadStatsRequest) Reset() { *x = GetWorkloadStatsRequest{} - mi := &file_ateom_proto_msgTypes[17] + mi := &file_ateom_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1424,7 +1577,7 @@ func (x *GetWorkloadStatsRequest) String() string { func (*GetWorkloadStatsRequest) ProtoMessage() {} func (x *GetWorkloadStatsRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[17] + mi := &file_ateom_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1437,7 +1590,7 @@ func (x *GetWorkloadStatsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkloadStatsRequest.ProtoReflect.Descriptor instead. func (*GetWorkloadStatsRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{17} + return file_ateom_proto_rawDescGZIP(), []int{18} } func (x *GetWorkloadStatsRequest) GetActorUid() string { @@ -1501,7 +1654,7 @@ type WorkloadStatsSample struct { func (x *WorkloadStatsSample) Reset() { *x = WorkloadStatsSample{} - mi := &file_ateom_proto_msgTypes[18] + mi := &file_ateom_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1513,7 +1666,7 @@ func (x *WorkloadStatsSample) String() string { func (*WorkloadStatsSample) ProtoMessage() {} func (x *WorkloadStatsSample) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[18] + mi := &file_ateom_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1526,7 +1679,7 @@ func (x *WorkloadStatsSample) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkloadStatsSample.ProtoReflect.Descriptor instead. func (*WorkloadStatsSample) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{18} + return file_ateom_proto_rawDescGZIP(), []int{19} } func (x *WorkloadStatsSample) GetAtespace() string { @@ -1622,7 +1775,7 @@ type GetWorkloadStatsResponse struct { func (x *GetWorkloadStatsResponse) Reset() { *x = GetWorkloadStatsResponse{} - mi := &file_ateom_proto_msgTypes[19] + mi := &file_ateom_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1634,7 +1787,7 @@ func (x *GetWorkloadStatsResponse) String() string { func (*GetWorkloadStatsResponse) ProtoMessage() {} func (x *GetWorkloadStatsResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[19] + mi := &file_ateom_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1647,7 +1800,7 @@ func (x *GetWorkloadStatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkloadStatsResponse.ProtoReflect.Descriptor instead. func (*GetWorkloadStatsResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{19} + return file_ateom_proto_rawDescGZIP(), []int{20} } func (x *GetWorkloadStatsResponse) GetSample() *WorkloadStatsSample { @@ -1665,7 +1818,7 @@ type GetActiveWorkloadStatsRequest struct { func (x *GetActiveWorkloadStatsRequest) Reset() { *x = GetActiveWorkloadStatsRequest{} - mi := &file_ateom_proto_msgTypes[20] + mi := &file_ateom_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1677,7 +1830,7 @@ func (x *GetActiveWorkloadStatsRequest) String() string { func (*GetActiveWorkloadStatsRequest) ProtoMessage() {} func (x *GetActiveWorkloadStatsRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[20] + mi := &file_ateom_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1690,7 +1843,7 @@ func (x *GetActiveWorkloadStatsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActiveWorkloadStatsRequest.ProtoReflect.Descriptor instead. func (*GetActiveWorkloadStatsRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{20} + return file_ateom_proto_rawDescGZIP(), []int{21} } type GetActiveWorkloadStatsResponse struct { @@ -1710,7 +1863,7 @@ type GetActiveWorkloadStatsResponse struct { func (x *GetActiveWorkloadStatsResponse) Reset() { *x = GetActiveWorkloadStatsResponse{} - mi := &file_ateom_proto_msgTypes[21] + mi := &file_ateom_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1722,7 +1875,7 @@ func (x *GetActiveWorkloadStatsResponse) String() string { func (*GetActiveWorkloadStatsResponse) ProtoMessage() {} func (x *GetActiveWorkloadStatsResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[21] + mi := &file_ateom_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1735,7 +1888,7 @@ func (x *GetActiveWorkloadStatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActiveWorkloadStatsResponse.ProtoReflect.Descriptor instead. func (*GetActiveWorkloadStatsResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{21} + return file_ateom_proto_rawDescGZIP(), []int{22} } func (x *GetActiveWorkloadStatsResponse) GetSamples() []*WorkloadStatsSample { @@ -1749,7 +1902,20 @@ var File_ateom_proto protoreflect.FileDescriptor const file_ateom_proto_rawDesc = "" + "\n" + - "\vateom.proto\x12\x05ateom\"\xa2\x02\n" + + "\vateom.proto\x12\x05ateom\"\x81\x03\n" + + "\tActorDirs\x12\x19\n" + + "\broot_dir\x18\x01 \x01(\tR\arootDir\x12$\n" + + "\x0eoci_bundle_dir\x18\x02 \x01(\tR\fociBundleDir\x12&\n" + + "\x0frunsc_state_dir\x18\x03 \x01(\tR\rrunscStateDir\x12 \n" + + "\fpid_file_dir\x18\x04 \x01(\tR\n" + + "pidFileDir\x12%\n" + + "\x0echeckpoint_dir\x18\x05 \x01(\tR\rcheckpointDir\x12\x1f\n" + + "\vrestore_dir\x18\x06 \x01(\tR\n" + + "restoreDir\x12@\n" + + "\x1ddurable_dir_volume_mounts_dir\x18\a \x01(\tR\x19durableDirVolumeMountsDir\x12>\n" + + "\x1csystem_info_volume_roots_dir\x18\b \x01(\tR\x18systemInfoVolumeRootsDir\x12\x1f\n" + + "\vvolumes_dir\x18\t \x01(\tR\n" + + "volumesDir\"\xd3\x02\n" + "\x18TerminateWorkloadRequest\x12\x1a\n" + "\batespace\x18\x01 \x01(\tR\batespace\x12\x1d\n" + "\n" + @@ -1759,8 +1925,10 @@ const file_ateom_proto_rawDesc = "" + "\x13actor_template_name\x18\x05 \x01(\tR\x11actorTemplateName\x12\x1d\n" + "\n" + "runsc_path\x18\x06 \x01(\tR\trunscPath\x12'\n" + - "\x04spec\x18\a \x01(\v2\x13.ateom.WorkloadSpecR\x04spec\"\x1b\n" + - "\x19TerminateWorkloadResponse\"\xd9\x04\n" + + "\x04spec\x18\a \x01(\v2\x13.ateom.WorkloadSpecR\x04spec\x12/\n" + + "\n" + + "actor_dirs\x18\b \x01(\v2\x10.ateom.ActorDirsR\tactorDirs\"\x1b\n" + + "\x19TerminateWorkloadResponse\"\x8a\x05\n" + "\x12RunWorkloadRequest\x12\x1a\n" + "\batespace\x18\x01 \x01(\tR\batespace\x12\x1d\n" + "\n" + @@ -1775,7 +1943,9 @@ const file_ateom_proto_rawDesc = "" + "\x0eegress_gateway\x18\n" + " \x01(\v2\x14.ateom.EgressGatewayH\x00R\regressGateway\x88\x01\x01\x12\x1b\n" + "\tcpu_milli\x18\v \x01(\x03R\bcpuMilli\x12!\n" + - "\fmemory_bytes\x18\f \x01(\x03R\vmemoryBytes\x1aD\n" + + "\fmemory_bytes\x18\f \x01(\x03R\vmemoryBytes\x12/\n" + + "\n" + + "actor_dirs\x18\r \x01(\v2\x10.ateom.ActorDirsR\tactorDirs\x1aD\n" + "\x16RuntimeAssetPathsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x11\n" + @@ -1819,7 +1989,7 @@ const file_ateom_proto_rawDesc = "" + "\rHTTPGetAction\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + "\x04port\x18\x02 \x01(\x05R\x04port\"\x15\n" + - "\x13RunWorkloadResponse\"\xa1\x04\n" + + "\x13RunWorkloadResponse\"\xd2\x04\n" + "\x19CheckpointWorkloadRequest\x12\x1a\n" + "\batespace\x18\x01 \x01(\tR\batespace\x12\x1d\n" + "\n" + @@ -1833,12 +2003,14 @@ const file_ateom_proto_rawDesc = "" + "\fsnapshot_uri\x18\b \x01(\tR\vsnapshotUri\x12g\n" + "\x13runtime_asset_paths\x18\t \x03(\v27.ateom.CheckpointWorkloadRequest.RuntimeAssetPathsEntryR\x11runtimeAssetPaths\x12*\n" + "\x05scope\x18\n" + - " \x01(\x0e2\x14.ateom.SnapshotScopeR\x05scope\x1aD\n" + + " \x01(\x0e2\x14.ateom.SnapshotScopeR\x05scope\x12/\n" + + "\n" + + "actor_dirs\x18\v \x01(\v2\x10.ateom.ActorDirsR\tactorDirs\x1aD\n" + "\x16RuntimeAssetPathsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"C\n" + "\x1aCheckpointWorkloadResponse\x12%\n" + - "\x0esnapshot_files\x18\x01 \x03(\tR\rsnapshotFiles\"\xe0\x05\n" + + "\x0esnapshot_files\x18\x01 \x03(\tR\rsnapshotFiles\"\x91\x06\n" + "\x16RestoreWorkloadRequest\x12\x1a\n" + "\batespace\x18\x01 \x01(\tR\batespace\x12\x1d\n" + "\n" + @@ -1856,7 +2028,9 @@ const file_ateom_proto_rawDesc = "" + "\x0eegress_gateway\x18\f \x01(\v2\x14.ateom.EgressGatewayH\x00R\regressGateway\x88\x01\x01\x12.\n" + "\x13golden_snapshot_uri\x18\r \x01(\tR\x11goldenSnapshotUri\x12\x1b\n" + "\tcpu_milli\x18\x0e \x01(\x03R\bcpuMilli\x12!\n" + - "\fmemory_bytes\x18\x0f \x01(\x03R\vmemoryBytes\x1aD\n" + + "\fmemory_bytes\x18\x0f \x01(\x03R\vmemoryBytes\x12/\n" + + "\n" + + "actor_dirs\x18\x10 \x01(\v2\x10.ateom.ActorDirsR\tactorDirs\x1aD\n" + "\x16RuntimeAssetPathsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x11\n" + @@ -1918,77 +2092,82 @@ func file_ateom_proto_rawDescGZIP() []byte { } var file_ateom_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_ateom_proto_msgTypes = make([]protoimpl.MessageInfo, 25) +var file_ateom_proto_msgTypes = make([]protoimpl.MessageInfo, 26) var file_ateom_proto_goTypes = []any{ (SnapshotScope)(0), // 0: ateom.SnapshotScope (SandboxClass)(0), // 1: ateom.SandboxClass (StatsSource)(0), // 2: ateom.StatsSource - (*TerminateWorkloadRequest)(nil), // 3: ateom.TerminateWorkloadRequest - (*TerminateWorkloadResponse)(nil), // 4: ateom.TerminateWorkloadResponse - (*RunWorkloadRequest)(nil), // 5: ateom.RunWorkloadRequest - (*EgressGateway)(nil), // 6: ateom.EgressGateway - (*WorkloadSpec)(nil), // 7: ateom.WorkloadSpec - (*Container)(nil), // 8: ateom.Container - (*VolumeMount)(nil), // 9: ateom.VolumeMount - (*DurableDirVolumeMount)(nil), // 10: ateom.DurableDirVolumeMount - (*SystemInfoVolumeMount)(nil), // 11: ateom.SystemInfoVolumeMount - (*ImageVolumeMount)(nil), // 12: ateom.ImageVolumeMount - (*WakeupProbe)(nil), // 13: ateom.WakeupProbe - (*HTTPGetAction)(nil), // 14: ateom.HTTPGetAction - (*RunWorkloadResponse)(nil), // 15: ateom.RunWorkloadResponse - (*CheckpointWorkloadRequest)(nil), // 16: ateom.CheckpointWorkloadRequest - (*CheckpointWorkloadResponse)(nil), // 17: ateom.CheckpointWorkloadResponse - (*RestoreWorkloadRequest)(nil), // 18: ateom.RestoreWorkloadRequest - (*RestoreWorkloadResponse)(nil), // 19: ateom.RestoreWorkloadResponse - (*GetWorkloadStatsRequest)(nil), // 20: ateom.GetWorkloadStatsRequest - (*WorkloadStatsSample)(nil), // 21: ateom.WorkloadStatsSample - (*GetWorkloadStatsResponse)(nil), // 22: ateom.GetWorkloadStatsResponse - (*GetActiveWorkloadStatsRequest)(nil), // 23: ateom.GetActiveWorkloadStatsRequest - (*GetActiveWorkloadStatsResponse)(nil), // 24: ateom.GetActiveWorkloadStatsResponse - nil, // 25: ateom.RunWorkloadRequest.RuntimeAssetPathsEntry - nil, // 26: ateom.CheckpointWorkloadRequest.RuntimeAssetPathsEntry - nil, // 27: ateom.RestoreWorkloadRequest.RuntimeAssetPathsEntry + (*ActorDirs)(nil), // 3: ateom.ActorDirs + (*TerminateWorkloadRequest)(nil), // 4: ateom.TerminateWorkloadRequest + (*TerminateWorkloadResponse)(nil), // 5: ateom.TerminateWorkloadResponse + (*RunWorkloadRequest)(nil), // 6: ateom.RunWorkloadRequest + (*EgressGateway)(nil), // 7: ateom.EgressGateway + (*WorkloadSpec)(nil), // 8: ateom.WorkloadSpec + (*Container)(nil), // 9: ateom.Container + (*VolumeMount)(nil), // 10: ateom.VolumeMount + (*DurableDirVolumeMount)(nil), // 11: ateom.DurableDirVolumeMount + (*SystemInfoVolumeMount)(nil), // 12: ateom.SystemInfoVolumeMount + (*ImageVolumeMount)(nil), // 13: ateom.ImageVolumeMount + (*WakeupProbe)(nil), // 14: ateom.WakeupProbe + (*HTTPGetAction)(nil), // 15: ateom.HTTPGetAction + (*RunWorkloadResponse)(nil), // 16: ateom.RunWorkloadResponse + (*CheckpointWorkloadRequest)(nil), // 17: ateom.CheckpointWorkloadRequest + (*CheckpointWorkloadResponse)(nil), // 18: ateom.CheckpointWorkloadResponse + (*RestoreWorkloadRequest)(nil), // 19: ateom.RestoreWorkloadRequest + (*RestoreWorkloadResponse)(nil), // 20: ateom.RestoreWorkloadResponse + (*GetWorkloadStatsRequest)(nil), // 21: ateom.GetWorkloadStatsRequest + (*WorkloadStatsSample)(nil), // 22: ateom.WorkloadStatsSample + (*GetWorkloadStatsResponse)(nil), // 23: ateom.GetWorkloadStatsResponse + (*GetActiveWorkloadStatsRequest)(nil), // 24: ateom.GetActiveWorkloadStatsRequest + (*GetActiveWorkloadStatsResponse)(nil), // 25: ateom.GetActiveWorkloadStatsResponse + nil, // 26: ateom.RunWorkloadRequest.RuntimeAssetPathsEntry + nil, // 27: ateom.CheckpointWorkloadRequest.RuntimeAssetPathsEntry + nil, // 28: ateom.RestoreWorkloadRequest.RuntimeAssetPathsEntry } var file_ateom_proto_depIdxs = []int32{ - 7, // 0: ateom.TerminateWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 7, // 1: ateom.RunWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 25, // 2: ateom.RunWorkloadRequest.runtime_asset_paths:type_name -> ateom.RunWorkloadRequest.RuntimeAssetPathsEntry - 6, // 3: ateom.RunWorkloadRequest.egress_gateway:type_name -> ateom.EgressGateway - 8, // 4: ateom.WorkloadSpec.containers:type_name -> ateom.Container - 13, // 5: ateom.Container.wakeup_probe:type_name -> ateom.WakeupProbe - 10, // 6: ateom.Container.durable_dir_volume_mounts:type_name -> ateom.DurableDirVolumeMount - 9, // 7: ateom.Container.csi_volume_mounts:type_name -> ateom.VolumeMount - 11, // 8: ateom.Container.system_info_volume_mounts:type_name -> ateom.SystemInfoVolumeMount - 12, // 9: ateom.Container.image_volume_mounts:type_name -> ateom.ImageVolumeMount - 14, // 10: ateom.WakeupProbe.http_get:type_name -> ateom.HTTPGetAction - 7, // 11: ateom.CheckpointWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 26, // 12: ateom.CheckpointWorkloadRequest.runtime_asset_paths:type_name -> ateom.CheckpointWorkloadRequest.RuntimeAssetPathsEntry - 0, // 13: ateom.CheckpointWorkloadRequest.scope:type_name -> ateom.SnapshotScope - 7, // 14: ateom.RestoreWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 27, // 15: ateom.RestoreWorkloadRequest.runtime_asset_paths:type_name -> ateom.RestoreWorkloadRequest.RuntimeAssetPathsEntry - 0, // 16: ateom.RestoreWorkloadRequest.scope:type_name -> ateom.SnapshotScope - 6, // 17: ateom.RestoreWorkloadRequest.egress_gateway:type_name -> ateom.EgressGateway - 1, // 18: ateom.WorkloadStatsSample.sandbox_class:type_name -> ateom.SandboxClass - 2, // 19: ateom.WorkloadStatsSample.source:type_name -> ateom.StatsSource - 21, // 20: ateom.GetWorkloadStatsResponse.sample:type_name -> ateom.WorkloadStatsSample - 21, // 21: ateom.GetActiveWorkloadStatsResponse.samples:type_name -> ateom.WorkloadStatsSample - 5, // 22: ateom.Ateom.RunWorkload:input_type -> ateom.RunWorkloadRequest - 16, // 23: ateom.Ateom.CheckpointWorkload:input_type -> ateom.CheckpointWorkloadRequest - 18, // 24: ateom.Ateom.RestoreWorkload:input_type -> ateom.RestoreWorkloadRequest - 20, // 25: ateom.Ateom.GetWorkloadStats:input_type -> ateom.GetWorkloadStatsRequest - 23, // 26: ateom.Ateom.GetActiveWorkloadStats:input_type -> ateom.GetActiveWorkloadStatsRequest - 3, // 27: ateom.Ateom.TerminateWorkload:input_type -> ateom.TerminateWorkloadRequest - 15, // 28: ateom.Ateom.RunWorkload:output_type -> ateom.RunWorkloadResponse - 17, // 29: ateom.Ateom.CheckpointWorkload:output_type -> ateom.CheckpointWorkloadResponse - 19, // 30: ateom.Ateom.RestoreWorkload:output_type -> ateom.RestoreWorkloadResponse - 22, // 31: ateom.Ateom.GetWorkloadStats:output_type -> ateom.GetWorkloadStatsResponse - 24, // 32: ateom.Ateom.GetActiveWorkloadStats:output_type -> ateom.GetActiveWorkloadStatsResponse - 4, // 33: ateom.Ateom.TerminateWorkload:output_type -> ateom.TerminateWorkloadResponse - 28, // [28:34] is the sub-list for method output_type - 22, // [22:28] is the sub-list for method input_type - 22, // [22:22] is the sub-list for extension type_name - 22, // [22:22] is the sub-list for extension extendee - 0, // [0:22] is the sub-list for field type_name + 8, // 0: ateom.TerminateWorkloadRequest.spec:type_name -> ateom.WorkloadSpec + 3, // 1: ateom.TerminateWorkloadRequest.actor_dirs:type_name -> ateom.ActorDirs + 8, // 2: ateom.RunWorkloadRequest.spec:type_name -> ateom.WorkloadSpec + 26, // 3: ateom.RunWorkloadRequest.runtime_asset_paths:type_name -> ateom.RunWorkloadRequest.RuntimeAssetPathsEntry + 7, // 4: ateom.RunWorkloadRequest.egress_gateway:type_name -> ateom.EgressGateway + 3, // 5: ateom.RunWorkloadRequest.actor_dirs:type_name -> ateom.ActorDirs + 9, // 6: ateom.WorkloadSpec.containers:type_name -> ateom.Container + 14, // 7: ateom.Container.wakeup_probe:type_name -> ateom.WakeupProbe + 11, // 8: ateom.Container.durable_dir_volume_mounts:type_name -> ateom.DurableDirVolumeMount + 10, // 9: ateom.Container.csi_volume_mounts:type_name -> ateom.VolumeMount + 12, // 10: ateom.Container.system_info_volume_mounts:type_name -> ateom.SystemInfoVolumeMount + 13, // 11: ateom.Container.image_volume_mounts:type_name -> ateom.ImageVolumeMount + 15, // 12: ateom.WakeupProbe.http_get:type_name -> ateom.HTTPGetAction + 8, // 13: ateom.CheckpointWorkloadRequest.spec:type_name -> ateom.WorkloadSpec + 27, // 14: ateom.CheckpointWorkloadRequest.runtime_asset_paths:type_name -> ateom.CheckpointWorkloadRequest.RuntimeAssetPathsEntry + 0, // 15: ateom.CheckpointWorkloadRequest.scope:type_name -> ateom.SnapshotScope + 3, // 16: ateom.CheckpointWorkloadRequest.actor_dirs:type_name -> ateom.ActorDirs + 8, // 17: ateom.RestoreWorkloadRequest.spec:type_name -> ateom.WorkloadSpec + 28, // 18: ateom.RestoreWorkloadRequest.runtime_asset_paths:type_name -> ateom.RestoreWorkloadRequest.RuntimeAssetPathsEntry + 0, // 19: ateom.RestoreWorkloadRequest.scope:type_name -> ateom.SnapshotScope + 7, // 20: ateom.RestoreWorkloadRequest.egress_gateway:type_name -> ateom.EgressGateway + 3, // 21: ateom.RestoreWorkloadRequest.actor_dirs:type_name -> ateom.ActorDirs + 1, // 22: ateom.WorkloadStatsSample.sandbox_class:type_name -> ateom.SandboxClass + 2, // 23: ateom.WorkloadStatsSample.source:type_name -> ateom.StatsSource + 22, // 24: ateom.GetWorkloadStatsResponse.sample:type_name -> ateom.WorkloadStatsSample + 22, // 25: ateom.GetActiveWorkloadStatsResponse.samples:type_name -> ateom.WorkloadStatsSample + 6, // 26: ateom.Ateom.RunWorkload:input_type -> ateom.RunWorkloadRequest + 17, // 27: ateom.Ateom.CheckpointWorkload:input_type -> ateom.CheckpointWorkloadRequest + 19, // 28: ateom.Ateom.RestoreWorkload:input_type -> ateom.RestoreWorkloadRequest + 21, // 29: ateom.Ateom.GetWorkloadStats:input_type -> ateom.GetWorkloadStatsRequest + 24, // 30: ateom.Ateom.GetActiveWorkloadStats:input_type -> ateom.GetActiveWorkloadStatsRequest + 4, // 31: ateom.Ateom.TerminateWorkload:input_type -> ateom.TerminateWorkloadRequest + 16, // 32: ateom.Ateom.RunWorkload:output_type -> ateom.RunWorkloadResponse + 18, // 33: ateom.Ateom.CheckpointWorkload:output_type -> ateom.CheckpointWorkloadResponse + 20, // 34: ateom.Ateom.RestoreWorkload:output_type -> ateom.RestoreWorkloadResponse + 23, // 35: ateom.Ateom.GetWorkloadStats:output_type -> ateom.GetWorkloadStatsResponse + 25, // 36: ateom.Ateom.GetActiveWorkloadStats:output_type -> ateom.GetActiveWorkloadStatsResponse + 5, // 37: ateom.Ateom.TerminateWorkload:output_type -> ateom.TerminateWorkloadResponse + 32, // [32:38] is the sub-list for method output_type + 26, // [26:32] is the sub-list for method input_type + 26, // [26:26] is the sub-list for extension type_name + 26, // [26:26] is the sub-list for extension extendee + 0, // [0:26] is the sub-list for field type_name } func init() { file_ateom_proto_init() } @@ -1996,15 +2175,15 @@ func file_ateom_proto_init() { if File_ateom_proto != nil { return } - file_ateom_proto_msgTypes[2].OneofWrappers = []any{} - file_ateom_proto_msgTypes[15].OneofWrappers = []any{} + file_ateom_proto_msgTypes[3].OneofWrappers = []any{} + file_ateom_proto_msgTypes[16].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_ateom_proto_rawDesc), len(file_ateom_proto_rawDesc)), NumEnums: 3, - NumMessages: 25, + NumMessages: 26, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/proto/ateompb/ateom.proto b/internal/proto/ateompb/ateom.proto index 2725339922..9342468343 100644 --- a/internal/proto/ateompb/ateom.proto +++ b/internal/proto/ateompb/ateom.proto @@ -106,6 +106,31 @@ service Ateom { rpc TerminateWorkload(TerminateWorkloadRequest) returns (TerminateWorkloadResponse) {} } +// ActorDirs is the set of per-actor directories atelet prepares on the volume +// it shares with ateom. ateom takes them from the RPC instead of deriving +// them from the actor UID. +// Every directory is under root_dir. +message ActorDirs { + // The actor's own directory. + string root_dir = 1; + // Holds one OCI bundle per container, named after the container. + string oci_bundle_dir = 2; + // runsc --root for the actor's sandbox. gVisor only. + string runsc_state_dir = 3; + // Where runsc writes .pid. gVisor only. + string pid_file_dir = 4; + // Where CheckpointWorkload writes the snapshot files. + string checkpoint_dir = 5; + // Where the snapshot files RestoreWorkload reads have been downloaded to. + string restore_dir = 6; + // Holds one subdirectory per durable-dir volume. + string durable_dir_volume_mounts_dir = 7; + // Holds one subdirectory per system-info volume. + string system_info_volume_roots_dir = 8; + // Holds one mount point per CSI volume. + string volumes_dir = 9; +} + message TerminateWorkloadRequest { string atespace = 1; string actor_name = 2; @@ -114,6 +139,7 @@ message TerminateWorkloadRequest { string actor_template_name = 5; string runsc_path = 6; WorkloadSpec spec = 7; + ActorDirs actor_dirs = 8; } message TerminateWorkloadResponse { @@ -145,6 +171,7 @@ message RunWorkloadRequest { // runtime default (unlimited for gVisor, ateom's own default for the micro-VM). int64 cpu_milli = 11; // CPU limit in millicores (1000 = one core). int64 memory_bytes = 12; // Memory limit in bytes. + ActorDirs actor_dirs = 13; } // EgressGateway configures tunneled egress for one actor activation. @@ -281,6 +308,7 @@ message CheckpointWorkloadRequest { // What content to include in the checkpoint. SnapshotScope scope = 10; + ActorDirs actor_dirs = 11; } message CheckpointWorkloadResponse { @@ -326,6 +354,7 @@ message RestoreWorkloadRequest { // these are ignored. Zero means "unset": keep the runtime default. int64 cpu_milli = 14; // CPU limit in millicores (1000 = one core). int64 memory_bytes = 15; // Memory limit in bytes. + ActorDirs actor_dirs = 16; } message RestoreWorkloadResponse { diff --git a/internal/resources/actorattribution.go b/internal/resources/actorattribution.go index 4f814f8bbf..cc2463f9b5 100644 --- a/internal/resources/actorattribution.go +++ b/internal/resources/actorattribution.go @@ -22,7 +22,7 @@ import "github.com/agent-substrate/substrate/pkg/proto/ateapipb" // ateom's usage sampling so those producers cannot drift apart. // // Unrelated to the credential sense of "actor identity" elsewhere in the repo -// (ateapi's ActorIdentity service, substratex509, ateompath.ActorIdentityDirPath) +// (ateapi's ActorIdentity service, substratex509) // — nothing here is a secret or is presented as proof of anything. type ActorAttribution struct { Ref ActorRef diff --git a/internal/resources/snapshot.go b/internal/resources/snapshot.go index ab4f932faf..d95e0141ae 100644 --- a/internal/resources/snapshot.go +++ b/internal/resources/snapshot.go @@ -34,6 +34,13 @@ const ( tagsOwnerKind = "tags" ) +// DurableDirTarFile is the snapshot file holding the tar of an actor's +// durable-dir volumes (entries are /... relative to the actor's +// durable-dir directory). Written by the ateoms at checkpoint; a DATA +// snapshot consists of this file alone, so atelet uses the name to carve the +// durable data out of a FULL snapshot's file set. +const DurableDirTarFile = "durable-dir.tar" + // NewSnapshotName returns a unique name for a new snapshot, durable or // node-local. func NewSnapshotName() string { diff --git a/internal/resources/validate.go b/internal/resources/validate.go index 829319e51c..17d945f05f 100644 --- a/internal/resources/validate.go +++ b/internal/resources/validate.go @@ -19,8 +19,10 @@ import ( "fmt" "net/netip" "net/url" + "path/filepath" "strings" + "github.com/agent-substrate/substrate/internal/proto/ateompb" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/api/validate/content" "k8s.io/apimachinery/pkg/util/validation/field" @@ -87,6 +89,50 @@ func ValidateAteomUID(targetAteomUID string) error { return nil } +// ValidateActorDirs checks the directories atelet passes to ateom: each one +// set, absolute and clean, and every one but root_dir below root_dir. +func ValidateActorDirs(actorDirs *ateompb.ActorDirs, fldPath *field.Path) field.ErrorList { + if actorDirs == nil { + return field.ErrorList{field.Required(fldPath, "")} + } + rootDir := actorDirs.GetRootDir() + errs := validateAbsDir(rootDir, fldPath.Child("root_dir")) + rootDirOK := len(errs) == 0 + for _, actorDir := range []struct{ name, path string }{ + {"oci_bundle_dir", actorDirs.GetOciBundleDir()}, + {"runsc_state_dir", actorDirs.GetRunscStateDir()}, + {"pid_file_dir", actorDirs.GetPidFileDir()}, + {"checkpoint_dir", actorDirs.GetCheckpointDir()}, + {"restore_dir", actorDirs.GetRestoreDir()}, + {"durable_dir_volume_mounts_dir", actorDirs.GetDurableDirVolumeMountsDir()}, + {"system_info_volume_roots_dir", actorDirs.GetSystemInfoVolumeRootsDir()}, + {"volumes_dir", actorDirs.GetVolumesDir()}, + } { + dirPath := fldPath.Child(actorDir.name) + if dirErrs := validateAbsDir(actorDir.path, dirPath); len(dirErrs) > 0 { + errs = append(errs, dirErrs...) + continue + } + if !rootDirOK { + continue + } + if rel, err := filepath.Rel(rootDir, actorDir.path); err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, "../") { + errs = append(errs, field.Invalid(dirPath, actorDir.path, "must be below root_dir")) + } + } + return errs +} + +func validateAbsDir(dir string, fldPath *field.Path) field.ErrorList { + if dir == "" { + return field.ErrorList{field.Required(fldPath, "")} + } + if !filepath.IsAbs(dir) || filepath.Clean(dir) != dir { + return field.ErrorList{field.Invalid(fldPath, dir, "must be an absolute, clean path")} + } + return nil +} + // ValidateContainerNames ensures every application container name is safe to // use as an OCI bundle path component. Each must be a DNS-1123 label (no // separator or ".."), must not be the reserved "pause" name (which would diff --git a/internal/resources/validate_test.go b/internal/resources/validate_test.go index 7b718b454f..aedc731718 100644 --- a/internal/resources/validate_test.go +++ b/internal/resources/validate_test.go @@ -19,6 +19,7 @@ import ( "strings" "testing" + "github.com/agent-substrate/substrate/internal/proto/ateompb" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -272,3 +273,63 @@ func TestValidateUUID(t *testing.T) { }) } } + +func testActorDirs() *ateompb.ActorDirs { + return &ateompb.ActorDirs{ + RootDir: "/node/actors/a", + OciBundleDir: "/node/actors/a/bundles", + RunscStateDir: "/node/actors/a/runsc-state", + PidFileDir: "/node/actors/a/pidfiles", + CheckpointDir: "/node/actors/a/checkpoint-state", + RestoreDir: "/node/actors/a/restore-state", + DurableDirVolumeMountsDir: "/node/actors/a/durable-dir", + SystemInfoVolumeRootsDir: "/node/actors/a/system-info", + VolumesDir: "/node/actors/a/volumes", + } +} + +func TestValidateActorDirs(t *testing.T) { + for _, tc := range []struct { + name string + mutate func(*ateompb.ActorDirs) *ateompb.ActorDirs + // wantField is the field the one expected error names; empty means valid. + wantField string + }{ + {"valid", func(actorDirs *ateompb.ActorDirs) *ateompb.ActorDirs { return actorDirs }, ""}, + {"nil", func(*ateompb.ActorDirs) *ateompb.ActorDirs { return nil }, "actor_dirs"}, + {"missing dir", func(actorDirs *ateompb.ActorDirs) *ateompb.ActorDirs { actorDirs.RestoreDir = ""; return actorDirs }, "actor_dirs.restore_dir"}, + {"relative dir", func(actorDirs *ateompb.ActorDirs) *ateompb.ActorDirs { + actorDirs.OciBundleDir = "bundles" + return actorDirs + }, "actor_dirs.oci_bundle_dir"}, + {"unclean dir", func(actorDirs *ateompb.ActorDirs) *ateompb.ActorDirs { + actorDirs.CheckpointDir = "/node/actors/a/../b/checkpoint-state" + return actorDirs + }, "actor_dirs.checkpoint_dir"}, + {"outside root", func(actorDirs *ateompb.ActorDirs) *ateompb.ActorDirs { + actorDirs.VolumesDir = "/node/actors/b/volumes" + return actorDirs + }, "actor_dirs.volumes_dir"}, + {"root itself", func(actorDirs *ateompb.ActorDirs) *ateompb.ActorDirs { + actorDirs.PidFileDir = actorDirs.RootDir + return actorDirs + }, "actor_dirs.pid_file_dir"}, + {"relative root", func(actorDirs *ateompb.ActorDirs) *ateompb.ActorDirs { + actorDirs.RootDir = "node/actors/a" + return actorDirs + }, "actor_dirs.root_dir"}, + } { + t.Run(tc.name, func(t *testing.T) { + errs := ValidateActorDirs(tc.mutate(testActorDirs()), field.NewPath("actor_dirs")) + if tc.wantField == "" { + if len(errs) != 0 { + t.Fatalf("ValidateActorDirs() = %v, want no errors", errs) + } + return + } + if len(errs) != 1 || errs[0].Field != tc.wantField { + t.Fatalf("ValidateActorDirs() = %v, want one error on %s", errs, tc.wantField) + } + }) + } +} diff --git a/internal/volume/csi/plugin.go b/internal/volume/csi/plugin.go index ab1337efd9..d1cede7777 100644 --- a/internal/volume/csi/plugin.go +++ b/internal/volume/csi/plugin.go @@ -24,8 +24,8 @@ import ( "path/filepath" "sync" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/credbundle" + "github.com/agent-substrate/substrate/internal/nodepath" "github.com/agent-substrate/substrate/internal/volume" v1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" listersv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" @@ -66,7 +66,7 @@ var _ volume.VolumePluginWorkerPlane = (*Plugin)(nil) func NewPlugin(client *Client) *Plugin { return &Plugin{ client: client, - stagingDirPrefix: ateompath.StagingDirPrefix(), + stagingDirPrefix: filepath.Join(nodepath.BasePath, "staging"), } } @@ -293,7 +293,7 @@ func newCSIPlugin(ctx context.Context, lister listersv1alpha1.CSIDriverConfigLis endpoint = cfg.Spec.NodeSocketOverride slog.InfoContext(ctx, "Found CSIDriverConfig with NodeSocketOverride", slog.String("driver", driverName), slog.String("endpoint", endpoint)) default: - endpoint = "unix://" + ateompath.KubeletPluginSocketPath(driverName) + endpoint = "unix://" + kubeletPluginSocketPath(driverName) } var tlsCfg *tls.Config @@ -453,3 +453,8 @@ func parseCertPool(path string) (*x509.CertPool, error) { } return pool, nil } + +// kubeletPluginSocketPath is the CSI driver socket in the kubelet plugins directory. +func kubeletPluginSocketPath(driverName string) string { + return filepath.Join("/var/lib/kubelet/plugins", driverName, "csi.sock") +} diff --git a/internal/volume/mock.go b/internal/volume/mock.go index 017d3ca11d..e6c9c99589 100644 --- a/internal/volume/mock.go +++ b/internal/volume/mock.go @@ -21,11 +21,11 @@ import ( "os" "path/filepath" - "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/internal/nodepath" ) // Use a directory that is shared between atelet and ateom but not cleaned up by atelet -var mockVolumeDirectories string = filepath.Join(ateompath.BasePath, "mockvolumes") +var mockVolumeDirectories string = filepath.Join(nodepath.BasePath, "mockvolumes") var ( _ VolumePluginControlPlane = (*MockVolumePlugin)(nil) diff --git a/tools/validate-image-cache/main.go b/tools/validate-image-cache/main.go index 2178fd035c..d3fd0391fb 100644 --- a/tools/validate-image-cache/main.go +++ b/tools/validate-image-cache/main.go @@ -29,7 +29,7 @@ // Disk is bounded by the cache's own eviction engine: below // --min-free-gb free space, the tool asks Store.EvictUnused to reclaim // the shortfall. --evict-all instead empties everything evictable and -// exits. Bundle-spec rooting (scanned from ateompath.ActorsDir) protects +// exits. Bundle-spec rooting (scanned from the node's actors dir) protects // placed actors' mounted images. The engine's locks are per-process, so a // run beside a live atelet is unsynchronized with its GC and pulls — the // pool cannot be corrupted (record-first pulls, two-phase retirement), @@ -54,13 +54,14 @@ import ( "math" "math/rand" "os" + "path/filepath" "strconv" "strings" "sync" "time" - "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/imagecache" + "github.com/agent-substrate/substrate/internal/nodepath" v1 "github.com/google/go-containerregistry/pkg/v1" googlecontainerauth "github.com/google/go-containerregistry/pkg/v1/google" "golang.org/x/sys/unix" @@ -137,24 +138,27 @@ func (c runConfig) validate() error { if c.live { if c.evictIdle < liveNodeIdleFloor { return fmt.Errorf("--evict-idle=%v is below %v with %s present: min-age is the only protection that applies across processes using the cache", - c.evictIdle, liveNodeIdleFloor, ateompath.ActorsDir) + c.evictIdle, liveNodeIdleFloor, actorsDir) } // Both modes evict here (evictIfLow is a low-water flush on a // loop), unsynchronized with every other user of the pool. if !c.force { - return fmt.Errorf("%s exists — this looks like a live node, and evictions are not synchronized with other processes using the cache; re-run with --force to proceed", ateompath.ActorsDir) + return fmt.Errorf("%s exists — this looks like a live node, and evictions are not synchronized with other processes using the cache; re-run with --force to proceed", actorsDir) } } return nil } +// actorsDir is where atelet keeps the per-actor directories. +var actorsDir = filepath.Join(nodepath.BasePath, "actors") + // newStore opens the cache with the options both modes share: min-age // from --evict-idle, and bundle-spec rooting from the node's actors dir // (absent on a validation host, which InUse treats as an empty root set). func newStore(extra ...imagecache.Option) (*imagecache.Store, error) { return imagecache.New(*cacheDir, append([]imagecache.Option{ imagecache.WithMinAge(*evictIdle), - imagecache.WithActorsDir(ateompath.ActorsDir), + imagecache.WithActorsDir(actorsDir), }, extra...)...) } @@ -172,7 +176,7 @@ func main() { cacheDir: *cacheDir, refsFile: *refsFile, evictAll: *evictAll, force: *force, evictIdle: *evictIdle, minFreeGB: *minFreeGB, - live: looksLikeLiveNode(ateompath.ActorsDir), + live: looksLikeLiveNode(actorsDir), } flag.Visit(func(f *flag.Flag) { cfg.setFlags = append(cfg.setFlags, f.Name) }) if err := cfg.validate(); err != nil {