diff --git a/cmd/ateapi/internal/controlapi/workload_spec.go b/cmd/ateapi/internal/controlapi/workload_spec.go index 448ac5f2b..cfb6a5084 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec.go +++ b/cmd/ateapi/internal/controlapi/workload_spec.go @@ -67,6 +67,16 @@ func workloadSpecFromActorTemplate(actorTemplate *atev1alpha1.ActorTemplate, act }, }) + case vol.VolumeSource.Image != nil: + workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ + Name: vol.Name, + Source: &ateletpb.Volume_Image{ + Image: &ateletpb.ImageVolumeSource{ + Reference: vol.VolumeSource.Image.Reference, + }, + }, + }) + default: continue // Drop unrecognized volumes. } diff --git a/cmd/ateapi/internal/controlapi/workload_spec_test.go b/cmd/ateapi/internal/controlapi/workload_spec_test.go index 6becf5ab1..bd33e295e 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec_test.go +++ b/cmd/ateapi/internal/controlapi/workload_spec_test.go @@ -136,6 +136,50 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { }, }, }, + { + name: "converts Image volume and mounts", + template: &atev1alpha1.ActorTemplate{ + ObjectMeta: metav1.ObjectMeta{Name: "tmpl1", Namespace: "agent-ns"}, + Spec: atev1alpha1.ActorTemplateSpec{ + Volumes: []atev1alpha1.Volume{ + {Name: "home", VolumeSource: atev1alpha1.VolumeSource{DurableDir: &atev1alpha1.DurableDirVolumeSource{}}}, + {Name: "agent", VolumeSource: atev1alpha1.VolumeSource{Image: &atev1alpha1.ImageVolumeSource{Reference: "example.com/agent@sha256:abc"}}}, + }, + Containers: []atev1alpha1.Container{ + { + Name: "main", + Image: "main", + VolumeMounts: []atev1alpha1.VolumeMount{ + {Name: "home", MountPath: "/home/user"}, + {Name: "agent", MountPath: "/ate"}, + }, + }, + }, + }, + }, + want: &ateletpb.WorkloadSpec{ + Volumes: []*ateletpb.Volume{ + { + Name: "home", + Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}, + }, + { + Name: "agent", + Source: &ateletpb.Volume_Image{Image: &ateletpb.ImageVolumeSource{Reference: "example.com/agent@sha256:abc"}}, + }, + }, + Containers: []*ateletpb.Container{ + { + Name: "main", + Image: "main", + VolumeMounts: []*ateletpb.VolumeMount{ + {Name: "home", MountPath: "/home/user"}, + {Name: "agent", MountPath: "/ate"}, + }, + }, + }, + }, + }, { name: "skips non-DurableDir volumes", template: &atev1alpha1.ActorTemplate{ diff --git a/cmd/atelet/imagevolume_test.go b/cmd/atelet/imagevolume_test.go new file mode 100644 index 000000000..a4df056f0 --- /dev/null +++ b/cmd/atelet/imagevolume_test.go @@ -0,0 +1,175 @@ +// 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 main + +import ( + "archive/tar" + "bytes" + "io" + "log" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/agent-substrate/substrate/internal/imagecache" + "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +// imageVolumeTestRegistry starts an in-memory OCI registry. Its 127.0.0.1 host +// makes the image cache treat it as a local registry and pull over plain HTTP. +func imageVolumeTestRegistry(t *testing.T) string { + t.Helper() + srv := httptest.NewServer(registry.New(registry.Logger(log.New(io.Discard, "", 0)))) + t.Cleanup(srv.Close) + u, err := url.Parse(srv.URL) + if err != nil { + t.Fatalf("parsing registry URL: %v", err) + } + return u.Host +} + +func singleFileLayer(t *testing.T, path, body string) v1.Layer { + t.Helper() + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + if err := tw.WriteHeader(&tar.Header{Name: path, Mode: 0o755, Size: int64(len(body))}); err != nil { + t.Fatalf("tar.WriteHeader: %v", err) + } + if _, err := tw.Write([]byte(body)); err != nil { + t.Fatalf("tar.Write: %v", err) + } + if err := tw.Close(); err != nil { + t.Fatalf("tar.Close: %v", err) + } + l, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(buf.Bytes())), nil + }) + if err != nil { + t.Fatalf("tarball.LayerFromOpener: %v", err) + } + return l +} + +func pushTestImage(t *testing.T, ref string, layers ...v1.Layer) { + t.Helper() + img, err := mutate.AppendLayers(empty.Image, layers...) + if err != nil { + t.Fatalf("mutate.AppendLayers: %v", err) + } + tag, err := name.ParseReference(ref, name.Insecure) + if err != nil { + t.Fatalf("name.ParseReference(%q): %v", ref, err) + } + if err := remote.Write(tag, img); err != nil { + t.Fatalf("remote.Write(%q): %v", ref, err) + } +} + +func newImageVolumeStore(t *testing.T) *imagecache.Store { + t.Helper() + s, err := imagecache.New(t.TempDir()) + if err != nil { + t.Fatalf("imagecache.New: %v", err) + } + return s +} + +// A mounted image volume records its layers for ateom to compose, and its +// digest so the cache GC can protect them. +func TestResolveImageVolumes_RecordsLayersAndDigest(t *testing.T) { + host := imageVolumeTestRegistry(t) + ref := host + "/agent:v1" + pushTestImage(t, ref, singleFileLayer(t, "payload-binary", "binary")) + + volumes := []*ateletpb.Volume{{ + Name: "agent", + Source: &ateletpb.Volume_Image{Image: &ateletpb.ImageVolumeSource{Reference: ref}}, + }} + mounts := []*ateletpb.VolumeMount{{Name: "agent", MountPath: "/ate"}} + + got, err := resolveImageVolumes(t.Context(), newImageVolumeStore(t), volumes, mounts) + if err != nil { + t.Fatalf("resolveImageVolumes: %v", err) + } + if len(got) != 1 || got[0].Name != "agent" { + t.Fatalf("resolveImageVolumes = %+v, want one entry named %q", got, "agent") + } + if len(got[0].Layers) != 1 { + t.Errorf("layers = %v, want 1", got[0].Layers) + } + if !strings.HasPrefix(got[0].ImageDigest, "sha256:") { + t.Errorf("image digest = %q, want a sha256 digest", got[0].ImageDigest) + } + // The returned path is a layer directory; the binary lives under its fs/ subtree. + if _, err := os.Stat(filepath.Join(got[0].Layers[0], "fs", "payload-binary")); err != nil { + t.Errorf("recorded path is not a layer directory: %v", err) + } +} + +// Multi-layer image volumes produce one entry with layers in bottom-most-first order. +func TestResolveImageVolumes_MultiLayer(t *testing.T) { + host := imageVolumeTestRegistry(t) + ref := host + "/agent:multi" + pushTestImage(t, ref, + singleFileLayer(t, "base", "one"), + singleFileLayer(t, "payload-binary", "binary"), + ) + + volumes := []*ateletpb.Volume{{ + Name: "agent", + Source: &ateletpb.Volume_Image{Image: &ateletpb.ImageVolumeSource{Reference: ref}}, + }} + mounts := []*ateletpb.VolumeMount{{Name: "agent", MountPath: "/ate"}} + + got, err := resolveImageVolumes(t.Context(), newImageVolumeStore(t), volumes, mounts) + if err != nil { + t.Fatalf("resolveImageVolumes: %v", err) + } + if len(got) != 1 || len(got[0].Layers) != 2 { + t.Fatalf("resolveImageVolumes = %+v, want one entry with 2 layers", got) + } + for i, want := range []string{"base", "payload-binary"} { + if _, err := os.Stat(filepath.Join(got[0].Layers[i], "fs", want)); err != nil { + t.Errorf("layer %d does not hold %q: %v", i, want, err) + } + } +} + +// An image volume no container mounts is never pulled, so a bad reference on an +// unused volume cannot fail the actor. +func TestResolveImageVolumes_UnmountedVolumeNotPulled(t *testing.T) { + volumes := []*ateletpb.Volume{{ + Name: "agent", + Source: &ateletpb.Volume_Image{Image: &ateletpb.ImageVolumeSource{Reference: "127.0.0.1:1/nope@sha256:abc"}}, + }} + + got, err := resolveImageVolumes(t.Context(), newImageVolumeStore(t), volumes, nil) + if err != nil { + t.Fatalf("resolveImageVolumes: %v", err) + } + if len(got) != 0 { + t.Errorf("resolveImageVolumes = %+v, want empty", got) + } +} diff --git a/cmd/atelet/main.go b/cmd/atelet/main.go index 9cf2a8e92..56a5bec52 100644 --- a/cmd/atelet/main.go +++ b/cmd/atelet/main.go @@ -1627,6 +1627,7 @@ func buildAteomWorkloadSpec(spec *ateletpb.WorkloadSpec) (*ateompb.WorkloadSpec, var ddMounts []*ateompb.DurableDirVolumeMount var csiMounts []*ateompb.VolumeMount var siMounts []*ateompb.SystemInfoVolumeMount + var imgMounts []*ateompb.ImageVolumeMount for _, vm := range ctr.GetVolumeMounts() { volName := vm.GetName() vol, ok := volumes[volName] @@ -1650,6 +1651,11 @@ func buildAteomWorkloadSpec(spec *ateletpb.WorkloadSpec) (*ateompb.WorkloadSpec, VolumeName: volName, MountPath: vm.GetMountPath(), }) + case *ateletpb.Volume_Image: + imgMounts = append(imgMounts, &ateompb.ImageVolumeMount{ + VolumeName: volName, + MountPath: vm.GetMountPath(), + }) default: return nil, fmt.Errorf("container %q mounts volume %q with unsupported source %T", ctr.GetName(), volName, vol.GetSource()) } @@ -1659,6 +1665,7 @@ func buildAteomWorkloadSpec(spec *ateletpb.WorkloadSpec) (*ateompb.WorkloadSpec, DurableDirVolumeMounts: ddMounts, CsiVolumeMounts: csiMounts, SystemInfoVolumeMounts: siMounts, + ImageVolumeMounts: imgMounts, Readyz: toAteomReadyz(ctr.GetReadyz()), }) } diff --git a/cmd/atelet/main_test.go b/cmd/atelet/main_test.go index 4db05deb3..761abe4f4 100644 --- a/cmd/atelet/main_test.go +++ b/cmd/atelet/main_test.go @@ -1284,6 +1284,44 @@ func TestDrainOnShutdownForceStopsAfterTimeout(t *testing.T) { } } +// Image volumes appear on their own ImageVolumeMounts field, separate from durable-dir mounts. +func TestBuildAteomWorkloadSpec_ImageVolumeMounts(t *testing.T) { + spec := &ateletpb.WorkloadSpec{ + Volumes: []*ateletpb.Volume{ + {Name: "agent", Source: &ateletpb.Volume_Image{Image: &ateletpb.ImageVolumeSource{}}}, + {Name: "data", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, + {Name: "ext", Source: &ateletpb.Volume_External{External: &ateletpb.ExternalVolumeSource{}}}, + }, + Containers: []*ateletpb.Container{{ + Name: "app", + VolumeMounts: []*ateletpb.VolumeMount{ + {Name: "agent", MountPath: "/ate"}, + {Name: "data", MountPath: "/var/data"}, + {Name: "ext", MountPath: "/mnt/ext"}, + }, + }}, + } + + got, err := buildAteomWorkloadSpec(spec) + if err != nil { + t.Fatalf("buildAteomWorkloadSpec: %v", err) + } + if len(got.GetContainers()) != 1 { + t.Fatalf("containers = %d, want 1", len(got.GetContainers())) + } + ctr := got.GetContainers()[0] + + if len(ctr.GetImageVolumeMounts()) != 1 { + t.Fatalf("image volume mounts = %v, want 1", ctr.GetImageVolumeMounts()) + } + if name, path := ctr.GetImageVolumeMounts()[0].GetVolumeName(), ctr.GetImageVolumeMounts()[0].GetMountPath(); name != "agent" || path != "/ate" { + t.Errorf("image volume mount = (%q, %q), want (agent, /ate)", name, path) + } + if len(ctr.GetDurableDirVolumeMounts()) != 1 || ctr.GetDurableDirVolumeMounts()[0].GetVolumeName() != "data" { + t.Errorf("durable mounts = %v, want just data", ctr.GetDurableDirVolumeMounts()) + } +} + // allocatedBytes reports how much disk a file actually occupies, which is less than its // size when it has holes. func allocatedBytes(t *testing.T, path string) int64 { diff --git a/cmd/atelet/oci.go b/cmd/atelet/oci.go index 1b7c73490..7468017de 100644 --- a/cmd/atelet/oci.go +++ b/cmd/atelet/oci.go @@ -31,6 +31,7 @@ import ( "github.com/opencontainers/runtime-spec/specs-go" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" + "golang.org/x/sync/errgroup" ) const ( @@ -103,9 +104,25 @@ func prepareOCIDirectory(ctx context.Context, imageCache *imagecache.Store, acto } } - img, err := imageCache.EnsureImage(ctx, ref) - if err != nil { - return fmt.Errorf("in imageCache.EnsureImage: %w", err) + var ( + img *imagecache.Image + imageVolumes []imagecache.ImageVolumeOverlay + ) + g, gctx := errgroup.WithContext(ctx) + g.Go(func() error { + var err error + if img, err = imageCache.EnsureImage(gctx, ref); err != nil { + return fmt.Errorf("in imageCache.EnsureImage: %w", err) + } + return nil + }) + g.Go(func() error { + var err error + imageVolumes, err = resolveImageVolumes(gctx, imageCache, volumes, volumeMounts) + return err + }) + if err := g.Wait(); err != nil { + return err } // Argv and env need only the image config; resolve them before writing @@ -124,14 +141,15 @@ func prepareOCIDirectory(ctx context.Context, imageCache *imagecache.Store, acto extraDirs = append(extraDirs, vm.GetMountPath()) } if err := imagecache.WriteSpec(bundlePath, &imagecache.OverlaySpec{ - ImageDigest: img.Digest.String(), - Layers: img.LayerDirs, - ExtraDirs: extraDirs, + ImageDigest: img.Digest.String(), + Layers: img.LayerDirs, + ExtraDirs: extraDirs, + ImageVolumes: imageVolumes, }); err != nil { return fmt.Errorf("while writing overlay spec: %w", err) } - ociSpec := buildActorOCISpec(actorUID, resolvedArgs, resolvedEnv, annotations, netns, volumes, volumeMounts, capabilities) + ociSpec := buildActorOCISpec(actorUID, containerName, resolvedArgs, resolvedEnv, annotations, netns, volumes, volumeMounts, capabilities) ociSpecBytes, err := json.MarshalIndent(ociSpec, "", " ") if err != nil { return fmt.Errorf("while marshaling OCI spec: %w", err) @@ -144,6 +162,46 @@ func prepareOCIDirectory(ctx context.Context, imageCache *imagecache.Store, acto return nil } +// resolveImageVolumes pulls the image behind every image-typed volume this +// container mounts and returns what the overlay spec needs to compose each. +func resolveImageVolumes(ctx context.Context, imageCache *imagecache.Store, volumes []*ateletpb.Volume, volumeMounts []*ateletpb.VolumeMount) ([]imagecache.ImageVolumeOverlay, error) { + mounted := make(map[string]bool, len(volumeMounts)) + for _, vm := range volumeMounts { + mounted[vm.GetName()] = true + } + + var wanted []*ateletpb.Volume + for _, vol := range volumes { + if vol.GetImage() == nil || !mounted[vol.GetName()] { + continue + } + wanted = append(wanted, vol) + } + + // Pull the volumes concurrently; each entry lands at its own index so the + // spec order stays the template order. + out := make([]imagecache.ImageVolumeOverlay, len(wanted)) + g, gctx := errgroup.WithContext(ctx) + for i, vol := range wanted { + g.Go(func() error { + img, err := imageCache.EnsureImage(gctx, vol.GetImage().GetReference()) + if err != nil { + return fmt.Errorf("in imageCache.EnsureImage for volume %q: %w", vol.GetName(), err) + } + out[i] = imagecache.ImageVolumeOverlay{ + Name: vol.GetName(), + ImageDigest: img.Digest.String(), + Layers: img.LayerDirs, + } + return nil + }) + } + if err := g.Wait(); err != nil { + return nil, err + } + return out, nil +} + // resolveActorEnv computes the final container environment from the image's ENV // and the ActorTemplate env, with the template taking precedence. Duplicate keys // are removed in favor of template env > image env, and a default PATH stands in @@ -207,7 +265,7 @@ func resolveProcessArgs(imageCfg *v1.Config, command, args []string) ([]string, // already-resolved args, env and capabilities (see resolveProcessArgs, // resolveActorEnv and resolveCapabilities). An empty capabilities set means the // process runs with none, which is what the pause container gets. -func buildActorOCISpec(actorUID string, args []string, env []string, annotations map[string]string, netns string, volumes []*ateletpb.Volume, volumeMounts []*ateletpb.VolumeMount, capabilities []string) *specs.Spec { +func buildActorOCISpec(actorUID, containerName string, args []string, env []string, annotations map[string]string, netns string, volumes []*ateletpb.Volume, volumeMounts []*ateletpb.VolumeMount, capabilities []string) *specs.Spec { mounts := []specs.Mount{ { Destination: "/proc", @@ -315,6 +373,9 @@ func buildActorOCISpec(actorUID string, args []string, env []string, annotations // reads them. srcPath = ateompath.SystemInfoVolumeRoot(actorUID, vm.GetName()) options = []string{"bind", "ro"} + case *ateletpb.Volume_Image: + srcPath = ateompath.ImageVolumeMountPath(actorUID, containerName, vm.GetName()) + options = []string{"bind", "ro"} default: continue } diff --git a/cmd/atelet/oci_test.go b/cmd/atelet/oci_test.go index 12f19e32d..842d2545f 100644 --- a/cmd/atelet/oci_test.go +++ b/cmd/atelet/oci_test.go @@ -23,6 +23,7 @@ import ( "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/proto/ateletpb" v1 "github.com/google/go-containerregistry/pkg/v1" + specs "github.com/opencontainers/runtime-spec/specs-go" ) // Each system-info volume mount becomes a read-only bind mount whose source @@ -40,7 +41,7 @@ func TestBuildActorOCISpec_SystemInfoVolumeMounts(t *testing.T) { {Name: "sysinfo", Source: &ateletpb.Volume_SystemInfo{SystemInfo: &ateletpb.SystemInfoVolume{}}}, } spec := buildActorOCISpec( - actorUID, + actorUID, "app", []string{"/app"}, []string{"FOO=bar"}, map[string]string{"k": "v"}, @@ -217,7 +218,7 @@ func TestBuildActorOCISpec_DurableDirVolumeMounts(t *testing.T) { {Name: "cache", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, } spec := buildActorOCISpec( - actorUID, + actorUID, "app", []string{"/app"}, nil, nil, "/run/netns/x", volumes, @@ -246,6 +247,45 @@ func TestBuildActorOCISpec_DurableDirVolumeMounts(t *testing.T) { } } +// An image volume binds the layer directory resolved for it, read-only. +func TestBuildActorOCISpec_ImageVolumeMounts(t *testing.T) { + volumes := []*ateletpb.Volume{ + {Name: "agent", Source: &ateletpb.Volume_Image{Image: &ateletpb.ImageVolumeSource{}}}, + {Name: "data", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, + } + mounts := []*ateletpb.VolumeMount{ + {Name: "agent", MountPath: "/ate"}, + {Name: "data", MountPath: "/var/data"}, + } + spec := buildActorOCISpec( + "actor_uid", "app", + []string{"/ate/payload-binary"}, nil, nil, + "/run/netns/x", + volumes, + mounts, + nil, + ) + + var got *specs.Mount + for i, m := range spec.Mounts { + if m.Destination == "/ate" { + got = &spec.Mounts[i] + } + } + if got == nil { + t.Fatalf("image volume mount for /ate missing; mounts=%v", spec.Mounts) + } + if want := ateompath.ImageVolumeMountPath("actor_uid", "app", "agent"); got.Source != want { + t.Errorf("image volume source = %q, want %q", got.Source, want) + } + if got.Type != "bind" { + t.Errorf("image volume type = %q, want bind", got.Type) + } + if want := []string{"bind", "ro"}; !slices.Equal(got.Options, want) { + t.Errorf("image volume options = %v, want %v", got.Options, want) + } +} + // wantDefaultCapabilities is the set a container gets when it asks for no // adjustment. It is spelled out rather than derived from defaultCapabilities so // that widening or narrowing the default is a deliberate test change. @@ -327,7 +367,7 @@ func TestResolveCapabilities(t *testing.T) { // ambient stay empty — see the comment in buildActorOCISpec. func TestBuildActorOCISpec_Capabilities(t *testing.T) { want := []string{"CAP_CHOWN", "CAP_KILL"} - spec := buildActorOCISpec("actor_uid", []string{"/app"}, nil, nil, "/run/netns/x", nil, nil, want) + spec := buildActorOCISpec("actor_uid", "app", []string{"/app"}, nil, nil, "/run/netns/x", nil, nil, want) caps := spec.Process.Capabilities if caps == nil { @@ -360,7 +400,7 @@ func TestBuildActorOCISpec_Capabilities(t *testing.T) { // The pause container only reaps, so it is built with no capabilities at all. func TestBuildActorOCISpec_NoCapabilitiesForPause(t *testing.T) { - spec := buildActorOCISpec("actor_uid", []string{"/pause"}, nil, nil, "/run/netns/x", nil, nil, nil) + spec := buildActorOCISpec("actor_uid", "pause", []string{"/pause"}, nil, nil, "/run/netns/x", nil, nil, nil) caps := spec.Process.Capabilities if caps == nil { diff --git a/cmd/ateom-microvm/durable.go b/cmd/ateom-microvm/durable.go index f41c061ff..92ae47a3b 100644 --- a/cmd/ateom-microvm/durable.go +++ b/cmd/ateom-microvm/durable.go @@ -87,13 +87,13 @@ func durableMounts(mounts []*ateompb.DurableDirVolumeMount) []specs.Mount { } // workloadSpec returns the OCI spec to start a container with: the prepared -// spec, plus a bind for each durable-dir volume (writable), CSI volume, and -// system-info volume (read-only) it mounts. +// spec, plus a bind for each durable-dir volume (writable), CSI volume, +// system-info volume (read-only), and image volume (read-only) it mounts. // // The spec is copied rather than mutated so the bundle's on-disk config.json // stays as prepared — only the started container sees the binds. func workloadSpec(c actorContainer) *specs.Spec { - if len(c.durableMounts) == 0 && len(c.csiMounts) == 0 && len(c.systemInfoMounts) == 0 { + if len(c.durableMounts) == 0 && len(c.csiMounts) == 0 && len(c.systemInfoMounts) == 0 && len(c.imageMounts) == 0 { return c.spec } spec := *c.spec @@ -103,6 +103,7 @@ func workloadSpec(c actorContainer) *specs.Spec { mounts = append(mounts, durableMounts(c.durableMounts)...) mounts = append(mounts, csiMounts(c.csiMounts)...) mounts = append(mounts, systemInfoMounts(c.systemInfoMounts)...) + mounts = append(mounts, imageVolumeMounts(c.imageMounts, c.name)...) spec.Mounts = mounts return &spec } diff --git a/cmd/ateom-microvm/imagevolume.go b/cmd/ateom-microvm/imagevolume.go new file mode 100644 index 000000000..9890062c0 --- /dev/null +++ b/cmd/ateom-microvm/imagevolume.go @@ -0,0 +1,36 @@ +//go:build linux + +// 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 main + +import ( + "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/kata" + "github.com/agent-substrate/substrate/internal/proto/ateompb" + specs "github.com/opencontainers/runtime-spec/specs-go" +) + +func imageVolumeMounts(mounts []*ateompb.ImageVolumeMount, cid string) []specs.Mount { + out := make([]specs.Mount, 0, len(mounts)) + for _, m := range mounts { + out = append(out, specs.Mount{ + Destination: m.GetMountPath(), + Source: kata.GuestSharedVolumeDir(cid, m.GetVolumeName()), + Type: "bind", + Options: []string{"rbind", "ro"}, + }) + } + return out +} diff --git a/cmd/ateom-microvm/internal/kata/overlay_linux.go b/cmd/ateom-microvm/internal/kata/overlay_linux.go index c5ac44b9e..9b470910b 100644 --- a/cmd/ateom-microvm/internal/kata/overlay_linux.go +++ b/cmd/ateom-microvm/internal/kata/overlay_linux.go @@ -100,6 +100,18 @@ func UpperWorkDirs(upperBase, containerID string) (upper, work string) { // stock kata flow. func GuestSharedRootfs(containerID string) string { return guestSharedDir + containerID + "/rootfs" } +// GuestSharedVolumeDir is the in-guest path one image volume's contents appear +// at, beside the container's rootfs in the same kataShared tree. +func GuestSharedVolumeDir(containerID, volumeName string) string { + return filepath.Join(guestSharedDir, containerID, "volumes", volumeName) +} + +// SharedVolumeDir is the host path under virtiofsd's served tree that +// GuestSharedVolumeDir resolves to. +func SharedVolumeDir(id, containerID, volumeName string) string { + return filepath.Join(SharedDir(id), containerID, "volumes", volumeName) +} + // VirtiofsdOptions configures StartVirtiofsd. type VirtiofsdOptions struct { Binary string // virtiofsd executable; defaults to "virtiofsd" @@ -176,6 +188,35 @@ func waitForSocket(ctx context.Context, path string, timeout time.Duration) erro } } +// StageImageVolume bind-mounts one composed image volume read-only at +// /volumes/ under SharedDir(id), so virtiofsd exposes it to the +// guest. +func StageImageVolume(ctx context.Context, src, id, cid, volumeName string) error { + if cid == "" || volumeName == "" { + return fmt.Errorf("StageImageVolume: empty container id or volume name") + } + dst := SharedVolumeDir(id, cid, volumeName) + if err := reaper.Run(exec.Command("umount", dst)); err != nil { + _ = reaper.Run(exec.Command("umount", "-l", dst)) + } + if err := os.MkdirAll(dst, 0o755); err != nil { + return fmt.Errorf("creating shared volume dir %q: %w", dst, err) + } + cmd := exec.CommandContext(ctx, "mount", "--rbind", src, dst) + var stderr strings.Builder + cmd.Stderr = &stderr + if err := reaper.Run(cmd); err != nil { + return fmt.Errorf("bind-mounting image volume %q -> %q: %w (%s)", src, dst, err, strings.TrimSpace(stderr.String())) + } + ro := exec.CommandContext(ctx, "mount", "-o", "remount,bind,ro", dst) + var roErr strings.Builder + ro.Stderr = &roErr + if err := reaper.Run(ro); err != nil { + return fmt.Errorf("remounting image volume %q read-only: %w (%s)", dst, err, strings.TrimSpace(roErr.String())) + } + return nil +} + // StageMergedRootfs mounts overlay(lower = the OCI image bundle rootfs, upper/work = // the actor's host rootfs-upper dirs for cid) at SharedDir(restoreID)//rootfs — // the merged tree the ONE virtiofsd serves and the guest runs the container on diff --git a/cmd/ateom-microvm/run.go b/cmd/ateom-microvm/run.go index fd575d7da..3f365b0b7 100644 --- a/cmd/ateom-microvm/run.go +++ b/cmd/ateom-microvm/run.go @@ -188,6 +188,8 @@ type actorContainer struct { // systemInfoMounts are the system-info volumes this container mounts, and // where (see systeminfo.go). Empty for containers that declare none. systemInfoMounts []*ateompb.SystemInfoVolumeMount + // imageMounts are the image volumes this container mounts, and where. + imageMounts []*ateompb.ImageVolumeMount } // resolvedRuntime holds the concrete binary/config paths for a request, taken @@ -651,6 +653,7 @@ func (s *AteomService) buildActorContainers(actorUID string, containers []*ateom durableMounts: c.GetDurableDirVolumeMounts(), csiMounts: c.GetCsiVolumeMounts(), systemInfoMounts: c.GetSystemInfoVolumeMounts(), + imageMounts: c.GetImageVolumeMounts(), } } return ctrs, nil @@ -672,6 +675,12 @@ func (s *AteomService) stageMergedRootfs(ctx context.Context, rr resolvedRuntime 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()) + 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 { diff --git a/internal/ateompath/ateompath.go b/internal/ateompath/ateompath.go index 5855c64e3..554781a0b 100644 --- a/internal/ateompath/ateompath.go +++ b/internal/ateompath/ateompath.go @@ -150,6 +150,19 @@ 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), diff --git a/internal/e2e/fixtures/capabilities/capabilities.yaml.tmpl b/internal/e2e/fixtures/capabilities/capabilities.yaml.tmpl index 280911f50..f9ce4e249 100644 --- a/internal/e2e/fixtures/capabilities/capabilities.yaml.tmpl +++ b/internal/e2e/fixtures/capabilities/capabilities.yaml.tmpl @@ -25,7 +25,7 @@ apiVersion: v1 kind: Namespace metadata: - name: ate-e2e-caps${FIXTURE_SUFFIX} + name: ate-e2e${FIXTURE_SUFFIX} --- @@ -33,7 +33,7 @@ apiVersion: ate.dev/v1alpha1 kind: WorkerPool metadata: name: caps - namespace: ate-e2e-caps${FIXTURE_SUFFIX} + namespace: ate-e2e${FIXTURE_SUFFIX} labels: workload: caps spec: @@ -51,7 +51,7 @@ apiVersion: ate.dev/v1alpha1 kind: ActorTemplate metadata: name: caps-default - namespace: ate-e2e-caps${FIXTURE_SUFFIX} + namespace: ate-e2e${FIXTURE_SUFFIX} spec: ${TEMPLATE_SANDBOX_CLASS} containers: @@ -79,7 +79,7 @@ apiVersion: ate.dev/v1alpha1 kind: ActorTemplate metadata: name: caps-exact - namespace: ate-e2e-caps${FIXTURE_SUFFIX} + namespace: ate-e2e${FIXTURE_SUFFIX} spec: ${TEMPLATE_SANDBOX_CLASS} containers: diff --git a/internal/e2e/fixtures/probe/main.go b/internal/e2e/fixtures/probe/main.go index e5a482e0d..4c121b71d 100644 --- a/internal/e2e/fixtures/probe/main.go +++ b/internal/e2e/fixtures/probe/main.go @@ -192,6 +192,30 @@ func whoami(w http.ResponseWriter, _ *http.Request) { writeJSON(w, resp) } +// readfile reports the contents of a path inside the actor, so a test can +// assert on what a volume actually delivered. +func readfile(w http.ResponseWriter, r *http.Request) { + path := r.URL.Query().Get("path") + resp := map[string]string{"path": path} + if b, err := os.ReadFile(path); err == nil { + resp["content"] = string(b) + } else { + resp["error"] = err.Error() + } + writeJSON(w, resp) +} + +func writefile(w http.ResponseWriter, r *http.Request) { + path := r.URL.Query().Get("path") + resp := map[string]string{"path": path} + if err := os.WriteFile(path, []byte("written by probe"), 0o644); err != nil { + resp["error"] = err.Error() + } else { + resp["ok"] = "true" + } + writeJSON(w, resp) +} + // readAllAt reads f's full contents from offset 0 without moving its offset, // so concurrent requests do not interleave seeks on the shared fd. func readAllAt(f *os.File) ([]byte, error) { @@ -282,6 +306,8 @@ func main() { mux := http.NewServeMux() mux.HandleFunc("/whoami", whoami) + mux.HandleFunc("/readfile", readfile) + mux.HandleFunc("/writefile", writefile) mux.HandleFunc("/resources", resources) mux.HandleFunc("/capabilities", capabilities) mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) diff --git a/internal/e2e/fixtures/probe/probe-sized.yaml.tmpl b/internal/e2e/fixtures/probe/probe-sized.yaml.tmpl index be27152f3..2a63ceda9 100644 --- a/internal/e2e/fixtures/probe/probe-sized.yaml.tmpl +++ b/internal/e2e/fixtures/probe/probe-sized.yaml.tmpl @@ -26,7 +26,7 @@ apiVersion: v1 kind: Namespace metadata: - name: ate-e2e-sizing${FIXTURE_SUFFIX} + name: ate-e2e${FIXTURE_SUFFIX} --- @@ -34,7 +34,7 @@ apiVersion: ate.dev/v1alpha1 kind: WorkerPool metadata: name: probe-sized - namespace: ate-e2e-sizing${FIXTURE_SUFFIX} + namespace: ate-e2e${FIXTURE_SUFFIX} labels: workload: probe-sized spec: @@ -48,7 +48,7 @@ apiVersion: ate.dev/v1alpha1 kind: ActorTemplate metadata: name: probe-sized - namespace: ate-e2e-sizing${FIXTURE_SUFFIX} + namespace: ate-e2e${FIXTURE_SUFFIX} spec: ${TEMPLATE_SANDBOX_CLASS} containers: @@ -74,4 +74,4 @@ ${TEMPLATE_SANDBOX_CLASS} matchLabels: workload: probe-sized snapshotsConfig: - location: gs://${BUCKET_NAME}/ate-e2e-sizing${FIXTURE_SUFFIX}/ + location: gs://${BUCKET_NAME}/ate-e2e${FIXTURE_SUFFIX}/ diff --git a/internal/e2e/probe.go b/internal/e2e/probe.go new file mode 100644 index 000000000..56d7ab05f --- /dev/null +++ b/internal/e2e/probe.go @@ -0,0 +1,68 @@ +// 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 e2e + +import ( + "path/filepath" + "testing" +) + +// ProbeName is the name of the probe fixture's WorkerPool and ActorTemplate, +// inside the namespace DeployProbe returns. +const ProbeName = "probe" + +// DeployProbe builds the probe fixture image and applies its manifest for the +// sandbox class under test, removing it when the test ends. name distinguishes +// the caller (by convention its suite name): each suite gets its own copy of +// the fixture, so no suite's cleanup can delete the fixture out from under +// another running concurrently. It returns the fixture's namespace. +func DeployProbe(t *testing.T, bucket, name string) string { + t.Helper() + + root, err := FindRepoRoot() + if err != nil { + t.Fatalf("FindRepoRoot: %v", err) + } + + // One manifest, rendered for the sandbox class under test, so both apply + // and delete consume the same file without any shell involved. + manifest := RenderFixtureManifest(t, "internal/e2e/fixtures/probe/probe.yaml.tmpl", bucket, name) + + // Build/push the probe image and apply through the repo's pinned ko; CI + // does not install ko on PATH. The trailing `-- --context=...` mirrors + // run_ko in hack/install-ate.sh: ko's apply subcommand forwards args after + // `--` to kubectl. KO_CONFIG_PATH is required because ko resolves .ko.yaml + // from its working directory, which is the test's package dir rather than + // the repo root; without it the build silently loses defaultPlatforms and + // produces images that cannot run on the cluster's nodes. + applyArgs := []string{"ko", "apply", "-f", manifest} + if KubeContext != "" { + applyArgs = append(applyArgs, "--", "--context="+KubeContext) + } + RunCmdWithEnv(t, []string{"KO_CONFIG_PATH=" + root}, filepath.Join(root, "hack/run-tool.sh"), applyArgs...) + + t.Cleanup(func() { + // Deletion needs no image build, so go straight to kubectl. `ko delete` + // rejects this arg shape ("you may not specify resource arguments as + // well"). + delArgs := []string{"delete", "--ignore-not-found", "-f", manifest} + if KubeContext != "" { + delArgs = append([]string{"--context=" + KubeContext}, delArgs...) + } + RunCmd(t, "kubectl", delArgs...) + }) + + return FixtureName("ate-e2e-probe") + "-" + name +} diff --git a/internal/e2e/sandbox.go b/internal/e2e/sandbox.go index a54d7702e..29778f10d 100644 --- a/internal/e2e/sandbox.go +++ b/internal/e2e/sandbox.go @@ -130,6 +130,11 @@ func TemplateReadyTimeout(t *testing.T) time.Duration { // the test's temp dir and returns that path. Both an apply and a later delete // can then consume the same file, with no shell involved. // +// name distinguishes the caller (by convention its suite name) and is appended +// to ${FIXTURE_SUFFIX}: suite packages run as concurrent processes, so each +// caller must get its own copy of a fixture or one suite's cleanup deletes it +// out from under another. +// // One template serves both sandbox classes so the two variants of a fixture // cannot drift apart. Templates carry two kinds of ${...} placeholder: // @@ -139,7 +144,7 @@ func TemplateReadyTimeout(t *testing.T) time.Duration { // the whole line with it — the same trick hack/install-demo-counter.sh // plays with `sed /.../d`. Requiring the placeholder to be the whole line // is what lets a comment mention one without being deleted. -func RenderFixtureManifest(t *testing.T, relPath, bucket string) string { +func RenderFixtureManifest(t *testing.T, relPath, bucket, name string) string { t.Helper() root, err := FindRepoRoot() if err != nil { @@ -150,7 +155,7 @@ func RenderFixtureManifest(t *testing.T, relPath, bucket string) string { t.Fatalf("reading fixture manifest %s: %v", relPath, err) } - inline, blocks := fixtureSubstitutions(bucket) + inline, blocks := fixtureSubstitutions(bucket, name) var out []string for line := range strings.SplitSeq(string(raw), "\n") { if value, isBlock := blocks[strings.TrimSpace(line)]; isBlock { @@ -175,13 +180,13 @@ func RenderFixtureManifest(t *testing.T, relPath, bucket string) string { // fixtureSubstitutions is the placeholder set the internal/e2e/fixtures // manifest templates carry, split into the inline and whole-line-block kinds // RenderFixtureManifest treats differently. -func fixtureSubstitutions(bucket string) (inline, blocks map[string]string) { +func fixtureSubstitutions(bucket, name string) (inline, blocks map[string]string) { inline = map[string]string{ "${BUCKET_NAME}": bucket, "${ATEOM_IMAGE}": "ko://github.com/agent-substrate/substrate/cmd/ateom-gvisor", // The manifest-side half of FixtureName: it suffixes the fixture's // namespace, and with it the snapshot prefix underneath. - "${FIXTURE_SUFFIX}": "", + "${FIXTURE_SUFFIX}": "-" + name, } blocks = map[string]string{ "${WORKERPOOL_RUNTIME}": "", @@ -193,7 +198,7 @@ func fixtureSubstitutions(bucket string) (inline, blocks map[string]string) { } inline["${ATEOM_IMAGE}"] = "ko://github.com/agent-substrate/substrate/cmd/ateom-microvm" - inline["${FIXTURE_SUFFIX}"] = "-" + SandboxClassMicroVM + inline["${FIXTURE_SUFFIX}"] = "-" + SandboxClassMicroVM + "-" + name // The cluster-wide SandboxConfig hack/install-microvm-deps.sh installs. A // micro-VM WorkerPool has to name it: it is deliberately not the class // default, so a missing or stale one fails loudly. diff --git a/internal/e2e/sandbox_test.go b/internal/e2e/sandbox_test.go index f96ee16ef..ae71b8edf 100644 --- a/internal/e2e/sandbox_test.go +++ b/internal/e2e/sandbox_test.go @@ -40,7 +40,7 @@ var fixtureManifests = []string{ // a WorkerPool that never gets a micro-VM worker. func renderFixture(t *testing.T, relPath string) (*v1alpha1.WorkerPool, *v1alpha1.ActorTemplate) { t.Helper() - raw, err := os.ReadFile(RenderFixtureManifest(t, relPath, "test-bucket")) + raw, err := os.ReadFile(RenderFixtureManifest(t, relPath, "test-bucket", "render")) if err != nil { t.Fatalf("reading the rendered %s: %v", relPath, err) } @@ -135,7 +135,7 @@ func TestRenderFixtureManifest_MicroVM(t *testing.T) { if template.Spec.Resources.Limits.Memory().IsZero() { t.Errorf("ActorTemplate declares no memory limit, so the guest would boot at the kata default: %+v", template.Spec.Resources) } - if want := "-microvm/"; !strings.HasSuffix(template.Spec.SnapshotsConfig.Location, want) { + if want := "-microvm-render/"; !strings.HasSuffix(template.Spec.SnapshotsConfig.Location, want) { t.Errorf("ActorTemplate snapshot location = %q, want it to end with %q", template.Spec.SnapshotsConfig.Location, want) } diff --git a/internal/e2e/suites/capabilities/capabilities_test.go b/internal/e2e/suites/capabilities/capabilities_test.go index ab121dc58..dfe073a2e 100644 --- a/internal/e2e/suites/capabilities/capabilities_test.go +++ b/internal/e2e/suites/capabilities/capabilities_test.go @@ -141,11 +141,11 @@ func deployFixture(t *testing.T, bucket string) string { t.Fatalf("FindRepoRoot: %v", err) } - namespace := e2e.FixtureName("ate-e2e-caps") + namespace := e2e.FixtureName("ate-e2e") + "-capabilities" // One manifest, rendered for the sandbox class under test (mirrors the // sizing suite). - manifest := e2e.RenderFixtureManifest(t, "internal/e2e/fixtures/capabilities/capabilities.yaml.tmpl", bucket) + manifest := e2e.RenderFixtureManifest(t, "internal/e2e/fixtures/capabilities/capabilities.yaml.tmpl", bucket, "capabilities") // Build/push the probe image and apply through the repo's pinned ko, as the // identity suite does; CI does not install ko on PATH, and KO_CONFIG_PATH is diff --git a/internal/e2e/suites/identity/identity_test.go b/internal/e2e/suites/identity/identity_test.go index a62c58f8d..99bfa908a 100644 --- a/internal/e2e/suites/identity/identity_test.go +++ b/internal/e2e/suites/identity/identity_test.go @@ -19,7 +19,6 @@ import ( "encoding/json" "io" "net/http" - "path/filepath" "testing" "time" @@ -32,11 +31,9 @@ import ( const probeTemplate = "probe" -// probeNamespace is where deployProbe applies the fixture, and the atespace its -// actors live in. Suffixed per sandbox class (see e2e.FixtureName) so the two -// lanes' fixtures never collide: they run one after the other, and this -// namespace is deleted by the test that created it. -var probeNamespace = e2e.FixtureName("ate-e2e-probe") +// probeNamespace is the suite's own probe fixture namespace, and the atespace +// its actors live in. +var probeNamespace string type whoamiResponse struct { File string `json:"file"` @@ -77,7 +74,7 @@ func TestActorIdentity_AfterRestore_IsOwnID_NotGolden(t *testing.T) { ctx := context.Background() clients := e2e.GetClients() - deployProbe(t, env["BUCKET_NAME"]) + probeNamespace = e2e.DeployProbe(t, env["BUCKET_NAME"], "identity") golden := waitForGolden(t, ctx, clients) // Two distinct actors from the same golden snapshot. @@ -195,45 +192,6 @@ func waitForActorState(t *testing.T, ctx context.Context, clients *e2e.Clients, t.Fatalf("timed out waiting for actor %q to reach state %v", actorName, want) } -func deployProbe(t *testing.T, bucket string) { - t.Helper() - root, err := e2e.FindRepoRoot() - if err != nil { - t.Fatalf("FindRepoRoot: %v", err) - } - - // One manifest, rendered for the sandbox class under test, so both apply and - // delete consume the same file without any shell involved. - manifest := e2e.RenderFixtureManifest(t, "internal/e2e/fixtures/probe/probe.yaml.tmpl", bucket) - - // Build/push the probe image and apply the manifest through the repo's - // pinned ko (hack/run-tool.sh ko); CI does not install ko on PATH, and every - // other deploy in this repo goes through this wrapper. The trailing - // `-- --context=...` mirrors run_ko in hack/install-ate.sh: ko's apply - // subcommand forwards args after `--` to kubectl. KO_CONFIG_PATH is - // required because ko resolves .ko.yaml from its working directory, which - // is the test's package dir, not the repo root; without it the build - // silently loses defaultPlatforms (and produces amd64-only images that - // cannot run on arm64 nodes). - applyArgs := []string{"ko", "apply", "-f", manifest} - if e2e.KubeContext != "" { - applyArgs = append(applyArgs, "--", "--context="+e2e.KubeContext) - } - e2e.RunCmdWithEnv(t, []string{"KO_CONFIG_PATH=" + root}, filepath.Join(root, "hack/run-tool.sh"), applyArgs...) - - t.Cleanup(func() { - // Deletion needs no image build, so go straight to kubectl (matching - // demo-counter_delete in hack/install-demo-counter.sh). `ko delete` - // rejects this arg shape ("you may not specify resource arguments as - // well"). - delArgs := []string{"delete", "--ignore-not-found", "-f", manifest} - if e2e.KubeContext != "" { - delArgs = append([]string{"--context=" + e2e.KubeContext}, delArgs...) - } - e2e.RunCmd(t, "kubectl", delArgs...) - }) -} - func waitForGolden(t *testing.T, ctx context.Context, clients *e2e.Clients) string { t.Helper() deadline := time.Now().Add(e2e.TemplateReadyTimeout(t)) diff --git a/internal/e2e/suites/imagevolume/imagevolume_test.go b/internal/e2e/suites/imagevolume/imagevolume_test.go new file mode 100644 index 000000000..1f95c7c15 --- /dev/null +++ b/internal/e2e/suites/imagevolume/imagevolume_test.go @@ -0,0 +1,316 @@ +// 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 imagevolume exercises image volumes against a live cluster. +package imagevolume + +import ( + "archive/tar" + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "slices" + "strings" + "testing" + "time" + + "github.com/agent-substrate/substrate/internal/e2e" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + atespace = "imagevolume" + + // mountPath must not collide with anything the probe's own image ships. + mountPath = "/mnt/ate-image-volume" + + payloadName = "payload.txt" + payloadContent = "delivered by an image volume" + // shadowedName is written by two layers; the upper one must win. + shadowedName = "shadowed.txt" + shadowedContent = "from the middle layer" + // deletedName is shipped by the bottom layer and whited out by the top. + deletedName = "deleted.txt" +) + +const probeName = e2e.ProbeName + +// tarLayer builds a layer from a set of paths to contents. A path whose base +// name starts with ".wh." is an OCI whiteout for the same-named lower path. +func tarLayer(t *testing.T, files map[string]string) v1.Layer { + t.Helper() + + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + for name, body := range files { + if err := tw.WriteHeader(&tar.Header{Name: name, Mode: 0o444, Size: int64(len(body))}); err != nil { + t.Fatalf("writing tar header for %q: %v", name, err) + } + if _, err := tw.Write([]byte(body)); err != nil { + t.Fatalf("writing tar body for %q: %v", name, err) + } + } + if err := tw.Close(); err != nil { + t.Fatalf("closing tar: %v", err) + } + + layer, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(buf.Bytes())), nil + }) + if err != nil { + t.Fatalf("building layer: %v", err) + } + return layer +} + +// buildFixtureImage pushes a three-layer image and returns its digest-pinned +// reference. +func buildFixtureImage(t *testing.T, repo string) string { + t.Helper() + + img, err := mutate.AppendLayers(empty.Image, + tarLayer(t, map[string]string{ + payloadName: "from-the-bottom-layer", + shadowedName: "from-the-bottom-layer", + deletedName: "should-not-survive", + }), + tarLayer(t, map[string]string{shadowedName: shadowedContent}), + tarLayer(t, map[string]string{ + payloadName: payloadContent, + ".wh." + deletedName: "", + }), + ) + if err != nil { + t.Fatalf("appending layers: %v", err) + } + + // A unique tag per run: some registries refuse to overwrite an existing + // tag. The returned reference is digest-pinned, so the tag itself is + // throwaway. + ref := fmt.Sprintf("%s/e2e-imagevolume-fixture:%d", strings.TrimSuffix(repo, "/"), time.Now().UnixNano()) + tag, err := name.ParseReference(ref, name.Insecure) + if err != nil { + t.Fatalf("parsing %q: %v", ref, err) + } + if err := remote.Write(tag, img); err != nil { + t.Fatalf("pushing %q: %v", ref, err) + } + + digest, err := img.Digest() + if err != nil { + t.Fatalf("computing digest: %v", err) + } + return fmt.Sprintf("%s@%s", tag.Context().Name(), digest) +} + +// createTemplate builds a probe ActorTemplate with the fixture attached as an +// image volume, copying the resolved runtime from the shared probe template. +func createTemplate(ctx context.Context, t *testing.T, clients *e2e.Clients, ns *e2e.Namespace, fixtureImage string) *v1alpha1.ActorTemplate { + t.Helper() + + env, err := e2e.CheckEnv("BUCKET_NAME") + if err != nil { + t.Fatalf("CheckEnv: %v", err) + } + + // The probe supplies this suite's container image and resolved runtime. + probeNamespace := e2e.DeployProbe(t, env["BUCKET_NAME"], "imagevolume") + + srcPool, err := clients.SubstrateK8s.ApiV1alpha1().WorkerPools(probeNamespace).Get(ctx, probeName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("getting WorkerPool %s/%s: %v", probeNamespace, probeName, err) + } + srcTemplate, err := clients.SubstrateK8s.ApiV1alpha1().ActorTemplates(probeNamespace).Get(ctx, probeName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("getting ActorTemplate %s/%s: %v", probeNamespace, probeName, err) + } + + // The pool is labeled uniquely to this namespace so the cluster-wide + // scheduler cannot hand its workers to another suite's actors. + poolLabels := map[string]string{"imagevolume": ns.Name} + pool := &v1alpha1.WorkerPool{ + ObjectMeta: metav1.ObjectMeta{Name: probeName, Namespace: ns.Name, Labels: poolLabels}, + Spec: v1alpha1.WorkerPoolSpec{ + Replicas: 2, + AteomImage: srcPool.Spec.AteomImage, + SandboxClass: srcPool.Spec.SandboxClass, + SandboxConfigName: srcPool.Spec.SandboxConfigName, + }, + } + if _, err := clients.SubstrateK8s.ApiV1alpha1().WorkerPools(ns.Name).Create(ctx, pool, metav1.CreateOptions{}); err != nil { + t.Fatalf("creating WorkerPool: %v", err) + } + + container := srcTemplate.Spec.Containers[0] + container.VolumeMounts = append(container.VolumeMounts, v1alpha1.VolumeMount{Name: "fixture", MountPath: mountPath}) + + volumes := append(slices.Clone(srcTemplate.Spec.Volumes), v1alpha1.Volume{ + Name: "fixture", + VolumeSource: v1alpha1.VolumeSource{Image: &v1alpha1.ImageVolumeSource{Reference: fixtureImage}}, + }) + + at := &v1alpha1.ActorTemplate{ + ObjectMeta: metav1.ObjectMeta{Name: probeName, Namespace: ns.Name}, + Spec: v1alpha1.ActorTemplateSpec{ + Containers: []v1alpha1.Container{container}, + WorkerSelector: &metav1.LabelSelector{MatchLabels: poolLabels}, + SandboxClass: srcTemplate.Spec.SandboxClass, + Volumes: volumes, + SnapshotsConfig: v1alpha1.SnapshotsConfig{ + Location: fmt.Sprintf("gs://%s/%s/", env["BUCKET_NAME"], ns.Name), + }, + }, + } + created, err := clients.SubstrateK8s.ApiV1alpha1().ActorTemplates(ns.Name).Create(ctx, at, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("creating ActorTemplate: %v", err) + } + + e2e.WaitForTemplateReady(ctx, t, clients, ns.Name, probeName) + return created +} + +// probeJSON calls a probe endpoint through the router and decodes its reply. +func probeJSON(ctx context.Context, t *testing.T, router *e2e.RouterClient, actorRef resources.ActorRef, path string) map[string]string { + t.Helper() + + resp, err := router.Get(ctx, actorRef, path) + if err != nil { + t.Fatalf("GET %s: %v", path, err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + t.Fatalf("GET %s: status %d: %s", path, resp.StatusCode, body) + } + + var out map[string]string + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + t.Fatalf("decoding %s: %v", path, err) + } + return out +} + +func TestImageVolume(t *testing.T) { + repo := os.Getenv("KO_DOCKER_REPO") + if repo == "" { + t.Skip("KO_DOCKER_REPO is unset; it names the registry both this host and the cluster can reach") + } + + ctx := context.Background() + clients := e2e.GetClients() + ns := e2e.CreateNamespace(t) + + fixtureImage := buildFixtureImage(t, repo) + t.Logf("fixture image: %s", fixtureImage) + createTemplate(ctx, t, clients, ns, fixtureImage) + + if _, err := clients.SubstrateAPI.CreateAtespace(ctx, &ateapipb.CreateAtespaceRequest{ + Atespace: &ateapipb.Atespace{Metadata: &ateapipb.ResourceMetadata{Name: atespace}}, + }); err != nil { + t.Logf("CreateAtespace (may already exist): %v", err) + } + + actorRef := resources.ActorRef{Atespace: atespace, Name: "iv-" + ns.Name} + if _, err := clients.SubstrateAPI.CreateActor(ctx, &ateapipb.CreateActorRequest{ + Actor: &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: actorRef.Atespace, Name: actorRef.Name}, + ActorTemplateNamespace: ns.Name, + ActorTemplateName: probeName, + }, + }); err != nil { + t.Fatalf("CreateActor: %v", err) + } + t.Cleanup(func() { + cleanupCtx := context.Background() + _, _ = clients.SubstrateAPI.SuspendActor(cleanupCtx, &ateapipb.SuspendActorRequest{Actor: actorRef.ToObjectRef()}) + _, _ = clients.SubstrateAPI.DeleteActor(cleanupCtx, &ateapipb.DeleteActorRequest{Actor: actorRef.ToObjectRef()}) + }) + + if _, err := clients.SubstrateAPI.ResumeActor(ctx, &ateapipb.ResumeActorRequest{Actor: actorRef.ToObjectRef()}); err != nil { + t.Fatalf("ResumeActor: %v", err) + } + + router, err := e2e.NewRouterClient(ctx) + if err != nil { + t.Fatalf("NewRouterClient: %v", err) + } + defer router.Close() + + payloadPath := mountPath + "/" + payloadName + + t.Run("DeliversImageContents", func(t *testing.T) { + got := probeJSON(ctx, t, router, actorRef, "/readfile?path="+payloadPath) + if got["error"] != "" { + t.Fatalf("reading %s: %s", payloadPath, got["error"]) + } + if got["content"] != payloadContent { + t.Errorf("content = %q, want %q", got["content"], payloadContent) + } + }) + + t.Run("UpperLayerWins", func(t *testing.T) { + got := probeJSON(ctx, t, router, actorRef, "/readfile?path="+mountPath+"/"+shadowedName) + if got["error"] != "" { + t.Fatalf("reading %s: %s", shadowedName, got["error"]) + } + if got["content"] != shadowedContent { + t.Errorf("content = %q, want %q from the upper layer", got["content"], shadowedContent) + } + }) + + t.Run("WhiteoutHidesLowerLayerFile", func(t *testing.T) { + got := probeJSON(ctx, t, router, actorRef, "/readfile?path="+mountPath+"/"+deletedName) + if got["error"] == "" { + t.Errorf("%s is readable (%q), want it hidden by the whiteout", deletedName, got["content"]) + } + }) + + t.Run("MountIsReadOnly", func(t *testing.T) { + got := probeJSON(ctx, t, router, actorRef, "/writefile?path="+mountPath+"/should-not-exist") + if got["error"] == "" { + t.Errorf("write to the image volume succeeded, want it rejected as read-only") + } + }) + + t.Run("SurvivesSuspendResume", func(t *testing.T) { + if _, err := clients.SubstrateAPI.SuspendActor(ctx, &ateapipb.SuspendActorRequest{Actor: actorRef.ToObjectRef()}); err != nil { + t.Fatalf("SuspendActor: %v", err) + } + + // No explicit resume: routing to the actor is what wakes it. + resumeCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) + defer cancel() + got := probeJSON(resumeCtx, t, router, actorRef, "/readfile?path="+payloadPath) + if got["error"] != "" { + t.Fatalf("reading %s after resume: %s", payloadPath, got["error"]) + } + if got["content"] != payloadContent { + t.Errorf("content after resume = %q, want %q", got["content"], payloadContent) + } + }) +} diff --git a/internal/e2e/suites/imagevolume/testmain_test.go b/internal/e2e/suites/imagevolume/testmain_test.go new file mode 100644 index 000000000..cda783977 --- /dev/null +++ b/internal/e2e/suites/imagevolume/testmain_test.go @@ -0,0 +1,24 @@ +// 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 imagevolume + +import ( + "os" + "testing" + + "github.com/agent-substrate/substrate/internal/e2e" +) + +func TestMain(m *testing.M) { os.Exit(e2e.RunTestMain(m)) } diff --git a/internal/e2e/suites/sizing/sizing_test.go b/internal/e2e/suites/sizing/sizing_test.go index 251795670..41aa4f34e 100644 --- a/internal/e2e/suites/sizing/sizing_test.go +++ b/internal/e2e/suites/sizing/sizing_test.go @@ -43,7 +43,7 @@ const ( // sizingNamespace is where deploySizedProbe applies the fixture, and the // atespace its actor lives in. Suffixed per sandbox class (see // e2e.FixtureName) so the two lanes' fixtures never collide. -var sizingNamespace = e2e.FixtureName("ate-e2e-sizing") +var sizingNamespace = e2e.FixtureName("ate-e2e") + "-sizing" // resourcesResponse mirrors the /resources endpoint of the probe fixture. type resourcesResponse struct { @@ -121,7 +121,7 @@ func deploySizedProbe(t *testing.T, bucket string) { // One manifest, rendered for the sandbox class under test (mirrors the // identity suite). - manifest := e2e.RenderFixtureManifest(t, "internal/e2e/fixtures/probe/probe-sized.yaml.tmpl", bucket) + manifest := e2e.RenderFixtureManifest(t, "internal/e2e/fixtures/probe/probe-sized.yaml.tmpl", bucket, "sizing") // Build/push the probe image and apply through the repo's pinned ko. See the // identity suite's deployProbe for why KO_CONFIG_PATH and the trailing diff --git a/internal/e2e/template.go b/internal/e2e/template.go new file mode 100644 index 000000000..e77987895 --- /dev/null +++ b/internal/e2e/template.go @@ -0,0 +1,62 @@ +// 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 e2e + +import ( + "context" + "os" + "testing" + "time" + + "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// WaitForTemplateReady blocks until the ActorTemplate's golden actor has +// booted and been snapshotted. The default 5 minute timeout can be +// overridden with E2E_TEMPLATE_READY_TIMEOUT. +func WaitForTemplateReady(ctx context.Context, t *testing.T, clients *Clients, namespace, name string) { + t.Helper() + + timeout := 5 * time.Minute + if v := os.Getenv("E2E_TEMPLATE_READY_TIMEOUT"); v != "" { + d, err := time.ParseDuration(v) + if err != nil { + t.Fatalf("invalid E2E_TEMPLATE_READY_TIMEOUT %q: %v", v, err) + } + timeout = d + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + var lastPhase v1alpha1.PhaseType + for { + at, err := clients.SubstrateK8s.ApiV1alpha1().ActorTemplates(namespace).Get(ctx, name, metav1.GetOptions{}) + if err == nil { + lastPhase = at.Status.Phase + if lastPhase == v1alpha1.PhaseReady { + return + } + if lastPhase == v1alpha1.PhaseFailed { + t.Fatalf("ActorTemplate %s/%s transitioned to Failed", namespace, name) + } + } + select { + case <-ctx.Done(): + t.Fatalf("timed out after %v waiting for ActorTemplate %s/%s to be Ready (last phase %q, err %v)", timeout, namespace, name, lastPhase, err) + case <-time.After(time.Second): + } + } +} diff --git a/internal/imagecache/bundle_linux.go b/internal/imagecache/bundle_linux.go index 5b1eeb88f..55fd4f357 100644 --- a/internal/imagecache/bundle_linux.go +++ b/internal/imagecache/bundle_linux.go @@ -28,6 +28,8 @@ 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 @@ -94,15 +96,63 @@ func SetupBundleRootfs(bundlePath string) error { if err := applyDirFixups(rootfs, fixups); err != nil { return fmt.Errorf("while repairing implicit dir metadata: %w", err) } + + if err := setupImageVolumes(bundlePath, spec.ImageVolumes); err != nil { + return fmt.Errorf("while setting up image volumes: %w", err) + } return nil } +// setupImageVolumes exposes each image volume's contents read-only at its +// bundle-local mount point, for the OCI spec to bind into the container. +func setupImageVolumes(bundlePath string, volumes []ImageVolumeOverlay) error { + for _, vol := range volumes { + for _, layerDir := range vol.Layers { + if err := FinalizeLayer(layerDir); err != nil { + return fmt.Errorf("while finalizing layer %q of image volume %q: %w", layerDir, vol.Name, err) + } + } + + mountpoint := ateompath.ImageVolumeMountPathInBundle(bundlePath, vol.Name) + if err := os.MkdirAll(mountpoint, 0o700); err != nil { + return fmt.Errorf("while creating image volume mount point %q: %w", mountpoint, err) + } + _ = unix.Unmount(mountpoint, unix.MNT_DETACH) + + if err := mountImageVolume(mountpoint, vol.Layers); err != nil { + return fmt.Errorf("while mounting image volume %q: %w", vol.Name, err) + } + } + return nil +} + +// mountImageVolume attaches layers read-only at mountpoint. +func mountImageVolume(mountpoint string, layers []string) error { + if len(layers) == 1 { + // The kernel rejects a single lowerdir without an upperdir, so bind + // the layer's fs/ tree directly for the single-layer case. + fsDir := filepath.Join(layers[0], layerFSDirName) + if err := unix.Mount(fsDir, mountpoint, "", unix.MS_BIND|unix.MS_REC, ""); err != nil { + return fmt.Errorf("while binding %q: %w", fsDir, err) + } + // MS_BIND and MS_RDONLY cannot be combined; a second call applies read-only. + if err := unix.Mount("", mountpoint, "", unix.MS_REMOUNT|unix.MS_BIND|unix.MS_RDONLY, ""); err != nil { + return fmt.Errorf("while remounting %q read-only: %w", mountpoint, err) + } + return nil + } + return mountOverlay(mountpoint, overlayLowerDirs(layers), "", "") +} + // mountOverlay attaches an overlay of lowers (top-most first) with the given // upper/work dirs at mountpoint, using the new mount API rather than // mount(2): appending lowerdirs one fsconfig(2) call at a time sidesteps // mount(2)'s single-page option-string cap, which digest-derived layer paths // (~114 bytes each) would hit at roughly 34 layers. // +// An empty upper mounts the overlay without a writable layer, which overlayfs +// makes read-only. +// // Minimum supported kernel: Linux 6.5, where overlayfs gained the // incremental "lowerdir+" option. Every current GKE channel is at or above // it (Stable runs COS 121 LTS on kernel 6.6; Regular and Rapid run COS @@ -125,11 +175,13 @@ func mountOverlay(mountpoint string, lowers []string, upper, work string) error return err } } - if err := set("upperdir", upper); err != nil { - return err - } - if err := set("workdir", work); err != nil { - return err + if upper != "" { + if err := set("upperdir", upper); err != nil { + return err + } + if err := set("workdir", work); err != nil { + return err + } } // volatile: skip overlayfs's syncs on this upper, including the one it does // at umount, which measured ~450ms per actor on GKE. The bundle upper holds diff --git a/internal/imagecache/bundle_linux_test.go b/internal/imagecache/bundle_linux_test.go index 8a3d88c50..3d3520604 100644 --- a/internal/imagecache/bundle_linux_test.go +++ b/internal/imagecache/bundle_linux_test.go @@ -18,6 +18,7 @@ package imagecache import ( "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -289,3 +290,70 @@ func TestSetupBundleRootfs_ManyLayers(t *testing.T) { t.Fatalf("UnmountAllUnder: %v", err) } } + +// Image volumes reach identical content through both arms: one layer binds, +// several overlay. +func TestSetupBundleRootfs_ImageVolumes(t *testing.T) { + roottest.Require(t, "mount/unmount") + + for _, tc := range []struct { + name string + layers int + }{ + {"one layer binds", 1}, + {"three layers overlay", 3}, + } { + t.Run(tc.name, func(t *testing.T) { + var layers []string + for i := range tc.layers { + dir := t.TempDir() + writeLayer(t, dir, map[string]string{ + fmt.Sprintf("layer%d.txt", i): "content", + "shadowed.txt": fmt.Sprintf("from-layer-%d", i), + }, nil) + layers = append(layers, dir) + } + + bundle := t.TempDir() + if err := WriteSpec(bundle, &OverlaySpec{ + Layers: []string{layers[0]}, + ImageVolumes: []ImageVolumeOverlay{{Name: "agent", Layers: layers}}, + }); err != nil { + t.Fatalf("WriteSpec: %v", err) + } + if err := SetupBundleRootfs(bundle); err != nil { + t.Fatalf("SetupBundleRootfs: %v", err) + } + t.Cleanup(func() { _ = UnmountAllUnder(bundle) }) + + mnt := filepath.Join(bundle, "volumes", "agent") + for i := range tc.layers { + if _, err := os.Stat(filepath.Join(mnt, fmt.Sprintf("layer%d.txt", i))); err != nil { + t.Errorf("layer %d not visible in the volume: %v", i, err) + } + } + // Later layers win, same as the rootfs overlay. + want := fmt.Sprintf("from-layer-%d", tc.layers-1) + if got, err := os.ReadFile(filepath.Join(mnt, "shadowed.txt")); err != nil || string(got) != want { + t.Errorf("shadowed.txt = %q (%v), want %q", got, err, want) + } + // No upper on either arm, so there is nowhere for a write to go. + if err := os.WriteFile(filepath.Join(mnt, "nope.txt"), []byte("x"), 0o644); err == nil { + t.Error("write succeeded through a read-only image volume") + } else if !errors.Is(err, unix.EROFS) { + t.Errorf("write failed with %v, want EROFS", err) + } + // The shared pool must never see the attempt. + if _, err := os.Stat(filepath.Join(layers[tc.layers-1], layerFSDirName, "nope.txt")); err == nil { + t.Error("write leaked into the shared layer pool") + } + + if err := UnmountAllUnder(bundle); err != nil { + t.Fatalf("UnmountAllUnder: %v", err) + } + if _, err := os.Stat(filepath.Join(mnt, "layer0.txt")); err == nil { + t.Error("volume still shows content after unmount") + } + }) + } +} diff --git a/internal/imagecache/gc.go b/internal/imagecache/gc.go index 8a27804fb..bdc1d6d19 100644 --- a/internal/imagecache/gc.go +++ b/internal/imagecache/gc.go @@ -166,24 +166,33 @@ func (s *Store) InUse() (RootSet, error) { return rs, errors.Join(errs...) } -// addSpecRoots roots one bundle spec's image digest, layers, and exact -// layer-set signature. +// addSpecRoots roots one bundle spec's image digests (including image volume +// images), layers, and exact layer-set signatures. func addSpecRoots(rs *RootSet, spec *OverlaySpec, bundle string, dbg bool) { - if spec.ImageDigest != "" { - rs.ImageDigests[spec.ImageDigest] = true + addImageRoots(rs, spec.ImageDigest, spec.Layers, bundle, dbg) + for _, vol := range spec.ImageVolumes { + addImageRoots(rs, vol.ImageDigest, vol.Layers, bundle+"/volumes/"+vol.Name, dbg) + } +} + +// addImageRoots roots one image's digest, layer hexes, and exact layer-set +// signature. +func addImageRoots(rs *RootSet, digest string, layers []string, bundle string, dbg bool) { + if digest != "" { + rs.ImageDigests[digest] = true if dbg { slog.Debug("Image cache root-set: bundle roots image", slog.String("bundle", bundle), - slog.String("digest", spec.ImageDigest), - slog.Int("layers", len(spec.Layers))) + slog.String("digest", digest), + slog.Int("layers", len(layers))) } - } else if len(spec.Layers) > 0 && dbg { + } else if len(layers) > 0 && dbg { slog.Debug("Image cache root-set: digestless bundle roots layers only", slog.String("bundle", bundle), - slog.Int("layers", len(spec.Layers))) + slog.Int("layers", len(layers))) } - hexes := make([]string, 0, len(spec.Layers)) - for _, layerDir := range spec.Layers { + hexes := make([]string, 0, len(layers)) + for _, layerDir := range layers { hex := filepath.Base(layerDir) rs.LayerHexes[hex] = true hexes = append(hexes, hex) diff --git a/internal/imagecache/gc_test.go b/internal/imagecache/gc_test.go index a4fae1160..8b0b6c8b9 100644 --- a/internal/imagecache/gc_test.go +++ b/internal/imagecache/gc_test.go @@ -233,6 +233,62 @@ func TestEvictUnusedRootSet(t *testing.T) { } } +// A bundle's image volumes root their images exactly like its rootfs does: +// the volume's layers are bind-mounted for as long as the bundle exists. +func TestEvictUnusedRootSetImageVolumes(t *testing.T) { + _, host := newTestRegistry(t) + refRootfs := host + "/test/rootfs:latest" + refVolume := host + "/test/volume:latest" + for _, r := range []string{refRootfs, refVolume} { + pushImage(t, r, v1.Config{}, layerFromEntries(t, []tarEntry{ + {name: "f-" + r[len(r)-8:], typeflag: tar.TypeReg, mode: 0o644, body: r}, + })) + } + + actorsDir := t.TempDir() + store := newTestStore(t, WithActorsDir(actorsDir)) + imgRootfs := mustEnsure(t, store, refRootfs) + imgVolume := mustEnsure(t, store, refVolume) + + bundle := filepath.Join(actorsDir, "actor-1", "bundles", "main") + if err := os.MkdirAll(bundle, 0o700); err != nil { + t.Fatal(err) + } + if err := WriteSpec(bundle, &OverlaySpec{ + ImageDigest: imgRootfs.Digest.String(), + Layers: imgRootfs.LayerDirs, + ImageVolumes: []ImageVolumeOverlay{{ + Name: "agent", + ImageDigest: imgVolume.Digest.String(), + Layers: imgVolume.LayerDirs, + }}, + }); err != nil { + t.Fatal(err) + } + + rs, err := store.InUse() + if err != nil { + t.Fatalf("InUse: %v", err) + } + if !rs.ImageDigests[imgVolume.Digest.String()] { + t.Error("InUse missing image volume digest") + } + if !rs.LayerHexes[filepath.Base(imgVolume.LayerDirs[0])] { + t.Error("InUse missing image volume layer") + } + + backdateStore(t, store, 3*time.Hour) + if _, err := store.EvictUnused(context.Background(), math.MaxInt64, false); err != nil { + t.Fatalf("EvictUnused: %v", err) + } + if _, err := os.Stat(store.recordPath(imgVolume.Digest)); err != nil { + t.Errorf("image volume record evicted while its bundle exists: %v", err) + } + if _, err := os.Stat(imgVolume.LayerDirs[0]); err != nil { + t.Errorf("image volume layer evicted while its bundle exists: %v", err) + } +} + // An unreadable record must gate the whole pass: its refcounts are // invisible, so a layer it shares with a readable candidate would hit // zero and be retired while the unreadable record still names it. diff --git a/internal/imagecache/spec.go b/internal/imagecache/spec.go index 1cba5b1fb..f8670bd70 100644 --- a/internal/imagecache/spec.go +++ b/internal/imagecache/spec.go @@ -53,6 +53,23 @@ type OverlaySpec struct { // that must exist for the runtime to attach them, e.g. the actor identity // mount. ExtraDirs []string `json:"extraDirs,omitempty"` + // ImageVolumes are read-only image contents to expose beside the rootfs, + // one per image-typed volume the container mounts. The consumer composes + // each at the volume's bundle-local mount point, which the OCI spec binds + // into the container. + ImageVolumes []ImageVolumeOverlay `json:"imageVolumes,omitempty"` +} + +// ImageVolumeOverlay is one image volume's contents. +type ImageVolumeOverlay struct { + // Name is the ActorTemplate's name for the volume. + Name string `json:"name"` + // ImageDigest is the manifest digest the volume's ref resolved to, in the + // same form and for the same reason as OverlaySpec.ImageDigest: the GC's + // root-set scan protects an image by digest. + ImageDigest string `json:"imageDigest,omitempty"` + // Layers are the cached layer directories, bottom-most first. + Layers []string `json:"layers"` } // WriteSpec writes spec into the bundle at bundlePath. diff --git a/internal/proto/ateletpb/atelet.pb.go b/internal/proto/ateletpb/atelet.pb.go index 6c541b485..4b978c61b 100644 --- a/internal/proto/ateletpb/atelet.pb.go +++ b/internal/proto/ateletpb/atelet.pb.go @@ -803,6 +803,50 @@ func (x *ExternalVolumeSource) GetVolumeContext() map[string]string { return nil } +type ImageVolumeSource struct { + state protoimpl.MessageState `protogen:"open.v1"` + Reference string `protobuf:"bytes,1,opt,name=reference,proto3" json:"reference,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImageVolumeSource) Reset() { + *x = ImageVolumeSource{} + mi := &file_atelet_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImageVolumeSource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImageVolumeSource) ProtoMessage() {} + +func (x *ImageVolumeSource) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[10] + 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 ImageVolumeSource.ProtoReflect.Descriptor instead. +func (*ImageVolumeSource) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{10} +} + +func (x *ImageVolumeSource) GetReference() string { + if x != nil { + return x.Reference + } + return "" +} + // ActorMetadataItem projects one actor identity field to one file at the // given path, relative to the root of the enclosing system-info volume. type ActorMetadataItem struct { @@ -815,7 +859,7 @@ type ActorMetadataItem struct { func (x *ActorMetadataItem) Reset() { *x = ActorMetadataItem{} - mi := &file_atelet_proto_msgTypes[10] + mi := &file_atelet_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -827,7 +871,7 @@ func (x *ActorMetadataItem) String() string { func (*ActorMetadataItem) ProtoMessage() {} func (x *ActorMetadataItem) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[10] + mi := &file_atelet_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -840,7 +884,7 @@ func (x *ActorMetadataItem) ProtoReflect() protoreflect.Message { // Deprecated: Use ActorMetadataItem.ProtoReflect.Descriptor instead. func (*ActorMetadataItem) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{10} + return file_atelet_proto_rawDescGZIP(), []int{11} } func (x *ActorMetadataItem) GetField() ActorMetadataField { @@ -868,7 +912,7 @@ type ActorMetadataDataSource struct { func (x *ActorMetadataDataSource) Reset() { *x = ActorMetadataDataSource{} - mi := &file_atelet_proto_msgTypes[11] + mi := &file_atelet_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -880,7 +924,7 @@ func (x *ActorMetadataDataSource) String() string { func (*ActorMetadataDataSource) ProtoMessage() {} func (x *ActorMetadataDataSource) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[11] + mi := &file_atelet_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -893,7 +937,7 @@ func (x *ActorMetadataDataSource) ProtoReflect() protoreflect.Message { // Deprecated: Use ActorMetadataDataSource.ProtoReflect.Descriptor instead. func (*ActorMetadataDataSource) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{11} + return file_atelet_proto_rawDescGZIP(), []int{12} } func (x *ActorMetadataDataSource) GetItems() []*ActorMetadataItem { @@ -915,7 +959,7 @@ type SystemInfoDataSource struct { func (x *SystemInfoDataSource) Reset() { *x = SystemInfoDataSource{} - mi := &file_atelet_proto_msgTypes[12] + mi := &file_atelet_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -927,7 +971,7 @@ func (x *SystemInfoDataSource) String() string { func (*SystemInfoDataSource) ProtoMessage() {} func (x *SystemInfoDataSource) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[12] + mi := &file_atelet_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -940,7 +984,7 @@ func (x *SystemInfoDataSource) ProtoReflect() protoreflect.Message { // Deprecated: Use SystemInfoDataSource.ProtoReflect.Descriptor instead. func (*SystemInfoDataSource) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{12} + return file_atelet_proto_rawDescGZIP(), []int{13} } func (x *SystemInfoDataSource) GetDataSource() isSystemInfoDataSource_DataSource { @@ -981,7 +1025,7 @@ type SystemInfoVolume struct { func (x *SystemInfoVolume) Reset() { *x = SystemInfoVolume{} - mi := &file_atelet_proto_msgTypes[13] + mi := &file_atelet_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -993,7 +1037,7 @@ func (x *SystemInfoVolume) String() string { func (*SystemInfoVolume) ProtoMessage() {} func (x *SystemInfoVolume) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[13] + mi := &file_atelet_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1006,7 +1050,7 @@ func (x *SystemInfoVolume) ProtoReflect() protoreflect.Message { // Deprecated: Use SystemInfoVolume.ProtoReflect.Descriptor instead. func (*SystemInfoVolume) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{13} + return file_atelet_proto_rawDescGZIP(), []int{14} } func (x *SystemInfoVolume) GetDataSources() []*SystemInfoDataSource { @@ -1024,6 +1068,7 @@ type Volume struct { // *Volume_DurableDir // *Volume_External // *Volume_SystemInfo + // *Volume_Image Source isVolume_Source `protobuf_oneof:"source"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1031,7 +1076,7 @@ type Volume struct { func (x *Volume) Reset() { *x = Volume{} - mi := &file_atelet_proto_msgTypes[14] + mi := &file_atelet_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1043,7 +1088,7 @@ func (x *Volume) String() string { func (*Volume) ProtoMessage() {} func (x *Volume) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[14] + mi := &file_atelet_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1056,7 +1101,7 @@ func (x *Volume) ProtoReflect() protoreflect.Message { // Deprecated: Use Volume.ProtoReflect.Descriptor instead. func (*Volume) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{14} + return file_atelet_proto_rawDescGZIP(), []int{15} } func (x *Volume) GetName() string { @@ -1100,6 +1145,15 @@ func (x *Volume) GetSystemInfo() *SystemInfoVolume { return nil } +func (x *Volume) GetImage() *ImageVolumeSource { + if x != nil { + if x, ok := x.Source.(*Volume_Image); ok { + return x.Image + } + } + return nil +} + type isVolume_Source interface { isVolume_Source() } @@ -1116,12 +1170,18 @@ type Volume_SystemInfo struct { SystemInfo *SystemInfoVolume `protobuf:"bytes,4,opt,name=system_info,json=systemInfo,proto3,oneof"` } +type Volume_Image struct { + Image *ImageVolumeSource `protobuf:"bytes,5,opt,name=image,proto3,oneof"` +} + func (*Volume_DurableDir) isVolume_Source() {} func (*Volume_External) isVolume_Source() {} func (*Volume_SystemInfo) isVolume_Source() {} +func (*Volume_Image) isVolume_Source() {} + type VolumeMount struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -1132,7 +1192,7 @@ type VolumeMount struct { func (x *VolumeMount) Reset() { *x = VolumeMount{} - mi := &file_atelet_proto_msgTypes[15] + mi := &file_atelet_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1144,7 +1204,7 @@ func (x *VolumeMount) String() string { func (*VolumeMount) ProtoMessage() {} func (x *VolumeMount) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[15] + mi := &file_atelet_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1157,7 +1217,7 @@ func (x *VolumeMount) ProtoReflect() protoreflect.Message { // Deprecated: Use VolumeMount.ProtoReflect.Descriptor instead. func (*VolumeMount) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{15} + return file_atelet_proto_rawDescGZIP(), []int{16} } func (x *VolumeMount) GetName() string { @@ -1190,7 +1250,7 @@ type Container struct { func (x *Container) Reset() { *x = Container{} - mi := &file_atelet_proto_msgTypes[16] + mi := &file_atelet_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1202,7 +1262,7 @@ func (x *Container) String() string { func (*Container) ProtoMessage() {} func (x *Container) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[16] + mi := &file_atelet_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1215,7 +1275,7 @@ func (x *Container) ProtoReflect() protoreflect.Message { // Deprecated: Use Container.ProtoReflect.Descriptor instead. func (*Container) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{16} + return file_atelet_proto_rawDescGZIP(), []int{17} } func (x *Container) GetName() string { @@ -1284,7 +1344,7 @@ type SecurityContext struct { func (x *SecurityContext) Reset() { *x = SecurityContext{} - mi := &file_atelet_proto_msgTypes[17] + mi := &file_atelet_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1296,7 +1356,7 @@ func (x *SecurityContext) String() string { func (*SecurityContext) ProtoMessage() {} func (x *SecurityContext) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[17] + mi := &file_atelet_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1309,7 +1369,7 @@ func (x *SecurityContext) ProtoReflect() protoreflect.Message { // Deprecated: Use SecurityContext.ProtoReflect.Descriptor instead. func (*SecurityContext) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{17} + return file_atelet_proto_rawDescGZIP(), []int{18} } func (x *SecurityContext) GetCapabilities() *Capabilities { @@ -1331,7 +1391,7 @@ type Capabilities struct { func (x *Capabilities) Reset() { *x = Capabilities{} - mi := &file_atelet_proto_msgTypes[18] + mi := &file_atelet_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1343,7 +1403,7 @@ func (x *Capabilities) String() string { func (*Capabilities) ProtoMessage() {} func (x *Capabilities) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[18] + mi := &file_atelet_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1356,7 +1416,7 @@ func (x *Capabilities) ProtoReflect() protoreflect.Message { // Deprecated: Use Capabilities.ProtoReflect.Descriptor instead. func (*Capabilities) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{18} + return file_atelet_proto_rawDescGZIP(), []int{19} } func (x *Capabilities) GetAdd() []string { @@ -1383,7 +1443,7 @@ type EnvEntry struct { func (x *EnvEntry) Reset() { *x = EnvEntry{} - mi := &file_atelet_proto_msgTypes[19] + mi := &file_atelet_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1395,7 +1455,7 @@ func (x *EnvEntry) String() string { func (*EnvEntry) ProtoMessage() {} func (x *EnvEntry) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[19] + mi := &file_atelet_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1408,7 +1468,7 @@ func (x *EnvEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use EnvEntry.ProtoReflect.Descriptor instead. func (*EnvEntry) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{19} + return file_atelet_proto_rawDescGZIP(), []int{20} } func (x *EnvEntry) GetName() string { @@ -1439,7 +1499,7 @@ type Readyz struct { func (x *Readyz) Reset() { *x = Readyz{} - mi := &file_atelet_proto_msgTypes[20] + mi := &file_atelet_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1451,7 +1511,7 @@ func (x *Readyz) String() string { func (*Readyz) ProtoMessage() {} func (x *Readyz) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[20] + mi := &file_atelet_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1464,7 +1524,7 @@ func (x *Readyz) ProtoReflect() protoreflect.Message { // Deprecated: Use Readyz.ProtoReflect.Descriptor instead. func (*Readyz) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{20} + return file_atelet_proto_rawDescGZIP(), []int{21} } func (x *Readyz) GetHttpGet() *HTTPGetAction { @@ -1494,7 +1554,7 @@ type HTTPGetAction struct { func (x *HTTPGetAction) Reset() { *x = HTTPGetAction{} - mi := &file_atelet_proto_msgTypes[21] + mi := &file_atelet_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1506,7 +1566,7 @@ func (x *HTTPGetAction) String() string { func (*HTTPGetAction) ProtoMessage() {} func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[21] + mi := &file_atelet_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1519,7 +1579,7 @@ func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { // Deprecated: Use HTTPGetAction.ProtoReflect.Descriptor instead. func (*HTTPGetAction) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{21} + return file_atelet_proto_rawDescGZIP(), []int{22} } func (x *HTTPGetAction) GetPath() string { @@ -1544,7 +1604,7 @@ type RunResponse struct { func (x *RunResponse) Reset() { *x = RunResponse{} - mi := &file_atelet_proto_msgTypes[22] + mi := &file_atelet_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1556,7 +1616,7 @@ func (x *RunResponse) String() string { func (*RunResponse) ProtoMessage() {} func (x *RunResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[22] + mi := &file_atelet_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1569,7 +1629,7 @@ func (x *RunResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RunResponse.ProtoReflect.Descriptor instead. func (*RunResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{22} + return file_atelet_proto_rawDescGZIP(), []int{23} } type LocalCheckpointConfiguration struct { @@ -1585,7 +1645,7 @@ type LocalCheckpointConfiguration struct { func (x *LocalCheckpointConfiguration) Reset() { *x = LocalCheckpointConfiguration{} - mi := &file_atelet_proto_msgTypes[23] + mi := &file_atelet_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1597,7 +1657,7 @@ func (x *LocalCheckpointConfiguration) String() string { func (*LocalCheckpointConfiguration) ProtoMessage() {} func (x *LocalCheckpointConfiguration) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[23] + mi := &file_atelet_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1610,7 +1670,7 @@ func (x *LocalCheckpointConfiguration) ProtoReflect() protoreflect.Message { // Deprecated: Use LocalCheckpointConfiguration.ProtoReflect.Descriptor instead. func (*LocalCheckpointConfiguration) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{23} + return file_atelet_proto_rawDescGZIP(), []int{24} } func (x *LocalCheckpointConfiguration) GetSnapshotName() string { @@ -1631,7 +1691,7 @@ type ExternalCheckpointConfiguration struct { func (x *ExternalCheckpointConfiguration) Reset() { *x = ExternalCheckpointConfiguration{} - mi := &file_atelet_proto_msgTypes[24] + mi := &file_atelet_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1643,7 +1703,7 @@ func (x *ExternalCheckpointConfiguration) String() string { func (*ExternalCheckpointConfiguration) ProtoMessage() {} func (x *ExternalCheckpointConfiguration) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[24] + mi := &file_atelet_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1656,7 +1716,7 @@ func (x *ExternalCheckpointConfiguration) ProtoReflect() protoreflect.Message { // Deprecated: Use ExternalCheckpointConfiguration.ProtoReflect.Descriptor instead. func (*ExternalCheckpointConfiguration) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{24} + return file_atelet_proto_rawDescGZIP(), []int{25} } func (x *ExternalCheckpointConfiguration) GetSnapshotUri() string { @@ -1694,7 +1754,7 @@ type CheckpointRequest struct { func (x *CheckpointRequest) Reset() { *x = CheckpointRequest{} - mi := &file_atelet_proto_msgTypes[25] + mi := &file_atelet_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1706,7 +1766,7 @@ func (x *CheckpointRequest) String() string { func (*CheckpointRequest) ProtoMessage() {} func (x *CheckpointRequest) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[25] + mi := &file_atelet_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1719,7 +1779,7 @@ func (x *CheckpointRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointRequest.ProtoReflect.Descriptor instead. func (*CheckpointRequest) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{25} + return file_atelet_proto_rawDescGZIP(), []int{26} } func (x *CheckpointRequest) GetTargetAteomUid() string { @@ -1834,7 +1894,7 @@ type CheckpointResponse struct { func (x *CheckpointResponse) Reset() { *x = CheckpointResponse{} - mi := &file_atelet_proto_msgTypes[26] + mi := &file_atelet_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1846,7 +1906,7 @@ func (x *CheckpointResponse) String() string { func (*CheckpointResponse) ProtoMessage() {} func (x *CheckpointResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[26] + mi := &file_atelet_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1859,7 +1919,7 @@ func (x *CheckpointResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointResponse.ProtoReflect.Descriptor instead. func (*CheckpointResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{26} + return file_atelet_proto_rawDescGZIP(), []int{27} } type UploadPausedCheckpointRequest struct { @@ -1887,7 +1947,7 @@ type UploadPausedCheckpointRequest struct { func (x *UploadPausedCheckpointRequest) Reset() { *x = UploadPausedCheckpointRequest{} - mi := &file_atelet_proto_msgTypes[27] + mi := &file_atelet_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1899,7 +1959,7 @@ func (x *UploadPausedCheckpointRequest) String() string { func (*UploadPausedCheckpointRequest) ProtoMessage() {} func (x *UploadPausedCheckpointRequest) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[27] + mi := &file_atelet_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1912,7 +1972,7 @@ func (x *UploadPausedCheckpointRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UploadPausedCheckpointRequest.ProtoReflect.Descriptor instead. func (*UploadPausedCheckpointRequest) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{27} + return file_atelet_proto_rawDescGZIP(), []int{28} } func (x *UploadPausedCheckpointRequest) GetAtespace() string { @@ -1979,7 +2039,7 @@ type UploadPausedCheckpointResponse struct { func (x *UploadPausedCheckpointResponse) Reset() { *x = UploadPausedCheckpointResponse{} - mi := &file_atelet_proto_msgTypes[28] + mi := &file_atelet_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1991,7 +2051,7 @@ func (x *UploadPausedCheckpointResponse) String() string { func (*UploadPausedCheckpointResponse) ProtoMessage() {} func (x *UploadPausedCheckpointResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[28] + mi := &file_atelet_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2004,7 +2064,7 @@ func (x *UploadPausedCheckpointResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UploadPausedCheckpointResponse.ProtoReflect.Descriptor instead. func (*UploadPausedCheckpointResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{28} + return file_atelet_proto_rawDescGZIP(), []int{29} } type RestoreRequest struct { @@ -2050,7 +2110,7 @@ type RestoreRequest struct { func (x *RestoreRequest) Reset() { *x = RestoreRequest{} - mi := &file_atelet_proto_msgTypes[29] + mi := &file_atelet_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2062,7 +2122,7 @@ func (x *RestoreRequest) String() string { func (*RestoreRequest) ProtoMessage() {} func (x *RestoreRequest) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[29] + mi := &file_atelet_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2075,7 +2135,7 @@ func (x *RestoreRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreRequest.ProtoReflect.Descriptor instead. func (*RestoreRequest) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{29} + return file_atelet_proto_rawDescGZIP(), []int{30} } func (x *RestoreRequest) GetTargetAteomUid() string { @@ -2218,7 +2278,7 @@ type RestoreResponse struct { func (x *RestoreResponse) Reset() { *x = RestoreResponse{} - mi := &file_atelet_proto_msgTypes[30] + mi := &file_atelet_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2230,7 +2290,7 @@ func (x *RestoreResponse) String() string { func (*RestoreResponse) ProtoMessage() {} func (x *RestoreResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[30] + mi := &file_atelet_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2243,7 +2303,7 @@ func (x *RestoreResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreResponse.ProtoReflect.Descriptor instead. func (*RestoreResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{30} + return file_atelet_proto_rawDescGZIP(), []int{31} } var File_atelet_proto protoreflect.FileDescriptor @@ -2305,7 +2365,9 @@ const file_atelet_proto_rawDesc = "" + "\x0evolume_context\x18\x03 \x03(\v2/.atelet.ExternalVolumeSource.VolumeContextEntryR\rvolumeContext\x1a@\n" + "\x12VolumeContextEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"Y\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"1\n" + + "\x11ImageVolumeSource\x12\x1c\n" + + "\treference\x18\x01 \x01(\tR\treference\"Y\n" + "\x11ActorMetadataItem\x120\n" + "\x05field\x18\x01 \x01(\x0e2\x1a.atelet.ActorMetadataFieldR\x05field\x12\x12\n" + "\x04path\x18\x02 \x01(\tR\x04path\"J\n" + @@ -2315,14 +2377,15 @@ const file_atelet_proto_rawDesc = "" + "\x0eactor_metadata\x18\x01 \x01(\v2\x1f.atelet.ActorMetadataDataSourceH\x00R\ractorMetadataB\r\n" + "\vdata_source\"S\n" + "\x10SystemInfoVolume\x12?\n" + - "\fdata_sources\x18\x01 \x03(\v2\x1c.atelet.SystemInfoDataSourceR\vdataSources\"\xdc\x01\n" + + "\fdata_sources\x18\x01 \x03(\v2\x1c.atelet.SystemInfoDataSourceR\vdataSources\"\x8f\x02\n" + "\x06Volume\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12;\n" + "\vdurable_dir\x18\x02 \x01(\v2\x18.atelet.DurableDirVolumeH\x00R\n" + "durableDir\x12:\n" + "\bexternal\x18\x03 \x01(\v2\x1c.atelet.ExternalVolumeSourceH\x00R\bexternal\x12;\n" + "\vsystem_info\x18\x04 \x01(\v2\x18.atelet.SystemInfoVolumeH\x00R\n" + - "systemInfoB\b\n" + + "systemInfo\x121\n" + + "\x05image\x18\x05 \x01(\v2\x19.atelet.ImageVolumeSourceH\x00R\x05imageB\b\n" + "\x06source\"@\n" + "\vVolumeMount\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" + @@ -2440,7 +2503,7 @@ func file_atelet_proto_rawDescGZIP() []byte { } var file_atelet_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_atelet_proto_msgTypes = make([]protoimpl.MessageInfo, 34) +var file_atelet_proto_msgTypes = make([]protoimpl.MessageInfo, 35) var file_atelet_proto_goTypes = []any{ (ActorMetadataField)(0), // 0: atelet.ActorMetadataField (CheckpointType)(0), // 1: atelet.CheckpointType @@ -2455,82 +2518,84 @@ var file_atelet_proto_goTypes = []any{ (*WorkloadSpec)(nil), // 10: atelet.WorkloadSpec (*DurableDirVolume)(nil), // 11: atelet.DurableDirVolume (*ExternalVolumeSource)(nil), // 12: atelet.ExternalVolumeSource - (*ActorMetadataItem)(nil), // 13: atelet.ActorMetadataItem - (*ActorMetadataDataSource)(nil), // 14: atelet.ActorMetadataDataSource - (*SystemInfoDataSource)(nil), // 15: atelet.SystemInfoDataSource - (*SystemInfoVolume)(nil), // 16: atelet.SystemInfoVolume - (*Volume)(nil), // 17: atelet.Volume - (*VolumeMount)(nil), // 18: atelet.VolumeMount - (*Container)(nil), // 19: atelet.Container - (*SecurityContext)(nil), // 20: atelet.SecurityContext - (*Capabilities)(nil), // 21: atelet.Capabilities - (*EnvEntry)(nil), // 22: atelet.EnvEntry - (*Readyz)(nil), // 23: atelet.Readyz - (*HTTPGetAction)(nil), // 24: atelet.HTTPGetAction - (*RunResponse)(nil), // 25: atelet.RunResponse - (*LocalCheckpointConfiguration)(nil), // 26: atelet.LocalCheckpointConfiguration - (*ExternalCheckpointConfiguration)(nil), // 27: atelet.ExternalCheckpointConfiguration - (*CheckpointRequest)(nil), // 28: atelet.CheckpointRequest - (*CheckpointResponse)(nil), // 29: atelet.CheckpointResponse - (*UploadPausedCheckpointRequest)(nil), // 30: atelet.UploadPausedCheckpointRequest - (*UploadPausedCheckpointResponse)(nil), // 31: atelet.UploadPausedCheckpointResponse - (*RestoreRequest)(nil), // 32: atelet.RestoreRequest - (*RestoreResponse)(nil), // 33: atelet.RestoreResponse - nil, // 34: atelet.ArchAssets.FilesEntry - nil, // 35: atelet.SandboxAssets.AssetsEntry - nil, // 36: atelet.ExternalVolumeSource.VolumeContextEntry + (*ImageVolumeSource)(nil), // 13: atelet.ImageVolumeSource + (*ActorMetadataItem)(nil), // 14: atelet.ActorMetadataItem + (*ActorMetadataDataSource)(nil), // 15: atelet.ActorMetadataDataSource + (*SystemInfoDataSource)(nil), // 16: atelet.SystemInfoDataSource + (*SystemInfoVolume)(nil), // 17: atelet.SystemInfoVolume + (*Volume)(nil), // 18: atelet.Volume + (*VolumeMount)(nil), // 19: atelet.VolumeMount + (*Container)(nil), // 20: atelet.Container + (*SecurityContext)(nil), // 21: atelet.SecurityContext + (*Capabilities)(nil), // 22: atelet.Capabilities + (*EnvEntry)(nil), // 23: atelet.EnvEntry + (*Readyz)(nil), // 24: atelet.Readyz + (*HTTPGetAction)(nil), // 25: atelet.HTTPGetAction + (*RunResponse)(nil), // 26: atelet.RunResponse + (*LocalCheckpointConfiguration)(nil), // 27: atelet.LocalCheckpointConfiguration + (*ExternalCheckpointConfiguration)(nil), // 28: atelet.ExternalCheckpointConfiguration + (*CheckpointRequest)(nil), // 29: atelet.CheckpointRequest + (*CheckpointResponse)(nil), // 30: atelet.CheckpointResponse + (*UploadPausedCheckpointRequest)(nil), // 31: atelet.UploadPausedCheckpointRequest + (*UploadPausedCheckpointResponse)(nil), // 32: atelet.UploadPausedCheckpointResponse + (*RestoreRequest)(nil), // 33: atelet.RestoreRequest + (*RestoreResponse)(nil), // 34: atelet.RestoreResponse + nil, // 35: atelet.ArchAssets.FilesEntry + nil, // 36: atelet.SandboxAssets.AssetsEntry + nil, // 37: atelet.ExternalVolumeSource.VolumeContextEntry } var file_atelet_proto_depIdxs = []int32{ 10, // 0: atelet.RunRequest.spec:type_name -> atelet.WorkloadSpec 9, // 1: atelet.RunRequest.sandbox_assets:type_name -> atelet.SandboxAssets 6, // 2: atelet.RunRequest.egress_gateway:type_name -> atelet.EgressGateway - 34, // 3: atelet.ArchAssets.files:type_name -> atelet.ArchAssets.FilesEntry - 35, // 4: atelet.SandboxAssets.assets:type_name -> atelet.SandboxAssets.AssetsEntry - 19, // 5: atelet.WorkloadSpec.containers:type_name -> atelet.Container - 17, // 6: atelet.WorkloadSpec.volumes:type_name -> atelet.Volume - 36, // 7: atelet.ExternalVolumeSource.volume_context:type_name -> atelet.ExternalVolumeSource.VolumeContextEntry + 35, // 3: atelet.ArchAssets.files:type_name -> atelet.ArchAssets.FilesEntry + 36, // 4: atelet.SandboxAssets.assets:type_name -> atelet.SandboxAssets.AssetsEntry + 20, // 5: atelet.WorkloadSpec.containers:type_name -> atelet.Container + 18, // 6: atelet.WorkloadSpec.volumes:type_name -> atelet.Volume + 37, // 7: atelet.ExternalVolumeSource.volume_context:type_name -> atelet.ExternalVolumeSource.VolumeContextEntry 0, // 8: atelet.ActorMetadataItem.field:type_name -> atelet.ActorMetadataField - 13, // 9: atelet.ActorMetadataDataSource.items:type_name -> atelet.ActorMetadataItem - 14, // 10: atelet.SystemInfoDataSource.actor_metadata:type_name -> atelet.ActorMetadataDataSource - 15, // 11: atelet.SystemInfoVolume.data_sources:type_name -> atelet.SystemInfoDataSource + 14, // 9: atelet.ActorMetadataDataSource.items:type_name -> atelet.ActorMetadataItem + 15, // 10: atelet.SystemInfoDataSource.actor_metadata:type_name -> atelet.ActorMetadataDataSource + 16, // 11: atelet.SystemInfoVolume.data_sources:type_name -> atelet.SystemInfoDataSource 11, // 12: atelet.Volume.durable_dir:type_name -> atelet.DurableDirVolume 12, // 13: atelet.Volume.external:type_name -> atelet.ExternalVolumeSource - 16, // 14: atelet.Volume.system_info:type_name -> atelet.SystemInfoVolume - 22, // 15: atelet.Container.env:type_name -> atelet.EnvEntry - 23, // 16: atelet.Container.readyz:type_name -> atelet.Readyz - 18, // 17: atelet.Container.volume_mounts:type_name -> atelet.VolumeMount - 20, // 18: atelet.Container.security_context:type_name -> atelet.SecurityContext - 21, // 19: atelet.SecurityContext.capabilities:type_name -> atelet.Capabilities - 24, // 20: atelet.Readyz.http_get:type_name -> atelet.HTTPGetAction - 10, // 21: atelet.CheckpointRequest.spec:type_name -> atelet.WorkloadSpec - 1, // 22: atelet.CheckpointRequest.type:type_name -> atelet.CheckpointType - 26, // 23: atelet.CheckpointRequest.local_config:type_name -> atelet.LocalCheckpointConfiguration - 27, // 24: atelet.CheckpointRequest.external_config:type_name -> atelet.ExternalCheckpointConfiguration - 2, // 25: atelet.CheckpointRequest.scope:type_name -> atelet.SnapshotScope - 2, // 26: atelet.UploadPausedCheckpointRequest.desired_scope:type_name -> atelet.SnapshotScope - 10, // 27: atelet.RestoreRequest.spec:type_name -> atelet.WorkloadSpec - 1, // 28: atelet.RestoreRequest.type:type_name -> atelet.CheckpointType - 26, // 29: atelet.RestoreRequest.local_config:type_name -> atelet.LocalCheckpointConfiguration - 27, // 30: atelet.RestoreRequest.external_config:type_name -> atelet.ExternalCheckpointConfiguration - 2, // 31: atelet.RestoreRequest.scope:type_name -> atelet.SnapshotScope - 6, // 32: atelet.RestoreRequest.egress_gateway:type_name -> atelet.EgressGateway - 7, // 33: atelet.ArchAssets.FilesEntry.value:type_name -> atelet.AssetFile - 8, // 34: atelet.SandboxAssets.AssetsEntry.value:type_name -> atelet.ArchAssets - 3, // 35: atelet.CredentialBroker.MintActorCertificate:input_type -> atelet.MintActorCertificateRequest - 5, // 36: atelet.AteomHerder.Run:input_type -> atelet.RunRequest - 28, // 37: atelet.AteomHerder.Checkpoint:input_type -> atelet.CheckpointRequest - 32, // 38: atelet.AteomHerder.Restore:input_type -> atelet.RestoreRequest - 30, // 39: atelet.AteomHerder.UploadPausedCheckpoint:input_type -> atelet.UploadPausedCheckpointRequest - 4, // 40: atelet.CredentialBroker.MintActorCertificate:output_type -> atelet.MintActorCertificateResponse - 25, // 41: atelet.AteomHerder.Run:output_type -> atelet.RunResponse - 29, // 42: atelet.AteomHerder.Checkpoint:output_type -> atelet.CheckpointResponse - 33, // 43: atelet.AteomHerder.Restore:output_type -> atelet.RestoreResponse - 31, // 44: atelet.AteomHerder.UploadPausedCheckpoint:output_type -> atelet.UploadPausedCheckpointResponse - 40, // [40:45] is the sub-list for method output_type - 35, // [35:40] is the sub-list for method input_type - 35, // [35:35] is the sub-list for extension type_name - 35, // [35:35] is the sub-list for extension extendee - 0, // [0:35] is the sub-list for field type_name + 17, // 14: atelet.Volume.system_info:type_name -> atelet.SystemInfoVolume + 13, // 15: atelet.Volume.image:type_name -> atelet.ImageVolumeSource + 23, // 16: atelet.Container.env:type_name -> atelet.EnvEntry + 24, // 17: atelet.Container.readyz:type_name -> atelet.Readyz + 19, // 18: atelet.Container.volume_mounts:type_name -> atelet.VolumeMount + 21, // 19: atelet.Container.security_context:type_name -> atelet.SecurityContext + 22, // 20: atelet.SecurityContext.capabilities:type_name -> atelet.Capabilities + 25, // 21: atelet.Readyz.http_get:type_name -> atelet.HTTPGetAction + 10, // 22: atelet.CheckpointRequest.spec:type_name -> atelet.WorkloadSpec + 1, // 23: atelet.CheckpointRequest.type:type_name -> atelet.CheckpointType + 27, // 24: atelet.CheckpointRequest.local_config:type_name -> atelet.LocalCheckpointConfiguration + 28, // 25: atelet.CheckpointRequest.external_config:type_name -> atelet.ExternalCheckpointConfiguration + 2, // 26: atelet.CheckpointRequest.scope:type_name -> atelet.SnapshotScope + 2, // 27: atelet.UploadPausedCheckpointRequest.desired_scope:type_name -> atelet.SnapshotScope + 10, // 28: atelet.RestoreRequest.spec:type_name -> atelet.WorkloadSpec + 1, // 29: atelet.RestoreRequest.type:type_name -> atelet.CheckpointType + 27, // 30: atelet.RestoreRequest.local_config:type_name -> atelet.LocalCheckpointConfiguration + 28, // 31: atelet.RestoreRequest.external_config:type_name -> atelet.ExternalCheckpointConfiguration + 2, // 32: atelet.RestoreRequest.scope:type_name -> atelet.SnapshotScope + 6, // 33: atelet.RestoreRequest.egress_gateway:type_name -> atelet.EgressGateway + 7, // 34: atelet.ArchAssets.FilesEntry.value:type_name -> atelet.AssetFile + 8, // 35: atelet.SandboxAssets.AssetsEntry.value:type_name -> atelet.ArchAssets + 3, // 36: atelet.CredentialBroker.MintActorCertificate:input_type -> atelet.MintActorCertificateRequest + 5, // 37: atelet.AteomHerder.Run:input_type -> atelet.RunRequest + 29, // 38: atelet.AteomHerder.Checkpoint:input_type -> atelet.CheckpointRequest + 33, // 39: atelet.AteomHerder.Restore:input_type -> atelet.RestoreRequest + 31, // 40: atelet.AteomHerder.UploadPausedCheckpoint:input_type -> atelet.UploadPausedCheckpointRequest + 4, // 41: atelet.CredentialBroker.MintActorCertificate:output_type -> atelet.MintActorCertificateResponse + 26, // 42: atelet.AteomHerder.Run:output_type -> atelet.RunResponse + 30, // 43: atelet.AteomHerder.Checkpoint:output_type -> atelet.CheckpointResponse + 34, // 44: atelet.AteomHerder.Restore:output_type -> atelet.RestoreResponse + 32, // 45: atelet.AteomHerder.UploadPausedCheckpoint:output_type -> atelet.UploadPausedCheckpointResponse + 41, // [41:46] is the sub-list for method output_type + 36, // [36:41] is the sub-list for method input_type + 36, // [36:36] is the sub-list for extension type_name + 36, // [36:36] is the sub-list for extension extendee + 0, // [0:36] is the sub-list for field type_name } func init() { file_atelet_proto_init() } @@ -2539,19 +2604,20 @@ func file_atelet_proto_init() { return } file_atelet_proto_msgTypes[2].OneofWrappers = []any{} - file_atelet_proto_msgTypes[12].OneofWrappers = []any{ + file_atelet_proto_msgTypes[13].OneofWrappers = []any{ (*SystemInfoDataSource_ActorMetadata)(nil), } - file_atelet_proto_msgTypes[14].OneofWrappers = []any{ + file_atelet_proto_msgTypes[15].OneofWrappers = []any{ (*Volume_DurableDir)(nil), (*Volume_External)(nil), (*Volume_SystemInfo)(nil), + (*Volume_Image)(nil), } - file_atelet_proto_msgTypes[25].OneofWrappers = []any{ + file_atelet_proto_msgTypes[26].OneofWrappers = []any{ (*CheckpointRequest_LocalConfig)(nil), (*CheckpointRequest_ExternalConfig)(nil), } - file_atelet_proto_msgTypes[29].OneofWrappers = []any{ + file_atelet_proto_msgTypes[30].OneofWrappers = []any{ (*RestoreRequest_LocalConfig)(nil), (*RestoreRequest_ExternalConfig)(nil), } @@ -2561,7 +2627,7 @@ func file_atelet_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_atelet_proto_rawDesc), len(file_atelet_proto_rawDesc)), NumEnums: 3, - NumMessages: 34, + NumMessages: 35, NumExtensions: 0, NumServices: 2, }, diff --git a/internal/proto/ateletpb/atelet.proto b/internal/proto/ateletpb/atelet.proto index 3cf2cd2e6..c3b31ee1b 100644 --- a/internal/proto/ateletpb/atelet.proto +++ b/internal/proto/ateletpb/atelet.proto @@ -141,6 +141,10 @@ message ExternalVolumeSource { map volume_context = 3; } +message ImageVolumeSource { + string reference = 1; +} + // ActorMetadataField selects one identity field of the actor. enum ActorMetadataField { ACTOR_METADATA_FIELD_UNSPECIFIED = 0; @@ -182,6 +186,7 @@ message Volume { DurableDirVolume durable_dir = 2; ExternalVolumeSource external = 3; SystemInfoVolume system_info = 4; + ImageVolumeSource image = 5; } } diff --git a/internal/proto/ateompb/ateom.pb.go b/internal/proto/ateompb/ateom.pb.go index e1c2c9390..2eed84507 100644 --- a/internal/proto/ateompb/ateom.pb.go +++ b/internal/proto/ateompb/ateom.pb.go @@ -506,8 +506,10 @@ type Container struct { // mounts, if any. Contents are generated by atelet on the host; the // container sees them read-only. SystemInfoVolumeMounts []*SystemInfoVolumeMount `protobuf:"bytes,6,rep,name=system_info_volume_mounts,json=systemInfoVolumeMounts,proto3" json:"system_info_volume_mounts,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // image_volume_mounts are the image volumes this container mounts, if any. + ImageVolumeMounts []*ImageVolumeMount `protobuf:"bytes,7,rep,name=image_volume_mounts,json=imageVolumeMounts,proto3" json:"image_volume_mounts,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Container) Reset() { @@ -575,6 +577,13 @@ func (x *Container) GetSystemInfoVolumeMounts() []*SystemInfoVolumeMount { return nil } +func (x *Container) GetImageVolumeMounts() []*ImageVolumeMount { + if x != nil { + return x.ImageVolumeMounts + } + return nil +} + // VolumeMount is one volume mounted into a container. type VolumeMount struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -742,6 +751,63 @@ func (x *SystemInfoVolumeMount) GetMountPath() string { return "" } +// ImageVolumeMount is one image volume mounted into a container. ateom uses +// these to construct the container's volume mounts — each names the volume +// and its destination path. +type ImageVolumeMount struct { + state protoimpl.MessageState `protogen:"open.v1"` + // volume_name is the name the ActorTemplate gave the volume. + VolumeName string `protobuf:"bytes,1,opt,name=volume_name,json=volumeName,proto3" json:"volume_name,omitempty"` + // mount_path is where the container sees the volume. + MountPath string `protobuf:"bytes,2,opt,name=mount_path,json=mountPath,proto3" json:"mount_path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImageVolumeMount) Reset() { + *x = ImageVolumeMount{} + mi := &file_ateom_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImageVolumeMount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImageVolumeMount) ProtoMessage() {} + +func (x *ImageVolumeMount) ProtoReflect() protoreflect.Message { + mi := &file_ateom_proto_msgTypes[7] + 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 ImageVolumeMount.ProtoReflect.Descriptor instead. +func (*ImageVolumeMount) Descriptor() ([]byte, []int) { + return file_ateom_proto_rawDescGZIP(), []int{7} +} + +func (x *ImageVolumeMount) GetVolumeName() string { + if x != nil { + return x.VolumeName + } + return "" +} + +func (x *ImageVolumeMount) GetMountPath() string { + if x != nil { + return x.MountPath + } + return "" +} + // Readyz describes how to check that a container is ready to serve. // Only HTTP is supported today. type Readyz struct { @@ -756,7 +822,7 @@ type Readyz struct { func (x *Readyz) Reset() { *x = Readyz{} - mi := &file_ateom_proto_msgTypes[7] + mi := &file_ateom_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -768,7 +834,7 @@ func (x *Readyz) String() string { func (*Readyz) ProtoMessage() {} func (x *Readyz) 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 { @@ -781,7 +847,7 @@ func (x *Readyz) ProtoReflect() protoreflect.Message { // Deprecated: Use Readyz.ProtoReflect.Descriptor instead. func (*Readyz) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{7} + return file_ateom_proto_rawDescGZIP(), []int{8} } func (x *Readyz) GetHttpGet() *HTTPGetAction { @@ -811,7 +877,7 @@ type HTTPGetAction struct { func (x *HTTPGetAction) Reset() { *x = HTTPGetAction{} - mi := &file_ateom_proto_msgTypes[8] + mi := &file_ateom_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -823,7 +889,7 @@ func (x *HTTPGetAction) String() string { func (*HTTPGetAction) ProtoMessage() {} func (x *HTTPGetAction) 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 { @@ -836,7 +902,7 @@ func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { // Deprecated: Use HTTPGetAction.ProtoReflect.Descriptor instead. func (*HTTPGetAction) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{8} + return file_ateom_proto_rawDescGZIP(), []int{9} } func (x *HTTPGetAction) GetPath() string { @@ -861,7 +927,7 @@ type RunWorkloadResponse struct { func (x *RunWorkloadResponse) Reset() { *x = RunWorkloadResponse{} - mi := &file_ateom_proto_msgTypes[9] + mi := &file_ateom_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -873,7 +939,7 @@ func (x *RunWorkloadResponse) String() string { func (*RunWorkloadResponse) ProtoMessage() {} func (x *RunWorkloadResponse) 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 { @@ -886,7 +952,7 @@ func (x *RunWorkloadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RunWorkloadResponse.ProtoReflect.Descriptor instead. func (*RunWorkloadResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{9} + return file_ateom_proto_rawDescGZIP(), []int{10} } type CheckpointWorkloadRequest struct { @@ -920,7 +986,7 @@ type CheckpointWorkloadRequest struct { func (x *CheckpointWorkloadRequest) Reset() { *x = CheckpointWorkloadRequest{} - mi := &file_ateom_proto_msgTypes[10] + mi := &file_ateom_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -932,7 +998,7 @@ func (x *CheckpointWorkloadRequest) String() string { func (*CheckpointWorkloadRequest) ProtoMessage() {} func (x *CheckpointWorkloadRequest) 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 { @@ -945,7 +1011,7 @@ func (x *CheckpointWorkloadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointWorkloadRequest.ProtoReflect.Descriptor instead. func (*CheckpointWorkloadRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{10} + return file_ateom_proto_rawDescGZIP(), []int{11} } func (x *CheckpointWorkloadRequest) GetAtespace() string { @@ -1030,7 +1096,7 @@ type CheckpointWorkloadResponse struct { func (x *CheckpointWorkloadResponse) Reset() { *x = CheckpointWorkloadResponse{} - mi := &file_ateom_proto_msgTypes[11] + mi := &file_ateom_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1042,7 +1108,7 @@ func (x *CheckpointWorkloadResponse) String() string { func (*CheckpointWorkloadResponse) ProtoMessage() {} func (x *CheckpointWorkloadResponse) 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 { @@ -1055,7 +1121,7 @@ func (x *CheckpointWorkloadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointWorkloadResponse.ProtoReflect.Descriptor instead. func (*CheckpointWorkloadResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{11} + return file_ateom_proto_rawDescGZIP(), []int{12} } func (x *CheckpointWorkloadResponse) GetSnapshotFiles() []string { @@ -1100,7 +1166,7 @@ type RestoreWorkloadRequest struct { func (x *RestoreWorkloadRequest) Reset() { *x = RestoreWorkloadRequest{} - mi := &file_ateom_proto_msgTypes[12] + mi := &file_ateom_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1112,7 +1178,7 @@ func (x *RestoreWorkloadRequest) String() string { func (*RestoreWorkloadRequest) ProtoMessage() {} func (x *RestoreWorkloadRequest) 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 { @@ -1125,7 +1191,7 @@ func (x *RestoreWorkloadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreWorkloadRequest.ProtoReflect.Descriptor instead. func (*RestoreWorkloadRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{12} + return file_ateom_proto_rawDescGZIP(), []int{13} } func (x *RestoreWorkloadRequest) GetAtespace() string { @@ -1234,7 +1300,7 @@ type RestoreWorkloadResponse struct { func (x *RestoreWorkloadResponse) Reset() { *x = RestoreWorkloadResponse{} - mi := &file_ateom_proto_msgTypes[13] + mi := &file_ateom_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1246,7 +1312,7 @@ func (x *RestoreWorkloadResponse) String() string { func (*RestoreWorkloadResponse) ProtoMessage() {} func (x *RestoreWorkloadResponse) 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 { @@ -1259,7 +1325,7 @@ func (x *RestoreWorkloadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreWorkloadResponse.ProtoReflect.Descriptor instead. func (*RestoreWorkloadResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{13} + return file_ateom_proto_rawDescGZIP(), []int{14} } type GetWorkloadStatsRequest struct { @@ -1275,7 +1341,7 @@ type GetWorkloadStatsRequest struct { func (x *GetWorkloadStatsRequest) Reset() { *x = GetWorkloadStatsRequest{} - mi := &file_ateom_proto_msgTypes[14] + mi := &file_ateom_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1287,7 +1353,7 @@ func (x *GetWorkloadStatsRequest) String() string { func (*GetWorkloadStatsRequest) ProtoMessage() {} func (x *GetWorkloadStatsRequest) 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 { @@ -1300,7 +1366,7 @@ func (x *GetWorkloadStatsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkloadStatsRequest.ProtoReflect.Descriptor instead. func (*GetWorkloadStatsRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{14} + return file_ateom_proto_rawDescGZIP(), []int{15} } func (x *GetWorkloadStatsRequest) GetActorUid() string { @@ -1364,7 +1430,7 @@ type WorkloadStatsSample struct { func (x *WorkloadStatsSample) Reset() { *x = WorkloadStatsSample{} - mi := &file_ateom_proto_msgTypes[15] + mi := &file_ateom_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1376,7 +1442,7 @@ func (x *WorkloadStatsSample) String() string { func (*WorkloadStatsSample) ProtoMessage() {} func (x *WorkloadStatsSample) 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 { @@ -1389,7 +1455,7 @@ func (x *WorkloadStatsSample) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkloadStatsSample.ProtoReflect.Descriptor instead. func (*WorkloadStatsSample) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{15} + return file_ateom_proto_rawDescGZIP(), []int{16} } func (x *WorkloadStatsSample) GetAtespace() string { @@ -1485,7 +1551,7 @@ type GetWorkloadStatsResponse struct { func (x *GetWorkloadStatsResponse) Reset() { *x = GetWorkloadStatsResponse{} - mi := &file_ateom_proto_msgTypes[16] + mi := &file_ateom_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1497,7 +1563,7 @@ func (x *GetWorkloadStatsResponse) String() string { func (*GetWorkloadStatsResponse) ProtoMessage() {} func (x *GetWorkloadStatsResponse) 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 { @@ -1510,7 +1576,7 @@ func (x *GetWorkloadStatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkloadStatsResponse.ProtoReflect.Descriptor instead. func (*GetWorkloadStatsResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{16} + return file_ateom_proto_rawDescGZIP(), []int{17} } func (x *GetWorkloadStatsResponse) GetSample() *WorkloadStatsSample { @@ -1528,7 +1594,7 @@ type GetActiveWorkloadStatsRequest struct { func (x *GetActiveWorkloadStatsRequest) Reset() { *x = GetActiveWorkloadStatsRequest{} - mi := &file_ateom_proto_msgTypes[17] + mi := &file_ateom_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1540,7 +1606,7 @@ func (x *GetActiveWorkloadStatsRequest) String() string { func (*GetActiveWorkloadStatsRequest) ProtoMessage() {} func (x *GetActiveWorkloadStatsRequest) 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 { @@ -1553,7 +1619,7 @@ func (x *GetActiveWorkloadStatsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActiveWorkloadStatsRequest.ProtoReflect.Descriptor instead. func (*GetActiveWorkloadStatsRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{17} + return file_ateom_proto_rawDescGZIP(), []int{18} } type GetActiveWorkloadStatsResponse struct { @@ -1575,7 +1641,7 @@ type GetActiveWorkloadStatsResponse struct { func (x *GetActiveWorkloadStatsResponse) Reset() { *x = GetActiveWorkloadStatsResponse{} - mi := &file_ateom_proto_msgTypes[18] + mi := &file_ateom_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1587,7 +1653,7 @@ func (x *GetActiveWorkloadStatsResponse) String() string { func (*GetActiveWorkloadStatsResponse) ProtoMessage() {} func (x *GetActiveWorkloadStatsResponse) 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 { @@ -1600,7 +1666,7 @@ func (x *GetActiveWorkloadStatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActiveWorkloadStatsResponse.ProtoReflect.Descriptor instead. func (*GetActiveWorkloadStatsResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{18} + return file_ateom_proto_rawDescGZIP(), []int{19} } func (x *GetActiveWorkloadStatsResponse) GetResult() isGetActiveWorkloadStatsResponse_Result { @@ -1673,13 +1739,14 @@ const file_ateom_proto_rawDesc = "" + "\fWorkloadSpec\x120\n" + "\n" + "containers\x18\x01 \x03(\v2\x10.ateom.ContainerR\n" + - "containers\"\xd3\x02\n" + + "containers\"\x9c\x03\n" + "\tContainer\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12%\n" + "\x06readyz\x18\x02 \x01(\v2\r.ateom.ReadyzR\x06readyz\x12W\n" + "\x19durable_dir_volume_mounts\x18\x04 \x03(\v2\x1c.ateom.DurableDirVolumeMountR\x16durableDirVolumeMounts\x12>\n" + "\x11csi_volume_mounts\x18\x05 \x03(\v2\x12.ateom.VolumeMountR\x0fcsiVolumeMounts\x12W\n" + - "\x19system_info_volume_mounts\x18\x06 \x03(\v2\x1c.ateom.SystemInfoVolumeMountR\x16systemInfoVolumeMountsJ\x04\b\x03\x10\x04R\x13durable_dir_volumes\"M\n" + + "\x19system_info_volume_mounts\x18\x06 \x03(\v2\x1c.ateom.SystemInfoVolumeMountR\x16systemInfoVolumeMounts\x12G\n" + + "\x13image_volume_mounts\x18\a \x03(\v2\x17.ateom.ImageVolumeMountR\x11imageVolumeMountsJ\x04\b\x03\x10\x04R\x13durable_dir_volumes\"M\n" + "\vVolumeMount\x12\x1f\n" + "\vvolume_name\x18\x01 \x01(\tR\n" + "volumeName\x12\x1d\n" + @@ -1694,6 +1761,11 @@ const file_ateom_proto_rawDesc = "" + "\vvolume_name\x18\x01 \x01(\tR\n" + "volumeName\x12\x1d\n" + "\n" + + "mount_path\x18\x02 \x01(\tR\tmountPath\"R\n" + + "\x10ImageVolumeMount\x12\x1f\n" + + "\vvolume_name\x18\x01 \x01(\tR\n" + + "volumeName\x12\x1d\n" + + "\n" + "mount_path\x18\x02 \x01(\tR\tmountPath\"b\n" + "\x06Readyz\x12/\n" + "\bhttp_get\x18\x01 \x01(\v2\x14.ateom.HTTPGetActionR\ahttpGet\x12'\n" + @@ -1805,7 +1877,7 @@ func file_ateom_proto_rawDescGZIP() []byte { } var file_ateom_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_ateom_proto_msgTypes = make([]protoimpl.MessageInfo, 22) +var file_ateom_proto_msgTypes = make([]protoimpl.MessageInfo, 23) var file_ateom_proto_goTypes = []any{ (SnapshotScope)(0), // 0: ateom.SnapshotScope (SandboxClass)(0), // 1: ateom.SandboxClass @@ -1818,59 +1890,61 @@ var file_ateom_proto_goTypes = []any{ (*VolumeMount)(nil), // 8: ateom.VolumeMount (*DurableDirVolumeMount)(nil), // 9: ateom.DurableDirVolumeMount (*SystemInfoVolumeMount)(nil), // 10: ateom.SystemInfoVolumeMount - (*Readyz)(nil), // 11: ateom.Readyz - (*HTTPGetAction)(nil), // 12: ateom.HTTPGetAction - (*RunWorkloadResponse)(nil), // 13: ateom.RunWorkloadResponse - (*CheckpointWorkloadRequest)(nil), // 14: ateom.CheckpointWorkloadRequest - (*CheckpointWorkloadResponse)(nil), // 15: ateom.CheckpointWorkloadResponse - (*RestoreWorkloadRequest)(nil), // 16: ateom.RestoreWorkloadRequest - (*RestoreWorkloadResponse)(nil), // 17: ateom.RestoreWorkloadResponse - (*GetWorkloadStatsRequest)(nil), // 18: ateom.GetWorkloadStatsRequest - (*WorkloadStatsSample)(nil), // 19: ateom.WorkloadStatsSample - (*GetWorkloadStatsResponse)(nil), // 20: ateom.GetWorkloadStatsResponse - (*GetActiveWorkloadStatsRequest)(nil), // 21: ateom.GetActiveWorkloadStatsRequest - (*GetActiveWorkloadStatsResponse)(nil), // 22: ateom.GetActiveWorkloadStatsResponse - nil, // 23: ateom.RunWorkloadRequest.RuntimeAssetPathsEntry - nil, // 24: ateom.CheckpointWorkloadRequest.RuntimeAssetPathsEntry - nil, // 25: ateom.RestoreWorkloadRequest.RuntimeAssetPathsEntry + (*ImageVolumeMount)(nil), // 11: ateom.ImageVolumeMount + (*Readyz)(nil), // 12: ateom.Readyz + (*HTTPGetAction)(nil), // 13: ateom.HTTPGetAction + (*RunWorkloadResponse)(nil), // 14: ateom.RunWorkloadResponse + (*CheckpointWorkloadRequest)(nil), // 15: ateom.CheckpointWorkloadRequest + (*CheckpointWorkloadResponse)(nil), // 16: ateom.CheckpointWorkloadResponse + (*RestoreWorkloadRequest)(nil), // 17: ateom.RestoreWorkloadRequest + (*RestoreWorkloadResponse)(nil), // 18: ateom.RestoreWorkloadResponse + (*GetWorkloadStatsRequest)(nil), // 19: ateom.GetWorkloadStatsRequest + (*WorkloadStatsSample)(nil), // 20: ateom.WorkloadStatsSample + (*GetWorkloadStatsResponse)(nil), // 21: ateom.GetWorkloadStatsResponse + (*GetActiveWorkloadStatsRequest)(nil), // 22: ateom.GetActiveWorkloadStatsRequest + (*GetActiveWorkloadStatsResponse)(nil), // 23: ateom.GetActiveWorkloadStatsResponse + nil, // 24: ateom.RunWorkloadRequest.RuntimeAssetPathsEntry + nil, // 25: ateom.CheckpointWorkloadRequest.RuntimeAssetPathsEntry + nil, // 26: ateom.RestoreWorkloadRequest.RuntimeAssetPathsEntry } var file_ateom_proto_depIdxs = []int32{ 6, // 0: ateom.RunWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 23, // 1: ateom.RunWorkloadRequest.runtime_asset_paths:type_name -> ateom.RunWorkloadRequest.RuntimeAssetPathsEntry + 24, // 1: ateom.RunWorkloadRequest.runtime_asset_paths:type_name -> ateom.RunWorkloadRequest.RuntimeAssetPathsEntry 5, // 2: ateom.RunWorkloadRequest.egress_gateway:type_name -> ateom.EgressGateway 7, // 3: ateom.WorkloadSpec.containers:type_name -> ateom.Container - 11, // 4: ateom.Container.readyz:type_name -> ateom.Readyz + 12, // 4: ateom.Container.readyz:type_name -> ateom.Readyz 9, // 5: ateom.Container.durable_dir_volume_mounts:type_name -> ateom.DurableDirVolumeMount 8, // 6: ateom.Container.csi_volume_mounts:type_name -> ateom.VolumeMount 10, // 7: ateom.Container.system_info_volume_mounts:type_name -> ateom.SystemInfoVolumeMount - 12, // 8: ateom.Readyz.http_get:type_name -> ateom.HTTPGetAction - 6, // 9: ateom.CheckpointWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 24, // 10: ateom.CheckpointWorkloadRequest.runtime_asset_paths:type_name -> ateom.CheckpointWorkloadRequest.RuntimeAssetPathsEntry - 0, // 11: ateom.CheckpointWorkloadRequest.scope:type_name -> ateom.SnapshotScope - 6, // 12: ateom.RestoreWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 25, // 13: ateom.RestoreWorkloadRequest.runtime_asset_paths:type_name -> ateom.RestoreWorkloadRequest.RuntimeAssetPathsEntry - 0, // 14: ateom.RestoreWorkloadRequest.scope:type_name -> ateom.SnapshotScope - 5, // 15: ateom.RestoreWorkloadRequest.egress_gateway:type_name -> ateom.EgressGateway - 1, // 16: ateom.WorkloadStatsSample.sandbox_class:type_name -> ateom.SandboxClass - 2, // 17: ateom.WorkloadStatsSample.source:type_name -> ateom.StatsSource - 19, // 18: ateom.GetWorkloadStatsResponse.sample:type_name -> ateom.WorkloadStatsSample - 19, // 19: ateom.GetActiveWorkloadStatsResponse.sample:type_name -> ateom.WorkloadStatsSample - 3, // 20: ateom.GetActiveWorkloadStatsResponse.no_sample_reason:type_name -> ateom.NoSampleReason - 4, // 21: ateom.Ateom.RunWorkload:input_type -> ateom.RunWorkloadRequest - 14, // 22: ateom.Ateom.CheckpointWorkload:input_type -> ateom.CheckpointWorkloadRequest - 16, // 23: ateom.Ateom.RestoreWorkload:input_type -> ateom.RestoreWorkloadRequest - 18, // 24: ateom.Ateom.GetWorkloadStats:input_type -> ateom.GetWorkloadStatsRequest - 21, // 25: ateom.Ateom.GetActiveWorkloadStats:input_type -> ateom.GetActiveWorkloadStatsRequest - 13, // 26: ateom.Ateom.RunWorkload:output_type -> ateom.RunWorkloadResponse - 15, // 27: ateom.Ateom.CheckpointWorkload:output_type -> ateom.CheckpointWorkloadResponse - 17, // 28: ateom.Ateom.RestoreWorkload:output_type -> ateom.RestoreWorkloadResponse - 20, // 29: ateom.Ateom.GetWorkloadStats:output_type -> ateom.GetWorkloadStatsResponse - 22, // 30: ateom.Ateom.GetActiveWorkloadStats:output_type -> ateom.GetActiveWorkloadStatsResponse - 26, // [26:31] is the sub-list for method output_type - 21, // [21:26] is the sub-list for method input_type - 21, // [21:21] is the sub-list for extension type_name - 21, // [21:21] is the sub-list for extension extendee - 0, // [0:21] is the sub-list for field type_name + 11, // 8: ateom.Container.image_volume_mounts:type_name -> ateom.ImageVolumeMount + 13, // 9: ateom.Readyz.http_get:type_name -> ateom.HTTPGetAction + 6, // 10: ateom.CheckpointWorkloadRequest.spec:type_name -> ateom.WorkloadSpec + 25, // 11: ateom.CheckpointWorkloadRequest.runtime_asset_paths:type_name -> ateom.CheckpointWorkloadRequest.RuntimeAssetPathsEntry + 0, // 12: ateom.CheckpointWorkloadRequest.scope:type_name -> ateom.SnapshotScope + 6, // 13: ateom.RestoreWorkloadRequest.spec:type_name -> ateom.WorkloadSpec + 26, // 14: ateom.RestoreWorkloadRequest.runtime_asset_paths:type_name -> ateom.RestoreWorkloadRequest.RuntimeAssetPathsEntry + 0, // 15: ateom.RestoreWorkloadRequest.scope:type_name -> ateom.SnapshotScope + 5, // 16: ateom.RestoreWorkloadRequest.egress_gateway:type_name -> ateom.EgressGateway + 1, // 17: ateom.WorkloadStatsSample.sandbox_class:type_name -> ateom.SandboxClass + 2, // 18: ateom.WorkloadStatsSample.source:type_name -> ateom.StatsSource + 20, // 19: ateom.GetWorkloadStatsResponse.sample:type_name -> ateom.WorkloadStatsSample + 20, // 20: ateom.GetActiveWorkloadStatsResponse.sample:type_name -> ateom.WorkloadStatsSample + 3, // 21: ateom.GetActiveWorkloadStatsResponse.no_sample_reason:type_name -> ateom.NoSampleReason + 4, // 22: ateom.Ateom.RunWorkload:input_type -> ateom.RunWorkloadRequest + 15, // 23: ateom.Ateom.CheckpointWorkload:input_type -> ateom.CheckpointWorkloadRequest + 17, // 24: ateom.Ateom.RestoreWorkload:input_type -> ateom.RestoreWorkloadRequest + 19, // 25: ateom.Ateom.GetWorkloadStats:input_type -> ateom.GetWorkloadStatsRequest + 22, // 26: ateom.Ateom.GetActiveWorkloadStats:input_type -> ateom.GetActiveWorkloadStatsRequest + 14, // 27: ateom.Ateom.RunWorkload:output_type -> ateom.RunWorkloadResponse + 16, // 28: ateom.Ateom.CheckpointWorkload:output_type -> ateom.CheckpointWorkloadResponse + 18, // 29: ateom.Ateom.RestoreWorkload:output_type -> ateom.RestoreWorkloadResponse + 21, // 30: ateom.Ateom.GetWorkloadStats:output_type -> ateom.GetWorkloadStatsResponse + 23, // 31: ateom.Ateom.GetActiveWorkloadStats:output_type -> ateom.GetActiveWorkloadStatsResponse + 27, // [27:32] is the sub-list for method output_type + 22, // [22:27] 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 } func init() { file_ateom_proto_init() } @@ -1879,8 +1953,8 @@ func file_ateom_proto_init() { return } file_ateom_proto_msgTypes[0].OneofWrappers = []any{} - file_ateom_proto_msgTypes[12].OneofWrappers = []any{} - file_ateom_proto_msgTypes[18].OneofWrappers = []any{ + file_ateom_proto_msgTypes[13].OneofWrappers = []any{} + file_ateom_proto_msgTypes[19].OneofWrappers = []any{ (*GetActiveWorkloadStatsResponse_Sample)(nil), (*GetActiveWorkloadStatsResponse_NoSampleReason)(nil), } @@ -1890,7 +1964,7 @@ func file_ateom_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_ateom_proto_rawDesc), len(file_ateom_proto_rawDesc)), NumEnums: 4, - NumMessages: 22, + NumMessages: 23, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/proto/ateompb/ateom.proto b/internal/proto/ateompb/ateom.proto index 0821f6787..f5134fb6e 100644 --- a/internal/proto/ateompb/ateom.proto +++ b/internal/proto/ateompb/ateom.proto @@ -160,6 +160,9 @@ message Container { // mounts, if any. Contents are generated by atelet on the host; the // container sees them read-only. repeated SystemInfoVolumeMount system_info_volume_mounts = 6; + + // image_volume_mounts are the image volumes this container mounts, if any. + repeated ImageVolumeMount image_volume_mounts = 7; } // VolumeMount is one volume mounted into a container. @@ -188,6 +191,16 @@ message SystemInfoVolumeMount { string mount_path = 2; } +// ImageVolumeMount is one image volume mounted into a container. ateom uses +// these to construct the container's volume mounts — each names the volume +// and its destination path. +message ImageVolumeMount { + // volume_name is the name the ActorTemplate gave the volume. + string volume_name = 1; + // mount_path is where the container sees the volume. + string mount_path = 2; +} + // Readyz describes how to check that a container is ready to serve. // Only HTTP is supported today. message Readyz { diff --git a/manifests/ate-install/generated/ate.dev_actortemplates.yaml b/manifests/ate-install/generated/ate.dev_actortemplates.yaml index 0b796e1fa..1bd443061 100644 --- a/manifests/ate-install/generated/ate.dev_actortemplates.yaml +++ b/manifests/ate-install/generated/ate.dev_actortemplates.yaml @@ -468,6 +468,21 @@ spec: - capacity - storageClassName type: object + image: + description: image represents the contents of an OCI image, + mounted read-only. + properties: + reference: + description: reference is the image to mount. + maxLength: 512 + type: string + x-kubernetes-validations: + - message: All images must be pinned (changing the image + invalidates snapshots) + rule: self.contains('@') + required: + - reference + type: object name: description: name of the volume. maxLength: 63 @@ -571,8 +586,8 @@ spec: type: object x-kubernetes-validations: - message: exactly one of the fields in [durableDir externalVolumeTemplate - systemInfo] must be set - rule: '[has(self.durableDir),has(self.externalVolumeTemplate),has(self.systemInfo)].filter(x,x==true).size() + image systemInfo] must be set + rule: '[has(self.durableDir),has(self.externalVolumeTemplate),has(self.image),has(self.systemInfo)].filter(x,x==true).size() == 1' maxItems: 32 type: array diff --git a/pkg/api/v1alpha1/actortemplate_types.go b/pkg/api/v1alpha1/actortemplate_types.go index bf9bdd392..579812b67 100644 --- a/pkg/api/v1alpha1/actortemplate_types.go +++ b/pkg/api/v1alpha1/actortemplate_types.go @@ -36,6 +36,16 @@ const ( type DurableDirVolumeSource struct { } +// Represents the contents of an OCI image, mounted read-only. +type ImageVolumeSource struct { + // reference is the image to mount. + // + // +required + // +kubebuilder:validation:MaxLength=512 + // +kubebuilder:validation:XValidation:rule="self.contains('@')",message="All images must be pinned (changing the image invalidates snapshots)" + Reference string `json:"reference"` +} + // Represents an external volume dynamically provisioned for each actor. type ExternalVolumeTemplate struct { // capacity specifies the size of the volume to create. @@ -130,13 +140,17 @@ type SystemInfoVolumeSource struct { // // When adding a new source type, list it in the ExactlyOneOf marker below. // -// +kubebuilder:validation:ExactlyOneOf={durableDir,externalVolumeTemplate,systemInfo} +// +kubebuilder:validation:ExactlyOneOf={durableDir,externalVolumeTemplate,image,systemInfo} type VolumeSource struct { // durableDir represents a durable directory on rootfs that persists across // resumes and participates in snapshots. // +optional DurableDir *DurableDirVolumeSource `json:"durableDir,omitempty"` + // image represents the contents of an OCI image, mounted read-only. + // +optional + Image *ImageVolumeSource `json:"image,omitempty"` + // externalVolumeTemplate represents an external volume dynamically provisioned // for each actor. The volume only lives as long as the actor and is deleted // when the actor is deleted. diff --git a/pkg/api/v1alpha1/actortemplate_validation_test.go b/pkg/api/v1alpha1/actortemplate_validation_test.go index 53821cb94..a35fa59af 100644 --- a/pkg/api/v1alpha1/actortemplate_validation_test.go +++ b/pkg/api/v1alpha1/actortemplate_validation_test.go @@ -610,6 +610,93 @@ func TestActorTemplateValidation(t *testing.T) { } }, wantErr: false, + }, { + name: "Volumes: 1 Image mount is valid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + {Name: "agent", VolumeSource: VolumeSource{Image: &ImageVolumeSource{ + Reference: "example.com/agent@sha256:326e0e090a9a4057e62a1b94236e7a2df2f2f76722f67232e0e47854e4df9c53", + }}}, + } + at.Spec.Containers[0].VolumeMounts = []VolumeMount{ + {Name: "agent", MountPath: "/ate"}, + } + }, + wantErr: false, + }, { + name: "Volumes: unpinned Image reference is invalid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + {Name: "agent", VolumeSource: VolumeSource{Image: &ImageVolumeSource{ + Reference: "example.com/agent:latest", + }}}, + } + at.Spec.Containers[0].VolumeMounts = []VolumeMount{ + {Name: "agent", MountPath: "/ate"}, + } + }, + wantErr: true, + errMsg: "All images must be pinned", + }, { + name: "Volumes: Image reference is required", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + {Name: "agent", VolumeSource: VolumeSource{Image: &ImageVolumeSource{}}}, + } + at.Spec.Containers[0].VolumeMounts = []VolumeMount{ + {Name: "agent", MountPath: "/ate"}, + } + }, + wantErr: true, + errMsg: "All images must be pinned", + }, { + name: "Volumes: VolumeSource with both Image and DurableDir set is invalid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + { + Name: "agent", + VolumeSource: VolumeSource{ + DurableDir: &DurableDirVolumeSource{}, + Image: &ImageVolumeSource{ + Reference: "example.com/agent@sha256:326e0e090a9a4057e62a1b94236e7a2df2f2f76722f67232e0e47854e4df9c53", + }, + }, + }, + } + at.Spec.Containers[0].VolumeMounts = []VolumeMount{ + {Name: "agent", MountPath: "/ate"}, + } + }, + wantErr: true, + errMsg: "exactly one of the fields in [durableDir externalVolumeTemplate image systemInfo] must be set", + }, { + name: "Volumes: an unmounted Image volume is invalid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + {Name: "agent", VolumeSource: VolumeSource{Image: &ImageVolumeSource{ + Reference: "example.com/agent@sha256:326e0e090a9a4057e62a1b94236e7a2df2f2f76722f67232e0e47854e4df9c53", + }}}, + } + }, + wantErr: true, + errMsg: "All volumes defined in spec.volumes must be mounted by at least one container", + }, { + name: "Volumes: 2 Image volumes in template is valid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + {Name: "agent", VolumeSource: VolumeSource{Image: &ImageVolumeSource{ + Reference: "example.com/agent@sha256:326e0e090a9a4057e62a1b94236e7a2df2f2f76722f67232e0e47854e4df9c53", + }}}, + {Name: "tools", VolumeSource: VolumeSource{Image: &ImageVolumeSource{ + Reference: "example.com/tools@sha256:326e0e090a9a4057e62a1b94236e7a2df2f2f76722f67232e0e47854e4df9c53", + }}}, + } + at.Spec.Containers[0].VolumeMounts = []VolumeMount{ + {Name: "agent", MountPath: "/ate"}, + {Name: "tools", MountPath: "/tools"}, + } + }, + wantErr: false, }, { name: "Volumes: 2 DurableDir volumes in template is valid", mutate: func(at *ActorTemplate) { @@ -761,7 +848,7 @@ func TestActorTemplateValidation(t *testing.T) { } }, wantErr: true, - errMsg: "exactly one of the fields in [durableDir externalVolumeTemplate systemInfo] must be set", + errMsg: "exactly one of the fields in [durableDir externalVolumeTemplate image systemInfo] must be set", }, { name: "Volumes: VolumeSource with no source set is invalid", mutate: func(at *ActorTemplate) { @@ -770,7 +857,7 @@ func TestActorTemplateValidation(t *testing.T) { } }, wantErr: true, - errMsg: "exactly one of the fields in [durableDir externalVolumeTemplate systemInfo] must be set", + errMsg: "exactly one of the fields in [durableDir externalVolumeTemplate image systemInfo] must be set", }, { name: "Volumes: VolumeSource with no source set is invalid (mixed with a valid DurableDir volume)", mutate: func(at *ActorTemplate) { @@ -784,7 +871,7 @@ func TestActorTemplateValidation(t *testing.T) { } }, wantErr: true, - errMsg: "exactly one of the fields in [durableDir externalVolumeTemplate systemInfo] must be set", + errMsg: "exactly one of the fields in [durableDir externalVolumeTemplate image systemInfo] must be set", }, { name: "Volumes: SystemInfo volume projecting all actor metadata fields is valid", mutate: func(at *ActorTemplate) { diff --git a/pkg/api/v1alpha1/zz_generated.deepcopy.go b/pkg/api/v1alpha1/zz_generated.deepcopy.go index a93ed6c46..abfb8bacb 100644 --- a/pkg/api/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/api/v1alpha1/zz_generated.deepcopy.go @@ -420,6 +420,21 @@ func (in *HTTPGetAction) DeepCopy() *HTTPGetAction { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImageVolumeSource) DeepCopyInto(out *ImageVolumeSource) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageVolumeSource. +func (in *ImageVolumeSource) DeepCopy() *ImageVolumeSource { + if in == nil { + return nil + } + out := new(ImageVolumeSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OnResumeConfig) DeepCopyInto(out *OnResumeConfig) { *out = *in @@ -643,6 +658,11 @@ func (in *VolumeSource) DeepCopyInto(out *VolumeSource) { *out = new(DurableDirVolumeSource) **out = **in } + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(ImageVolumeSource) + **out = **in + } if in.ExternalVolumeTemplate != nil { in, out := &in.ExternalVolumeTemplate, &out.ExternalVolumeTemplate *out = new(ExternalVolumeTemplate)