diff --git a/cmd/ateapi/internal/controlapi/workload_spec.go b/cmd/ateapi/internal/controlapi/workload_spec.go index f34501082..bc86d63e9 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec.go +++ b/cmd/ateapi/internal/controlapi/workload_spec.go @@ -27,17 +27,48 @@ import ( func workloadSpecFromActorTemplate(actorTemplate *atev1alpha1.ActorTemplate, actor *ateapipb.Actor) (*ateletpb.WorkloadSpec, error) { workloadSpec := &ateletpb.WorkloadSpec{} - // add volumes + // Convert volumes to atelet's representation. ActorTemplate validation has + // already ensured that only one source is set. for _, vol := range actorTemplate.Spec.Volumes { - // volume is durable-dir type - if vol.VolumeSource.DurableDir != nil { + switch { + case vol.VolumeSource.DurableDir != nil: workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ Name: vol.Name, - Type: ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR, Source: &ateletpb.Volume_DurableDir{ DurableDir: &ateletpb.DurableDirVolume{}, }, }) + + case vol.VolumeSource.SystemInfo != nil: + ateletSystemInfo := &ateletpb.SystemInfoVolume{} + for _, dataSource := range vol.VolumeSource.SystemInfo.DataSources { + switch { + case dataSource.ActorMetadata != nil: + actorMetadata := &ateletpb.ActorMetadataDataSource{} + for _, item := range dataSource.ActorMetadata.Items { + actorMetadata.Items = append(actorMetadata.Items, &ateletpb.ActorMetadataItem{ + Field: toAteletActorMetadataField(item.Field), + Path: item.Path, + }) + } + ateletSystemInfo.DataSources = append(ateletSystemInfo.DataSources, &ateletpb.SystemInfoDataSource{ + DataSource: &ateletpb.SystemInfoDataSource_ActorMetadata{ + ActorMetadata: actorMetadata, + }, + }) + default: + continue // Drop unrecognized data sources + } + } + workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ + Name: vol.Name, + Source: &ateletpb.Volume_SystemInfo{ + SystemInfo: ateletSystemInfo, + }, + }) + + default: + continue // Drop unrecognized volumes. } } @@ -104,7 +135,6 @@ func appendExternalVolumes(workloadSpec *ateletpb.WorkloadSpec, template *atev1a } workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ Name: vol.Name, - Type: ateletpb.VolumeType_VOLUME_TYPE_EXTERNAL, Source: &ateletpb.Volume_External{ External: &ateletpb.ExternalVolumeSource{ StorageVolumeId: storageVolID, @@ -132,6 +162,22 @@ func isVolumeMounted(volumeName string, template *atev1alpha1.ActorTemplate) boo // toAteletReadyz projects the CRD readyz field onto the ateletpb wire type. // Returns nil when the source is nil so containers without a probe stay // unchanged on the wire. +// toAteletActorMetadataField projects the CRD field selector onto the atelet +// wire enum. Unknown values map to UNSPECIFIED, which atelet skips; CRD enum +// validation makes that unreachable for stored templates. +func toAteletActorMetadataField(in atev1alpha1.ActorMetadataField) ateletpb.ActorMetadataField { + switch in { + case atev1alpha1.ActorMetadataFieldName: + return ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_NAME + case atev1alpha1.ActorMetadataFieldAtespace: + return ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_ATESPACE + case atev1alpha1.ActorMetadataFieldUID: + return ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_UID + default: + return ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_UNSPECIFIED + } +} + func toAteletReadyz(in *atev1alpha1.ContainerReadyz) *ateletpb.Readyz { if in == nil { return nil diff --git a/cmd/ateapi/internal/controlapi/workload_spec_test.go b/cmd/ateapi/internal/controlapi/workload_spec_test.go index bded1d6ca..2949ae69d 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec_test.go +++ b/cmd/ateapi/internal/controlapi/workload_spec_test.go @@ -55,7 +55,6 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { Volumes: []*ateletpb.Volume{ { Name: "home", - Type: ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR, Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}, }, }, @@ -71,6 +70,72 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { }, }, }, + { + name: "converts SystemInfo volume with actorMetadata items", + template: &atev1alpha1.ActorTemplate{ + ObjectMeta: metav1.ObjectMeta{Name: "tmpl1", Namespace: "agent-ns"}, + Spec: atev1alpha1.ActorTemplateSpec{ + Volumes: []atev1alpha1.Volume{ + { + Name: "system-info", + VolumeSource: atev1alpha1.VolumeSource{ + SystemInfo: &atev1alpha1.SystemInfoVolumeSource{ + DataSources: []atev1alpha1.SystemInfoDataSource{ + {ActorMetadata: &atev1alpha1.ActorMetadataDataSource{ + Items: []atev1alpha1.ActorMetadataItem{ + {Field: atev1alpha1.ActorMetadataFieldName, Path: "actor-name"}, + {Field: atev1alpha1.ActorMetadataFieldAtespace, Path: "atespace"}, + {Field: atev1alpha1.ActorMetadataFieldUID, Path: "identity/actor-uid"}, + }, + }}, + }, + }, + }, + }, + }, + Containers: []atev1alpha1.Container{ + { + Name: "main", + Image: "main", + VolumeMounts: []atev1alpha1.VolumeMount{ + {Name: "system-info", MountPath: "/run/ate"}, + }, + }, + }, + }, + }, + want: &ateletpb.WorkloadSpec{ + Volumes: []*ateletpb.Volume{ + { + Name: "system-info", + Source: &ateletpb.Volume_SystemInfo{ + SystemInfo: &ateletpb.SystemInfoVolume{ + DataSources: []*ateletpb.SystemInfoDataSource{ + {DataSource: &ateletpb.SystemInfoDataSource_ActorMetadata{ + ActorMetadata: &ateletpb.ActorMetadataDataSource{ + Items: []*ateletpb.ActorMetadataItem{ + {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_NAME, Path: "actor-name"}, + {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_ATESPACE, Path: "atespace"}, + {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_UID, Path: "identity/actor-uid"}, + }, + }, + }}, + }, + }, + }, + }, + }, + Containers: []*ateletpb.Container{ + { + Name: "main", + Image: "main", + VolumeMounts: []*ateletpb.VolumeMount{ + {Name: "system-info", MountPath: "/run/ate"}, + }, + }, + }, + }, + }, { name: "skips non-DurableDir volumes", template: &atev1alpha1.ActorTemplate{ @@ -95,7 +160,6 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { Volumes: []*ateletpb.Volume{ { Name: "home", - Type: ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR, Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}, }, }, @@ -127,7 +191,6 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { Volumes: []*ateletpb.Volume{ { Name: "home", - Type: ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR, Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}, }, }, @@ -310,7 +373,6 @@ func TestAppendExternalVolumes(t *testing.T) { Volumes: []*ateletpb.Volume{ { Name: "vol-1", - Type: ateletpb.VolumeType_VOLUME_TYPE_EXTERNAL, Source: &ateletpb.Volume_External{ External: &ateletpb.ExternalVolumeSource{ StorageVolumeId: "vol-gce-pd-123", diff --git a/cmd/atelet/main.go b/cmd/atelet/main.go index bd6707780..7aaabaf72 100644 --- a/cmd/atelet/main.go +++ b/cmd/atelet/main.go @@ -29,6 +29,7 @@ import ( "path/filepath" "slices" "strconv" + "strings" "syscall" "time" @@ -455,7 +456,7 @@ func (s *AteomHerder) Run(ctx context.Context, req *ateletpb.RunRequest) (resp * return nil, fmt.Errorf("while recording sandbox assets: %w", err) } - if err := s.prepareOCIBundles(ctx, actorUID, actorRef.Name, + if err := s.prepareOCIBundles(ctx, actorUID, actorRef, req.GetSpec(), sandboxRec.PauseImage, req.GetTargetAteomUid(), ); err != nil { return nil, ateerrors.CrashIfReason(ctx, err, ateerrors.ReasonInvalidContainerConfig) @@ -710,7 +711,7 @@ func shouldHaveSnapshots(req *ateletpb.CheckpointRequest) bool { } for _, vol := range req.GetSpec().GetVolumes() { - if vol.GetType() == ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR { + if _, ok := vol.GetSource().(*ateletpb.Volume_DurableDir); ok { return true } } @@ -1103,7 +1104,7 @@ func (s *AteomHerder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) return ateerrors.CrashIfReason(ctx, err, ateerrors.ReasonFailedGetExternalObject, ateerrors.ReasonInvalidObjectURL, ateerrors.ReasonTerminalFileSystemError, ateerrors.ReasonInvalidSandboxAsset) } t := time.Now() - err = s.prepareOCIBundles(gctx, actorUID, actorRef.Name, req.GetSpec(), runtimeRec.PauseImage, req.GetTargetAteomUid()) + err = s.prepareOCIBundles(gctx, actorUID, actorRef, req.GetSpec(), runtimeRec.PauseImage, req.GetTargetAteomUid()) dBundles = time.Since(t) if err != nil { prepFailedPhase = ateattr.SnapshotPhaseOCIUnpack @@ -1413,28 +1414,25 @@ func (s *AteomHerder) downloadExternalCheckpoint(ctx context.Context, snapshotUR func (s *AteomHerder) prepareOCIBundles( ctx context.Context, actorUID string, - actorName string, + actorRef resources.ActorRef, spec *ateletpb.WorkloadSpec, pauseImage string, targetAteomUid string, ) error { - // Populate the per-actor identity directory that gets bind-mounted into - // the application containers. Regenerated on every resume, so it carries - // the correct per-actor name even when restoring from the golden snapshot. - identityDir := ateompath.ActorIdentityDirPath(actorUID) - if err := os.MkdirAll(identityDir, 0o755); err != nil { - return fmt.Errorf("while creating actor identity dir: %w", err) - } - if err := writeFileAtomic(filepath.Join(identityDir, ActorIDFileName), []byte(actorName), 0o644); err != nil { - return fmt.Errorf("while writing actor identity file: %w", err) - } - // make directories for all durable-dir volumes + // Prepare host folders for volume types that need them. for _, vol := range spec.GetVolumes() { - if vol.GetType() == ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR { + switch volSrc := vol.GetSource().(type) { + case *ateletpb.Volume_DurableDir: volPath := ateompath.DurableDirVolumeMountPoint(actorUID, vol.GetName()) if err := os.MkdirAll(volPath, 0o700); err != nil { return fmt.Errorf("while creating %q: %w", volPath, err) } + + case *ateletpb.Volume_SystemInfo: + volRootHostPath := ateompath.SystemInfoVolumeRoot(actorUID, vol.GetName()) + if err := writeSystemInfoVolume(ctx, volRootHostPath, actorRef, actorUID, volSrc.SystemInfo); err != nil { + return fmt.Errorf("while populating system-info volume %q: %w", vol.GetName(), err) + } } } @@ -1449,7 +1447,7 @@ func (s *AteomHerder) prepareOCIBundles( // Declare durable-dir volumes to gVisor. We use the volume name as the // mount hint name to support multiple durable-dir volumes. for _, vol := range spec.GetVolumes() { - if vol.GetType() == ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR { + if vol.GetDurableDir() != nil { annotations[fmt.Sprintf("dev.gvisor.spec.mount.%s.type", vol.GetName())] = "bind" annotations[fmt.Sprintf("dev.gvisor.spec.mount.%s.share", vol.GetName())] = "container" annotations[fmt.Sprintf("dev.gvisor.spec.mount.%s.source", vol.GetName())] = ateompath.DurableDirVolumeMountPoint(actorUID, vol.GetName()) @@ -1467,8 +1465,7 @@ func (s *AteomHerder) prepareOCIBundles( nil, annotations, ateompath.AteomNetNSPath(targetAteomUid), - "", // pause is sandbox infra; it gets no actor identity mount. - nil, + nil, // pause is sandbox infra; it mounts no volumes. nil, ); err != nil { return wrapFileSystemErr("while creating pause OCI bundle", err) @@ -1499,7 +1496,6 @@ func (s *AteomHerder) prepareOCIBundles( "io.kubernetes.cri.container-name": ctr.GetName(), }, ateompath.AteomNetNSPath(targetAteomUid), - identityDir, spec.GetVolumes(), ctr.GetVolumeMounts(), ); err != nil { @@ -1512,6 +1508,80 @@ func (s *AteomHerder) prepareOCIBundles( return g.Wait() } +// writeSystemInfoVolume populates the root directory of a system-info volume +// with one file per projected item. It runs on every Run/Restore, before the +// sandbox starts, so the files carry the values of the actor actually being +// started, no matter what checkpointed state it boots from. +// +// Every file must be a plain file at a stable real path across regenerations: +// the micro-VM virtiofsds run in find-paths migration mode, which re-binds +// the guest's FUSE state to files by the paths recorded at suspend, and +// gVisor's gofer likewise re-opens files by path on restore. Symlink-swap +// schemes (kubelet's atomic writer) move the payload files to a new +// timestamped directory on every write and delete the old one, so guest +// state from the snapshot could not re-bind. Per-file write-to-temp-and- +// rename is atomic enough: this only runs while the sandbox is down, so no +// reader can observe a partial write. +// +// TODO(#802): rotating data sources (identity JWTs, certificates) will need +// these files refreshed while the actor runs, not just at Run/Restore — and +// must keep the per-file rename discipline so visible paths never move. +// actorMetadata never changes after start, so writing here is enough for it. +func writeSystemInfoVolume(ctx context.Context, rootPath string, actorRef resources.ActorRef, actorUID string, si *ateletpb.SystemInfoVolume) error { + if err := os.MkdirAll(rootPath, 0o755); err != nil { + return fmt.Errorf("while creating %q: %w", rootPath, err) + } + + for _, dataSourceAny := range si.GetDataSources() { + switch dataSource := dataSourceAny.GetDataSource().(type) { + case *ateletpb.SystemInfoDataSource_ActorMetadata: + for _, item := range dataSource.ActorMetadata.GetItems() { + var value string + switch item.GetField() { + case ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_NAME: + value = actorRef.Name + case ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_ATESPACE: + value = actorRef.Atespace + case ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_UID: + value = actorUID + default: + // Unknown fields come only from a newer ateapi; skip the + // item rather than write an empty file under its path. + continue + } + if err := writeSystemInfoFile(rootPath, item.GetPath(), []byte(value)); err != nil { + return err + } + } + } + } + return nil +} + +// writeSystemInfoFile writes one projected file at relPath under rootPath via +// write-to-temp-and-rename, creating parent directories as needed. relPath is +// validated defensively even though ActorTemplate validation already rejects +// non-clean paths: atelet is the last line before the value hits the host +// filesystem. +func writeSystemInfoFile(rootPath, relPath string, data []byte) error { + if relPath == "" || strings.HasPrefix(relPath, "/") { + return fmt.Errorf("invalid system-info path %q: must be a non-empty relative path", relPath) + } + for _, seg := range strings.Split(relPath, "/") { + if seg == ".." || seg == "." || seg == "" { + return fmt.Errorf("invalid system-info path %q: must not contain empty, '.', or '..' segments", relPath) + } + } + dst := filepath.Join(rootPath, filepath.FromSlash(relPath)) + if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { + return fmt.Errorf("while creating parent of %q: %w", dst, err) + } + if err := writeFileAtomic(dst, data, 0o644); err != nil { + return fmt.Errorf("while writing system-info file %q: %w", dst, err) + } + return nil +} + // dialAteom opens (or reuses) the gRPC connection to the target ateom // pod and returns an ateom client. func (s *AteomHerder) dialAteom(ctx context.Context, targetAteomUid string) (ateompb.AteomClient, error) { @@ -1525,45 +1595,52 @@ func (s *AteomHerder) dialAteom(ctx context.Context, targetAteomUid string) (ate // buildAteomWorkloadSpec projects the atelet-facing workload spec onto // the ateom-facing one. func buildAteomWorkloadSpec(spec *ateletpb.WorkloadSpec) (*ateompb.WorkloadSpec, error) { - volumes := make(map[string]ateletpb.VolumeType) + volumes := make(map[string]*ateletpb.Volume) for _, vol := range spec.GetVolumes() { name := vol.GetName() if _, duplicate := volumes[name]; duplicate { return nil, fmt.Errorf("duplicate volume name %q in workload spec", name) } - volumes[name] = vol.GetType() + volumes[name] = vol } out := &ateompb.WorkloadSpec{} for _, ctr := range spec.GetContainers() { var ddMounts []*ateompb.DurableDirVolumeMount var csiMounts []*ateompb.VolumeMount + var siMounts []*ateompb.SystemInfoVolumeMount for _, vm := range ctr.GetVolumeMounts() { volName := vm.GetName() - volType, ok := volumes[volName] + vol, ok := volumes[volName] if !ok { return nil, fmt.Errorf("container %q mounts volume %q which is not defined in workload volumes", ctr.GetName(), volName) } - switch volType { - case ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR: + switch vol.GetSource().(type) { + case *ateletpb.Volume_DurableDir: ddMounts = append(ddMounts, &ateompb.DurableDirVolumeMount{ VolumeName: volName, MountPath: vm.GetMountPath(), }) - case ateletpb.VolumeType_VOLUME_TYPE_EXTERNAL: + case *ateletpb.Volume_External: csiMounts = append(csiMounts, &ateompb.VolumeMount{ VolumeName: volName, MountPath: vm.GetMountPath(), }) + case *ateletpb.Volume_SystemInfo: + siMounts = append(siMounts, &ateompb.SystemInfoVolumeMount{ + VolumeName: volName, + MountPath: vm.GetMountPath(), + }) default: - return nil, fmt.Errorf("container %q mounts volume %q with unsupported type %v", ctr.GetName(), volName, volType) + return nil, fmt.Errorf("container %q mounts volume %q with unsupported source %T", ctr.GetName(), volName, vol.GetSource()) } } out.Containers = append(out.Containers, &ateompb.Container{ Name: ctr.GetName(), DurableDirVolumeMounts: ddMounts, CsiVolumeMounts: csiMounts, + SystemInfoVolumeMounts: siMounts, Readyz: toAteomReadyz(ctr.GetReadyz()), }) } @@ -1894,16 +1971,6 @@ func resetActorDirs(actorUID string) error { return wrapFileSystemErr("while creating restore-state dir: %w", err) } - // World-readable (0o755): bind-mounted into the actor, whose workload - // reads it through the gofer. - identityDir := ateompath.ActorIdentityDirPath(actorUID) - if err := os.RemoveAll(identityDir); err != nil { - return wrapFileSystemErr("while deleting actor identity dir: %w", err) - } - if err := os.MkdirAll(identityDir, 0o755); err != nil { - return wrapFileSystemErr("while creating actor identity dir: %w", err) - } - durableDirVolumesMountDir := ateompath.DurableDirVolumeMountsDir(actorUID) if err := os.RemoveAll(durableDirVolumesMountDir); err != nil { return wrapFileSystemErr("while deleting durable-dir volumes mount dir: %w", err) @@ -1912,6 +1979,16 @@ func resetActorDirs(actorUID string) error { return wrapFileSystemErr("while creating durable-dir volumes mount dir: %w", err) } + // World-readable (0o755): bind-mounted read-only into the actor, whose + // workload reads it through the gofer. + systemInfoVolumeRootsDir := ateompath.SystemInfoVolumeRootsDir(actorUID) + if err := os.RemoveAll(systemInfoVolumeRootsDir); err != nil { + return wrapFileSystemErr("while deleting system-info volume roots dir: %w", err) + } + if err := os.MkdirAll(systemInfoVolumeRootsDir, 0o755); err != nil { + return wrapFileSystemErr("while creating system-info volume roots dir: %w", err) + } + // Do not call RemoveAll on volume directories in case the unmount failed. // We do not want to delete mount content. volumesDir := ateompath.VolumesDir(actorUID) diff --git a/cmd/atelet/main_test.go b/cmd/atelet/main_test.go index 50788d63a..4db05deb3 100644 --- a/cmd/atelet/main_test.go +++ b/cmd/atelet/main_test.go @@ -119,6 +119,127 @@ func TestSnapshotManifestRequiresPauseImage(t *testing.T) { } } +func TestWriteSystemInfoVolume(t *testing.T) { + ctx := context.Background() + root := filepath.Join(t.TempDir(), "system-info", "vol1") + si := &ateletpb.SystemInfoVolume{ + DataSources: []*ateletpb.SystemInfoDataSource{ + {DataSource: &ateletpb.SystemInfoDataSource_ActorMetadata{ + ActorMetadata: &ateletpb.ActorMetadataDataSource{ + Items: []*ateletpb.ActorMetadataItem{ + {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_NAME, Path: "actor-name"}, + {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_ATESPACE, Path: "atespace"}, + {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_UID, Path: "identity/actor-uid"}, + }, + }, + }}, + }, + } + + golden := resources.ActorRef{Atespace: "ate-e2e-probe", Name: "golden-actor"} + if err := writeSystemInfoVolume(ctx, root, golden, "uid-golden", si); err != nil { + t.Fatalf("writeSystemInfoVolume: %v", err) + } + + // Overwrite with a different actor, as happens when a snapshot taken from + // one actor seeds another on resume: files must carry the new values. + alpha := resources.ActorRef{Atespace: "ate-e2e-probe", Name: "probe-alpha"} + if err := writeSystemInfoVolume(ctx, root, alpha, "uid-alpha", si); err != nil { + t.Fatalf("writeSystemInfoVolume (rewrite): %v", err) + } + + // Values are written raw, no trailing newline. + for path, want := range map[string]string{ + "actor-name": "probe-alpha", + "atespace": "ate-e2e-probe", + "identity/actor-uid": "uid-alpha", + } { + t.Run(path, func(t *testing.T) { + target := filepath.Join(root, path) + got, err := os.ReadFile(target) + if err != nil { + t.Fatalf("reading %q: %v", target, err) + } + if string(got) != want { + t.Errorf("content = %q, want %q", got, want) + } + info, err := os.Stat(target) + if err != nil { + t.Fatalf("stat %q: %v", target, err) + } + if perm := info.Mode().Perm(); perm != 0o644 { + t.Errorf("perm = %o, want 644", perm) + } + }) + } +} + +// TestWriteSystemInfoVolume_StableRealPaths pins the path-stability contract +// the restore paths depend on: the micro-VM virtiofsds run in find-paths +// migration mode, which re-binds the guest's FUSE state to files by the paths +// recorded at suspend, and gVisor's gofer likewise re-opens files by path on +// restore. Projected files must therefore be plain files at stable real +// paths — no symlink indirection — and regenerating the volume must not move +// or delete a path that guest state may reference. +func TestWriteSystemInfoVolume_StableRealPaths(t *testing.T) { + ctx := context.Background() + root := filepath.Join(t.TempDir(), "system-info", "vol1") + si := &ateletpb.SystemInfoVolume{ + DataSources: []*ateletpb.SystemInfoDataSource{ + {DataSource: &ateletpb.SystemInfoDataSource_ActorMetadata{ + ActorMetadata: &ateletpb.ActorMetadataDataSource{ + Items: []*ateletpb.ActorMetadataItem{ + {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_NAME, Path: "actor-name"}, + {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_UID, Path: "identity/actor-uid"}, + }, + }, + }}, + }, + } + + golden := resources.ActorRef{Atespace: "ate-e2e-probe", Name: "golden-actor"} + if err := writeSystemInfoVolume(ctx, root, golden, "uid-golden", si); err != nil { + t.Fatalf("writeSystemInfoVolume: %v", err) + } + + realBefore := map[string]string{} + for _, p := range []string{"actor-name", "identity/actor-uid"} { + visible := filepath.Join(root, p) + fi, err := os.Lstat(visible) + if err != nil { + t.Fatalf("lstat %q: %v", visible, err) + } + if !fi.Mode().IsRegular() { + t.Errorf("%q is %v, want a regular file: symlink indirection moves the real path on regeneration, which find-paths cannot re-bind", visible, fi.Mode().Type()) + } + real, err := filepath.EvalSymlinks(visible) + if err != nil { + t.Fatalf("eval symlinks %q: %v", visible, err) + } + realBefore[p] = real + } + + // Regenerate for a different actor, as a restore from a shared golden + // snapshot does. + alpha := resources.ActorRef{Atespace: "ate-e2e-probe", Name: "probe-alpha"} + if err := writeSystemInfoVolume(ctx, root, alpha, "uid-alpha", si); err != nil { + t.Fatalf("writeSystemInfoVolume (rewrite): %v", err) + } + + for _, p := range []string{"actor-name", "identity/actor-uid"} { + real, err := filepath.EvalSymlinks(filepath.Join(root, p)) + if err != nil { + t.Fatalf("eval symlinks after rewrite %q: %v", p, err) + } + if real != realBefore[p] { + t.Errorf("%q real path moved on regeneration: %q -> %q; guest state recorded at suspend cannot re-bind", p, realBefore[p], real) + } + if _, err := os.Stat(realBefore[p]); err != nil { + t.Errorf("pre-rewrite real path %q gone after regeneration: %v; find-paths re-open of a suspend-time path would fail", realBefore[p], err) + } + } +} + func TestWriteFileAtomic(t *testing.T) { dir := t.TempDir() target := filepath.Join(dir, "actor-id") @@ -705,9 +826,10 @@ func TestBuildAteomWorkloadSpecForwardsReadyz(t *testing.T) { func TestBuildAteomWorkloadSpecForwardsDurableDirMounts(t *testing.T) { in := &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "data", Type: ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR}, - {Name: "cache", Type: ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR}, - {Name: "scratch", Type: ateletpb.VolumeType_VOLUME_TYPE_EXTERNAL}, + {Name: "data", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, + {Name: "cache", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, + {Name: "scratch", Source: &ateletpb.Volume_External{External: &ateletpb.ExternalVolumeSource{}}}, + {Name: "system-info", Source: &ateletpb.Volume_SystemInfo{SystemInfo: &ateletpb.SystemInfoVolume{}}}, }, Containers: []*ateletpb.Container{ { @@ -715,9 +837,8 @@ func TestBuildAteomWorkloadSpecForwardsDurableDirMounts(t *testing.T) { VolumeMounts: []*ateletpb.VolumeMount{ {Name: "data", MountPath: "/home/counter"}, {Name: "cache", MountPath: "/var/cache"}, - // Only durable-dir volumes cross to ateom; other volume - // types are mounted by atelet itself. {Name: "scratch", MountPath: "/scratch"}, + {Name: "system-info", MountPath: "/run/ate"}, }, }, { @@ -742,6 +863,9 @@ func TestBuildAteomWorkloadSpecForwardsDurableDirMounts(t *testing.T) { CsiVolumeMounts: []*ateompb.VolumeMount{ {VolumeName: "scratch", MountPath: "/scratch"}, }, + SystemInfoVolumeMounts: []*ateompb.SystemInfoVolumeMount{ + {VolumeName: "system-info", MountPath: "/run/ate"}, + }, }, { Name: "sidecar", @@ -771,7 +895,7 @@ func TestBuildAteomWorkloadSpecValidation(t *testing.T) { name: "missing volume definition", in: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "data", Type: ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR}, + {Name: "data", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, }, Containers: []*ateletpb.Container{ { @@ -785,10 +909,10 @@ func TestBuildAteomWorkloadSpecValidation(t *testing.T) { wantErr: `container "ctr" mounts volume "missing-vol" which is not defined in workload volumes`, }, { - name: "unsupported volume type", + name: "unsupported volume source", in: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "data", Type: ateletpb.VolumeType_VOLUME_TYPE_UNSPECIFIED}, + {Name: "data"}, }, Containers: []*ateletpb.Container{ { @@ -799,14 +923,14 @@ func TestBuildAteomWorkloadSpecValidation(t *testing.T) { }, }, }, - wantErr: `container "ctr" mounts volume "data" with unsupported type VOLUME_TYPE_UNSPECIFIED`, + wantErr: `container "ctr" mounts volume "data" with unsupported source `, }, { name: "duplicate volume names", in: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "data", Type: ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR}, - {Name: "data", Type: ateletpb.VolumeType_VOLUME_TYPE_EXTERNAL}, + {Name: "data", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, + {Name: "data", Source: &ateletpb.Volume_External{External: &ateletpb.ExternalVolumeSource{}}}, }, Containers: []*ateletpb.Container{ { @@ -1682,7 +1806,7 @@ func TestShouldHaveSnapshots(t *testing.T) { Scope: ateletpb.SnapshotScope_SNAPSHOT_SCOPE_DATA, Spec: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "durable", Type: ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR}, + {Name: "durable", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, }, }, }, @@ -1694,7 +1818,7 @@ func TestShouldHaveSnapshots(t *testing.T) { Scope: ateletpb.SnapshotScope_SNAPSHOT_SCOPE_DATA, Spec: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "csi", Type: ateletpb.VolumeType_VOLUME_TYPE_EXTERNAL}, + {Name: "csi", Source: &ateletpb.Volume_External{External: &ateletpb.ExternalVolumeSource{}}}, }, }, }, @@ -1706,8 +1830,8 @@ func TestShouldHaveSnapshots(t *testing.T) { Scope: ateletpb.SnapshotScope_SNAPSHOT_SCOPE_DATA, Spec: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "durable", Type: ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR}, - {Name: "csi", Type: ateletpb.VolumeType_VOLUME_TYPE_EXTERNAL}, + {Name: "durable", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, + {Name: "csi", Source: &ateletpb.Volume_External{External: &ateletpb.ExternalVolumeSource{}}}, }, }, }, diff --git a/cmd/atelet/oci.go b/cmd/atelet/oci.go index e1476610c..94a4274ea 100644 --- a/cmd/atelet/oci.go +++ b/cmd/atelet/oci.go @@ -25,32 +25,14 @@ import ( "github.com/agent-substrate/substrate/internal/ateerrors" "github.com/agent-substrate/substrate/internal/ateompath" "github.com/agent-substrate/substrate/internal/imagecache" + "github.com/agent-substrate/substrate/internal/proto/ateletpb" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/opencontainers/runtime-spec/specs-go" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - - "github.com/agent-substrate/substrate/internal/proto/ateletpb" -) - -const ( - // IdentityMountPath is the in-actor directory at which atelet bind-mounts - // the actor's identity data. Workloads read the files inside it (at - // request time, not cached at startup) to learn about themselves. It is - // delivered as a per-actor bind mount rather than environment variables - // because env lives in the checkpointed process memory and would be - // frozen at the golden snapshot's values after a restore; a bind mount is - // re-attached per-actor on every resume. A directory (rather than a - // single-file mount) so further identity data can be added without - // changing the mount shape. - IdentityMountPath = "/run/ate" - - // ActorIDFileName is the file inside IdentityMountPath holding the - // actor's own ID, raw with no trailing newline. - ActorIDFileName = "actor-id" ) -func prepareOCIDirectory(ctx context.Context, imageCache *imagecache.Store, actorUID, containerName, ref string, command, args []string, env []string, annotations map[string]string, netns string, identityDir string, volumes []*ateletpb.Volume, volumeMounts []*ateletpb.VolumeMount) error { +func prepareOCIDirectory(ctx context.Context, imageCache *imagecache.Store, actorUID, containerName, ref string, command, args []string, env []string, annotations map[string]string, netns string, volumes []*ateletpb.Volume, volumeMounts []*ateletpb.VolumeMount) error { tracer := otel.Tracer("prepareOCIDirectory") ctx, span := tracer.Start(ctx, "prepareOCIDirectory") @@ -90,14 +72,10 @@ func prepareOCIDirectory(ctx context.Context, imageCache *imagecache.Store, acto } resolvedEnv := resolveActorEnv(&img.Config, env) - // The identity bind target must exist in the rootfs for the mount to - // attach; ateom creates it through the mounted overlay (it lands in the - // actor's upper) so the workload can read its own name at - // IdentityMountPath/ActorIDFileName. + // Every bind target must exist in the rootfs for the mount to attach; + // ateom creates them through the mounted overlay (they land in the + // actor's upper). var extraDirs []string - if identityDir != "" { - extraDirs = append(extraDirs, IdentityMountPath) - } for _, vm := range volumeMounts { extraDirs = append(extraDirs, vm.GetMountPath()) } @@ -109,7 +87,7 @@ func prepareOCIDirectory(ctx context.Context, imageCache *imagecache.Store, acto return fmt.Errorf("while writing overlay spec: %w", err) } - ociSpec := buildActorOCISpec(actorUID, resolvedArgs, resolvedEnv, annotations, netns, identityDir, volumes, volumeMounts) + ociSpec := buildActorOCISpec(actorUID, resolvedArgs, resolvedEnv, annotations, netns, volumes, volumeMounts) ociSpecBytes, err := json.MarshalIndent(ociSpec, "", " ") if err != nil { return fmt.Errorf("while marshaling OCI spec: %w", err) @@ -183,10 +161,7 @@ func resolveProcessArgs(imageCfg *v1.Config, command, args []string) ([]string, // buildActorOCISpec assembles the OCI runtime spec for an actor container from // already-resolved args and env (see resolveProcessArgs and resolveActorEnv). -// When identityDir is non-empty it adds a read-only bind mount of that host -// directory at IdentityMountPath so the actor can read its own ID (see -// IdentityMountPath for why this is a bind mount rather than env vars). -func buildActorOCISpec(actorUID string, args []string, env []string, annotations map[string]string, netns string, identityDir string, volumes []*ateletpb.Volume, volumeMounts []*ateletpb.VolumeMount) *specs.Spec { +func buildActorOCISpec(actorUID string, args []string, env []string, annotations map[string]string, netns string, volumes []*ateletpb.Volume, volumeMounts []*ateletpb.VolumeMount) *specs.Spec { mounts := []specs.Mount{ { Destination: "/proc", @@ -216,14 +191,6 @@ func buildActorOCISpec(actorUID string, args []string, env []string, annotations Options: []string{"ro"}, }, } - if identityDir != "" { - mounts = append(mounts, specs.Mount{ - Destination: IdentityMountPath, - Type: "bind", - Source: identityDir, - Options: []string{"ro"}, - }) - } spec := &specs.Spec{ Process: &specs.Process{ @@ -295,18 +262,24 @@ func buildActorOCISpec(actorUID string, args []string, env []string, annotations } // Prepare and mount all volumes. - volumeTypes := make(map[string]ateletpb.VolumeType) + volumesByName := make(map[string]*ateletpb.Volume) for _, vol := range volumes { - volumeTypes[vol.GetName()] = vol.GetType() + volumesByName[vol.GetName()] = vol } for _, vm := range volumeMounts { var srcPath string - switch volumeTypes[vm.GetName()] { - case ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR: + options := []string{"bind", "rw"} + switch volumesByName[vm.GetName()].GetSource().(type) { + case *ateletpb.Volume_DurableDir: srcPath = ateompath.DurableDirVolumeMountPoint(actorUID, vm.GetName()) - case ateletpb.VolumeType_VOLUME_TYPE_EXTERNAL: + case *ateletpb.Volume_External: srcPath = ateompath.VolumeHostPath(actorUID, vm.GetName()) + case *ateletpb.Volume_SystemInfo: + // System-info contents are generated by atelet; the workload only + // reads them. + srcPath = ateompath.SystemInfoVolumeRoot(actorUID, vm.GetName()) + options = []string{"bind", "ro"} default: continue } @@ -314,7 +287,7 @@ func buildActorOCISpec(actorUID string, args []string, env []string, annotations Destination: vm.GetMountPath(), Type: "bind", Source: srcPath, - Options: []string{"bind", "rw"}, + Options: options, }) } diff --git a/cmd/atelet/oci_test.go b/cmd/atelet/oci_test.go index 433c082c3..64d7aedc5 100644 --- a/cmd/atelet/oci_test.go +++ b/cmd/atelet/oci_test.go @@ -25,36 +25,47 @@ import ( v1 "github.com/google/go-containerregistry/pkg/v1" ) -// With an identity dir, a read-only bind mount appears at IdentityMountPath. -func TestBuildActorOCISpec_IdentityMount(t *testing.T) { +// Each system-info volume mount becomes a read-only bind mount whose source +// is the per-actor on-host SystemInfoVolumeRoot for that volume name. It is +// delivered as a bind mount rather than environment variables because env +// lives in the checkpointed process memory and would be frozen at the golden +// snapshot's values after a restore; a bind mount is re-attached per-actor on +// every resume. +func TestBuildActorOCISpec_SystemInfoVolumeMounts(t *testing.T) { + const actorUID = "actor_uid" + volumeMounts := []*ateletpb.VolumeMount{ + {Name: "sysinfo", MountPath: "/run/ate"}, + } + volumes := []*ateletpb.Volume{ + {Name: "sysinfo", Source: &ateletpb.Volume_SystemInfo{SystemInfo: &ateletpb.SystemInfoVolume{}}}, + } spec := buildActorOCISpec( - "actor_uid", + actorUID, []string{"/app"}, []string{"FOO=bar"}, map[string]string{"k": "v"}, "/run/netns/x", - "/host/actors/actor_uid/identity", - nil, - nil, + volumes, + volumeMounts, ) found := false for _, m := range spec.Mounts { - if m.Destination != IdentityMountPath { + if m.Destination != "/run/ate" { continue } found = true - if m.Source != "/host/actors/actor_uid/identity" { - t.Errorf("identity mount source = %q, want the per-actor identity dir", m.Source) + if want := ateompath.SystemInfoVolumeRoot(actorUID, "sysinfo"); m.Source != want { + t.Errorf("system-info mount source = %q, want %q", m.Source, want) } if m.Type != "bind" { - t.Errorf("identity mount type = %q, want bind", m.Type) + t.Errorf("system-info mount type = %q, want bind", m.Type) } if !slices.Contains(m.Options, "ro") { - t.Errorf("identity mount must be read-only, options=%v", m.Options) + t.Errorf("system-info mount must be read-only, options=%v", m.Options) } } if !found { - t.Fatalf("identity mount %q missing; mounts=%v", IdentityMountPath, spec.Mounts) + t.Fatalf("system-info mount %q missing; mounts=%v", "/run/ate", spec.Mounts) } } @@ -192,16 +203,6 @@ func TestResolveProcessArgs(t *testing.T) { } } -// Without an identity dir (the pause container), no identity mount appears. -func TestBuildActorOCISpec_NoIdentityMountForPause(t *testing.T) { - bare := buildActorOCISpec("actor_uid", []string{"/pause"}, nil, nil, "/run/netns/x", "", nil, nil) - for _, m := range bare.Mounts { - if m.Destination == IdentityMountPath { - t.Errorf("identity mount must be absent when identityDir is empty") - } - } -} - // Each durable-dir volume mount becomes a bind mount whose source is the // per-actor on-host DurableDirVolumeMountPoint for that volume name. func TestBuildActorOCISpec_DurableDirVolumeMounts(t *testing.T) { @@ -211,14 +212,13 @@ func TestBuildActorOCISpec_DurableDirVolumeMounts(t *testing.T) { {Name: "cache", MountPath: "/var/cache"}, } volumes := []*ateletpb.Volume{ - {Name: "data", Type: ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR}, - {Name: "cache", Type: ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR}, + {Name: "data", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, + {Name: "cache", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, } spec := buildActorOCISpec( actorUID, []string{"/app"}, nil, nil, "/run/netns/x", - "", volumes, durableDirs, ) diff --git a/cmd/atelet/volumes.go b/cmd/atelet/volumes.go index 639bb6e75..492aadc10 100644 --- a/cmd/atelet/volumes.go +++ b/cmd/atelet/volumes.go @@ -31,9 +31,6 @@ import ( func (s *AteomHerder) mountExternalVolumes(ctx context.Context, actorUID string, volumes []*ateletpb.Volume) error { for _, vol := range volumes { - if vol.GetType() != ateletpb.VolumeType_VOLUME_TYPE_EXTERNAL { - continue - } ext := vol.GetExternal() if ext == nil { continue @@ -57,9 +54,6 @@ func (s *AteomHerder) mountExternalVolumes(ctx context.Context, actorUID string, func (s *AteomHerder) unmountExternalVolumes(ctx context.Context, actorUID string, volumes []*ateletpb.Volume) error { var errs []error for _, vol := range volumes { - if vol.GetType() != ateletpb.VolumeType_VOLUME_TYPE_EXTERNAL { - continue - } ext := vol.GetExternal() if ext == nil { continue diff --git a/cmd/atelet/volumes_test.go b/cmd/atelet/volumes_test.go index 6d009a53c..588d1f637 100644 --- a/cmd/atelet/volumes_test.go +++ b/cmd/atelet/volumes_test.go @@ -47,7 +47,6 @@ func TestUnmountExternalVolumes(t *testing.T) { extVol1 := &ateletpb.Volume{ Name: "vol-1", - Type: ateletpb.VolumeType_VOLUME_TYPE_EXTERNAL, Source: &ateletpb.Volume_External{ External: &ateletpb.ExternalVolumeSource{ StorageVolumeId: "mock-vol-1", @@ -57,7 +56,6 @@ func TestUnmountExternalVolumes(t *testing.T) { } extVol2 := &ateletpb.Volume{ Name: "vol-2", - Type: ateletpb.VolumeType_VOLUME_TYPE_EXTERNAL, Source: &ateletpb.Volume_External{ External: &ateletpb.ExternalVolumeSource{ StorageVolumeId: "mock-vol-2", @@ -67,7 +65,9 @@ func TestUnmountExternalVolumes(t *testing.T) { } durableVol := &ateletpb.Volume{ Name: "durable-1", - Type: ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR, + Source: &ateletpb.Volume_DurableDir{ + DurableDir: &ateletpb.DurableDirVolume{}, + }, } t.Run("success", func(t *testing.T) { diff --git a/cmd/ateom-microvm/durable.go b/cmd/ateom-microvm/durable.go index 18f997447..f41c061ff 100644 --- a/cmd/ateom-microvm/durable.go +++ b/cmd/ateom-microvm/durable.go @@ -87,12 +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 it mounts. +// spec, plus a bind for each durable-dir volume (writable), CSI volume, and +// system-info 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 { + if len(c.durableMounts) == 0 && len(c.csiMounts) == 0 && len(c.systemInfoMounts) == 0 { return c.spec } spec := *c.spec @@ -101,6 +102,7 @@ func workloadSpec(c actorContainer) *specs.Spec { mounts = append(mounts, c.spec.Mounts...) mounts = append(mounts, durableMounts(c.durableMounts)...) mounts = append(mounts, csiMounts(c.csiMounts)...) + mounts = append(mounts, systemInfoMounts(c.systemInfoMounts)...) spec.Mounts = mounts return &spec } diff --git a/cmd/ateom-microvm/internal/kata/overlay_linux.go b/cmd/ateom-microvm/internal/kata/overlay_linux.go index 4c7154fe0..68d93a52f 100644 --- a/cmd/ateom-microvm/internal/kata/overlay_linux.go +++ b/cmd/ateom-microvm/internal/kata/overlay_linux.go @@ -50,8 +50,9 @@ const ( virtioFSDriver = "virtio-fs" // guestSharedDir is where the agent mounts the kataShared tag in the guest; // per-container rootfs then lives at //rootfs, durable - // volumes at /durable/, and CSI volumes at - // /csi/. + // volumes at /durable/, CSI volumes at + // /csi/, and system-info volumes at + // /system-info/. guestSharedDir = "/run/kata-containers/shared/containers/" ) @@ -67,6 +68,20 @@ func GuestCSIVolumeDir(volumeName string) string { return guestSharedDir + "csi/" + volumeName } +// GuestSystemInfoVolumeDir is the in-guest path holding one system-info +// volume's contents, i.e. the read-only bind source for that volume's +// container mount points. +func GuestSystemInfoVolumeDir(volumeName string) string { + return guestSharedDir + "system-info/" + volumeName +} + +// GuestSystemInfoVolumeDir is the in-guest path holding one system-info +// volume's contents, i.e. the bind source for that volume's container mount +// points. +func GuestSystemInfoVolumeDir(volumeName string) string { + return guestSystemInfoDir + "/" + volumeName +} + // SharedDir is the host directory virtiofsd serves into the guest as the RO base. // Its layout (/rootfs) is what find-paths re-opens by path on restore. func SharedDir(id string) string { @@ -289,8 +304,8 @@ type CreateSandboxOpts struct { } // CreateSandboxForActor creates the guest sandbox with the kataShared virtio-fs mount -// (the merged rootfs trees, durable volumes, and CSI volumes every container runs on). -// Mirrors kata startSandbox. +// (the merged rootfs trees, durable volumes, CSI volumes, and system-info +// volumes every container runs on). Mirrors kata startSandbox. func (a *AgentClient) CreateSandboxForActor(ctx context.Context, opts CreateSandboxOpts) error { storages := []*agentpb.Storage{{ Driver: virtioFSDriver, diff --git a/cmd/ateom-microvm/restore.go b/cmd/ateom-microvm/restore.go index 3da49c7e1..ab87e403c 100644 --- a/cmd/ateom-microvm/restore.go +++ b/cmd/ateom-microvm/restore.go @@ -271,6 +271,7 @@ func (s *AteomService) restoreFullScope(ctx context.Context, p actorBootParams, tLowers := time.Now() tDurable := tLowers + // Networking: rebuild the per-activation veth + tap; the snapshot's virtio-net // is fd-backed, so CH needs fresh tap FDs (net_fds) on restore. if err := ateomnet.SetupActorNetwork(ctx, ateomnet.NetworkConfig{ diff --git a/cmd/ateom-microvm/run.go b/cmd/ateom-microvm/run.go index 3015fed3b..fd575d7da 100644 --- a/cmd/ateom-microvm/run.go +++ b/cmd/ateom-microvm/run.go @@ -185,6 +185,9 @@ type actorContainer struct { // csiMounts are the CSI volumes this container mounts, and where (see csi.go). // Empty for containers that declare none. csiMounts []*ateompb.VolumeMount + // systemInfoMounts are the system-info volumes this container mounts, and + // where (see systeminfo.go). Empty for containers that declare none. + systemInfoMounts []*ateompb.SystemInfoVolumeMount } // resolvedRuntime holds the concrete binary/config paths for a request, taken @@ -642,11 +645,12 @@ func (s *AteomService) buildActorContainers(actorUID string, containers []*ateom return nil, fmt.Errorf("while writing guest resolv.conf for %q: %w", cn, err) } ctrs[i] = actorContainer{ - name: cn, - bundleRootfs: bundleRootfs, - spec: spec, - durableMounts: c.GetDurableDirVolumeMounts(), - csiMounts: c.GetCsiVolumeMounts(), + name: cn, + bundleRootfs: bundleRootfs, + spec: spec, + durableMounts: c.GetDurableDirVolumeMounts(), + csiMounts: c.GetCsiVolumeMounts(), + systemInfoMounts: c.GetSystemInfoVolumeMounts(), } } return ctrs, nil @@ -654,8 +658,9 @@ func (s *AteomService) buildActorContainers(actorUID string, containers []*ateom // stageMergedRootfs assembles each container's merged rootfs on the host // (overlay: image lower + the actor's rootfs-upper dirs) at virtiofsd's -// find-paths location (SharedDir(id)//rootfs), stages durable-dir volumes -// and CSI volumes (if any) under SharedDir(id)/durable and SharedDir(id)/csi, +// find-paths location (SharedDir(id)//rootfs), stages durable-dir volumes, +// CSI volumes, and system-info volumes (if any) under SharedDir(id)/durable, +// SharedDir(id)/csi, and SharedDir(id)/system-info, // then starts the ONE virtiofsd that serves them all. Must run AFTER CleanupSandboxState (which // wipes SharedDir) and resetRootfsUpperDir/untarRootfsUpper (which own the // upper contents). The returned virtiofsd cmd outlives this call (CH @@ -678,6 +683,11 @@ func (s *AteomService) stageMergedRootfs(ctx context.Context, rr resolvedRuntime return nil, fmt.Errorf("while staging CSI volumes: %w", err) } } + if hasSystemInfoVolumes(containers) { + if err := s.stageSystemInfoVolumes(ctx, id); err != nil { + return nil, fmt.Errorf("while staging system-info volumes: %w", err) + } + } vfsdLog, _ := os.OpenFile(virtiofsdLogPath(id), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600) vfsdCmd, err := kata.StartVirtiofsd(ctx, kata.VirtiofsdOptions{ Binary: rr.virtiofsd, @@ -869,8 +879,9 @@ func earlyconParam() string { } // buildFsConfigs returns the VM's virtio-fs device: the unified share hosting -// container rootfs trees, durable volumes, and CSI volumes. Sits on PCI -// segment 1 (the segment buildVMConfig reserves for virtio-fs). +// container rootfs trees, durable volumes, CSI volumes, and system-info +// volumes. Sits on PCI segment 1 (the segment buildVMConfig reserves for +// virtio-fs). func buildFsConfigs(id string) []ch.FsConfig { return []ch.FsConfig{{ Tag: kata.FsTag, Socket: kata.VirtiofsdSocketPath(id), @@ -884,8 +895,8 @@ func buildFsConfigs(id string) []ch.FsConfig { // container on its own overlay rootfs. On failure it dumps guest diagnostics. func (s *AteomService) startActorContainers(ctx context.Context, ac *kata.AgentClient, id, vsockPath string, ctrs []actorContainer) error { // Establish the agent sandbox + the kataShared virtio-fs mount (every - // container's merged rootfs, durable volumes, and CSI volumes). All containers - // share it, so use the first container's hostname. + // container's merged rootfs, durable volumes, CSI volumes, and system-info + // volumes). All containers share it, so use the first container's hostname. tStart := time.Now() sbCtx, sbCancel := context.WithTimeout(ctx, 20*time.Second) err := ac.CreateSandboxForActor(sbCtx, kata.CreateSandboxOpts{ diff --git a/cmd/ateom-microvm/spec.go b/cmd/ateom-microvm/spec.go index 10617212c..a98f28b58 100644 --- a/cmd/ateom-microvm/spec.go +++ b/cmd/ateom-microvm/spec.go @@ -95,12 +95,12 @@ func ensureKataCompatibleSpec(bundle, id, netnsPath string, size sizing.SandboxS // the exact set `ctr run --runtime io.containerd.kata.v2` emits, which kata's // agent accepts. (Static shaper; pod DNS integration is future work.) // - // KNOWN GAP vs the gVisor runtime: this also drops atelet's read-only actor - // identity bind mount (/run/ate/actor-id). The micro-VM guest can't see - // arbitrary host paths (it sees only the virtio-fs shares), so atelet's - // host-path identity mount has nothing to bind to. - // Exposing the identity needs a per-actor volume plumbed into the guest; not yet - // implemented. No micro-VM workload depends on it today. + // Dropping atelet's volume bind mounts here is fine: host-path binds can't + // attach inside the guest anyway. Volumes reach micro-VM containers over + // per-actor virtio-fs shares instead — durable-dir volumes via the + // writable share (durable.go) and system-info volumes via the read-only + // share (systeminfo.go) — with the binds added to the workload specs ateom + // drives through the kata-agent (see workloadSpec). spec.Mounts = defaultKataMounts() out, err := json.MarshalIndent(&spec, "", " ") diff --git a/cmd/ateom-microvm/systeminfo.go b/cmd/ateom-microvm/systeminfo.go new file mode 100644 index 000000000..ac89094e6 --- /dev/null +++ b/cmd/ateom-microvm/systeminfo.go @@ -0,0 +1,115 @@ +//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. + +// System-info volume support for the micro-VM runtime. +// +// A system-info volume is a read-only directory of files generated by atelet +// on the host on every Run/Restore (e.g. the actorMetadata data-source files), +// so its contents always describe the actor actually being started, whatever +// checkpointed state it boots from. The host side is owned by atelet, which +// creates one directory per volume under +// ateompath.SystemInfoVolumeRootsDir(actorUID) and wipes/rebuilds them when +// the actor's directories are reset. +// +// ateom exposes that host directory to the guest under the single kataShared +// virtio-fs share at SharedDir(actorUID)/system-info (in-guest path: +// kata.GuestSystemInfoVolumeDir(volume)), like durable-dir and CSI volumes — +// no extra virtio-fs device. Read-only is enforced twice: the host bind is +// remounted read-only (so nothing in the guest can write through the share), +// and each container bind adds "ro" (so the workload can't write the mount). +// +// Unlike durable-dir volumes, system-info volumes are deliberately absent +// from the checkpoint path: their contents must never be captured into +// snapshots (see the SystemInfo semantics in docs/api-guide.md). Regeneration +// is safe under find-paths migration because atelet rewrites the files at +// their stable share-relative paths before the share's virtiofsd starts. + +package main + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/kata" + "github.com/agent-substrate/substrate/cmd/ateom-microvm/internal/reaper" + "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/internal/proto/ateompb" + specs "github.com/opencontainers/runtime-spec/specs-go" +) + +// hasSystemInfoVolumes reports whether any container mounts a system-info +// volume. +func hasSystemInfoVolumes(containers []*ateompb.Container) bool { + for _, c := range containers { + if len(c.GetSystemInfoVolumeMounts()) > 0 { + return true + } + } + return false +} + +// systemInfoMounts returns the OCI mounts that expose a container's +// system-info volumes at the paths it declared, read-only. Each source is that +// volume's directory inside the guest's shared tree, which the agent mounts at +// sandbox creation. +func systemInfoMounts(mounts []*ateompb.SystemInfoVolumeMount) []specs.Mount { + out := make([]specs.Mount, 0, len(mounts)) + for _, m := range mounts { + out = append(out, specs.Mount{ + Destination: m.GetMountPath(), + Source: kata.GuestSystemInfoVolumeDir(m.GetVolumeName()), + Type: "bind", + Options: []string{"rbind", "ro"}, + }) + } + return out +} + +// stageSystemInfoVolumes bind-mounts the actor's host system-info directory +// into the sandbox's shared virtio-fs tree at SharedDir(actorUID)/system-info, +// then remounts the bind read-only: atelet is the only writer, and it writes +// the host source directly, never through the share. +func (s *AteomService) stageSystemInfoVolumes(ctx context.Context, actorUID string) error { + src := ateompath.SystemInfoVolumeRootsDir(actorUID) + if _, err := os.Stat(src); err != nil { + return fmt.Errorf("while checking system-info volumes dir %q: %w", src, err) + } + dst := filepath.Join(kata.SharedDir(actorUID), "system-info") + // Drop any stale mount first (lazy if busy), then ensure clean mountpoint. + 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 %q: %w", dst, err) + } + cmd := exec.CommandContext(ctx, "mount", "--bind", src, dst) + var stderr strings.Builder + cmd.Stderr = &stderr + if err := reaper.Run(cmd); err != nil { + return fmt.Errorf("bind-mounting system-info volumes at %q: %w (%s)", 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 system-info volumes read-only %q: %w (%s)", dst, err, strings.TrimSpace(roErr.String())) + } + return nil +} diff --git a/docs/api-guide.md b/docs/api-guide.md index e4ce87a2a..46fc005a2 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -151,7 +151,7 @@ The `ActorTemplate` defines the code, environment, and state-management policies | `sandboxClass` | `string` | Optional. The sandbox runtime family this template's actors require: `gvisor` (default) or `microvm`. Only `WorkerPool`s whose `sandboxClass` matches are eligible. | | `workerSelector` | `*LabelSelector` | Optional. Gates which `WorkerPool`s actors from this template may use, by matching against each pool's labels. If unset, all pools are eligible (subject to the actor's own `worker_selector`). | | `snapshotsConfig` | `SnapshotsConfig` | **Required.** The base object-storage location snapshots are written under, plus the pause/commit/resume scopes. See [Snapshot Storage Layout](#snapshot-storage-layout). | -| `volumes` | `[]Volume` | Optional. Volumes the containers may mount, each either a `durableDir` or an `externalVolumeTemplate`. Every declared volume must be mounted by at least one container. A `microvm` template may declare several `durableDir` volumes; a `gvisor` template is limited to one, and `externalVolumeTemplate` is `gvisor`-only. | +| `volumes` | `[]Volume` | Optional. Volumes the containers may mount, each a `durableDir`, an `externalVolumeTemplate`, or a `systemInfo` volume (see [SystemInfo Volumes](#systeminfo-volumes)). Every declared volume must be mounted by at least one container. A `microvm` template may declare several `durableDir` volumes; a `gvisor` template is limited to one, and `externalVolumeTemplate` is `gvisor`-only. | | `resources` | `*ResourceRequirements` | Optional. Declares each actor's compute size via `limits` — see [Sandbox Right-Sizing](#sandbox-right-sizing-specresources). Immutable, like the rest of the spec. | The sandbox itself — the binaries (e.g. the gVisor `runsc` binary) and the `pauseImage` holding the sandbox's namespaces — is **not configured on the `ActorTemplate`**. It is resolved from the referenced `WorkerPool`'s [`SandboxConfig`](#3-sandboxconfig-the-sandbox-itself) — by name (`workerPool.spec.sandboxConfigName`) or, by default, the cluster default `SandboxConfig` for the pool's `sandboxClass`. @@ -177,10 +177,38 @@ Substrate uses a **Uniform DNS Mesh**: every actor created from a template is au **Format:** `..actors.resources.substrate.ate.dev` -### Actor Identity -Substrate bind-mounts a read-only, per-actor identity directory at **`/run/ate`** into each of the actor's containers. An actor can learn its own name without parsing the `Host` header by reading the file **`/run/ate/actor-id`** inside it, which contains the raw actor name with no trailing newline. Further identity and configuration data may appear in this directory over time. +### SystemInfo Volumes -Read it fresh rather than caching it at process start. It is delivered as a per-actor bind mount, not an environment variable, precisely so it carries the correct name after a resume from the golden snapshot — an env var (or a file baked into the image) would be frozen at the *golden* actor's name, since it lives in the checkpointed process memory, and would therefore be identical for every actor of the template. +To deliver identity information, including credentials, to a running actor, you can use a SystemInfo volume. Define it in `spec.volumes`, and mount it into each container that needs it. + +Available information sources: + +#### actorMetadata +The actorMetadata data source projects the actor's identity fields to files, one per item, analogous to the [Kubernetes downwardAPI volume](https://kubernetes.io/docs/concepts/storage/downward-api/). Each item selects a `field` — `name` (unique within an atespace), `atespace` (together with the name, the actor's full identity and DNS name), or `uid` (server-generated, distinguishes incarnations of the same name) — and the relative `path` the value is written to, raw with no trailing newline. + +```yaml +spec: + volumes: + - name: system-info + systemInfo: + dataSources: + - actorMetadata: + items: + - field: name + path: actor-name + - field: atespace + path: atespace + - field: uid + path: actor-uid + containers: + - name: main + # ... + volumeMounts: + - name: system-info + mountPath: /run/ate # the actor reads e.g. /run/ate/actor-name +``` + +The values are delivered as files on a read-only per-actor bind mount, not environment variables, precisely so they carry the correct values after a resume from a shared snapshot — an env var (or a file baked into the image) would be frozen at the snapshot-source actor's values, since it lives in the checkpointed process memory, and would therefore be identical for every actor restored from that snapshot. The metadata fields themselves are fixed for the actor's lifetime, so workloads may cache them; future data sources that rotate (identity tokens and certificates) must be re-read at time of use. ### Container Fields @@ -391,7 +419,7 @@ Query the physical resource pool. ## 7. Advanced: Actor Identity Credentials -Workloads can exchange their ephemeral Kubernetes credentials for stable **Actor Identity** credentials that persist even as the process migrates between different physical workers. This is distinct from the `/run/ate/actor-id` bind mount described under [Actor Identity](#actor-identity), which only tells an actor its own name. +Workloads can exchange their ephemeral Kubernetes credentials for stable **Actor Identity** credentials that persist even as the process migrates between different physical workers. This is distinct from the `actorMetadata` data source described under [SystemInfo Volumes](#systeminfo-volumes), which only tells an actor its own identity fields (name, atespace, uid). ### Service: `ateapi.ActorIdentity` * **`MintJWT`:** Generates an OIDC-compatible JWT identifying the Substrate Actor. diff --git a/internal/ateompath/ateompath.go b/internal/ateompath/ateompath.go index bd715695c..ffe2e0764 100644 --- a/internal/ateompath/ateompath.go +++ b/internal/ateompath/ateompath.go @@ -113,18 +113,6 @@ func ActorPath(actorUID string) string { ) } -// ActorIdentityDirPath is the host directory atelet populates with the -// actor's identity data (currently the single file "actor-id") and -// bind-mounts read-only into the actor. It is per-actor and regenerated on -// every resume, so (unlike the checkpointed process environment) it reflects -// the correct ID after a restore from the golden snapshot. -func ActorIdentityDirPath(actorUID string) string { - return filepath.Join( - ActorPath(actorUID), - "identity", - ) -} - // ActorSandboxAssetsFile is the per-actor file where atelet records the sandbox // binaries (class + content-addressed asset set, for this node's architecture) // the actor is currently running. It is written at Run/Restore and read at @@ -211,6 +199,37 @@ func DurableDirVolumeMountPoint(actorUID, volumeName string) string { ) } +// SystemInfoVolumeRootsDir is the directory containing the per-volume root +// directories of system-info volumes. Snapshots must capture durable-dir +// data but never system-info contents, which atelet regenerates on every +// Run/Restore; each sandbox class excludes them differently: +// +// - micro-VM captures by location: its checkpoint tars all of +// DurableDirVolumeMountsDir (see ateom-microvm's tarDurableVolumes), so +// system-info roots are excluded by living in this separate directory. +// - gVisor captures by declaration: durable mounts are registered with +// the sandbox (mount-hint annotations for FULL checkpoints, the +// enumerated durable mount paths for DATA fscheckpoints); system-info +// mounts are plain undeclared binds, never captured regardless of host +// layout. +// +// The separate directory is therefore critical only for micro-VM. +func SystemInfoVolumeRootsDir(actorUID string) string { + return filepath.Join( + ActorPath(actorUID), + "system-info", + ) +} + +// SystemInfoVolumeRoot returns the host path of the root directory for a +// specific system-info volume. +func SystemInfoVolumeRoot(actorUID, volumeName string) string { + return filepath.Join( + SystemInfoVolumeRootsDir(actorUID), + volumeName, + ) +} + // RestoreStateDir is the local directory to use to restore an actor from a // checkpoint downloaded from GCS. // diff --git a/internal/e2e/fixtures/probe/main.go b/internal/e2e/fixtures/probe/main.go index ec2192bea..6f47c1abc 100644 --- a/internal/e2e/fixtures/probe/main.go +++ b/internal/e2e/fixtures/probe/main.go @@ -23,6 +23,7 @@ package main import ( "bufio" "encoding/json" + "io" "log" "net/http" "os" @@ -31,9 +32,22 @@ import ( "strings" ) -// identityFile is the actor-id file inside the identity directory atelet -// bind-mounts at IdentityMountPath. -const identityFile = "/run/ate/actor-id" +// The actorMetadata data-source files of the systemInfo volume that +// probe.yaml.tmpl mounts at /run/ate. +const ( + identityFile = "/run/ate/actor-id" + atespaceFile = "/run/ate/atespace" + uidFile = "/run/ate/actor-uid" +) + +// heldIdentity is identityFile opened at startup and held open for the +// probe's whole life, deliberately violating the read-at-time-of-use +// guidance. It exists so a snapshot taken after startup carries live guest +// file state for a system-info file, and a restore must re-bind it (virtiofsd +// find-paths / gofer re-open by path). whoami reads through it on every +// request; after a restore from a shared golden snapshot the read must +// succeed and yield the restored actor's own id, not the golden's. +var heldIdentity *os.File // whoami reports the actor's identity as observed at request time from the // bind-mounted identity file. A read failure is reported in the response @@ -42,16 +56,43 @@ func whoami(w http.ResponseWriter, _ *http.Request) { host, _ := os.Hostname() resp := map[string]string{"hostname": host} - if b, err := os.ReadFile(identityFile); err == nil { - resp["file"] = string(b) + for key, path := range map[string]string{ + "file": identityFile, + "atespace": atespaceFile, + "uid": uidFile, + } { + if b, err := os.ReadFile(path); err == nil { + resp[key] = string(b) + } else { + resp[key] = "" + // Concatenate: a failed assertion should explain every missing file. + resp["error"] += err.Error() + "; " + } + } + + resp["held"] = "" + if heldIdentity == nil { + resp["error"] += "identity file was not open at startup; " + } else if b, err := readAllAt(heldIdentity); err == nil { + resp["held"] = string(b) } else { - resp["file"] = "" - resp["error"] = err.Error() + resp["error"] += "reading held identity fd: " + err.Error() + "; " } 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) { + buf := make([]byte, 4096) + n, err := f.ReadAt(buf, 0) + if err != nil && err != io.EOF { + return nil, err + } + return buf[:n], nil +} + // resources reports the compute envelope the actor observes from inside the // sandbox, so the sizing e2e suite can assert the actor's declared limits // actually shaped the runtime. @@ -120,6 +161,15 @@ func writeJSON(w http.ResponseWriter, v any) { } func main() { + // Hold the identity file open before serving: every snapshot of this actor + // then contains an open guest handle on a system-info file (see + // heldIdentity). + if f, err := os.Open(identityFile); err == nil { + heldIdentity = f + } else { + log.Printf("probe: opening %s at startup: %v", identityFile, err) + } + mux := http.NewServeMux() mux.HandleFunc("/whoami", whoami) mux.HandleFunc("/resources", resources) diff --git a/internal/e2e/fixtures/probe/probe.yaml.tmpl b/internal/e2e/fixtures/probe/probe.yaml.tmpl index a22bf12df..6bf31e64b 100644 --- a/internal/e2e/fixtures/probe/probe.yaml.tmpl +++ b/internal/e2e/fixtures/probe/probe.yaml.tmpl @@ -45,10 +45,25 @@ metadata: namespace: ate-e2e-probe${FIXTURE_SUFFIX} spec: ${TEMPLATE_SANDBOX_CLASS} + volumes: + - name: system-info + systemInfo: + dataSources: + - actorMetadata: + items: + - field: name + path: actor-id + - field: atespace + path: atespace + - field: uid + path: actor-uid containers: - name: probe image: ko://github.com/agent-substrate/substrate/internal/e2e/fixtures/probe command: ["/ko-app/probe"] + volumeMounts: + - name: system-info + mountPath: /run/ate # the probe reads /run/ate/actor-id # The probe binary binds :80 immediately, so this gates actor start on a # readiness signal rather than a guess, and carries a non-default # timeoutSeconds so e2e covers the value crossing ateapi -> atelet -> ateom diff --git a/internal/e2e/suites/identity/identity_test.go b/internal/e2e/suites/identity/identity_test.go index c454abd74..a62c58f8d 100644 --- a/internal/e2e/suites/identity/identity_test.go +++ b/internal/e2e/suites/identity/identity_test.go @@ -40,9 +40,16 @@ var probeNamespace = e2e.FixtureName("ate-e2e-probe") type whoamiResponse struct { File string `json:"file"` + Atespace string `json:"atespace"` + UID string `json:"uid"` Hostname string `json:"hostname"` - // Error is the probe's identity-file read error, if any, so a failed - // assertion explains why the ID was missing. + // Held is the actor id read through a file descriptor the probe opened at + // startup and holds across checkpoints — the snapshot therefore carries an + // open guest handle on a system-info file, and restore must re-bind it to + // the regenerated file (virtiofsd find-paths / gofer re-open by path). + Held string `json:"held"` + // Error is the probe's file read error(s), if any, so a failed assertion + // explains why a value was missing. Error string `json:"error"` } @@ -52,19 +59,17 @@ type whoamiResponse struct { // snapshot all reported the golden actor's ID. This test catches that by // restoring TWO actors from one golden snapshot and asserting each observes its // OWN id — and explicitly that it is not the golden id. +// +// It then suspends and resumes one of them: atelet wipes and regenerates the +// system-info files between suspend and resume, so the suspend-time guest +// state (the probe's startup-held fd, plus every inode the pre-suspend whoami +// indexed) must re-bind to the regenerated files at the same paths. The +// micro-VM lane enforces that the hardest: virtiofsd's find-paths migration +// re-opens recorded paths on restore and its default --migration-on-error=abort +// fails the resume outright if any path moved — a write scheme that relocates +// real files (e.g. a timestamped-directory symlink swap) would make any actor +// that ever touched a system-info file unable to resume. func TestActorIdentity_AfterRestore_IsOwnID_NotGolden(t *testing.T) { - // The micro-VM runtime does not expose the identity file yet. ateom-microvm - // replaces atelet's mount set with the one the kata agent accepts, and drops - // atelet's read-only /run/ate/actor-id bind with it: the guest sees only the - // virtio-fs shares, so a host-path bind has nothing to bind to. Exposing it - // needs a per-actor volume plumbed into the guest — see the KNOWN GAP comment - // in cmd/ateom-microvm/spec.go. Running this against micro-VM reports the - // probe reading an empty ID, which is that gap and not a regression, so skip - // until the gap closes rather than encode it as expected behavior. - if e2e.IsMicroVM() { - t.Skip("micro-VM does not mount /run/ate/actor-id yet (KNOWN GAP in cmd/ateom-microvm/spec.go)") - } - env, err := e2e.CheckEnv("BUCKET_NAME", "KO_DOCKER_REPO") if err != nil { t.Fatalf("CheckEnv failed: %v", err) @@ -88,6 +93,7 @@ func TestActorIdentity_AfterRestore_IsOwnID_NotGolden(t *testing.T) { defer rc.Close() seen := map[string]string{} + seenUIDs := map[string]string{} for _, id := range ids { got := whoami(t, ctx, rc, id) @@ -101,7 +107,92 @@ func TestActorIdentity_AfterRestore_IsOwnID_NotGolden(t *testing.T) { t.Errorf("actor %q and %q both report identity %q — actors are not distinct", id, other, got.File) } seen[got.File] = id + + // The fd held open since before the golden snapshot must survive the + // restore and read the restored actor's OWN id: system-info files are + // regenerated at stable paths precisely so suspend-time guest handles + // re-bind (a moved or deleted path would fail the restore or the read). + if got.Held != id { + t.Errorf("actor %q: id via startup-held fd = %q, want %q (probe read error: %q)", id, got.Held, id, got.Error) + } + + if got.Atespace != probeNamespace { + t.Errorf("actor %q: /run/ate/atespace = %q, want %q (probe read error: %q)", id, got.Atespace, probeNamespace, got.Error) + } + + // The projected UID must match the control plane's authoritative view + // of this actor, and be distinct per actor even though both actors + // were seeded from the same golden snapshot. + actor, err := clients.SubstrateAPI.GetActor(ctx, &ateapipb.GetActorRequest{Actor: &ateapipb.ObjectRef{Atespace: probeNamespace, Name: id}}) + if err != nil { + t.Fatalf("GetActor %q: %v", id, err) + } + if wantUID := actor.GetMetadata().GetUid(); got.UID != wantUID { + t.Errorf("actor %q: /run/ate/actor-uid = %q, want %q (probe read error: %q)", id, got.UID, wantUID, got.Error) + } + if other, dup := seenUIDs[got.UID]; dup { + t.Errorf("actor %q and %q both report uid %q — actors are not distinct", id, other, got.UID) + } + seenUIDs[got.UID] = id + } + + // Full suspend/resume cycle of one actor (see the doc comment): the whoami + // calls above deliberately seeded the guest state a suspend records — the + // held fd from probe startup plus the freshly indexed file inodes — and the + // resume regenerates every file underneath that state. + id := ids[0] + ref := &ateapipb.ObjectRef{Atespace: probeNamespace, Name: id} + if _, err := clients.SubstrateAPI.SuspendActor(ctx, &ateapipb.SuspendActorRequest{Actor: ref}); err != nil { + t.Fatalf("SuspendActor %q: %v", id, err) + } + waitForActorState(t, ctx, clients, id, ateapipb.ActorState_ACTOR_STATE_SUSPENDED) + if _, err := clients.SubstrateAPI.ResumeActor(ctx, &ateapipb.ResumeActorRequest{Actor: ref}); err != nil { + t.Fatalf("ResumeActor %q (after suspend): %v", id, err) + } + waitForActorState(t, ctx, clients, id, ateapipb.ActorState_ACTOR_STATE_RUNNING) + + got := whoami(t, ctx, rc, id) + if got.File != id { + t.Errorf("after suspend/resume: /run/ate/actor-id = %q, want %q (probe read error: %q)", got.File, id, got.Error) } + if got.Held != id { + t.Errorf("after suspend/resume: id via startup-held fd = %q, want %q (probe read error: %q)", got.Held, id, got.Error) + } + if got.Atespace != probeNamespace { + t.Errorf("after suspend/resume: /run/ate/atespace = %q, want %q (probe read error: %q)", got.Atespace, probeNamespace, got.Error) + } + if wantUID := seenUIDFor(t, seenUIDs, id); got.UID != wantUID { + t.Errorf("after suspend/resume: /run/ate/actor-uid = %q, want %q (probe read error: %q)", got.UID, wantUID, got.Error) + } +} + +// seenUIDFor returns the UID recorded for actor id in the first phase of the +// test, so the post-resume assertion checks against the same authoritative +// value rather than a fresh lookup that could mask a UID change. +func seenUIDFor(t *testing.T, seenUIDs map[string]string, id string) string { + t.Helper() + for uid, actor := range seenUIDs { + if actor == id { + return uid + } + } + t.Fatalf("no UID recorded for actor %q", id) + return "" +} + +func waitForActorState(t *testing.T, ctx context.Context, clients *e2e.Clients, actorName string, want ateapipb.ActorState) { + t.Helper() + deadline := time.Now().Add(60 * time.Second) + for time.Now().Before(deadline) { + resp, err := clients.SubstrateAPI.GetActor(ctx, &ateapipb.GetActorRequest{ + Actor: &ateapipb.ObjectRef{Atespace: probeNamespace, Name: actorName}, + }) + if err == nil && resp.GetStatus().GetState() == want { + return + } + time.Sleep(1 * time.Second) + } + t.Fatalf("timed out waiting for actor %q to reach state %v", actorName, want) } func deployProbe(t *testing.T, bucket string) { @@ -167,6 +258,13 @@ func createAndResumeActor(t *testing.T, ctx context.Context, clients *e2e.Client t.Helper() // CreateActor requires the atespace to exist first. _, _ = clients.SubstrateAPI.CreateAtespace(ctx, &ateapipb.CreateAtespaceRequest{Atespace: &ateapipb.Atespace{Metadata: &ateapipb.ResourceMetadata{Name: probeNamespace}}}) + ref := &ateapipb.ObjectRef{Atespace: probeNamespace, Name: id} + // The actor record lives in the ateapi store and outlives the fixture + // namespace, so a failed prior run can leak it and wedge every rerun on + // AlreadyExists. Best-effort clear it before creating (DeleteActor + // requires SUSPENDED or CRASHED, hence the suspend first). + _, _ = clients.SubstrateAPI.SuspendActor(ctx, &ateapipb.SuspendActorRequest{Actor: ref}) + _, _ = clients.SubstrateAPI.DeleteActor(ctx, &ateapipb.DeleteActorRequest{Actor: ref}) if _, err := clients.SubstrateAPI.CreateActor(ctx, &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{ Metadata: &ateapipb.ResourceMetadata{Atespace: probeNamespace, Name: id}, ActorTemplateNamespace: probeNamespace, @@ -175,9 +273,13 @@ func createAndResumeActor(t *testing.T, ctx context.Context, clients *e2e.Client t.Fatalf("CreateActor %q: %v", id, err) } t.Cleanup(func() { - // DeleteActor requires the actor to be suspended. - _, _ = clients.SubstrateAPI.SuspendActor(ctx, &ateapipb.SuspendActorRequest{Actor: &ateapipb.ObjectRef{Atespace: probeNamespace, Name: id}}) - _, _ = clients.SubstrateAPI.DeleteActor(ctx, &ateapipb.DeleteActorRequest{Actor: &ateapipb.ObjectRef{Atespace: probeNamespace, Name: id}}) + // Suspend is best-effort: the actor may already be suspended, or may + // never have resumed. A failed delete is only logged — the pre-create + // clear above keeps the next run working regardless. + _, _ = clients.SubstrateAPI.SuspendActor(ctx, &ateapipb.SuspendActorRequest{Actor: ref}) + if _, err := clients.SubstrateAPI.DeleteActor(ctx, &ateapipb.DeleteActorRequest{Actor: ref}); err != nil { + t.Logf("cleanup: DeleteActor %q failed, actor leaked (remove with: kubectl ate delete actor %s -a %s): %v", id, id, probeNamespace, err) + } }) // Resume from the golden snapshot (the restore path, not --boot). diff --git a/internal/proto/ateletpb/atelet.pb.go b/internal/proto/ateletpb/atelet.pb.go index dab3709fb..154d7d816 100644 --- a/internal/proto/ateletpb/atelet.pb.go +++ b/internal/proto/ateletpb/atelet.pb.go @@ -35,52 +35,56 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -type VolumeType int32 +// ActorMetadataField selects one identity field of the actor. +type ActorMetadataField int32 const ( - VolumeType_VOLUME_TYPE_UNSPECIFIED VolumeType = 0 - VolumeType_VOLUME_TYPE_DURABLE_DIR VolumeType = 1 - VolumeType_VOLUME_TYPE_EXTERNAL VolumeType = 2 + ActorMetadataField_ACTOR_METADATA_FIELD_UNSPECIFIED ActorMetadataField = 0 + ActorMetadataField_ACTOR_METADATA_FIELD_NAME ActorMetadataField = 1 + ActorMetadataField_ACTOR_METADATA_FIELD_ATESPACE ActorMetadataField = 2 + ActorMetadataField_ACTOR_METADATA_FIELD_UID ActorMetadataField = 3 ) -// Enum value maps for VolumeType. +// Enum value maps for ActorMetadataField. var ( - VolumeType_name = map[int32]string{ - 0: "VOLUME_TYPE_UNSPECIFIED", - 1: "VOLUME_TYPE_DURABLE_DIR", - 2: "VOLUME_TYPE_EXTERNAL", - } - VolumeType_value = map[string]int32{ - "VOLUME_TYPE_UNSPECIFIED": 0, - "VOLUME_TYPE_DURABLE_DIR": 1, - "VOLUME_TYPE_EXTERNAL": 2, + ActorMetadataField_name = map[int32]string{ + 0: "ACTOR_METADATA_FIELD_UNSPECIFIED", + 1: "ACTOR_METADATA_FIELD_NAME", + 2: "ACTOR_METADATA_FIELD_ATESPACE", + 3: "ACTOR_METADATA_FIELD_UID", + } + ActorMetadataField_value = map[string]int32{ + "ACTOR_METADATA_FIELD_UNSPECIFIED": 0, + "ACTOR_METADATA_FIELD_NAME": 1, + "ACTOR_METADATA_FIELD_ATESPACE": 2, + "ACTOR_METADATA_FIELD_UID": 3, } ) -func (x VolumeType) Enum() *VolumeType { - p := new(VolumeType) +func (x ActorMetadataField) Enum() *ActorMetadataField { + p := new(ActorMetadataField) *p = x return p } -func (x VolumeType) String() string { +func (x ActorMetadataField) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } -func (VolumeType) Descriptor() protoreflect.EnumDescriptor { +func (ActorMetadataField) Descriptor() protoreflect.EnumDescriptor { return file_atelet_proto_enumTypes[0].Descriptor() } -func (VolumeType) Type() protoreflect.EnumType { +func (ActorMetadataField) Type() protoreflect.EnumType { return &file_atelet_proto_enumTypes[0] } -func (x VolumeType) Number() protoreflect.EnumNumber { +func (x ActorMetadataField) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } -// Deprecated: Use VolumeType.Descriptor instead. -func (VolumeType) EnumDescriptor() ([]byte, []int) { +// Deprecated: Use ActorMetadataField.Descriptor instead. +func (ActorMetadataField) EnumDescriptor() ([]byte, []int) { return file_atelet_proto_rawDescGZIP(), []int{0} } @@ -799,14 +803,227 @@ func (x *ExternalVolumeSource) GetVolumeContext() map[string]string { return nil } +// 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 { + state protoimpl.MessageState `protogen:"open.v1"` + Field ActorMetadataField `protobuf:"varint,1,opt,name=field,proto3,enum=atelet.ActorMetadataField" json:"field,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ActorMetadataItem) Reset() { + *x = ActorMetadataItem{} + mi := &file_atelet_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ActorMetadataItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ActorMetadataItem) ProtoMessage() {} + +func (x *ActorMetadataItem) 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 ActorMetadataItem.ProtoReflect.Descriptor instead. +func (*ActorMetadataItem) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{10} +} + +func (x *ActorMetadataItem) GetField() ActorMetadataField { + if x != nil { + return x.Field + } + return ActorMetadataField_ACTOR_METADATA_FIELD_UNSPECIFIED +} + +func (x *ActorMetadataItem) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +// ActorMetadataDataSource projects the actor's identity fields to files, one +// per item. Values are written raw with no trailing newline. +type ActorMetadataDataSource struct { + state protoimpl.MessageState `protogen:"open.v1"` + Items []*ActorMetadataItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ActorMetadataDataSource) Reset() { + *x = ActorMetadataDataSource{} + mi := &file_atelet_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ActorMetadataDataSource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ActorMetadataDataSource) ProtoMessage() {} + +func (x *ActorMetadataDataSource) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[11] + 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 ActorMetadataDataSource.ProtoReflect.Descriptor instead. +func (*ActorMetadataDataSource) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{11} +} + +func (x *ActorMetadataDataSource) GetItems() []*ActorMetadataItem { + if x != nil { + return x.Items + } + return nil +} + +type SystemInfoDataSource struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to DataSource: + // + // *SystemInfoDataSource_ActorMetadata + DataSource isSystemInfoDataSource_DataSource `protobuf_oneof:"data_source"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SystemInfoDataSource) Reset() { + *x = SystemInfoDataSource{} + mi := &file_atelet_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SystemInfoDataSource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SystemInfoDataSource) ProtoMessage() {} + +func (x *SystemInfoDataSource) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[12] + 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 SystemInfoDataSource.ProtoReflect.Descriptor instead. +func (*SystemInfoDataSource) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{12} +} + +func (x *SystemInfoDataSource) GetDataSource() isSystemInfoDataSource_DataSource { + if x != nil { + return x.DataSource + } + return nil +} + +func (x *SystemInfoDataSource) GetActorMetadata() *ActorMetadataDataSource { + if x != nil { + if x, ok := x.DataSource.(*SystemInfoDataSource_ActorMetadata); ok { + return x.ActorMetadata + } + } + return nil +} + +type isSystemInfoDataSource_DataSource interface { + isSystemInfoDataSource_DataSource() +} + +type SystemInfoDataSource_ActorMetadata struct { + ActorMetadata *ActorMetadataDataSource `protobuf:"bytes,1,opt,name=actor_metadata,json=actorMetadata,proto3,oneof"` +} + +func (*SystemInfoDataSource_ActorMetadata) isSystemInfoDataSource_DataSource() {} + +// SystemInfoVolume is a read-only volume whose files are generated by atelet +// on every Run/Restore, so they carry the values of the actor actually being +// started, whatever checkpointed state it boots from. +type SystemInfoVolume struct { + state protoimpl.MessageState `protogen:"open.v1"` + DataSources []*SystemInfoDataSource `protobuf:"bytes,1,rep,name=data_sources,json=dataSources,proto3" json:"data_sources,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SystemInfoVolume) Reset() { + *x = SystemInfoVolume{} + mi := &file_atelet_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SystemInfoVolume) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SystemInfoVolume) ProtoMessage() {} + +func (x *SystemInfoVolume) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[13] + 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 SystemInfoVolume.ProtoReflect.Descriptor instead. +func (*SystemInfoVolume) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{13} +} + +func (x *SystemInfoVolume) GetDataSources() []*SystemInfoDataSource { + if x != nil { + return x.DataSources + } + return nil +} + type Volume struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Type VolumeType `protobuf:"varint,2,opt,name=type,proto3,enum=atelet.VolumeType" json:"type,omitempty"` // Types that are valid to be assigned to Source: // // *Volume_DurableDir // *Volume_External + // *Volume_SystemInfo Source isVolume_Source `protobuf_oneof:"source"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -814,7 +1031,7 @@ type Volume struct { func (x *Volume) Reset() { *x = Volume{} - mi := &file_atelet_proto_msgTypes[10] + mi := &file_atelet_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -826,7 +1043,7 @@ func (x *Volume) String() string { func (*Volume) ProtoMessage() {} func (x *Volume) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[10] + mi := &file_atelet_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -839,7 +1056,7 @@ func (x *Volume) ProtoReflect() protoreflect.Message { // Deprecated: Use Volume.ProtoReflect.Descriptor instead. func (*Volume) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{10} + return file_atelet_proto_rawDescGZIP(), []int{14} } func (x *Volume) GetName() string { @@ -849,13 +1066,6 @@ func (x *Volume) GetName() string { return "" } -func (x *Volume) GetType() VolumeType { - if x != nil { - return x.Type - } - return VolumeType_VOLUME_TYPE_UNSPECIFIED -} - func (x *Volume) GetSource() isVolume_Source { if x != nil { return x.Source @@ -881,22 +1091,37 @@ func (x *Volume) GetExternal() *ExternalVolumeSource { return nil } +func (x *Volume) GetSystemInfo() *SystemInfoVolume { + if x != nil { + if x, ok := x.Source.(*Volume_SystemInfo); ok { + return x.SystemInfo + } + } + return nil +} + type isVolume_Source interface { isVolume_Source() } type Volume_DurableDir struct { - DurableDir *DurableDirVolume `protobuf:"bytes,3,opt,name=durable_dir,json=durableDir,proto3,oneof"` + DurableDir *DurableDirVolume `protobuf:"bytes,2,opt,name=durable_dir,json=durableDir,proto3,oneof"` } type Volume_External struct { - External *ExternalVolumeSource `protobuf:"bytes,4,opt,name=external,proto3,oneof"` + External *ExternalVolumeSource `protobuf:"bytes,3,opt,name=external,proto3,oneof"` +} + +type Volume_SystemInfo struct { + SystemInfo *SystemInfoVolume `protobuf:"bytes,4,opt,name=system_info,json=systemInfo,proto3,oneof"` } func (*Volume_DurableDir) isVolume_Source() {} func (*Volume_External) isVolume_Source() {} +func (*Volume_SystemInfo) isVolume_Source() {} + type VolumeMount struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -907,7 +1132,7 @@ type VolumeMount struct { func (x *VolumeMount) Reset() { *x = VolumeMount{} - mi := &file_atelet_proto_msgTypes[11] + mi := &file_atelet_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -919,7 +1144,7 @@ func (x *VolumeMount) String() string { func (*VolumeMount) ProtoMessage() {} func (x *VolumeMount) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[11] + mi := &file_atelet_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -932,7 +1157,7 @@ func (x *VolumeMount) ProtoReflect() protoreflect.Message { // Deprecated: Use VolumeMount.ProtoReflect.Descriptor instead. func (*VolumeMount) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{11} + return file_atelet_proto_rawDescGZIP(), []int{15} } func (x *VolumeMount) GetName() string { @@ -964,7 +1189,7 @@ type Container struct { func (x *Container) Reset() { *x = Container{} - mi := &file_atelet_proto_msgTypes[12] + mi := &file_atelet_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -976,7 +1201,7 @@ func (x *Container) String() string { func (*Container) ProtoMessage() {} func (x *Container) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[12] + mi := &file_atelet_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -989,7 +1214,7 @@ func (x *Container) ProtoReflect() protoreflect.Message { // Deprecated: Use Container.ProtoReflect.Descriptor instead. func (*Container) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{12} + return file_atelet_proto_rawDescGZIP(), []int{16} } func (x *Container) GetName() string { @@ -1051,7 +1276,7 @@ type EnvEntry struct { func (x *EnvEntry) Reset() { *x = EnvEntry{} - mi := &file_atelet_proto_msgTypes[13] + mi := &file_atelet_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1063,7 +1288,7 @@ func (x *EnvEntry) String() string { func (*EnvEntry) ProtoMessage() {} func (x *EnvEntry) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[13] + mi := &file_atelet_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1076,7 +1301,7 @@ func (x *EnvEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use EnvEntry.ProtoReflect.Descriptor instead. func (*EnvEntry) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{13} + return file_atelet_proto_rawDescGZIP(), []int{17} } func (x *EnvEntry) GetName() string { @@ -1107,7 +1332,7 @@ type Readyz struct { func (x *Readyz) Reset() { *x = Readyz{} - mi := &file_atelet_proto_msgTypes[14] + mi := &file_atelet_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1119,7 +1344,7 @@ func (x *Readyz) String() string { func (*Readyz) ProtoMessage() {} func (x *Readyz) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[14] + mi := &file_atelet_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1132,7 +1357,7 @@ func (x *Readyz) ProtoReflect() protoreflect.Message { // Deprecated: Use Readyz.ProtoReflect.Descriptor instead. func (*Readyz) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{14} + return file_atelet_proto_rawDescGZIP(), []int{18} } func (x *Readyz) GetHttpGet() *HTTPGetAction { @@ -1162,7 +1387,7 @@ type HTTPGetAction struct { func (x *HTTPGetAction) Reset() { *x = HTTPGetAction{} - mi := &file_atelet_proto_msgTypes[15] + mi := &file_atelet_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1174,7 +1399,7 @@ func (x *HTTPGetAction) String() string { func (*HTTPGetAction) ProtoMessage() {} func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[15] + mi := &file_atelet_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1187,7 +1412,7 @@ func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { // Deprecated: Use HTTPGetAction.ProtoReflect.Descriptor instead. func (*HTTPGetAction) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{15} + return file_atelet_proto_rawDescGZIP(), []int{19} } func (x *HTTPGetAction) GetPath() string { @@ -1212,7 +1437,7 @@ type RunResponse struct { func (x *RunResponse) Reset() { *x = RunResponse{} - mi := &file_atelet_proto_msgTypes[16] + mi := &file_atelet_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1224,7 +1449,7 @@ func (x *RunResponse) String() string { func (*RunResponse) ProtoMessage() {} func (x *RunResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[16] + mi := &file_atelet_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1237,7 +1462,7 @@ func (x *RunResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RunResponse.ProtoReflect.Descriptor instead. func (*RunResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{16} + return file_atelet_proto_rawDescGZIP(), []int{20} } type LocalCheckpointConfiguration struct { @@ -1253,7 +1478,7 @@ type LocalCheckpointConfiguration struct { func (x *LocalCheckpointConfiguration) Reset() { *x = LocalCheckpointConfiguration{} - mi := &file_atelet_proto_msgTypes[17] + mi := &file_atelet_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1265,7 +1490,7 @@ func (x *LocalCheckpointConfiguration) String() string { func (*LocalCheckpointConfiguration) ProtoMessage() {} func (x *LocalCheckpointConfiguration) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[17] + mi := &file_atelet_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1278,7 +1503,7 @@ func (x *LocalCheckpointConfiguration) ProtoReflect() protoreflect.Message { // Deprecated: Use LocalCheckpointConfiguration.ProtoReflect.Descriptor instead. func (*LocalCheckpointConfiguration) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{17} + return file_atelet_proto_rawDescGZIP(), []int{21} } func (x *LocalCheckpointConfiguration) GetSnapshotName() string { @@ -1299,7 +1524,7 @@ type ExternalCheckpointConfiguration struct { func (x *ExternalCheckpointConfiguration) Reset() { *x = ExternalCheckpointConfiguration{} - mi := &file_atelet_proto_msgTypes[18] + mi := &file_atelet_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1311,7 +1536,7 @@ func (x *ExternalCheckpointConfiguration) String() string { func (*ExternalCheckpointConfiguration) ProtoMessage() {} func (x *ExternalCheckpointConfiguration) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[18] + mi := &file_atelet_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1324,7 +1549,7 @@ func (x *ExternalCheckpointConfiguration) ProtoReflect() protoreflect.Message { // Deprecated: Use ExternalCheckpointConfiguration.ProtoReflect.Descriptor instead. func (*ExternalCheckpointConfiguration) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{18} + return file_atelet_proto_rawDescGZIP(), []int{22} } func (x *ExternalCheckpointConfiguration) GetSnapshotUri() string { @@ -1362,7 +1587,7 @@ type CheckpointRequest struct { func (x *CheckpointRequest) Reset() { *x = CheckpointRequest{} - mi := &file_atelet_proto_msgTypes[19] + mi := &file_atelet_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1374,7 +1599,7 @@ func (x *CheckpointRequest) String() string { func (*CheckpointRequest) ProtoMessage() {} func (x *CheckpointRequest) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[19] + mi := &file_atelet_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1387,7 +1612,7 @@ func (x *CheckpointRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointRequest.ProtoReflect.Descriptor instead. func (*CheckpointRequest) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{19} + return file_atelet_proto_rawDescGZIP(), []int{23} } func (x *CheckpointRequest) GetTargetAteomUid() string { @@ -1502,7 +1727,7 @@ type CheckpointResponse struct { func (x *CheckpointResponse) Reset() { *x = CheckpointResponse{} - mi := &file_atelet_proto_msgTypes[20] + mi := &file_atelet_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1514,7 +1739,7 @@ func (x *CheckpointResponse) String() string { func (*CheckpointResponse) ProtoMessage() {} func (x *CheckpointResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[20] + mi := &file_atelet_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1527,7 +1752,7 @@ func (x *CheckpointResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointResponse.ProtoReflect.Descriptor instead. func (*CheckpointResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{20} + return file_atelet_proto_rawDescGZIP(), []int{24} } type UploadPausedCheckpointRequest struct { @@ -1555,7 +1780,7 @@ type UploadPausedCheckpointRequest struct { func (x *UploadPausedCheckpointRequest) Reset() { *x = UploadPausedCheckpointRequest{} - mi := &file_atelet_proto_msgTypes[21] + mi := &file_atelet_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1567,7 +1792,7 @@ func (x *UploadPausedCheckpointRequest) String() string { func (*UploadPausedCheckpointRequest) ProtoMessage() {} func (x *UploadPausedCheckpointRequest) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[21] + mi := &file_atelet_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1580,7 +1805,7 @@ func (x *UploadPausedCheckpointRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UploadPausedCheckpointRequest.ProtoReflect.Descriptor instead. func (*UploadPausedCheckpointRequest) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{21} + return file_atelet_proto_rawDescGZIP(), []int{25} } func (x *UploadPausedCheckpointRequest) GetAtespace() string { @@ -1647,7 +1872,7 @@ type UploadPausedCheckpointResponse struct { func (x *UploadPausedCheckpointResponse) Reset() { *x = UploadPausedCheckpointResponse{} - mi := &file_atelet_proto_msgTypes[22] + mi := &file_atelet_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1659,7 +1884,7 @@ func (x *UploadPausedCheckpointResponse) String() string { func (*UploadPausedCheckpointResponse) ProtoMessage() {} func (x *UploadPausedCheckpointResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[22] + mi := &file_atelet_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1672,7 +1897,7 @@ func (x *UploadPausedCheckpointResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UploadPausedCheckpointResponse.ProtoReflect.Descriptor instead. func (*UploadPausedCheckpointResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{22} + return file_atelet_proto_rawDescGZIP(), []int{26} } type RestoreRequest struct { @@ -1718,7 +1943,7 @@ type RestoreRequest struct { func (x *RestoreRequest) Reset() { *x = RestoreRequest{} - mi := &file_atelet_proto_msgTypes[23] + mi := &file_atelet_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1730,7 +1955,7 @@ func (x *RestoreRequest) String() string { func (*RestoreRequest) ProtoMessage() {} func (x *RestoreRequest) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[23] + mi := &file_atelet_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1743,7 +1968,7 @@ func (x *RestoreRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreRequest.ProtoReflect.Descriptor instead. func (*RestoreRequest) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{23} + return file_atelet_proto_rawDescGZIP(), []int{27} } func (x *RestoreRequest) GetTargetAteomUid() string { @@ -1886,7 +2111,7 @@ type RestoreResponse struct { func (x *RestoreResponse) Reset() { *x = RestoreResponse{} - mi := &file_atelet_proto_msgTypes[24] + mi := &file_atelet_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1898,7 +2123,7 @@ func (x *RestoreResponse) String() string { func (*RestoreResponse) ProtoMessage() {} func (x *RestoreResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[24] + mi := &file_atelet_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1911,7 +2136,7 @@ func (x *RestoreResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreResponse.ProtoReflect.Descriptor instead. func (*RestoreResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{24} + return file_atelet_proto_rawDescGZIP(), []int{28} } var File_atelet_proto protoreflect.FileDescriptor @@ -1973,13 +2198,24 @@ 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\"\xc7\x01\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"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" + + "\x17ActorMetadataDataSource\x12/\n" + + "\x05items\x18\x01 \x03(\v2\x19.atelet.ActorMetadataItemR\x05items\"o\n" + + "\x14SystemInfoDataSource\x12H\n" + + "\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" + "\x06Volume\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12&\n" + - "\x04type\x18\x02 \x01(\x0e2\x12.atelet.VolumeTypeR\x04type\x12;\n" + - "\vdurable_dir\x18\x03 \x01(\v2\x18.atelet.DurableDirVolumeH\x00R\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\x04 \x01(\v2\x1c.atelet.ExternalVolumeSourceH\x00R\bexternalB\b\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" + "\x06source\"@\n" + "\vVolumeMount\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" + @@ -2054,12 +2290,12 @@ const file_atelet_proto_rawDesc = "" + "\fmemory_bytes\x18\x0f \x01(\x03R\vmemoryBytesB\b\n" + "\x06configB\x11\n" + "\x0f_egress_gateway\"\x11\n" + - "\x0fRestoreResponse*`\n" + - "\n" + - "VolumeType\x12\x1b\n" + - "\x17VOLUME_TYPE_UNSPECIFIED\x10\x00\x12\x1b\n" + - "\x17VOLUME_TYPE_DURABLE_DIR\x10\x01\x12\x18\n" + - "\x14VOLUME_TYPE_EXTERNAL\x10\x02*j\n" + + "\x0fRestoreResponse*\x9a\x01\n" + + "\x12ActorMetadataField\x12$\n" + + " ACTOR_METADATA_FIELD_UNSPECIFIED\x10\x00\x12\x1d\n" + + "\x19ACTOR_METADATA_FIELD_NAME\x10\x01\x12!\n" + + "\x1dACTOR_METADATA_FIELD_ATESPACE\x10\x02\x12\x1c\n" + + "\x18ACTOR_METADATA_FIELD_UID\x10\x03*j\n" + "\x0eCheckpointType\x12\x1f\n" + "\x1bCHECKPOINT_TYPE_UNSPECIFIED\x10\x00\x12\x19\n" + "\x15CHECKPOINT_TYPE_LOCAL\x10\x01\x12\x1c\n" + @@ -2091,9 +2327,9 @@ func file_atelet_proto_rawDescGZIP() []byte { } var file_atelet_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_atelet_proto_msgTypes = make([]protoimpl.MessageInfo, 28) +var file_atelet_proto_msgTypes = make([]protoimpl.MessageInfo, 32) var file_atelet_proto_goTypes = []any{ - (VolumeType)(0), // 0: atelet.VolumeType + (ActorMetadataField)(0), // 0: atelet.ActorMetadataField (CheckpointType)(0), // 1: atelet.CheckpointType (SnapshotScope)(0), // 2: atelet.SnapshotScope (*MintActorCertificateRequest)(nil), // 3: atelet.MintActorCertificateRequest @@ -2106,70 +2342,78 @@ var file_atelet_proto_goTypes = []any{ (*WorkloadSpec)(nil), // 10: atelet.WorkloadSpec (*DurableDirVolume)(nil), // 11: atelet.DurableDirVolume (*ExternalVolumeSource)(nil), // 12: atelet.ExternalVolumeSource - (*Volume)(nil), // 13: atelet.Volume - (*VolumeMount)(nil), // 14: atelet.VolumeMount - (*Container)(nil), // 15: atelet.Container - (*EnvEntry)(nil), // 16: atelet.EnvEntry - (*Readyz)(nil), // 17: atelet.Readyz - (*HTTPGetAction)(nil), // 18: atelet.HTTPGetAction - (*RunResponse)(nil), // 19: atelet.RunResponse - (*LocalCheckpointConfiguration)(nil), // 20: atelet.LocalCheckpointConfiguration - (*ExternalCheckpointConfiguration)(nil), // 21: atelet.ExternalCheckpointConfiguration - (*CheckpointRequest)(nil), // 22: atelet.CheckpointRequest - (*CheckpointResponse)(nil), // 23: atelet.CheckpointResponse - (*UploadPausedCheckpointRequest)(nil), // 24: atelet.UploadPausedCheckpointRequest - (*UploadPausedCheckpointResponse)(nil), // 25: atelet.UploadPausedCheckpointResponse - (*RestoreRequest)(nil), // 26: atelet.RestoreRequest - (*RestoreResponse)(nil), // 27: atelet.RestoreResponse - nil, // 28: atelet.ArchAssets.FilesEntry - nil, // 29: atelet.SandboxAssets.AssetsEntry - nil, // 30: atelet.ExternalVolumeSource.VolumeContextEntry + (*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 + (*EnvEntry)(nil), // 20: atelet.EnvEntry + (*Readyz)(nil), // 21: atelet.Readyz + (*HTTPGetAction)(nil), // 22: atelet.HTTPGetAction + (*RunResponse)(nil), // 23: atelet.RunResponse + (*LocalCheckpointConfiguration)(nil), // 24: atelet.LocalCheckpointConfiguration + (*ExternalCheckpointConfiguration)(nil), // 25: atelet.ExternalCheckpointConfiguration + (*CheckpointRequest)(nil), // 26: atelet.CheckpointRequest + (*CheckpointResponse)(nil), // 27: atelet.CheckpointResponse + (*UploadPausedCheckpointRequest)(nil), // 28: atelet.UploadPausedCheckpointRequest + (*UploadPausedCheckpointResponse)(nil), // 29: atelet.UploadPausedCheckpointResponse + (*RestoreRequest)(nil), // 30: atelet.RestoreRequest + (*RestoreResponse)(nil), // 31: atelet.RestoreResponse + nil, // 32: atelet.ArchAssets.FilesEntry + nil, // 33: atelet.SandboxAssets.AssetsEntry + nil, // 34: 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 - 28, // 3: atelet.ArchAssets.files:type_name -> atelet.ArchAssets.FilesEntry - 29, // 4: atelet.SandboxAssets.assets:type_name -> atelet.SandboxAssets.AssetsEntry - 15, // 5: atelet.WorkloadSpec.containers:type_name -> atelet.Container - 13, // 6: atelet.WorkloadSpec.volumes:type_name -> atelet.Volume - 30, // 7: atelet.ExternalVolumeSource.volume_context:type_name -> atelet.ExternalVolumeSource.VolumeContextEntry - 0, // 8: atelet.Volume.type:type_name -> atelet.VolumeType - 11, // 9: atelet.Volume.durable_dir:type_name -> atelet.DurableDirVolume - 12, // 10: atelet.Volume.external:type_name -> atelet.ExternalVolumeSource - 16, // 11: atelet.Container.env:type_name -> atelet.EnvEntry - 17, // 12: atelet.Container.readyz:type_name -> atelet.Readyz - 14, // 13: atelet.Container.volume_mounts:type_name -> atelet.VolumeMount - 18, // 14: atelet.Readyz.http_get:type_name -> atelet.HTTPGetAction - 10, // 15: atelet.CheckpointRequest.spec:type_name -> atelet.WorkloadSpec - 1, // 16: atelet.CheckpointRequest.type:type_name -> atelet.CheckpointType - 20, // 17: atelet.CheckpointRequest.local_config:type_name -> atelet.LocalCheckpointConfiguration - 21, // 18: atelet.CheckpointRequest.external_config:type_name -> atelet.ExternalCheckpointConfiguration - 2, // 19: atelet.CheckpointRequest.scope:type_name -> atelet.SnapshotScope - 2, // 20: atelet.UploadPausedCheckpointRequest.desired_scope:type_name -> atelet.SnapshotScope - 10, // 21: atelet.RestoreRequest.spec:type_name -> atelet.WorkloadSpec - 1, // 22: atelet.RestoreRequest.type:type_name -> atelet.CheckpointType - 20, // 23: atelet.RestoreRequest.local_config:type_name -> atelet.LocalCheckpointConfiguration - 21, // 24: atelet.RestoreRequest.external_config:type_name -> atelet.ExternalCheckpointConfiguration - 2, // 25: atelet.RestoreRequest.scope:type_name -> atelet.SnapshotScope - 6, // 26: atelet.RestoreRequest.egress_gateway:type_name -> atelet.EgressGateway - 7, // 27: atelet.ArchAssets.FilesEntry.value:type_name -> atelet.AssetFile - 8, // 28: atelet.SandboxAssets.AssetsEntry.value:type_name -> atelet.ArchAssets - 3, // 29: atelet.CredentialBroker.MintActorCertificate:input_type -> atelet.MintActorCertificateRequest - 5, // 30: atelet.AteomHerder.Run:input_type -> atelet.RunRequest - 22, // 31: atelet.AteomHerder.Checkpoint:input_type -> atelet.CheckpointRequest - 26, // 32: atelet.AteomHerder.Restore:input_type -> atelet.RestoreRequest - 24, // 33: atelet.AteomHerder.UploadPausedCheckpoint:input_type -> atelet.UploadPausedCheckpointRequest - 4, // 34: atelet.CredentialBroker.MintActorCertificate:output_type -> atelet.MintActorCertificateResponse - 19, // 35: atelet.AteomHerder.Run:output_type -> atelet.RunResponse - 23, // 36: atelet.AteomHerder.Checkpoint:output_type -> atelet.CheckpointResponse - 27, // 37: atelet.AteomHerder.Restore:output_type -> atelet.RestoreResponse - 25, // 38: atelet.AteomHerder.UploadPausedCheckpoint:output_type -> atelet.UploadPausedCheckpointResponse - 34, // [34:39] is the sub-list for method output_type - 29, // [29:34] is the sub-list for method input_type - 29, // [29:29] is the sub-list for extension type_name - 29, // [29:29] is the sub-list for extension extendee - 0, // [0:29] is the sub-list for field type_name + 32, // 3: atelet.ArchAssets.files:type_name -> atelet.ArchAssets.FilesEntry + 33, // 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 + 34, // 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 + 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 + 20, // 15: atelet.Container.env:type_name -> atelet.EnvEntry + 21, // 16: atelet.Container.readyz:type_name -> atelet.Readyz + 18, // 17: atelet.Container.volume_mounts:type_name -> atelet.VolumeMount + 22, // 18: atelet.Readyz.http_get:type_name -> atelet.HTTPGetAction + 10, // 19: atelet.CheckpointRequest.spec:type_name -> atelet.WorkloadSpec + 1, // 20: atelet.CheckpointRequest.type:type_name -> atelet.CheckpointType + 24, // 21: atelet.CheckpointRequest.local_config:type_name -> atelet.LocalCheckpointConfiguration + 25, // 22: atelet.CheckpointRequest.external_config:type_name -> atelet.ExternalCheckpointConfiguration + 2, // 23: atelet.CheckpointRequest.scope:type_name -> atelet.SnapshotScope + 2, // 24: atelet.UploadPausedCheckpointRequest.desired_scope:type_name -> atelet.SnapshotScope + 10, // 25: atelet.RestoreRequest.spec:type_name -> atelet.WorkloadSpec + 1, // 26: atelet.RestoreRequest.type:type_name -> atelet.CheckpointType + 24, // 27: atelet.RestoreRequest.local_config:type_name -> atelet.LocalCheckpointConfiguration + 25, // 28: atelet.RestoreRequest.external_config:type_name -> atelet.ExternalCheckpointConfiguration + 2, // 29: atelet.RestoreRequest.scope:type_name -> atelet.SnapshotScope + 6, // 30: atelet.RestoreRequest.egress_gateway:type_name -> atelet.EgressGateway + 7, // 31: atelet.ArchAssets.FilesEntry.value:type_name -> atelet.AssetFile + 8, // 32: atelet.SandboxAssets.AssetsEntry.value:type_name -> atelet.ArchAssets + 3, // 33: atelet.CredentialBroker.MintActorCertificate:input_type -> atelet.MintActorCertificateRequest + 5, // 34: atelet.AteomHerder.Run:input_type -> atelet.RunRequest + 26, // 35: atelet.AteomHerder.Checkpoint:input_type -> atelet.CheckpointRequest + 30, // 36: atelet.AteomHerder.Restore:input_type -> atelet.RestoreRequest + 28, // 37: atelet.AteomHerder.UploadPausedCheckpoint:input_type -> atelet.UploadPausedCheckpointRequest + 4, // 38: atelet.CredentialBroker.MintActorCertificate:output_type -> atelet.MintActorCertificateResponse + 23, // 39: atelet.AteomHerder.Run:output_type -> atelet.RunResponse + 27, // 40: atelet.AteomHerder.Checkpoint:output_type -> atelet.CheckpointResponse + 31, // 41: atelet.AteomHerder.Restore:output_type -> atelet.RestoreResponse + 29, // 42: atelet.AteomHerder.UploadPausedCheckpoint:output_type -> atelet.UploadPausedCheckpointResponse + 38, // [38:43] is the sub-list for method output_type + 33, // [33:38] is the sub-list for method input_type + 33, // [33:33] is the sub-list for extension type_name + 33, // [33:33] is the sub-list for extension extendee + 0, // [0:33] is the sub-list for field type_name } func init() { file_atelet_proto_init() } @@ -2178,15 +2422,19 @@ func file_atelet_proto_init() { return } file_atelet_proto_msgTypes[2].OneofWrappers = []any{} - file_atelet_proto_msgTypes[10].OneofWrappers = []any{ + file_atelet_proto_msgTypes[12].OneofWrappers = []any{ + (*SystemInfoDataSource_ActorMetadata)(nil), + } + file_atelet_proto_msgTypes[14].OneofWrappers = []any{ (*Volume_DurableDir)(nil), (*Volume_External)(nil), + (*Volume_SystemInfo)(nil), } - file_atelet_proto_msgTypes[19].OneofWrappers = []any{ + file_atelet_proto_msgTypes[23].OneofWrappers = []any{ (*CheckpointRequest_LocalConfig)(nil), (*CheckpointRequest_ExternalConfig)(nil), } - file_atelet_proto_msgTypes[23].OneofWrappers = []any{ + file_atelet_proto_msgTypes[27].OneofWrappers = []any{ (*RestoreRequest_LocalConfig)(nil), (*RestoreRequest_ExternalConfig)(nil), } @@ -2196,7 +2444,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: 28, + NumMessages: 32, NumExtensions: 0, NumServices: 2, }, diff --git a/internal/proto/ateletpb/atelet.proto b/internal/proto/ateletpb/atelet.proto index 6b5a68c2e..7914147c3 100644 --- a/internal/proto/ateletpb/atelet.proto +++ b/internal/proto/ateletpb/atelet.proto @@ -132,12 +132,6 @@ message WorkloadSpec { reserved "pause_image"; // moved to SandboxAssets } -enum VolumeType { - VOLUME_TYPE_UNSPECIFIED = 0; - VOLUME_TYPE_DURABLE_DIR = 1; - VOLUME_TYPE_EXTERNAL = 2; -} - message DurableDirVolume { } @@ -147,14 +141,47 @@ message ExternalVolumeSource { map volume_context = 3; } +// ActorMetadataField selects one identity field of the actor. +enum ActorMetadataField { + ACTOR_METADATA_FIELD_UNSPECIFIED = 0; + ACTOR_METADATA_FIELD_NAME = 1; + ACTOR_METADATA_FIELD_ATESPACE = 2; + ACTOR_METADATA_FIELD_UID = 3; +} + +// ActorMetadataItem projects one actor identity field to one file at the +// given path, relative to the root of the enclosing system-info volume. +message ActorMetadataItem { + ActorMetadataField field = 1; + string path = 2; +} + +// ActorMetadataDataSource projects the actor's identity fields to files, one +// per item. Values are written raw with no trailing newline. +message ActorMetadataDataSource { + repeated ActorMetadataItem items = 1; +} + +message SystemInfoDataSource { + oneof data_source { + ActorMetadataDataSource actor_metadata = 1; + } +} + +// SystemInfoVolume is a read-only volume whose files are generated by atelet +// on every Run/Restore, so they carry the values of the actor actually being +// started, whatever checkpointed state it boots from. +message SystemInfoVolume { + repeated SystemInfoDataSource data_sources = 1; +} + message Volume { string name = 1; - VolumeType type = 2; - oneof source { - DurableDirVolume durable_dir = 3; - ExternalVolumeSource external = 4; + DurableDirVolume durable_dir = 2; + ExternalVolumeSource external = 3; + SystemInfoVolume system_info = 4; } } diff --git a/internal/proto/ateompb/ateom.pb.go b/internal/proto/ateompb/ateom.pb.go index f6dfc12b6..e1c2c9390 100644 --- a/internal/proto/ateompb/ateom.pb.go +++ b/internal/proto/ateompb/ateom.pb.go @@ -502,8 +502,12 @@ type Container struct { DurableDirVolumeMounts []*DurableDirVolumeMount `protobuf:"bytes,4,rep,name=durable_dir_volume_mounts,json=durableDirVolumeMounts,proto3" json:"durable_dir_volume_mounts,omitempty"` // csi_volume_mounts are the CSI volumes this container mounts, if any. CsiVolumeMounts []*VolumeMount `protobuf:"bytes,5,rep,name=csi_volume_mounts,json=csiVolumeMounts,proto3" json:"csi_volume_mounts,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // system_info_volume_mounts are the system-info volumes this container + // 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 } func (x *Container) Reset() { @@ -564,6 +568,13 @@ func (x *Container) GetCsiVolumeMounts() []*VolumeMount { return nil } +func (x *Container) GetSystemInfoVolumeMounts() []*SystemInfoVolumeMount { + if x != nil { + return x.SystemInfoVolumeMounts + } + return nil +} + // VolumeMount is one volume mounted into a container. type VolumeMount struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -673,6 +684,64 @@ func (x *DurableDirVolumeMount) GetMountPath() string { return "" } +// SystemInfoVolumeMount is one system-info volume mounted (read-only) into a +// container. Unlike durable-dir volumes, system-info contents are generated +// by atelet on every Run/Restore and are never captured into snapshots. +type SystemInfoVolumeMount struct { + state protoimpl.MessageState `protogen:"open.v1"` + // volume_name is the name the ActorTemplate gave the volume. It selects the + // per-volume directory atelet prepared for the actor on the host. + 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 *SystemInfoVolumeMount) Reset() { + *x = SystemInfoVolumeMount{} + mi := &file_ateom_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SystemInfoVolumeMount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SystemInfoVolumeMount) ProtoMessage() {} + +func (x *SystemInfoVolumeMount) ProtoReflect() protoreflect.Message { + mi := &file_ateom_proto_msgTypes[6] + 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 SystemInfoVolumeMount.ProtoReflect.Descriptor instead. +func (*SystemInfoVolumeMount) Descriptor() ([]byte, []int) { + return file_ateom_proto_rawDescGZIP(), []int{6} +} + +func (x *SystemInfoVolumeMount) GetVolumeName() string { + if x != nil { + return x.VolumeName + } + return "" +} + +func (x *SystemInfoVolumeMount) 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 { @@ -687,7 +756,7 @@ type Readyz struct { func (x *Readyz) Reset() { *x = Readyz{} - mi := &file_ateom_proto_msgTypes[6] + mi := &file_ateom_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -699,7 +768,7 @@ func (x *Readyz) String() string { func (*Readyz) ProtoMessage() {} func (x *Readyz) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[6] + mi := &file_ateom_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -712,7 +781,7 @@ func (x *Readyz) ProtoReflect() protoreflect.Message { // Deprecated: Use Readyz.ProtoReflect.Descriptor instead. func (*Readyz) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{6} + return file_ateom_proto_rawDescGZIP(), []int{7} } func (x *Readyz) GetHttpGet() *HTTPGetAction { @@ -742,7 +811,7 @@ type HTTPGetAction struct { func (x *HTTPGetAction) Reset() { *x = HTTPGetAction{} - mi := &file_ateom_proto_msgTypes[7] + mi := &file_ateom_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -754,7 +823,7 @@ func (x *HTTPGetAction) String() string { func (*HTTPGetAction) ProtoMessage() {} func (x *HTTPGetAction) 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 { @@ -767,7 +836,7 @@ func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { // Deprecated: Use HTTPGetAction.ProtoReflect.Descriptor instead. func (*HTTPGetAction) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{7} + return file_ateom_proto_rawDescGZIP(), []int{8} } func (x *HTTPGetAction) GetPath() string { @@ -792,7 +861,7 @@ type RunWorkloadResponse struct { func (x *RunWorkloadResponse) Reset() { *x = RunWorkloadResponse{} - mi := &file_ateom_proto_msgTypes[8] + mi := &file_ateom_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -804,7 +873,7 @@ func (x *RunWorkloadResponse) String() string { func (*RunWorkloadResponse) ProtoMessage() {} func (x *RunWorkloadResponse) 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 { @@ -817,7 +886,7 @@ func (x *RunWorkloadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RunWorkloadResponse.ProtoReflect.Descriptor instead. func (*RunWorkloadResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{8} + return file_ateom_proto_rawDescGZIP(), []int{9} } type CheckpointWorkloadRequest struct { @@ -851,7 +920,7 @@ type CheckpointWorkloadRequest struct { func (x *CheckpointWorkloadRequest) Reset() { *x = CheckpointWorkloadRequest{} - mi := &file_ateom_proto_msgTypes[9] + mi := &file_ateom_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -863,7 +932,7 @@ func (x *CheckpointWorkloadRequest) String() string { func (*CheckpointWorkloadRequest) ProtoMessage() {} func (x *CheckpointWorkloadRequest) 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 { @@ -876,7 +945,7 @@ func (x *CheckpointWorkloadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointWorkloadRequest.ProtoReflect.Descriptor instead. func (*CheckpointWorkloadRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{9} + return file_ateom_proto_rawDescGZIP(), []int{10} } func (x *CheckpointWorkloadRequest) GetAtespace() string { @@ -961,7 +1030,7 @@ type CheckpointWorkloadResponse struct { func (x *CheckpointWorkloadResponse) Reset() { *x = CheckpointWorkloadResponse{} - mi := &file_ateom_proto_msgTypes[10] + mi := &file_ateom_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -973,7 +1042,7 @@ func (x *CheckpointWorkloadResponse) String() string { func (*CheckpointWorkloadResponse) ProtoMessage() {} func (x *CheckpointWorkloadResponse) 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 { @@ -986,7 +1055,7 @@ func (x *CheckpointWorkloadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointWorkloadResponse.ProtoReflect.Descriptor instead. func (*CheckpointWorkloadResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{10} + return file_ateom_proto_rawDescGZIP(), []int{11} } func (x *CheckpointWorkloadResponse) GetSnapshotFiles() []string { @@ -1031,7 +1100,7 @@ type RestoreWorkloadRequest struct { func (x *RestoreWorkloadRequest) Reset() { *x = RestoreWorkloadRequest{} - mi := &file_ateom_proto_msgTypes[11] + mi := &file_ateom_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1043,7 +1112,7 @@ func (x *RestoreWorkloadRequest) String() string { func (*RestoreWorkloadRequest) ProtoMessage() {} func (x *RestoreWorkloadRequest) 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 { @@ -1056,7 +1125,7 @@ func (x *RestoreWorkloadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreWorkloadRequest.ProtoReflect.Descriptor instead. func (*RestoreWorkloadRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{11} + return file_ateom_proto_rawDescGZIP(), []int{12} } func (x *RestoreWorkloadRequest) GetAtespace() string { @@ -1165,7 +1234,7 @@ type RestoreWorkloadResponse struct { func (x *RestoreWorkloadResponse) Reset() { *x = RestoreWorkloadResponse{} - mi := &file_ateom_proto_msgTypes[12] + mi := &file_ateom_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1177,7 +1246,7 @@ func (x *RestoreWorkloadResponse) String() string { func (*RestoreWorkloadResponse) ProtoMessage() {} func (x *RestoreWorkloadResponse) 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 { @@ -1190,7 +1259,7 @@ func (x *RestoreWorkloadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreWorkloadResponse.ProtoReflect.Descriptor instead. func (*RestoreWorkloadResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{12} + return file_ateom_proto_rawDescGZIP(), []int{13} } type GetWorkloadStatsRequest struct { @@ -1206,7 +1275,7 @@ type GetWorkloadStatsRequest struct { func (x *GetWorkloadStatsRequest) Reset() { *x = GetWorkloadStatsRequest{} - mi := &file_ateom_proto_msgTypes[13] + mi := &file_ateom_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1218,7 +1287,7 @@ func (x *GetWorkloadStatsRequest) String() string { func (*GetWorkloadStatsRequest) ProtoMessage() {} func (x *GetWorkloadStatsRequest) 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 { @@ -1231,7 +1300,7 @@ func (x *GetWorkloadStatsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkloadStatsRequest.ProtoReflect.Descriptor instead. func (*GetWorkloadStatsRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{13} + return file_ateom_proto_rawDescGZIP(), []int{14} } func (x *GetWorkloadStatsRequest) GetActorUid() string { @@ -1295,7 +1364,7 @@ type WorkloadStatsSample struct { func (x *WorkloadStatsSample) Reset() { *x = WorkloadStatsSample{} - mi := &file_ateom_proto_msgTypes[14] + mi := &file_ateom_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1307,7 +1376,7 @@ func (x *WorkloadStatsSample) String() string { func (*WorkloadStatsSample) ProtoMessage() {} func (x *WorkloadStatsSample) 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 { @@ -1320,7 +1389,7 @@ func (x *WorkloadStatsSample) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkloadStatsSample.ProtoReflect.Descriptor instead. func (*WorkloadStatsSample) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{14} + return file_ateom_proto_rawDescGZIP(), []int{15} } func (x *WorkloadStatsSample) GetAtespace() string { @@ -1416,7 +1485,7 @@ type GetWorkloadStatsResponse struct { func (x *GetWorkloadStatsResponse) Reset() { *x = GetWorkloadStatsResponse{} - mi := &file_ateom_proto_msgTypes[15] + mi := &file_ateom_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1428,7 +1497,7 @@ func (x *GetWorkloadStatsResponse) String() string { func (*GetWorkloadStatsResponse) ProtoMessage() {} func (x *GetWorkloadStatsResponse) 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 { @@ -1441,7 +1510,7 @@ func (x *GetWorkloadStatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkloadStatsResponse.ProtoReflect.Descriptor instead. func (*GetWorkloadStatsResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{15} + return file_ateom_proto_rawDescGZIP(), []int{16} } func (x *GetWorkloadStatsResponse) GetSample() *WorkloadStatsSample { @@ -1459,7 +1528,7 @@ type GetActiveWorkloadStatsRequest struct { func (x *GetActiveWorkloadStatsRequest) Reset() { *x = GetActiveWorkloadStatsRequest{} - mi := &file_ateom_proto_msgTypes[16] + mi := &file_ateom_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1471,7 +1540,7 @@ func (x *GetActiveWorkloadStatsRequest) String() string { func (*GetActiveWorkloadStatsRequest) ProtoMessage() {} func (x *GetActiveWorkloadStatsRequest) 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 { @@ -1484,7 +1553,7 @@ func (x *GetActiveWorkloadStatsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActiveWorkloadStatsRequest.ProtoReflect.Descriptor instead. func (*GetActiveWorkloadStatsRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{16} + return file_ateom_proto_rawDescGZIP(), []int{17} } type GetActiveWorkloadStatsResponse struct { @@ -1506,7 +1575,7 @@ type GetActiveWorkloadStatsResponse struct { func (x *GetActiveWorkloadStatsResponse) Reset() { *x = GetActiveWorkloadStatsResponse{} - mi := &file_ateom_proto_msgTypes[17] + mi := &file_ateom_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1518,7 +1587,7 @@ func (x *GetActiveWorkloadStatsResponse) String() string { func (*GetActiveWorkloadStatsResponse) ProtoMessage() {} func (x *GetActiveWorkloadStatsResponse) 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 { @@ -1531,7 +1600,7 @@ func (x *GetActiveWorkloadStatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActiveWorkloadStatsResponse.ProtoReflect.Descriptor instead. func (*GetActiveWorkloadStatsResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{17} + return file_ateom_proto_rawDescGZIP(), []int{18} } func (x *GetActiveWorkloadStatsResponse) GetResult() isGetActiveWorkloadStatsResponse_Result { @@ -1604,12 +1673,13 @@ const file_ateom_proto_rawDesc = "" + "\fWorkloadSpec\x120\n" + "\n" + "containers\x18\x01 \x03(\v2\x10.ateom.ContainerR\n" + - "containers\"\xfa\x01\n" + + "containers\"\xd3\x02\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\x0fcsiVolumeMountsJ\x04\b\x03\x10\x04R\x13durable_dir_volumes\"M\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" + "\vVolumeMount\x12\x1f\n" + "\vvolume_name\x18\x01 \x01(\tR\n" + "volumeName\x12\x1d\n" + @@ -1619,6 +1689,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\"W\n" + + "\x15SystemInfoVolumeMount\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" + @@ -1730,7 +1805,7 @@ func file_ateom_proto_rawDescGZIP() []byte { } var file_ateom_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_ateom_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_ateom_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_ateom_proto_goTypes = []any{ (SnapshotScope)(0), // 0: ateom.SnapshotScope (SandboxClass)(0), // 1: ateom.SandboxClass @@ -1742,58 +1817,60 @@ var file_ateom_proto_goTypes = []any{ (*Container)(nil), // 7: ateom.Container (*VolumeMount)(nil), // 8: ateom.VolumeMount (*DurableDirVolumeMount)(nil), // 9: ateom.DurableDirVolumeMount - (*Readyz)(nil), // 10: ateom.Readyz - (*HTTPGetAction)(nil), // 11: ateom.HTTPGetAction - (*RunWorkloadResponse)(nil), // 12: ateom.RunWorkloadResponse - (*CheckpointWorkloadRequest)(nil), // 13: ateom.CheckpointWorkloadRequest - (*CheckpointWorkloadResponse)(nil), // 14: ateom.CheckpointWorkloadResponse - (*RestoreWorkloadRequest)(nil), // 15: ateom.RestoreWorkloadRequest - (*RestoreWorkloadResponse)(nil), // 16: ateom.RestoreWorkloadResponse - (*GetWorkloadStatsRequest)(nil), // 17: ateom.GetWorkloadStatsRequest - (*WorkloadStatsSample)(nil), // 18: ateom.WorkloadStatsSample - (*GetWorkloadStatsResponse)(nil), // 19: ateom.GetWorkloadStatsResponse - (*GetActiveWorkloadStatsRequest)(nil), // 20: ateom.GetActiveWorkloadStatsRequest - (*GetActiveWorkloadStatsResponse)(nil), // 21: ateom.GetActiveWorkloadStatsResponse - nil, // 22: ateom.RunWorkloadRequest.RuntimeAssetPathsEntry - nil, // 23: ateom.CheckpointWorkloadRequest.RuntimeAssetPathsEntry - nil, // 24: ateom.RestoreWorkloadRequest.RuntimeAssetPathsEntry + (*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 } var file_ateom_proto_depIdxs = []int32{ 6, // 0: ateom.RunWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 22, // 1: ateom.RunWorkloadRequest.runtime_asset_paths:type_name -> ateom.RunWorkloadRequest.RuntimeAssetPathsEntry + 23, // 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 - 10, // 4: ateom.Container.readyz:type_name -> ateom.Readyz + 11, // 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 - 11, // 7: ateom.Readyz.http_get:type_name -> ateom.HTTPGetAction - 6, // 8: ateom.CheckpointWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 23, // 9: ateom.CheckpointWorkloadRequest.runtime_asset_paths:type_name -> ateom.CheckpointWorkloadRequest.RuntimeAssetPathsEntry - 0, // 10: ateom.CheckpointWorkloadRequest.scope:type_name -> ateom.SnapshotScope - 6, // 11: ateom.RestoreWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 24, // 12: ateom.RestoreWorkloadRequest.runtime_asset_paths:type_name -> ateom.RestoreWorkloadRequest.RuntimeAssetPathsEntry - 0, // 13: ateom.RestoreWorkloadRequest.scope:type_name -> ateom.SnapshotScope - 5, // 14: ateom.RestoreWorkloadRequest.egress_gateway:type_name -> ateom.EgressGateway - 1, // 15: ateom.WorkloadStatsSample.sandbox_class:type_name -> ateom.SandboxClass - 2, // 16: ateom.WorkloadStatsSample.source:type_name -> ateom.StatsSource - 18, // 17: ateom.GetWorkloadStatsResponse.sample:type_name -> ateom.WorkloadStatsSample - 18, // 18: ateom.GetActiveWorkloadStatsResponse.sample:type_name -> ateom.WorkloadStatsSample - 3, // 19: ateom.GetActiveWorkloadStatsResponse.no_sample_reason:type_name -> ateom.NoSampleReason - 4, // 20: ateom.Ateom.RunWorkload:input_type -> ateom.RunWorkloadRequest - 13, // 21: ateom.Ateom.CheckpointWorkload:input_type -> ateom.CheckpointWorkloadRequest - 15, // 22: ateom.Ateom.RestoreWorkload:input_type -> ateom.RestoreWorkloadRequest - 17, // 23: ateom.Ateom.GetWorkloadStats:input_type -> ateom.GetWorkloadStatsRequest - 20, // 24: ateom.Ateom.GetActiveWorkloadStats:input_type -> ateom.GetActiveWorkloadStatsRequest - 12, // 25: ateom.Ateom.RunWorkload:output_type -> ateom.RunWorkloadResponse - 14, // 26: ateom.Ateom.CheckpointWorkload:output_type -> ateom.CheckpointWorkloadResponse - 16, // 27: ateom.Ateom.RestoreWorkload:output_type -> ateom.RestoreWorkloadResponse - 19, // 28: ateom.Ateom.GetWorkloadStats:output_type -> ateom.GetWorkloadStatsResponse - 21, // 29: ateom.Ateom.GetActiveWorkloadStats:output_type -> ateom.GetActiveWorkloadStatsResponse - 25, // [25:30] is the sub-list for method output_type - 20, // [20:25] is the sub-list for method input_type - 20, // [20:20] is the sub-list for extension type_name - 20, // [20:20] is the sub-list for extension extendee - 0, // [0:20] is the sub-list for field type_name + 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 } func init() { file_ateom_proto_init() } @@ -1802,8 +1879,8 @@ func file_ateom_proto_init() { return } file_ateom_proto_msgTypes[0].OneofWrappers = []any{} - file_ateom_proto_msgTypes[11].OneofWrappers = []any{} - file_ateom_proto_msgTypes[17].OneofWrappers = []any{ + file_ateom_proto_msgTypes[12].OneofWrappers = []any{} + file_ateom_proto_msgTypes[18].OneofWrappers = []any{ (*GetActiveWorkloadStatsResponse_Sample)(nil), (*GetActiveWorkloadStatsResponse_NoSampleReason)(nil), } @@ -1813,7 +1890,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: 21, + NumMessages: 22, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/proto/ateompb/ateom.proto b/internal/proto/ateompb/ateom.proto index b84c1b09e..0821f6787 100644 --- a/internal/proto/ateompb/ateom.proto +++ b/internal/proto/ateompb/ateom.proto @@ -155,6 +155,11 @@ message Container { // csi_volume_mounts are the CSI volumes this container mounts, if any. repeated VolumeMount csi_volume_mounts = 5; + + // system_info_volume_mounts are the system-info volumes this 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; } // VolumeMount is one volume mounted into a container. @@ -172,6 +177,17 @@ message DurableDirVolumeMount { string mount_path = 2; } +// SystemInfoVolumeMount is one system-info volume mounted (read-only) into a +// container. Unlike durable-dir volumes, system-info contents are generated +// by atelet on every Run/Restore and are never captured into snapshots. +message SystemInfoVolumeMount { + // volume_name is the name the ActorTemplate gave the volume. It selects the + // per-volume directory atelet prepared for the actor on the host. + 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 66bc74f9d..ba30d3fde 100644 --- a/manifests/ate-install/generated/ate.dev_actortemplates.yaml +++ b/manifests/ate-install/generated/ate.dev_actortemplates.yaml @@ -416,13 +416,104 @@ spec: x-kubernetes-validations: - message: Name must be a valid DNS label rule: '!format.dns1123Label().validate(self).hasValue()' + systemInfo: + description: systemInfo configures a system information volume. + properties: + dataSources: + description: |- + DataSources is the list of data sources to place within the SystemInfo + volume. + + At most one actorMetadata entry may appear; this is what keeps file + paths unique across the whole volume (uniqueness within the entry is + enforced on its items). + items: + description: |- + SystemInfoDataSource is a container allowing you to pick a particular + SystemInfo data source. + + Exactly one member must be set. + properties: + actorMetadata: + description: |- + ActorMetadataDataSource is a SystemInfo volume data source that projects the + actor's identity fields (name, atespace, uid) to files, one per item — + analogous to the Kubernetes downwardAPI volume. Values are written raw with + no trailing newline, and are fixed for the actor's lifetime across + suspend/resume/migration. + properties: + items: + description: |- + Items is the list of fields to project and the file path each is + written to. + items: + description: ActorMetadataItem projects one + actor identity field to one file. + properties: + field: + description: Field selects which identity + field to project. + enum: + - name + - atespace + - uid + type: string + path: + description: |- + Relative path from the root of the SystemInfo volume at which the + field's value is written. Must be a clean relative Unix path: must not + start or end with '/', and contain no ':', '..', '.', '//', or control + characters. + maxLength: 255 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'path must be a clean relative + Unix path: must not start or end with + ''/'', and contain no '':'', ''..'', + ''.'', ''//'', or control characters' + rule: '!self.startsWith(''/'') && !self.endsWith(''/'') + && !self.contains(''//'') && !self.contains('':'') + && !self.matches(''[\x00-\x1f\x7f]'') + && !self.matches(''(^|/)[.][.]?(/|$)'')' + required: + - field + - path + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-validations: + - message: items must not project the same field + twice + rule: self.all(x, self.exists_one(y, y.field + == x.field)) + - message: items must not contain duplicate paths + rule: self.all(x, self.exists_one(y, y.path + == x.path)) + required: + - items + type: object + type: object + x-kubernetes-validations: + - message: exactly one of the fields in [actorMetadata] + must be set + rule: '[has(self.actorMetadata)].filter(x,x==true).size() + == 1' + maxItems: 32 + type: array + x-kubernetes-validations: + - message: dataSources must contain at most one actorMetadata + entry + rule: self.filter(x, has(x.actorMetadata)).size() <= 1 + type: object required: - name type: object x-kubernetes-validations: - - message: exactly one of the fields in [durableDir externalVolumeTemplate] - must be set - rule: '[has(self.durableDir),has(self.externalVolumeTemplate)].filter(x,x==true).size() + - 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() == 1' maxItems: 32 type: array diff --git a/pkg/api/v1alpha1/actortemplate_types.go b/pkg/api/v1alpha1/actortemplate_types.go index f3856d305..79c97ba4f 100644 --- a/pkg/api/v1alpha1/actortemplate_types.go +++ b/pkg/api/v1alpha1/actortemplate_types.go @@ -46,12 +46,91 @@ type ExternalVolumeTemplate struct { StorageClassName string `json:"storageClassName"` } +// ActorMetadataField selects one identity field of the actor, following the +// resource identity model (see docs/api-style-guide.md#2-resource-naming-and-identity). +// +// +kubebuilder:validation:Enum=name;atespace;uid +type ActorMetadataField string + +const ( + // ActorMetadataFieldName is the actor's metadata.name, unique within its + // atespace. + ActorMetadataFieldName ActorMetadataField = "name" + // ActorMetadataFieldAtespace is the atespace the actor belongs to. + ActorMetadataFieldAtespace ActorMetadataField = "atespace" + // ActorMetadataFieldUID is the actor's server-generated UID, which + // distinguishes incarnations of the same (atespace, name). + ActorMetadataFieldUID ActorMetadataField = "uid" +) + +// ActorMetadataItem projects one actor identity field to one file. +type ActorMetadataItem struct { + // Field selects which identity field to project. + // + // +required + Field ActorMetadataField `json:"field"` + + // Relative path from the root of the SystemInfo volume at which the + // field's value is written. Must be a clean relative Unix path: must not + // start or end with '/', and contain no ':', '..', '.', '//', or control + // characters. + // + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=255 + // +kubebuilder:validation:XValidation:rule="!self.startsWith('/') && !self.endsWith('/') && !self.contains('//') && !self.contains(':') && !self.matches('[\\x00-\\x1f\\x7f]') && !self.matches('(^|/)[.][.]?(/|$)')",message="path must be a clean relative Unix path: must not start or end with '/', and contain no ':', '..', '.', '//', or control characters" + Path string `json:"path"` +} + +// ActorMetadataDataSource is a SystemInfo volume data source that projects the +// actor's identity fields (name, atespace, uid) to files, one per item — +// analogous to the Kubernetes downwardAPI volume. Values are written raw with +// no trailing newline, and are fixed for the actor's lifetime across +// suspend/resume/migration. +type ActorMetadataDataSource struct { + // Items is the list of fields to project and the file path each is + // written to. + // + // +required + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=8 + // +kubebuilder:validation:XValidation:rule="self.all(x, self.exists_one(y, y.field == x.field))",message="items must not project the same field twice" + // +kubebuilder:validation:XValidation:rule="self.all(x, self.exists_one(y, y.path == x.path))",message="items must not contain duplicate paths" + Items []ActorMetadataItem `json:"items"` +} + +// SystemInfoDataSource is a container allowing you to pick a particular +// SystemInfo data source. +// +// Exactly one member must be set. +// +// +kubebuilder:validation:ExactlyOneOf={actorMetadata} +type SystemInfoDataSource struct { + ActorMetadata *ActorMetadataDataSource `json:"actorMetadata,omitempty"` +} + +// Represents a system information volume, which provides files containing +// substrate-generated per-actor data such as the actor's identity fields +// (and, in the future, identity JWTs and certificates). +type SystemInfoVolumeSource struct { + // DataSources is the list of data sources to place within the SystemInfo + // volume. + // + // At most one actorMetadata entry may appear; this is what keeps file + // paths unique across the whole volume (uniqueness within the entry is + // enforced on its items). + // + // +kubebuilder:validation:MaxItems=32 + // +kubebuilder:validation:XValidation:rule="self.filter(x, has(x.actorMetadata)).size() <= 1",message="dataSources must contain at most one actorMetadata entry" + DataSources []SystemInfoDataSource `json:"dataSources,omitempty"` +} + // Represents the source of a volume to mount. // Exactly one of its members must be specified. // // When adding a new source type, list it in the ExactlyOneOf marker below. // -// +kubebuilder:validation:ExactlyOneOf={durableDir,externalVolumeTemplate} +// +kubebuilder:validation:ExactlyOneOf={durableDir,externalVolumeTemplate,systemInfo} type VolumeSource struct { // durableDir represents a durable directory on rootfs that persists across // resumes and participates in snapshots. @@ -63,6 +142,11 @@ type VolumeSource struct { // when the actor is deleted. // +optional ExternalVolumeTemplate *ExternalVolumeTemplate `json:"externalVolumeTemplate,omitempty"` + + // systemInfo configures a system information volume. + // + // +optional + SystemInfo *SystemInfoVolumeSource `json:"systemInfo,omitempty"` } type Volume struct { diff --git a/pkg/api/v1alpha1/actortemplate_validation_test.go b/pkg/api/v1alpha1/actortemplate_validation_test.go index 554bdc045..01cec0a81 100644 --- a/pkg/api/v1alpha1/actortemplate_validation_test.go +++ b/pkg/api/v1alpha1/actortemplate_validation_test.go @@ -761,7 +761,7 @@ func TestActorTemplateValidation(t *testing.T) { } }, wantErr: true, - errMsg: "exactly one of the fields in [durableDir externalVolumeTemplate] must be set", + errMsg: "exactly one of the fields in [durableDir externalVolumeTemplate systemInfo] must be set", }, { name: "Volumes: VolumeSource with no source set is invalid", mutate: func(at *ActorTemplate) { @@ -770,7 +770,7 @@ func TestActorTemplateValidation(t *testing.T) { } }, wantErr: true, - errMsg: "exactly one of the fields in [durableDir externalVolumeTemplate] must be set", + errMsg: "exactly one of the fields in [durableDir externalVolumeTemplate 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 +784,221 @@ func TestActorTemplateValidation(t *testing.T) { } }, wantErr: true, - errMsg: "exactly one of the fields in [durableDir externalVolumeTemplate] must be set", + errMsg: "exactly one of the fields in [durableDir externalVolumeTemplate systemInfo] must be set", + }, { + name: "Volumes: SystemInfo volume projecting all actor metadata fields is valid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + { + Name: "system-info", + VolumeSource: VolumeSource{ + SystemInfo: &SystemInfoVolumeSource{ + DataSources: []SystemInfoDataSource{ + {ActorMetadata: &ActorMetadataDataSource{ + Items: []ActorMetadataItem{ + {Field: ActorMetadataFieldName, Path: "actor-name"}, + {Field: ActorMetadataFieldAtespace, Path: "atespace"}, + {Field: ActorMetadataFieldUID, Path: "identity/actor-uid"}, + }, + }}, + }, + }, + }, + }, + } + at.Spec.Containers[0].VolumeMounts = []VolumeMount{ + {Name: "system-info", MountPath: "/run/ate"}, + } + }, + wantErr: false, + }, { + name: "Volumes: SystemInfo data source with no member set is invalid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + { + Name: "system-info", + VolumeSource: VolumeSource{ + SystemInfo: &SystemInfoVolumeSource{ + DataSources: []SystemInfoDataSource{{}}, + }, + }, + }, + } + }, + wantErr: true, + errMsg: "exactly one of the fields in [actorMetadata] must be set", + }, { + name: "Volumes: SystemInfo actorMetadata with no items is invalid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + { + Name: "system-info", + VolumeSource: VolumeSource{ + SystemInfo: &SystemInfoVolumeSource{ + DataSources: []SystemInfoDataSource{ + {ActorMetadata: &ActorMetadataDataSource{Items: []ActorMetadataItem{}}}, + }, + }, + }, + }, + } + }, + wantErr: true, + }, { + name: "Volumes: SystemInfo item with unknown field is invalid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + { + Name: "system-info", + VolumeSource: VolumeSource{ + SystemInfo: &SystemInfoVolumeSource{ + DataSources: []SystemInfoDataSource{ + {ActorMetadata: &ActorMetadataDataSource{ + Items: []ActorMetadataItem{ + {Field: ActorMetadataField("hostname"), Path: "hostname"}, + }, + }}, + }, + }, + }, + }, + } + }, + wantErr: true, + }, { + name: "Volumes: SystemInfo item with empty path is invalid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + { + Name: "system-info", + VolumeSource: VolumeSource{ + SystemInfo: &SystemInfoVolumeSource{ + DataSources: []SystemInfoDataSource{ + {ActorMetadata: &ActorMetadataDataSource{ + Items: []ActorMetadataItem{ + {Field: ActorMetadataFieldName, Path: ""}, + }, + }}, + }, + }, + }, + }, + } + }, + wantErr: true, + }, { + name: "Volumes: SystemInfo item with absolute path is invalid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + { + Name: "system-info", + VolumeSource: VolumeSource{ + SystemInfo: &SystemInfoVolumeSource{ + DataSources: []SystemInfoDataSource{ + {ActorMetadata: &ActorMetadataDataSource{ + Items: []ActorMetadataItem{ + {Field: ActorMetadataFieldName, Path: "/etc/actor-name"}, + }, + }}, + }, + }, + }, + }, + } + }, + wantErr: true, + errMsg: "path must be a clean relative Unix path", + }, { + name: "Volumes: SystemInfo item with path traversal is invalid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + { + Name: "system-info", + VolumeSource: VolumeSource{ + SystemInfo: &SystemInfoVolumeSource{ + DataSources: []SystemInfoDataSource{ + {ActorMetadata: &ActorMetadataDataSource{ + Items: []ActorMetadataItem{ + {Field: ActorMetadataFieldName, Path: "../escape"}, + }, + }}, + }, + }, + }, + }, + } + }, + wantErr: true, + errMsg: "path must be a clean relative Unix path", + }, { + name: "Volumes: SystemInfo items projecting the same field twice are invalid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + { + Name: "system-info", + VolumeSource: VolumeSource{ + SystemInfo: &SystemInfoVolumeSource{ + DataSources: []SystemInfoDataSource{ + {ActorMetadata: &ActorMetadataDataSource{ + Items: []ActorMetadataItem{ + {Field: ActorMetadataFieldName, Path: "actor-name"}, + {Field: ActorMetadataFieldName, Path: "name-again"}, + }, + }}, + }, + }, + }, + }, + } + }, + wantErr: true, + errMsg: "items must not project the same field twice", + }, { + name: "Volumes: SystemInfo items with duplicate paths are invalid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + { + Name: "system-info", + VolumeSource: VolumeSource{ + SystemInfo: &SystemInfoVolumeSource{ + DataSources: []SystemInfoDataSource{ + {ActorMetadata: &ActorMetadataDataSource{ + Items: []ActorMetadataItem{ + {Field: ActorMetadataFieldName, Path: "actor-name"}, + {Field: ActorMetadataFieldUID, Path: "actor-name"}, + }, + }}, + }, + }, + }, + }, + } + }, + wantErr: true, + errMsg: "items must not contain duplicate paths", + }, { + name: "Volumes: SystemInfo with two actorMetadata entries is invalid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + { + Name: "system-info", + VolumeSource: VolumeSource{ + SystemInfo: &SystemInfoVolumeSource{ + DataSources: []SystemInfoDataSource{ + {ActorMetadata: &ActorMetadataDataSource{ + Items: []ActorMetadataItem{{Field: ActorMetadataFieldName, Path: "actor-name"}}, + }}, + {ActorMetadata: &ActorMetadataDataSource{ + Items: []ActorMetadataItem{{Field: ActorMetadataFieldUID, Path: "actor-uid"}}, + }}, + }, + }, + }, + }, + } + }, + wantErr: true, + errMsg: "dataSources must contain at most one actorMetadata entry", }, { name: "Volumes: DurableDir MountPath with nested absolute path 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 8b54a4259..4f0caef9a 100644 --- a/pkg/api/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/api/v1alpha1/zz_generated.deepcopy.go @@ -24,6 +24,41 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActorMetadataDataSource) DeepCopyInto(out *ActorMetadataDataSource) { + *out = *in + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ActorMetadataItem, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActorMetadataDataSource. +func (in *ActorMetadataDataSource) DeepCopy() *ActorMetadataDataSource { + if in == nil { + return nil + } + out := new(ActorMetadataDataSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActorMetadataItem) DeepCopyInto(out *ActorMetadataItem) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActorMetadataItem. +func (in *ActorMetadataItem) DeepCopy() *ActorMetadataItem { + if in == nil { + return nil + } + out := new(ActorMetadataItem) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ActorTemplate) DeepCopyInto(out *ActorTemplate) { *out = *in @@ -477,6 +512,48 @@ func (in *SnapshotsConfig) DeepCopy() *SnapshotsConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SystemInfoDataSource) DeepCopyInto(out *SystemInfoDataSource) { + *out = *in + if in.ActorMetadata != nil { + in, out := &in.ActorMetadata, &out.ActorMetadata + *out = new(ActorMetadataDataSource) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SystemInfoDataSource. +func (in *SystemInfoDataSource) DeepCopy() *SystemInfoDataSource { + if in == nil { + return nil + } + out := new(SystemInfoDataSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SystemInfoVolumeSource) DeepCopyInto(out *SystemInfoVolumeSource) { + *out = *in + if in.DataSources != nil { + in, out := &in.DataSources, &out.DataSources + *out = make([]SystemInfoDataSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SystemInfoVolumeSource. +func (in *SystemInfoVolumeSource) DeepCopy() *SystemInfoVolumeSource { + if in == nil { + return nil + } + out := new(SystemInfoVolumeSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Volume) DeepCopyInto(out *Volume) { *out = *in @@ -521,6 +598,11 @@ func (in *VolumeSource) DeepCopyInto(out *VolumeSource) { *out = new(ExternalVolumeTemplate) (*in).DeepCopyInto(*out) } + if in.SystemInfo != nil { + in, out := &in.SystemInfo, &out.SystemInfo + *out = new(SystemInfoVolumeSource) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VolumeSource.