From 1b3ad2acf5fad57089dc92b1d495847f4a32f604 Mon Sep 17 00:00:00 2001 From: msau42 Date: Tue, 7 Jul 2026 23:38:32 +0000 Subject: [PATCH 1/6] ActorTemplate api changes to add per-actor external volumes. This adds a new externalVolumeTemplate where users specify the capacity, storage driver and storage properties they would like via StorageClass. When an actor is created, we will also provision a volume for the actor according to the template. The volume is deleted when the actor is deleted. --- .../generated/ate.dev_actortemplates.yaml | 28 +++++- pkg/api/v1alpha1/actortemplate_types.go | 18 +++- .../v1alpha1/actortemplate_validation_test.go | 95 ++++++++++++++++++- pkg/api/v1alpha1/zz_generated.deepcopy.go | 21 ++++ 4 files changed, 157 insertions(+), 5 deletions(-) diff --git a/manifests/ate-install/generated/ate.dev_actortemplates.yaml b/manifests/ate-install/generated/ate.dev_actortemplates.yaml index dfc94dca3..f7c5e240a 100644 --- a/manifests/ate-install/generated/ate.dev_actortemplates.yaml +++ b/manifests/ate-install/generated/ate.dev_actortemplates.yaml @@ -306,6 +306,28 @@ spec: durableDir represents a durable directory on rootfs that persists across resumes and participates in snapshots. type: object + externalVolumeTemplate: + description: |- + externalVolumeTemplate represents an external volume dynamically provisioned + for each actor. The volume only lives as long as the actor and is deleted + when the actor is deleted. + properties: + capacity: + anyOf: + - type: integer + - type: string + description: capacity specifies the size of the volume to + create. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + storageClassName: + description: storageClassName refers to the StorageClass + to create the volume from. + type: string + required: + - capacity + - storageClassName + type: object name: description: name of the volume. maxLength: 63 @@ -317,8 +339,10 @@ spec: - name type: object x-kubernetes-validations: - - message: exactly one of the fields in [durableDir] must be set - rule: '[has(self.durableDir)].filter(x,x==true).size() == 1' + - message: exactly one of the fields in [durableDir externalVolumeTemplate] + must be set + rule: '[has(self.durableDir),has(self.externalVolumeTemplate)].filter(x,x==true).size() + == 1' maxItems: 32 type: array workerSelector: diff --git a/pkg/api/v1alpha1/actortemplate_types.go b/pkg/api/v1alpha1/actortemplate_types.go index 192a5932f..3c0184601 100644 --- a/pkg/api/v1alpha1/actortemplate_types.go +++ b/pkg/api/v1alpha1/actortemplate_types.go @@ -15,6 +15,7 @@ package v1alpha1 import ( + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -39,12 +40,27 @@ type DurableDirVolumeSource struct { // // When adding a new source type, list it in the ExactlyOneOf marker below. // -// +kubebuilder:validation:ExactlyOneOf={durableDir} +// +kubebuilder:validation:ExactlyOneOf={durableDir,externalVolumeTemplate} type VolumeSource struct { // durableDir represents a durable directory on rootfs that persists across // resumes and participates in snapshots. // +optional DurableDir *DurableDirVolumeSource `json:"durableDir,omitempty" protobuf:"bytes,2,opt,name=durableDir"` + + // externalVolumeTemplate represents an external volume dynamically provisioned + // for each actor. The volume only lives as long as the actor and is deleted + // when the actor is deleted. + // +optional + ExternalVolumeTemplate *ExternalVolumeTemplate `json:"externalVolumeTemplate,omitempty" protobuf:"bytes,3,opt,name=externalVolumeTemplate"` +} + +type ExternalVolumeTemplate struct { + // capacity specifies the size of the volume to create. + // +required + Capacity resource.Quantity `json:"capacity" protobuf:"bytes,1,opt,name=capacity"` + // storageClassName refers to the StorageClass to create the volume from. + // +required + StorageClassName string `json:"storageClassName" protobuf:"bytes,2,opt,name=storageClassName"` } type Volume struct { diff --git a/pkg/api/v1alpha1/actortemplate_validation_test.go b/pkg/api/v1alpha1/actortemplate_validation_test.go index 86557ef9d..e233ed8f4 100644 --- a/pkg/api/v1alpha1/actortemplate_validation_test.go +++ b/pkg/api/v1alpha1/actortemplate_validation_test.go @@ -23,6 +23,7 @@ import ( "github.com/agent-substrate/substrate/internal/envtestbins" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -612,6 +613,96 @@ func TestActorTemplateValidation(t *testing.T) { } }, wantErr: false, + }, { + name: "Volumes: 1 ExternalVolumeTemplate mount is valid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + { + Name: "vol1", + VolumeSource: VolumeSource{ + ExternalVolumeTemplate: &ExternalVolumeTemplate{ + Capacity: resource.MustParse("10Gi"), + StorageClassName: "standard", + }, + }, + }, + } + at.Spec.Containers[0].VolumeMounts = []VolumeMount{ + {Name: "vol1", MountPath: "/mnt/data"}, + } + }, + wantErr: false, + }, { + name: "Volumes: multiple ExternalVolumeTemplate mounts are valid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + { + Name: "vol1", + VolumeSource: VolumeSource{ + ExternalVolumeTemplate: &ExternalVolumeTemplate{ + Capacity: resource.MustParse("10Gi"), + StorageClassName: "standard", + }, + }, + }, + { + Name: "vol2", + VolumeSource: VolumeSource{ + ExternalVolumeTemplate: &ExternalVolumeTemplate{ + Capacity: resource.MustParse("20Gi"), + StorageClassName: "pd-ssd", + }, + }, + }, + } + at.Spec.Containers[0].VolumeMounts = []VolumeMount{ + {Name: "vol1", MountPath: "/mnt/vol1"}, + {Name: "vol2", MountPath: "/mnt/vol2"}, + } + }, + wantErr: false, + }, { + name: "Volumes: 1 DurableDir and 1 ExternalVolumeTemplate on same ActorTemplate is valid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + {Name: "home", VolumeSource: VolumeSource{DurableDir: &DurableDirVolumeSource{}}}, + { + Name: "ext", + VolumeSource: VolumeSource{ + ExternalVolumeTemplate: &ExternalVolumeTemplate{ + Capacity: resource.MustParse("10Gi"), + StorageClassName: "standard", + }, + }, + }, + } + at.Spec.Containers[0].VolumeMounts = []VolumeMount{ + {Name: "home", MountPath: "/home"}, + {Name: "ext", MountPath: "/mnt/ext"}, + } + }, + wantErr: false, + }, { + name: "Volumes: VolumeSource with both DurableDir and ExternalVolumeTemplate set is invalid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + { + Name: "vol1", + VolumeSource: VolumeSource{ + DurableDir: &DurableDirVolumeSource{}, + ExternalVolumeTemplate: &ExternalVolumeTemplate{ + Capacity: resource.MustParse("10Gi"), + StorageClassName: "standard", + }, + }, + }, + } + at.Spec.Containers[0].VolumeMounts = []VolumeMount{ + {Name: "vol1", MountPath: "/mnt/data"}, + } + }, + wantErr: true, + errMsg: "exactly one of the fields in [durableDir externalVolumeTemplate] must be set", }, { name: "Volumes: VolumeSource with no source set is invalid", mutate: func(at *ActorTemplate) { @@ -620,7 +711,7 @@ func TestActorTemplateValidation(t *testing.T) { } }, wantErr: true, - errMsg: "exactly one of the fields in [durableDir] must be set", + errMsg: "exactly one of the fields in [durableDir externalVolumeTemplate] must be set", }, { name: "Volumes: VolumeSource with no source set is invalid (mixed with a valid DurableDir volume)", mutate: func(at *ActorTemplate) { @@ -634,7 +725,7 @@ func TestActorTemplateValidation(t *testing.T) { } }, wantErr: true, - errMsg: "exactly one of the fields in [durableDir] must be set", + errMsg: "exactly one of the fields in [durableDir externalVolumeTemplate] must be set", }, { 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 e977908e9..e45f96b0c 100644 --- a/pkg/api/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/api/v1alpha1/zz_generated.deepcopy.go @@ -273,6 +273,22 @@ func (in *EnvVarSource) DeepCopy() *EnvVarSource { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalVolumeTemplate) DeepCopyInto(out *ExternalVolumeTemplate) { + *out = *in + out.Capacity = in.Capacity.DeepCopy() +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalVolumeTemplate. +func (in *ExternalVolumeTemplate) DeepCopy() *ExternalVolumeTemplate { + if in == nil { + return nil + } + out := new(ExternalVolumeTemplate) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HTTPGetAction) DeepCopyInto(out *HTTPGetAction) { *out = *in @@ -453,6 +469,11 @@ func (in *VolumeSource) DeepCopyInto(out *VolumeSource) { *out = new(DurableDirVolumeSource) **out = **in } + if in.ExternalVolumeTemplate != nil { + in, out := &in.ExternalVolumeTemplate, &out.ExternalVolumeTemplate + *out = new(ExternalVolumeTemplate) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VolumeSource. From a9c90dbdd3deddb72b84caf453c361c82086c83b Mon Sep 17 00:00:00 2001 From: msau42 Date: Tue, 7 Jul 2026 23:39:30 +0000 Subject: [PATCH 2/6] ateapi handling of external volumes. Adds external volumes to the Actor object and integrates control plane Actor workflows with the corresponding volume operations: CreateActor -> CreateVolume DeleteActor -> DeleteVolume ResumeActor -> AttachVolume Pause/Suspend Actor -> DetachVolume --- .../internal/controlapi/create_actor.go | 16 +- .../internal/controlapi/delete_actor.go | 16 +- cmd/ateapi/internal/controlapi/volumes.go | 111 ++++ cmd/ateapi/internal/controlapi/workflow.go | 3 + .../internal/controlapi/workflow_pause.go | 50 +- .../internal/controlapi/workflow_resume.go | 41 +- .../internal/controlapi/workflow_suspend.go | 50 +- .../internal/controlapi/workload_spec.go | 70 ++- .../internal/controlapi/workload_spec_test.go | 110 +++- pkg/proto/ateapipb/ateapi.pb.go | 579 +++++++++++------- pkg/proto/ateapipb/ateapi.proto | 22 + 11 files changed, 834 insertions(+), 234 deletions(-) create mode 100644 cmd/ateapi/internal/controlapi/volumes.go diff --git a/cmd/ateapi/internal/controlapi/create_actor.go b/cmd/ateapi/internal/controlapi/create_actor.go index 50eb05473..577edabb4 100644 --- a/cmd/ateapi/internal/controlapi/create_actor.go +++ b/cmd/ateapi/internal/controlapi/create_actor.go @@ -33,12 +33,11 @@ func (s *Service) CreateActor(ctx context.Context, req *ateapipb.CreateActorRequ if err := validateCreateActorRequest(req); err != nil { return nil, err } - in := req.GetActor() templateNamespace := in.GetActorTemplateNamespace() templateName := in.GetActorTemplateName() - _, err := s.actorTemplateLister.ActorTemplates(templateNamespace).Get(templateName) + template, err := s.actorTemplateLister.ActorTemplates(templateNamespace).Get(templateName) if err != nil { if k8serrors.IsNotFound(err) { return nil, status.Errorf(codes.FailedPrecondition, "ActorTemplate %s/%s not found", templateNamespace, templateName) @@ -58,6 +57,16 @@ func (s *Service) CreateActor(ctx context.Context, req *ateapipb.CreateActorRequ return nil, status.Errorf(codes.FailedPrecondition, "Atespace %s not found", atespace) } + actorRef := &ateapipb.ObjectRef{ + Atespace: atespace, + Name: name, + } + + volumes, err := s.createActorVolumes(ctx, actorRef, template) + if err != nil { + return nil, err + } + actor := &ateapipb.Actor{ Metadata: &ateapipb.ResourceMetadata{ Atespace: atespace, @@ -67,9 +76,12 @@ func (s *Service) CreateActor(ctx context.Context, req *ateapipb.CreateActorRequ ActorTemplateNamespace: templateNamespace, ActorTemplateName: templateName, WorkerSelector: in.GetWorkerSelector(), + ActorVolumes: volumes, } stored, err := s.persistence.CreateActor(ctx, actor) if err != nil { + // Cleanup created volumes if DB write fails + s.deleteActorVolumes(ctx, actorRef, volumes) if errors.Is(err, store.ErrAlreadyExists) { return nil, status.Errorf(codes.AlreadyExists, "Actor %s already exists", name) } diff --git a/cmd/ateapi/internal/controlapi/delete_actor.go b/cmd/ateapi/internal/controlapi/delete_actor.go index e58df4cb3..72fe60375 100644 --- a/cmd/ateapi/internal/controlapi/delete_actor.go +++ b/cmd/ateapi/internal/controlapi/delete_actor.go @@ -32,7 +32,21 @@ func (s *Service) DeleteActor(ctx context.Context, req *ateapipb.DeleteActorRequ return nil, err } - deleted, err := s.persistence.DeleteActor(ctx, req.GetActor().GetAtespace(), req.GetActor().GetName()) + atespace := req.GetActor().GetAtespace() + name := req.GetActor().GetName() + + actor, err := s.persistence.GetActor(ctx, atespace, name) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + return nil, status.Errorf(codes.NotFound, "Actor %s not found", name) + } + return nil, fmt.Errorf("while fetching actor: %w", err) + } + + // Delete associated volumes (TODO: best effort?) + s.deleteActorVolumes(ctx, req.GetActor(), actor.GetActorVolumes()) + + deleted, err := s.persistence.DeleteActor(ctx, atespace, name) if err != nil { if errors.Is(err, store.ErrNotFound) { return nil, status.Errorf(codes.NotFound, "Actor %s not found", req.GetActor().GetName()) diff --git a/cmd/ateapi/internal/controlapi/volumes.go b/cmd/ateapi/internal/controlapi/volumes.go new file mode 100644 index 000000000..b415074e3 --- /dev/null +++ b/cmd/ateapi/internal/controlapi/volumes.go @@ -0,0 +1,111 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controlapi + +import ( + "context" + "fmt" + "log/slog" + + "github.com/agent-substrate/substrate/internal/volume" + atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var ( + globalVolumePlugin = volume.NewMockVolumePlugin() +) + +// TODO: Replace with actual volume plugin search +func getVolumePlugin() volume.VolumePlugin { + return globalVolumePlugin +} + +// TODO: we should persist creation first so that we can handle background cleanup. +// this probably requires us to add a PROVISIONING actor state. + +// createActorVolumes provisions external volumes specified in the actor template. +// It returns the list of created external volumes, or an error if any creation fails. +// If any volume creation fails, it cleans up any volumes created in this call on a best-effort basis. +func (s *Service) createActorVolumes(ctx context.Context, ref *ateapipb.ObjectRef, template *atev1alpha1.ActorTemplate) ([]*ateapipb.ExternalVolume, error) { + var volumes []*ateapipb.ExternalVolume + for _, vol := range template.Spec.Volumes { + if vol.ExternalVolumeTemplate != nil { + // Use a unique name for the volume to ensure idempotency + uniqueVolName := actorVolumeID(ref, vol.Name) + storageVolumeID, err := getVolumePlugin().CreateVolume(ctx, uniqueVolName, vol.ExternalVolumeTemplate.Capacity.String(), vol.ExternalVolumeTemplate.StorageClassName) + if err != nil { + // TODO: need better system - best effort cleanup of already created volumes + s.deleteActorVolumes(ctx, ref, volumes) + return nil, status.Errorf(codes.Internal, "failed to create volume %q: %v", vol.Name, err) + } + volumes = append(volumes, &ateapipb.ExternalVolume{ + ActorVolumeId: uniqueVolName, + StorageVolumeId: storageVolumeID, + VolumeType: "mock", // TODO fix when we support multiple plugins + Status: ateapipb.ExternalVolume_CREATED, + }) + } + } + return volumes, nil +} + +// deleteActorVolumes deletes all external volumes in the list on a best-effort basis. +func (s *Service) deleteActorVolumes(ctx context.Context, ref *ateapipb.ObjectRef, volumes []*ateapipb.ExternalVolume) { + for _, vol := range volumes { + if err := getVolumePlugin().DeleteVolume(ctx, vol.GetStorageVolumeId()); err != nil { + slog.ErrorContext(ctx, "failed to delete volume", + slog.String("atespace", ref.GetAtespace()), + slog.String("actor_id", ref.GetName()), + slog.String("volume_id", vol.GetStorageVolumeId()), + slog.Any("error", err)) + } + } +} + +// getMountedActorVolumes filters the actor's volumes and returns only those that are declared and mounted in the ActorTemplate. +func getMountedActorVolumes(ctx context.Context, ref *ateapipb.ObjectRef, volumes []*ateapipb.ExternalVolume, template *atev1alpha1.ActorTemplate) []*ateapipb.ExternalVolume { + var mounted []*ateapipb.ExternalVolume + for _, vol := range volumes { + // Find the corresponding volume in the ActorTemplate to check if it's mounted + var matchedTemplateVol *atev1alpha1.Volume + for _, tVol := range template.Spec.Volumes { + expectedID := actorVolumeID(ref, tVol.Name) + if vol.GetActorVolumeId() == expectedID { + matchedTemplateVol = &tVol + break + } + } + + if matchedTemplateVol == nil { + slog.WarnContext(ctx, "Volume not found in template, skipping", slog.String("volume_id", vol.GetStorageVolumeId())) + continue + } + + if !isVolumeMounted(matchedTemplateVol.Name, template) { + slog.InfoContext(ctx, "Volume not mounted in template, skipping", slog.String("volume_id", vol.GetStorageVolumeId())) + continue + } + mounted = append(mounted, vol) + } + return mounted +} + +func actorVolumeID(ref *ateapipb.ObjectRef, volumeName string) string { + // TODO consider if this should be actor UUID + return fmt.Sprintf("%s-%s-%s", ref.GetAtespace(), ref.GetName(), volumeName) +} diff --git a/cmd/ateapi/internal/controlapi/workflow.go b/cmd/ateapi/internal/controlapi/workflow.go index 496e1bb89..ae1d321ad 100644 --- a/cmd/ateapi/internal/controlapi/workflow.go +++ b/cmd/ateapi/internal/controlapi/workflow.go @@ -167,6 +167,7 @@ func (w *ActorWorkflow) ResumeActor(ctx context.Context, atespace, name string, steps := []WorkflowStep[*ResumeInput, *ResumeState]{ &LoadActorForResumeStep{store: w.store, actorTemplateLister: w.actorTemplateLister}, &AssignWorkerStep{store: w.store, workerCache: w.workerCache}, + &AttachVolumesStep{store: w.store}, &CallAteletRestoreStep{store: w.store, dialer: w.dialer, kubeClient: w.kubeClient, secretCache: w.secretCache, workerPoolLister: w.workerPoolLister, sandboxConfigLister: w.sandboxConfigLister}, &FinalizeRunningStep{store: w.store}, } @@ -198,6 +199,7 @@ func (w *ActorWorkflow) SuspendActor(ctx context.Context, atespace, name string) &LoadActorForSuspendStep{store: w.store, actorTemplateLister: w.actorTemplateLister}, &MarkSuspendingStep{store: w.store}, &CallAteletSuspendStep{store: w.store, dialer: w.dialer}, + &DetachVolumesStep{store: w.store}, &FinalizeSuspendedStep{store: w.store}, } @@ -228,6 +230,7 @@ func (w *ActorWorkflow) PauseActor(ctx context.Context, atespace, name string) ( &LoadActorForPauseStep{store: w.store, actorTemplateLister: w.actorTemplateLister}, &MarkPausingStep{store: w.store}, &CallAteletPauseStep{store: w.store, dialer: w.dialer}, + &DetachVolumesForPauseStep{store: w.store}, &FinalizePausedStep{store: w.store}, } diff --git a/cmd/ateapi/internal/controlapi/workflow_pause.go b/cmd/ateapi/internal/controlapi/workflow_pause.go index 5ac831ce2..8dd438521 100644 --- a/cmd/ateapi/internal/controlapi/workflow_pause.go +++ b/cmd/ateapi/internal/controlapi/workflow_pause.go @@ -124,7 +124,10 @@ func (s *CallAteletPauseStep) Execute(ctx context.Context, input *PauseInput, st } client := ateletpb.NewAteomHerderClient(ateletConn) - workloadSpec := workloadSpecFromActorTemplate(state.ActorTemplate) + workloadSpec, err := workloadSpecFromActorTemplate(state.ActorTemplate, state.Actor) + if err != nil { + return err + } // Checkpoint does not carry the sandbox config: atelet uses the version the // actor is currently running (recorded on-node at Run/Restore) and pins it @@ -151,6 +154,51 @@ func (s *CallAteletPauseStep) Execute(ctx context.Context, input *PauseInput, st func (s *CallAteletPauseStep) RetryBackoff() *wait.Backoff { return nil } +type DetachVolumesForPauseStep struct { + store store.Interface +} + +func (s *DetachVolumesForPauseStep) Name() string { return "DetachVolumesForPause" } + +func (s *DetachVolumesForPauseStep) IsComplete(ctx context.Context, input *PauseInput, state *PauseState) (bool, error) { + // TODO replace with a proper check on the volumes. + return state.Actor.GetStatus() == ateapipb.Actor_STATUS_PAUSED && state.Actor.GetAteomPodNamespace() == "", nil +} + +func (s *DetachVolumesForPauseStep) Execute(ctx context.Context, input *PauseInput, state *PauseState) error { + if state.Actor.GetAteomPodNamespace() == "" { + slog.WarnContext(ctx, "Actor has no assigned worker pod during pause, skipping detach volumes", slog.String("actor_id", input.ActorName)) + return nil + } + + worker, err := s.store.GetWorker(ctx, state.Actor.GetAteomPodNamespace(), state.Actor.GetWorkerPoolName(), state.Actor.GetAteomPodName()) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + slog.WarnContext(ctx, "Worker not found in store during pause, skipping detach volumes", slog.String("actor_id", input.ActorName)) + return nil + } + return fmt.Errorf("failed to get worker: %w", err) + } + + node := worker.GetNodeName() + if node == "" { + slog.WarnContext(ctx, "Worker has no assigned node name during pause, skipping detach volumes", slog.String("actor_id", input.ActorName)) + return nil + } + + ref := &ateapipb.ObjectRef{Atespace: state.Actor.GetMetadata().GetAtespace(), Name: state.Actor.GetMetadata().GetName()} + for _, vol := range getMountedActorVolumes(ctx, ref, state.Actor.GetActorVolumes(), state.ActorTemplate) { + slog.InfoContext(ctx, "Detaching volume from node", slog.String("volume_id", vol.GetStorageVolumeId()), slog.String("node", node)) + err := getVolumePlugin().DetachVolume(ctx, vol.GetStorageVolumeId(), node) + if err != nil { + return fmt.Errorf("failed to detach volume %q from node %q: %w", vol.GetStorageVolumeId(), node, err) + } + } + return nil +} + +func (s *DetachVolumesForPauseStep) RetryBackoff() *wait.Backoff { return nil } + type FinalizePausedStep struct { store store.Interface } diff --git a/cmd/ateapi/internal/controlapi/workflow_resume.go b/cmd/ateapi/internal/controlapi/workflow_resume.go index 992b2755a..ff2d114f5 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume.go @@ -208,6 +208,45 @@ func (s *AssignWorkerStep) RetryBackoff() *wait.Backoff { } } +type AttachVolumesStep struct { + store store.Interface +} + +func (s *AttachVolumesStep) Name() string { return "AttachVolumes" } + +func (s *AttachVolumesStep) IsComplete(ctx context.Context, input *ResumeInput, state *ResumeState) (bool, error) { + // TODO replace with a proper check on the volumes. + return state.Actor.GetStatus() == ateapipb.Actor_STATUS_RUNNING, nil +} + +func (s *AttachVolumesStep) Execute(ctx context.Context, input *ResumeInput, state *ResumeState) error { + if state.Actor.GetAteomPodNamespace() == "" { + return fmt.Errorf("actor has no assigned worker pod") + } + + worker, err := s.store.GetWorker(ctx, state.Actor.GetAteomPodNamespace(), state.Actor.GetWorkerPoolName(), state.Actor.GetAteomPodName()) + if err != nil { + return fmt.Errorf("failed to get worker for volume attachment: %w", err) + } + + node := worker.GetNodeName() + if node == "" { + return fmt.Errorf("assigned worker has no node name") + } + + ref := &ateapipb.ObjectRef{Atespace: state.Actor.GetMetadata().GetAtespace(), Name: state.Actor.GetMetadata().GetName()} + for _, vol := range getMountedActorVolumes(ctx, ref, state.Actor.GetActorVolumes(), state.ActorTemplate) { + slog.InfoContext(ctx, "Attaching volume to node", slog.String("volume_id", vol.GetStorageVolumeId()), slog.String("node", node)) + err := getVolumePlugin().AttachVolume(ctx, vol.GetStorageVolumeId(), node) + if err != nil { + return fmt.Errorf("failed to attach volume %q to node %q: %w", vol.GetStorageVolumeId(), node, err) + } + } + return nil +} + +func (s *AttachVolumesStep) RetryBackoff() *wait.Backoff { return nil } + func (s *AssignWorkerStep) findFreeWorker( workers []*ateapipb.Worker, templateClass atev1alpha1.SandboxClass, @@ -261,7 +300,7 @@ func (s *CallAteletRestoreStep) Execute(ctx context.Context, input *ResumeInput, } client := ateletpb.NewAteomHerderClient(ateletConn) - workloadSpec, err := workloadSpecFromActorTemplateWithEnv(ctx, s.kubeClient, s.secretCache, state.ActorTemplate) + workloadSpec, err := workloadSpecFromActorTemplateWithEnv(ctx, s.kubeClient, s.secretCache, state.ActorTemplate, state.Actor) if err != nil { return err } diff --git a/cmd/ateapi/internal/controlapi/workflow_suspend.go b/cmd/ateapi/internal/controlapi/workflow_suspend.go index 28297d1fd..01c5d12d4 100644 --- a/cmd/ateapi/internal/controlapi/workflow_suspend.go +++ b/cmd/ateapi/internal/controlapi/workflow_suspend.go @@ -126,7 +126,10 @@ func (s *CallAteletSuspendStep) Execute(ctx context.Context, input *SuspendInput } client := ateletpb.NewAteomHerderClient(ateletConn) - workloadSpec := workloadSpecFromActorTemplate(state.ActorTemplate) + workloadSpec, err := workloadSpecFromActorTemplate(state.ActorTemplate, state.Actor) + if err != nil { + return err + } // Checkpoint does not carry the sandbox config: atelet uses the version the // actor is currently running (recorded on-node at Run/Restore) and pins it @@ -153,6 +156,51 @@ func (s *CallAteletSuspendStep) Execute(ctx context.Context, input *SuspendInput func (s *CallAteletSuspendStep) RetryBackoff() *wait.Backoff { return nil } +type DetachVolumesStep struct { + store store.Interface +} + +func (s *DetachVolumesStep) Name() string { return "DetachVolumes" } + +func (s *DetachVolumesStep) IsComplete(ctx context.Context, input *SuspendInput, state *SuspendState) (bool, error) { + // TODO replace with a proper check on the volumes. + return state.Actor.GetStatus() == ateapipb.Actor_STATUS_SUSPENDED && state.Actor.GetAteomPodNamespace() == "", nil +} + +func (s *DetachVolumesStep) Execute(ctx context.Context, input *SuspendInput, state *SuspendState) error { + if state.Actor.GetAteomPodNamespace() == "" { + slog.WarnContext(ctx, "Actor has no assigned worker pod during suspend, skipping detach volumes", slog.String("actor_id", input.ActorName)) + return nil + } + + worker, err := s.store.GetWorker(ctx, state.Actor.GetAteomPodNamespace(), state.Actor.GetWorkerPoolName(), state.Actor.GetAteomPodName()) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + slog.WarnContext(ctx, "Worker not found in store during suspend, skipping detach volumes", slog.String("actor_id", input.ActorName)) + return nil + } + return fmt.Errorf("failed to get worker: %w", err) + } + + node := worker.GetNodeName() + if node == "" { + slog.WarnContext(ctx, "Worker has no assigned node name during suspend, skipping detach volumes", slog.String("actor_id", input.ActorName)) + return nil + } + + ref := &ateapipb.ObjectRef{Atespace: state.Actor.GetMetadata().GetAtespace(), Name: state.Actor.GetMetadata().GetName()} + for _, vol := range getMountedActorVolumes(ctx, ref, state.Actor.GetActorVolumes(), state.ActorTemplate) { + slog.InfoContext(ctx, "Detaching volume from node", slog.String("volume_id", vol.GetStorageVolumeId()), slog.String("node", node)) + err := getVolumePlugin().DetachVolume(ctx, vol.GetStorageVolumeId(), node) + if err != nil { + return fmt.Errorf("failed to detach volume %q from node %q: %w", vol.GetStorageVolumeId(), node, err) + } + } + return nil +} + +func (s *DetachVolumesStep) RetryBackoff() *wait.Backoff { return nil } + type FinalizeSuspendedStep struct { store store.Interface } diff --git a/cmd/ateapi/internal/controlapi/workload_spec.go b/cmd/ateapi/internal/controlapi/workload_spec.go index a0d0a806b..eb89339ae 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec.go +++ b/cmd/ateapi/internal/controlapi/workload_spec.go @@ -22,6 +22,7 @@ import ( "github.com/agent-substrate/substrate/internal/proto/ateletpb" atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" corev1 "k8s.io/api/core/v1" @@ -35,7 +36,7 @@ const envSecretCacheTTL = 30 * time.Second // workloadSpecFromActorTemplate builds a WorkloadSpec without resolving // container env vars. Use this when downstream consumers (e.g. checkpoint // requests) don't need env entries materialized. -func workloadSpecFromActorTemplate(actorTemplate *atev1alpha1.ActorTemplate) *ateletpb.WorkloadSpec { +func workloadSpecFromActorTemplate(actorTemplate *atev1alpha1.ActorTemplate, actor *ateapipb.Actor) (*ateletpb.WorkloadSpec, error) { workloadSpec := &ateletpb.WorkloadSpec{ PauseImage: actorTemplate.Spec.PauseImage, } @@ -54,6 +55,10 @@ func workloadSpecFromActorTemplate(actorTemplate *atev1alpha1.ActorTemplate) *at } } + if err := appendExternalVolumes(workloadSpec, actorTemplate, actor); err != nil { + return nil, err + } + for _, ctr := range actorTemplate.Spec.Containers { ateletCtr := &ateletpb.Container{ Name: ctr.Name, @@ -70,14 +75,17 @@ func workloadSpecFromActorTemplate(actorTemplate *atev1alpha1.ActorTemplate) *at workloadSpec.Containers = append(workloadSpec.Containers, ateletCtr) } - return workloadSpec + return workloadSpec, nil } // workloadSpecFromActorTemplateWithEnv builds a WorkloadSpec and resolves each // container's env vars against the cluster. kubeClient must be non-nil; // secretCache is optional and, when supplied, deduplicates Secret reads. -func workloadSpecFromActorTemplateWithEnv(ctx context.Context, kubeClient kubernetes.Interface, secretCache *envSecretCache, actorTemplate *atev1alpha1.ActorTemplate) (*ateletpb.WorkloadSpec, error) { - workloadSpec := workloadSpecFromActorTemplate(actorTemplate) +func workloadSpecFromActorTemplateWithEnv(ctx context.Context, kubeClient kubernetes.Interface, secretCache *envSecretCache, actorTemplate *atev1alpha1.ActorTemplate, actor *ateapipb.Actor) (*ateletpb.WorkloadSpec, error) { + workloadSpec, err := workloadSpecFromActorTemplate(actorTemplate, actor) + if err != nil { + return nil, err + } resolver := envResolver{ kubeClient: kubeClient, @@ -100,6 +108,60 @@ func workloadSpecFromActorTemplateWithEnv(ctx context.Context, kubeClient kubern return workloadSpec, nil } +// appendExternalVolumes maps template external volumes to resolved actor volumes and appends them to workloadSpec +// if they are referenced in container volumeMounts. +func appendExternalVolumes(workloadSpec *ateletpb.WorkloadSpec, template *atev1alpha1.ActorTemplate, actor *ateapipb.Actor) error { + if template == nil { + return nil + } + for _, vol := range template.Spec.Volumes { + if vol.ExternalVolumeTemplate != nil { + if !isVolumeMounted(vol.Name, template) { + continue + } + if actor == nil { + return fmt.Errorf("actor is required when externalVolumeTemplate is present") + } + + var storageVolID string + var volType string + expectedID := actorVolumeID(&ateapipb.ObjectRef{Atespace: actor.GetMetadata().GetAtespace(), Name: actor.GetMetadata().GetName()}, vol.Name) + for _, dbVol := range actor.GetActorVolumes() { + if dbVol.GetActorVolumeId() == expectedID { + storageVolID = dbVol.GetStorageVolumeId() + volType = dbVol.GetVolumeType() + break + } + } + if storageVolID == "" { + return fmt.Errorf("volume %s not found for actor %s", vol.Name, actor.GetMetadata().GetName()) + } + 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, + VolumeType: volType, + }, + }, + }) + } + } + return nil +} + +func isVolumeMounted(volumeName string, template *atev1alpha1.ActorTemplate) bool { + for _, ctr := range template.Spec.Containers { + for _, mount := range ctr.VolumeMounts { + if mount.Name == volumeName { + return true + } + } + } + return false +} + // 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. diff --git a/cmd/ateapi/internal/controlapi/workload_spec_test.go b/cmd/ateapi/internal/controlapi/workload_spec_test.go index 24918f17f..39605cfcb 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec_test.go +++ b/cmd/ateapi/internal/controlapi/workload_spec_test.go @@ -20,6 +20,7 @@ import ( "github.com/agent-substrate/substrate/internal/proto/ateletpb" atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -172,7 +173,10 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := workloadSpecFromActorTemplate(tt.template) + got, err := workloadSpecFromActorTemplate(tt.template, nil) + if err != nil { + t.Fatalf("workloadSpecFromActorTemplate failed: %v", err) + } if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { t.Errorf("WorkloadSpec mismatch (-want +got):\n%s", diff) } @@ -304,7 +308,7 @@ func TestWorkloadSpecFromActorTemplateWithEnv(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { kubeClient := fake.NewSimpleClientset(tt.secrets...) - got, err := workloadSpecFromActorTemplateWithEnv(context.Background(), kubeClient, nil, tt.template) + got, err := workloadSpecFromActorTemplateWithEnv(context.Background(), kubeClient, nil, tt.template, nil) if tt.wantErrCode != codes.OK { if status.Code(err) != tt.wantErrCode { t.Fatalf("error code = %v, want %v: %v", status.Code(err), tt.wantErrCode, err) @@ -322,7 +326,7 @@ func TestWorkloadSpecFromActorTemplateWithEnv(t *testing.T) { } func TestWorkloadSpecFromActorTemplatePropagatesReadyz(t *testing.T) { - got := workloadSpecFromActorTemplate(&atev1alpha1.ActorTemplate{ + got, err := workloadSpecFromActorTemplate(&atev1alpha1.ActorTemplate{ ObjectMeta: metav1.ObjectMeta{Name: "tmpl-readyz", Namespace: "agent-ns"}, Spec: atev1alpha1.ActorTemplateSpec{ Containers: []atev1alpha1.Container{ @@ -339,7 +343,10 @@ func TestWorkloadSpecFromActorTemplatePropagatesReadyz(t *testing.T) { }, }, }, - }) + }, nil) + if err != nil { + t.Fatalf("workloadSpecFromActorTemplate failed: %v", err) + } want := &ateletpb.WorkloadSpec{ Containers: []*ateletpb.Container{ @@ -401,10 +408,10 @@ func TestWorkloadSpecFromActorTemplateWithEnvCachesSecretsAcrossCalls(t *testing }, } - if _, err := workloadSpecFromActorTemplateWithEnv(ctx, kubeClient, secretCache, actorTemplate); err != nil { + if _, err := workloadSpecFromActorTemplateWithEnv(ctx, kubeClient, secretCache, actorTemplate, nil); err != nil { t.Fatalf("first workloadSpecFromActorTemplateWithEnv failed: %v", err) } - if _, err := workloadSpecFromActorTemplateWithEnv(ctx, kubeClient, secretCache, actorTemplate); err != nil { + if _, err := workloadSpecFromActorTemplateWithEnv(ctx, kubeClient, secretCache, actorTemplate, nil); err != nil { t.Fatalf("second workloadSpecFromActorTemplateWithEnv failed: %v", err) } if got := secretGetCount(kubeClient); got != 1 { @@ -412,7 +419,7 @@ func TestWorkloadSpecFromActorTemplateWithEnvCachesSecretsAcrossCalls(t *testing } expireSecretCache(secretCache) - if _, err := workloadSpecFromActorTemplateWithEnv(ctx, kubeClient, secretCache, actorTemplate); err != nil { + if _, err := workloadSpecFromActorTemplateWithEnv(ctx, kubeClient, secretCache, actorTemplate, nil); err != nil { t.Fatalf("third workloadSpecFromActorTemplateWithEnv failed: %v", err) } if got := secretGetCount(kubeClient); got != 2 { @@ -439,3 +446,92 @@ func secretGetCount(kubeClient *fake.Clientset) int { } return count } + +func TestAppendExternalVolumes(t *testing.T) { + template := &atev1alpha1.ActorTemplate{ + Spec: atev1alpha1.ActorTemplateSpec{ + Containers: []atev1alpha1.Container{ + { + Name: "main", + VolumeMounts: []atev1alpha1.VolumeMount{ + {Name: "vol-1", MountPath: "/mnt/vol1"}, + }, + }, + }, + Volumes: []atev1alpha1.Volume{ + { + Name: "vol-1", + VolumeSource: atev1alpha1.VolumeSource{ + ExternalVolumeTemplate: &atev1alpha1.ExternalVolumeTemplate{ + StorageClassName: "pd-standard", + }, + }, + }, + { + Name: "vol-2", + VolumeSource: atev1alpha1.VolumeSource{ + DurableDir: &atev1alpha1.DurableDirVolumeSource{}, + }, + }, + { + Name: "unmounted-vol", + VolumeSource: atev1alpha1.VolumeSource{ + ExternalVolumeTemplate: &atev1alpha1.ExternalVolumeTemplate{ + StorageClassName: "pd-standard", + }, + }, + }, + }, + }, + } + + actor := &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{ + Atespace: "space-abc", + Name: "actor-123", + }, + ActorVolumes: []*ateapipb.ExternalVolume{ + { + ActorVolumeId: "space-abc-actor-123-vol-1", + StorageVolumeId: "vol-gce-pd-123", + VolumeType: "pd-standard", + }, + }, + } + + workloadSpec := &ateletpb.WorkloadSpec{} + if err := appendExternalVolumes(workloadSpec, template, actor); err != nil { + t.Fatalf("appendExternalVolumes unexpected error: %v", err) + } + + want := &ateletpb.WorkloadSpec{ + Volumes: []*ateletpb.Volume{ + { + Name: "vol-1", + Type: ateletpb.VolumeType_VOLUME_TYPE_EXTERNAL, + Source: &ateletpb.Volume_External{ + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "vol-gce-pd-123", + VolumeType: "pd-standard", + }, + }, + }, + }, + } + + if diff := cmp.Diff(want, workloadSpec, protocmp.Transform()); diff != "" { + t.Errorf("appendExternalVolumes mismatch (-want +got):\n%s", diff) + } + + // Test missing mounted volume returns an error + missingActor := &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{ + Atespace: "space-abc", + Name: "actor-123", + }, + ActorVolumes: []*ateapipb.ExternalVolume{}, + } + if err := appendExternalVolumes(&ateletpb.WorkloadSpec{}, template, missingActor); err == nil { + t.Errorf("appendExternalVolumes expected error for missing volume, got nil") + } +} diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index 0350f9fd9..b17ae2b94 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -38,6 +38,55 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type ExternalVolume_Status int32 + +const ( + ExternalVolume_PROVISIONING ExternalVolume_Status = 0 + ExternalVolume_CREATED ExternalVolume_Status = 1 + ExternalVolume_DELETING ExternalVolume_Status = 2 +) + +// Enum value maps for ExternalVolume_Status. +var ( + ExternalVolume_Status_name = map[int32]string{ + 0: "PROVISIONING", + 1: "CREATED", + 2: "DELETING", + } + ExternalVolume_Status_value = map[string]int32{ + "PROVISIONING": 0, + "CREATED": 1, + "DELETING": 2, + } +) + +func (x ExternalVolume_Status) Enum() *ExternalVolume_Status { + p := new(ExternalVolume_Status) + *p = x + return p +} + +func (x ExternalVolume_Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ExternalVolume_Status) Descriptor() protoreflect.EnumDescriptor { + return file_ateapi_proto_enumTypes[0].Descriptor() +} + +func (ExternalVolume_Status) Type() protoreflect.EnumType { + return &file_ateapi_proto_enumTypes[0] +} + +func (x ExternalVolume_Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ExternalVolume_Status.Descriptor instead. +func (ExternalVolume_Status) EnumDescriptor() ([]byte, []int) { + return file_ateapi_proto_rawDescGZIP(), []int{5, 0} +} + type Actor_Status int32 const ( @@ -86,11 +135,11 @@ func (x Actor_Status) String() string { } func (Actor_Status) Descriptor() protoreflect.EnumDescriptor { - return file_ateapi_proto_enumTypes[0].Descriptor() + return file_ateapi_proto_enumTypes[1].Descriptor() } func (Actor_Status) Type() protoreflect.EnumType { - return &file_ateapi_proto_enumTypes[0] + return &file_ateapi_proto_enumTypes[1] } func (x Actor_Status) Number() protoreflect.EnumNumber { @@ -99,7 +148,7 @@ func (x Actor_Status) Number() protoreflect.EnumNumber { // Deprecated: Use Actor_Status.Descriptor instead. func (Actor_Status) EnumDescriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{5, 0} + return file_ateapi_proto_rawDescGZIP(), []int{6, 0} } type ExternalSnapshotInfo struct { @@ -421,6 +470,77 @@ func (x *ResourceMetadata) GetUpdateTime() *timestamppb.Timestamp { return nil } +type ExternalVolume struct { + state protoimpl.MessageState `protogen:"open.v1"` + // actor_id + the volume name specified in the actor template. + ActorVolumeId string `protobuf:"bytes,1,opt,name=actor_volume_id,json=actorVolumeId,proto3" json:"actor_volume_id,omitempty"` + // The volume_id returned from the storage system. + StorageVolumeId string `protobuf:"bytes,2,opt,name=storage_volume_id,json=storageVolumeId,proto3" json:"storage_volume_id,omitempty"` + // Internal volume plugin name or CSI driver name. + VolumeType string `protobuf:"bytes,3,opt,name=volume_type,json=volumeType,proto3" json:"volume_type,omitempty"` + Status ExternalVolume_Status `protobuf:"varint,4,opt,name=status,proto3,enum=ateapi.ExternalVolume_Status" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExternalVolume) Reset() { + *x = ExternalVolume{} + mi := &file_ateapi_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExternalVolume) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExternalVolume) ProtoMessage() {} + +func (x *ExternalVolume) ProtoReflect() protoreflect.Message { + mi := &file_ateapi_proto_msgTypes[5] + 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 ExternalVolume.ProtoReflect.Descriptor instead. +func (*ExternalVolume) Descriptor() ([]byte, []int) { + return file_ateapi_proto_rawDescGZIP(), []int{5} +} + +func (x *ExternalVolume) GetActorVolumeId() string { + if x != nil { + return x.ActorVolumeId + } + return "" +} + +func (x *ExternalVolume) GetStorageVolumeId() string { + if x != nil { + return x.StorageVolumeId + } + return "" +} + +func (x *ExternalVolume) GetVolumeType() string { + if x != nil { + return x.VolumeType + } + return "" +} + +func (x *ExternalVolume) GetStatus() ExternalVolume_Status { + if x != nil { + return x.Status + } + return ExternalVolume_PROVISIONING +} + type Actor struct { state protoimpl.MessageState `protogen:"open.v1"` // Common resource metadata: atespace, name, uid, version, timestamps. @@ -446,13 +566,16 @@ type Actor struct { // suspend/pause since eligibility is no longer a single fixed pool // reference on the ActorTemplate. WorkerPoolName string `protobuf:"bytes,12,opt,name=worker_pool_name,json=workerPoolName,proto3" json:"worker_pool_name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Volumes attached to the actor. These volumes only live as long as the actor. + // They are deleted when the actor is deleted. + ActorVolumes []*ExternalVolume `protobuf:"bytes,16,rep,name=actor_volumes,json=actorVolumes,proto3" json:"actor_volumes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Actor) Reset() { *x = Actor{} - mi := &file_ateapi_proto_msgTypes[5] + mi := &file_ateapi_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -464,7 +587,7 @@ func (x *Actor) String() string { func (*Actor) ProtoMessage() {} func (x *Actor) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[5] + mi := &file_ateapi_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -477,7 +600,7 @@ func (x *Actor) ProtoReflect() protoreflect.Message { // Deprecated: Use Actor.ProtoReflect.Descriptor instead. func (*Actor) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{5} + return file_ateapi_proto_rawDescGZIP(), []int{6} } func (x *Actor) GetMetadata() *ResourceMetadata { @@ -564,6 +687,13 @@ func (x *Actor) GetWorkerPoolName() string { return "" } +func (x *Actor) GetActorVolumes() []*ExternalVolume { + if x != nil { + return x.ActorVolumes + } + return nil +} + // Atespace is the isolation boundary an Actor is created into. Global-scoped: // metadata.atespace is always empty; the atespace's identity is metadata.name. type Atespace struct { @@ -576,7 +706,7 @@ type Atespace struct { func (x *Atespace) Reset() { *x = Atespace{} - mi := &file_ateapi_proto_msgTypes[6] + mi := &file_ateapi_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -588,7 +718,7 @@ func (x *Atespace) String() string { func (*Atespace) ProtoMessage() {} func (x *Atespace) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[6] + mi := &file_ateapi_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -601,7 +731,7 @@ func (x *Atespace) ProtoReflect() protoreflect.Message { // Deprecated: Use Atespace.ProtoReflect.Descriptor instead. func (*Atespace) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{6} + return file_ateapi_proto_rawDescGZIP(), []int{7} } func (x *Atespace) GetMetadata() *ResourceMetadata { @@ -625,7 +755,7 @@ type ObjectRef struct { func (x *ObjectRef) Reset() { *x = ObjectRef{} - mi := &file_ateapi_proto_msgTypes[7] + mi := &file_ateapi_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -637,7 +767,7 @@ func (x *ObjectRef) String() string { func (*ObjectRef) ProtoMessage() {} func (x *ObjectRef) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[7] + mi := &file_ateapi_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -650,7 +780,7 @@ func (x *ObjectRef) ProtoReflect() protoreflect.Message { // Deprecated: Use ObjectRef.ProtoReflect.Descriptor instead. func (*ObjectRef) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{7} + return file_ateapi_proto_rawDescGZIP(), []int{8} } func (x *ObjectRef) GetAtespace() string { @@ -677,7 +807,7 @@ type CreateAtespaceRequest struct { func (x *CreateAtespaceRequest) Reset() { *x = CreateAtespaceRequest{} - mi := &file_ateapi_proto_msgTypes[8] + mi := &file_ateapi_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -689,7 +819,7 @@ func (x *CreateAtespaceRequest) String() string { func (*CreateAtespaceRequest) ProtoMessage() {} func (x *CreateAtespaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[8] + mi := &file_ateapi_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -702,7 +832,7 @@ func (x *CreateAtespaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateAtespaceRequest.ProtoReflect.Descriptor instead. func (*CreateAtespaceRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{8} + return file_ateapi_proto_rawDescGZIP(), []int{9} } func (x *CreateAtespaceRequest) GetAtespace() *Atespace { @@ -721,7 +851,7 @@ type GetAtespaceRequest struct { func (x *GetAtespaceRequest) Reset() { *x = GetAtespaceRequest{} - mi := &file_ateapi_proto_msgTypes[9] + mi := &file_ateapi_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -733,7 +863,7 @@ func (x *GetAtespaceRequest) String() string { func (*GetAtespaceRequest) ProtoMessage() {} func (x *GetAtespaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[9] + mi := &file_ateapi_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -746,7 +876,7 @@ func (x *GetAtespaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetAtespaceRequest.ProtoReflect.Descriptor instead. func (*GetAtespaceRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{9} + return file_ateapi_proto_rawDescGZIP(), []int{10} } func (x *GetAtespaceRequest) GetAtespace() *ObjectRef { @@ -764,7 +894,7 @@ type ListAtespacesRequest struct { func (x *ListAtespacesRequest) Reset() { *x = ListAtespacesRequest{} - mi := &file_ateapi_proto_msgTypes[10] + mi := &file_ateapi_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -776,7 +906,7 @@ func (x *ListAtespacesRequest) String() string { func (*ListAtespacesRequest) ProtoMessage() {} func (x *ListAtespacesRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[10] + mi := &file_ateapi_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -789,7 +919,7 @@ func (x *ListAtespacesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAtespacesRequest.ProtoReflect.Descriptor instead. func (*ListAtespacesRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{10} + return file_ateapi_proto_rawDescGZIP(), []int{11} } type ListAtespacesResponse struct { @@ -801,7 +931,7 @@ type ListAtespacesResponse struct { func (x *ListAtespacesResponse) Reset() { *x = ListAtespacesResponse{} - mi := &file_ateapi_proto_msgTypes[11] + mi := &file_ateapi_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -813,7 +943,7 @@ func (x *ListAtespacesResponse) String() string { func (*ListAtespacesResponse) ProtoMessage() {} func (x *ListAtespacesResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[11] + mi := &file_ateapi_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -826,7 +956,7 @@ func (x *ListAtespacesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAtespacesResponse.ProtoReflect.Descriptor instead. func (*ListAtespacesResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{11} + return file_ateapi_proto_rawDescGZIP(), []int{12} } func (x *ListAtespacesResponse) GetAtespaces() []*Atespace { @@ -845,7 +975,7 @@ type DeleteAtespaceRequest struct { func (x *DeleteAtespaceRequest) Reset() { *x = DeleteAtespaceRequest{} - mi := &file_ateapi_proto_msgTypes[12] + mi := &file_ateapi_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -857,7 +987,7 @@ func (x *DeleteAtespaceRequest) String() string { func (*DeleteAtespaceRequest) ProtoMessage() {} func (x *DeleteAtespaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[12] + mi := &file_ateapi_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -870,7 +1000,7 @@ func (x *DeleteAtespaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteAtespaceRequest.ProtoReflect.Descriptor instead. func (*DeleteAtespaceRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{12} + return file_ateapi_proto_rawDescGZIP(), []int{13} } func (x *DeleteAtespaceRequest) GetAtespace() *ObjectRef { @@ -889,7 +1019,7 @@ type GetActorRequest struct { func (x *GetActorRequest) Reset() { *x = GetActorRequest{} - mi := &file_ateapi_proto_msgTypes[13] + mi := &file_ateapi_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -901,7 +1031,7 @@ func (x *GetActorRequest) String() string { func (*GetActorRequest) ProtoMessage() {} func (x *GetActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[13] + mi := &file_ateapi_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -914,7 +1044,7 @@ func (x *GetActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActorRequest.ProtoReflect.Descriptor instead. func (*GetActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{13} + return file_ateapi_proto_rawDescGZIP(), []int{14} } func (x *GetActorRequest) GetActor() *ObjectRef { @@ -935,7 +1065,7 @@ type CreateActorRequest struct { func (x *CreateActorRequest) Reset() { *x = CreateActorRequest{} - mi := &file_ateapi_proto_msgTypes[14] + mi := &file_ateapi_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -947,7 +1077,7 @@ func (x *CreateActorRequest) String() string { func (*CreateActorRequest) ProtoMessage() {} func (x *CreateActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[14] + mi := &file_ateapi_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -960,7 +1090,7 @@ func (x *CreateActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateActorRequest.ProtoReflect.Descriptor instead. func (*CreateActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{14} + return file_ateapi_proto_rawDescGZIP(), []int{15} } func (x *CreateActorRequest) GetActor() *Actor { @@ -985,7 +1115,7 @@ type UpdateActorRequest struct { func (x *UpdateActorRequest) Reset() { *x = UpdateActorRequest{} - mi := &file_ateapi_proto_msgTypes[15] + mi := &file_ateapi_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -997,7 +1127,7 @@ func (x *UpdateActorRequest) String() string { func (*UpdateActorRequest) ProtoMessage() {} func (x *UpdateActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[15] + mi := &file_ateapi_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1010,7 +1140,7 @@ func (x *UpdateActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateActorRequest.ProtoReflect.Descriptor instead. func (*UpdateActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{15} + return file_ateapi_proto_rawDescGZIP(), []int{16} } func (x *UpdateActorRequest) GetActor() *ObjectRef { @@ -1036,7 +1166,7 @@ type UpdateActorResponse struct { func (x *UpdateActorResponse) Reset() { *x = UpdateActorResponse{} - mi := &file_ateapi_proto_msgTypes[16] + mi := &file_ateapi_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1048,7 +1178,7 @@ func (x *UpdateActorResponse) String() string { func (*UpdateActorResponse) ProtoMessage() {} func (x *UpdateActorResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[16] + mi := &file_ateapi_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1061,7 +1191,7 @@ func (x *UpdateActorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateActorResponse.ProtoReflect.Descriptor instead. func (*UpdateActorResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{16} + return file_ateapi_proto_rawDescGZIP(), []int{17} } func (x *UpdateActorResponse) GetActor() *Actor { @@ -1080,7 +1210,7 @@ type SuspendActorRequest struct { func (x *SuspendActorRequest) Reset() { *x = SuspendActorRequest{} - mi := &file_ateapi_proto_msgTypes[17] + mi := &file_ateapi_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1092,7 +1222,7 @@ func (x *SuspendActorRequest) String() string { func (*SuspendActorRequest) ProtoMessage() {} func (x *SuspendActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[17] + mi := &file_ateapi_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1105,7 +1235,7 @@ func (x *SuspendActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SuspendActorRequest.ProtoReflect.Descriptor instead. func (*SuspendActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{17} + return file_ateapi_proto_rawDescGZIP(), []int{18} } func (x *SuspendActorRequest) GetActor() *ObjectRef { @@ -1124,7 +1254,7 @@ type SuspendActorResponse struct { func (x *SuspendActorResponse) Reset() { *x = SuspendActorResponse{} - mi := &file_ateapi_proto_msgTypes[18] + mi := &file_ateapi_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1136,7 +1266,7 @@ func (x *SuspendActorResponse) String() string { func (*SuspendActorResponse) ProtoMessage() {} func (x *SuspendActorResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[18] + mi := &file_ateapi_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1149,7 +1279,7 @@ func (x *SuspendActorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SuspendActorResponse.ProtoReflect.Descriptor instead. func (*SuspendActorResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{18} + return file_ateapi_proto_rawDescGZIP(), []int{19} } func (x *SuspendActorResponse) GetActor() *Actor { @@ -1168,7 +1298,7 @@ type PauseActorRequest struct { func (x *PauseActorRequest) Reset() { *x = PauseActorRequest{} - mi := &file_ateapi_proto_msgTypes[19] + mi := &file_ateapi_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1180,7 +1310,7 @@ func (x *PauseActorRequest) String() string { func (*PauseActorRequest) ProtoMessage() {} func (x *PauseActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[19] + mi := &file_ateapi_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1193,7 +1323,7 @@ func (x *PauseActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PauseActorRequest.ProtoReflect.Descriptor instead. func (*PauseActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{19} + return file_ateapi_proto_rawDescGZIP(), []int{20} } func (x *PauseActorRequest) GetActor() *ObjectRef { @@ -1212,7 +1342,7 @@ type PauseActorResponse struct { func (x *PauseActorResponse) Reset() { *x = PauseActorResponse{} - mi := &file_ateapi_proto_msgTypes[20] + mi := &file_ateapi_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1224,7 +1354,7 @@ func (x *PauseActorResponse) String() string { func (*PauseActorResponse) ProtoMessage() {} func (x *PauseActorResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[20] + mi := &file_ateapi_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1237,7 +1367,7 @@ func (x *PauseActorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PauseActorResponse.ProtoReflect.Descriptor instead. func (*PauseActorResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{20} + return file_ateapi_proto_rawDescGZIP(), []int{21} } func (x *PauseActorResponse) GetActor() *Actor { @@ -1258,7 +1388,7 @@ type ResumeActorRequest struct { func (x *ResumeActorRequest) Reset() { *x = ResumeActorRequest{} - mi := &file_ateapi_proto_msgTypes[21] + mi := &file_ateapi_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1270,7 +1400,7 @@ func (x *ResumeActorRequest) String() string { func (*ResumeActorRequest) ProtoMessage() {} func (x *ResumeActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[21] + mi := &file_ateapi_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1283,7 +1413,7 @@ func (x *ResumeActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResumeActorRequest.ProtoReflect.Descriptor instead. func (*ResumeActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{21} + return file_ateapi_proto_rawDescGZIP(), []int{22} } func (x *ResumeActorRequest) GetActor() *ObjectRef { @@ -1309,7 +1439,7 @@ type ResumeActorResponse struct { func (x *ResumeActorResponse) Reset() { *x = ResumeActorResponse{} - mi := &file_ateapi_proto_msgTypes[22] + mi := &file_ateapi_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1321,7 +1451,7 @@ func (x *ResumeActorResponse) String() string { func (*ResumeActorResponse) ProtoMessage() {} func (x *ResumeActorResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[22] + mi := &file_ateapi_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1334,7 +1464,7 @@ func (x *ResumeActorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResumeActorResponse.ProtoReflect.Descriptor instead. func (*ResumeActorResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{22} + return file_ateapi_proto_rawDescGZIP(), []int{23} } func (x *ResumeActorResponse) GetActor() *Actor { @@ -1353,7 +1483,7 @@ type DeleteActorRequest struct { func (x *DeleteActorRequest) Reset() { *x = DeleteActorRequest{} - mi := &file_ateapi_proto_msgTypes[23] + mi := &file_ateapi_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1365,7 +1495,7 @@ func (x *DeleteActorRequest) String() string { func (*DeleteActorRequest) ProtoMessage() {} func (x *DeleteActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[23] + mi := &file_ateapi_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1378,7 +1508,7 @@ func (x *DeleteActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteActorRequest.ProtoReflect.Descriptor instead. func (*DeleteActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{23} + return file_ateapi_proto_rawDescGZIP(), []int{24} } func (x *DeleteActorRequest) GetActor() *ObjectRef { @@ -1396,7 +1526,7 @@ type ListWorkersRequest struct { func (x *ListWorkersRequest) Reset() { *x = ListWorkersRequest{} - mi := &file_ateapi_proto_msgTypes[24] + mi := &file_ateapi_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1408,7 +1538,7 @@ func (x *ListWorkersRequest) String() string { func (*ListWorkersRequest) ProtoMessage() {} func (x *ListWorkersRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[24] + mi := &file_ateapi_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1421,7 +1551,7 @@ func (x *ListWorkersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkersRequest.ProtoReflect.Descriptor instead. func (*ListWorkersRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{24} + return file_ateapi_proto_rawDescGZIP(), []int{25} } type ListWorkersResponse struct { @@ -1433,7 +1563,7 @@ type ListWorkersResponse struct { func (x *ListWorkersResponse) Reset() { *x = ListWorkersResponse{} - mi := &file_ateapi_proto_msgTypes[25] + mi := &file_ateapi_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1445,7 +1575,7 @@ func (x *ListWorkersResponse) String() string { func (*ListWorkersResponse) ProtoMessage() {} func (x *ListWorkersResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[25] + mi := &file_ateapi_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1458,7 +1588,7 @@ func (x *ListWorkersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkersResponse.ProtoReflect.Descriptor instead. func (*ListWorkersResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{25} + return file_ateapi_proto_rawDescGZIP(), []int{26} } func (x *ListWorkersResponse) GetWorkers() []*Worker { @@ -1487,7 +1617,7 @@ type ListActorsRequest struct { func (x *ListActorsRequest) Reset() { *x = ListActorsRequest{} - mi := &file_ateapi_proto_msgTypes[26] + mi := &file_ateapi_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1499,7 +1629,7 @@ func (x *ListActorsRequest) String() string { func (*ListActorsRequest) ProtoMessage() {} func (x *ListActorsRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[26] + mi := &file_ateapi_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1512,7 +1642,7 @@ func (x *ListActorsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListActorsRequest.ProtoReflect.Descriptor instead. func (*ListActorsRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{26} + return file_ateapi_proto_rawDescGZIP(), []int{27} } func (x *ListActorsRequest) GetPageSize() int32 { @@ -1550,7 +1680,7 @@ type ListActorsResponse struct { func (x *ListActorsResponse) Reset() { *x = ListActorsResponse{} - mi := &file_ateapi_proto_msgTypes[27] + mi := &file_ateapi_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1562,7 +1692,7 @@ func (x *ListActorsResponse) String() string { func (*ListActorsResponse) ProtoMessage() {} func (x *ListActorsResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[27] + mi := &file_ateapi_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1575,7 +1705,7 @@ func (x *ListActorsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListActorsResponse.ProtoReflect.Descriptor instead. func (*ListActorsResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{27} + return file_ateapi_proto_rawDescGZIP(), []int{28} } func (x *ListActorsResponse) GetActors() []*Actor { @@ -1610,7 +1740,7 @@ type Worker struct { func (x *Worker) Reset() { *x = Worker{} - mi := &file_ateapi_proto_msgTypes[28] + mi := &file_ateapi_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1622,7 +1752,7 @@ func (x *Worker) String() string { func (*Worker) ProtoMessage() {} func (x *Worker) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[28] + mi := &file_ateapi_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1635,7 +1765,7 @@ func (x *Worker) ProtoReflect() protoreflect.Message { // Deprecated: Use Worker.ProtoReflect.Descriptor instead. func (*Worker) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{28} + return file_ateapi_proto_rawDescGZIP(), []int{29} } func (x *Worker) GetWorkerNamespace() string { @@ -1718,7 +1848,7 @@ type Assignment struct { func (x *Assignment) Reset() { *x = Assignment{} - mi := &file_ateapi_proto_msgTypes[29] + mi := &file_ateapi_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1730,7 +1860,7 @@ func (x *Assignment) String() string { func (*Assignment) ProtoMessage() {} func (x *Assignment) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[29] + mi := &file_ateapi_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1743,7 +1873,7 @@ func (x *Assignment) ProtoReflect() protoreflect.Message { // Deprecated: Use Assignment.ProtoReflect.Descriptor instead. func (*Assignment) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{29} + return file_ateapi_proto_rawDescGZIP(), []int{30} } func (x *Assignment) GetActorTemplate() *KubeNamespacedObjectRef { @@ -1770,7 +1900,7 @@ type KubeNamespacedObjectRef struct { func (x *KubeNamespacedObjectRef) Reset() { *x = KubeNamespacedObjectRef{} - mi := &file_ateapi_proto_msgTypes[30] + mi := &file_ateapi_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1782,7 +1912,7 @@ func (x *KubeNamespacedObjectRef) String() string { func (*KubeNamespacedObjectRef) ProtoMessage() {} func (x *KubeNamespacedObjectRef) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[30] + mi := &file_ateapi_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1795,7 +1925,7 @@ func (x *KubeNamespacedObjectRef) ProtoReflect() protoreflect.Message { // Deprecated: Use KubeNamespacedObjectRef.ProtoReflect.Descriptor instead. func (*KubeNamespacedObjectRef) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{30} + return file_ateapi_proto_rawDescGZIP(), []int{31} } func (x *KubeNamespacedObjectRef) GetNamespace() string { @@ -1820,7 +1950,7 @@ type DebugClearRequest struct { func (x *DebugClearRequest) Reset() { *x = DebugClearRequest{} - mi := &file_ateapi_proto_msgTypes[31] + mi := &file_ateapi_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1832,7 +1962,7 @@ func (x *DebugClearRequest) String() string { func (*DebugClearRequest) ProtoMessage() {} func (x *DebugClearRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[31] + mi := &file_ateapi_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1845,7 +1975,7 @@ func (x *DebugClearRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DebugClearRequest.ProtoReflect.Descriptor instead. func (*DebugClearRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{31} + return file_ateapi_proto_rawDescGZIP(), []int{32} } type DebugClearResponse struct { @@ -1856,7 +1986,7 @@ type DebugClearResponse struct { func (x *DebugClearResponse) Reset() { *x = DebugClearResponse{} - mi := &file_ateapi_proto_msgTypes[32] + mi := &file_ateapi_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1868,7 +1998,7 @@ func (x *DebugClearResponse) String() string { func (*DebugClearResponse) ProtoMessage() {} func (x *DebugClearResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[32] + mi := &file_ateapi_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1881,7 +2011,7 @@ func (x *DebugClearResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DebugClearResponse.ProtoReflect.Descriptor instead. func (*DebugClearResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{32} + return file_ateapi_proto_rawDescGZIP(), []int{33} } type MintJWTRequest struct { @@ -1896,7 +2026,7 @@ type MintJWTRequest struct { func (x *MintJWTRequest) Reset() { *x = MintJWTRequest{} - mi := &file_ateapi_proto_msgTypes[33] + mi := &file_ateapi_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1908,7 +2038,7 @@ func (x *MintJWTRequest) String() string { func (*MintJWTRequest) ProtoMessage() {} func (x *MintJWTRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[33] + mi := &file_ateapi_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1921,7 +2051,7 @@ func (x *MintJWTRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MintJWTRequest.ProtoReflect.Descriptor instead. func (*MintJWTRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{33} + return file_ateapi_proto_rawDescGZIP(), []int{34} } func (x *MintJWTRequest) GetAudience() []string { @@ -1981,7 +2111,7 @@ type MintJWTResponse struct { func (x *MintJWTResponse) Reset() { *x = MintJWTResponse{} - mi := &file_ateapi_proto_msgTypes[34] + mi := &file_ateapi_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1993,7 +2123,7 @@ func (x *MintJWTResponse) String() string { func (*MintJWTResponse) ProtoMessage() {} func (x *MintJWTResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[34] + mi := &file_ateapi_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2006,7 +2136,7 @@ func (x *MintJWTResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MintJWTResponse.ProtoReflect.Descriptor instead. func (*MintJWTResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{34} + return file_ateapi_proto_rawDescGZIP(), []int{35} } func (x *MintJWTResponse) GetSessionJwt() string { @@ -2031,7 +2161,7 @@ type MintCertRequest struct { func (x *MintCertRequest) Reset() { *x = MintCertRequest{} - mi := &file_ateapi_proto_msgTypes[35] + mi := &file_ateapi_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2043,7 +2173,7 @@ func (x *MintCertRequest) String() string { func (*MintCertRequest) ProtoMessage() {} func (x *MintCertRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[35] + mi := &file_ateapi_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2056,7 +2186,7 @@ func (x *MintCertRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MintCertRequest.ProtoReflect.Descriptor instead. func (*MintCertRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{35} + return file_ateapi_proto_rawDescGZIP(), []int{36} } func (x *MintCertRequest) GetAppId() string { @@ -2099,7 +2229,7 @@ type MintCertResponse struct { func (x *MintCertResponse) Reset() { *x = MintCertResponse{} - mi := &file_ateapi_proto_msgTypes[36] + mi := &file_ateapi_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2111,7 +2241,7 @@ func (x *MintCertResponse) String() string { func (*MintCertResponse) ProtoMessage() {} func (x *MintCertResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[36] + mi := &file_ateapi_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2124,7 +2254,7 @@ func (x *MintCertResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MintCertResponse.ProtoReflect.Descriptor instead. func (*MintCertResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{36} + return file_ateapi_proto_rawDescGZIP(), []int{37} } func (x *MintCertResponse) GetSessionCertificates() [][]byte { @@ -2161,7 +2291,17 @@ const file_ateapi_proto_rawDesc = "" + "\vcreate_time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\n" + "createTime\x12;\n" + "\vupdate_time\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\n" + - "updateTime\"\x84\x06\n" + + "updateTime\"\xf3\x01\n" + + "\x0eExternalVolume\x12&\n" + + "\x0factor_volume_id\x18\x01 \x01(\tR\ractorVolumeId\x12*\n" + + "\x11storage_volume_id\x18\x02 \x01(\tR\x0fstorageVolumeId\x12\x1f\n" + + "\vvolume_type\x18\x03 \x01(\tR\n" + + "volumeType\x125\n" + + "\x06status\x18\x04 \x01(\x0e2\x1d.ateapi.ExternalVolume.StatusR\x06status\"5\n" + + "\x06Status\x12\x10\n" + + "\fPROVISIONING\x10\x00\x12\v\n" + + "\aCREATED\x10\x01\x12\f\n" + + "\bDELETING\x10\x02\"\xc1\x06\n" + "\x05Actor\x124\n" + "\bmetadata\x18\x01 \x01(\v2\x18.ateapi.ResourceMetadataR\bmetadata\x128\n" + "\x18actor_template_namespace\x18\x02 \x01(\tR\x16actorTemplateNamespace\x12.\n" + @@ -2176,7 +2316,8 @@ const file_ateapi_proto_rawDesc = "" + "\x14latest_snapshot_info\x18\n" + " \x01(\v2\x14.ateapi.SnapshotInfoR\x12latestSnapshotInfo\x129\n" + "\x0fworker_selector\x18\v \x01(\v2\x10.ateapi.SelectorR\x0eworkerSelector\x12(\n" + - "\x10worker_pool_name\x18\f \x01(\tR\x0eworkerPoolName\"\xb1\x01\n" + + "\x10worker_pool_name\x18\f \x01(\tR\x0eworkerPoolName\x12;\n" + + "\ractor_volumes\x18\x10 \x03(\v2\x16.ateapi.ExternalVolumeR\factorVolumes\"\xb1\x01\n" + "\x06Status\x12\x16\n" + "\x12STATUS_UNSPECIFIED\x10\x00\x12\x13\n" + "\x0fSTATUS_RESUMING\x10\x01\x12\x12\n" + @@ -2314,121 +2455,125 @@ func file_ateapi_proto_rawDescGZIP() []byte { return file_ateapi_proto_rawDescData } -var file_ateapi_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_ateapi_proto_msgTypes = make([]protoimpl.MessageInfo, 39) +var file_ateapi_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_ateapi_proto_msgTypes = make([]protoimpl.MessageInfo, 40) var file_ateapi_proto_goTypes = []any{ - (Actor_Status)(0), // 0: ateapi.Actor.Status - (*ExternalSnapshotInfo)(nil), // 1: ateapi.ExternalSnapshotInfo - (*LocalSnapshotInfo)(nil), // 2: ateapi.LocalSnapshotInfo - (*SnapshotInfo)(nil), // 3: ateapi.SnapshotInfo - (*Selector)(nil), // 4: ateapi.Selector - (*ResourceMetadata)(nil), // 5: ateapi.ResourceMetadata - (*Actor)(nil), // 6: ateapi.Actor - (*Atespace)(nil), // 7: ateapi.Atespace - (*ObjectRef)(nil), // 8: ateapi.ObjectRef - (*CreateAtespaceRequest)(nil), // 9: ateapi.CreateAtespaceRequest - (*GetAtespaceRequest)(nil), // 10: ateapi.GetAtespaceRequest - (*ListAtespacesRequest)(nil), // 11: ateapi.ListAtespacesRequest - (*ListAtespacesResponse)(nil), // 12: ateapi.ListAtespacesResponse - (*DeleteAtespaceRequest)(nil), // 13: ateapi.DeleteAtespaceRequest - (*GetActorRequest)(nil), // 14: ateapi.GetActorRequest - (*CreateActorRequest)(nil), // 15: ateapi.CreateActorRequest - (*UpdateActorRequest)(nil), // 16: ateapi.UpdateActorRequest - (*UpdateActorResponse)(nil), // 17: ateapi.UpdateActorResponse - (*SuspendActorRequest)(nil), // 18: ateapi.SuspendActorRequest - (*SuspendActorResponse)(nil), // 19: ateapi.SuspendActorResponse - (*PauseActorRequest)(nil), // 20: ateapi.PauseActorRequest - (*PauseActorResponse)(nil), // 21: ateapi.PauseActorResponse - (*ResumeActorRequest)(nil), // 22: ateapi.ResumeActorRequest - (*ResumeActorResponse)(nil), // 23: ateapi.ResumeActorResponse - (*DeleteActorRequest)(nil), // 24: ateapi.DeleteActorRequest - (*ListWorkersRequest)(nil), // 25: ateapi.ListWorkersRequest - (*ListWorkersResponse)(nil), // 26: ateapi.ListWorkersResponse - (*ListActorsRequest)(nil), // 27: ateapi.ListActorsRequest - (*ListActorsResponse)(nil), // 28: ateapi.ListActorsResponse - (*Worker)(nil), // 29: ateapi.Worker - (*Assignment)(nil), // 30: ateapi.Assignment - (*KubeNamespacedObjectRef)(nil), // 31: ateapi.KubeNamespacedObjectRef - (*DebugClearRequest)(nil), // 32: ateapi.DebugClearRequest - (*DebugClearResponse)(nil), // 33: ateapi.DebugClearResponse - (*MintJWTRequest)(nil), // 34: ateapi.MintJWTRequest - (*MintJWTResponse)(nil), // 35: ateapi.MintJWTResponse - (*MintCertRequest)(nil), // 36: ateapi.MintCertRequest - (*MintCertResponse)(nil), // 37: ateapi.MintCertResponse - nil, // 38: ateapi.Selector.MatchLabelsEntry - nil, // 39: ateapi.Worker.LabelsEntry - (*timestamppb.Timestamp)(nil), // 40: google.protobuf.Timestamp + (ExternalVolume_Status)(0), // 0: ateapi.ExternalVolume.Status + (Actor_Status)(0), // 1: ateapi.Actor.Status + (*ExternalSnapshotInfo)(nil), // 2: ateapi.ExternalSnapshotInfo + (*LocalSnapshotInfo)(nil), // 3: ateapi.LocalSnapshotInfo + (*SnapshotInfo)(nil), // 4: ateapi.SnapshotInfo + (*Selector)(nil), // 5: ateapi.Selector + (*ResourceMetadata)(nil), // 6: ateapi.ResourceMetadata + (*ExternalVolume)(nil), // 7: ateapi.ExternalVolume + (*Actor)(nil), // 8: ateapi.Actor + (*Atespace)(nil), // 9: ateapi.Atespace + (*ObjectRef)(nil), // 10: ateapi.ObjectRef + (*CreateAtespaceRequest)(nil), // 11: ateapi.CreateAtespaceRequest + (*GetAtespaceRequest)(nil), // 12: ateapi.GetAtespaceRequest + (*ListAtespacesRequest)(nil), // 13: ateapi.ListAtespacesRequest + (*ListAtespacesResponse)(nil), // 14: ateapi.ListAtespacesResponse + (*DeleteAtespaceRequest)(nil), // 15: ateapi.DeleteAtespaceRequest + (*GetActorRequest)(nil), // 16: ateapi.GetActorRequest + (*CreateActorRequest)(nil), // 17: ateapi.CreateActorRequest + (*UpdateActorRequest)(nil), // 18: ateapi.UpdateActorRequest + (*UpdateActorResponse)(nil), // 19: ateapi.UpdateActorResponse + (*SuspendActorRequest)(nil), // 20: ateapi.SuspendActorRequest + (*SuspendActorResponse)(nil), // 21: ateapi.SuspendActorResponse + (*PauseActorRequest)(nil), // 22: ateapi.PauseActorRequest + (*PauseActorResponse)(nil), // 23: ateapi.PauseActorResponse + (*ResumeActorRequest)(nil), // 24: ateapi.ResumeActorRequest + (*ResumeActorResponse)(nil), // 25: ateapi.ResumeActorResponse + (*DeleteActorRequest)(nil), // 26: ateapi.DeleteActorRequest + (*ListWorkersRequest)(nil), // 27: ateapi.ListWorkersRequest + (*ListWorkersResponse)(nil), // 28: ateapi.ListWorkersResponse + (*ListActorsRequest)(nil), // 29: ateapi.ListActorsRequest + (*ListActorsResponse)(nil), // 30: ateapi.ListActorsResponse + (*Worker)(nil), // 31: ateapi.Worker + (*Assignment)(nil), // 32: ateapi.Assignment + (*KubeNamespacedObjectRef)(nil), // 33: ateapi.KubeNamespacedObjectRef + (*DebugClearRequest)(nil), // 34: ateapi.DebugClearRequest + (*DebugClearResponse)(nil), // 35: ateapi.DebugClearResponse + (*MintJWTRequest)(nil), // 36: ateapi.MintJWTRequest + (*MintJWTResponse)(nil), // 37: ateapi.MintJWTResponse + (*MintCertRequest)(nil), // 38: ateapi.MintCertRequest + (*MintCertResponse)(nil), // 39: ateapi.MintCertResponse + nil, // 40: ateapi.Selector.MatchLabelsEntry + nil, // 41: ateapi.Worker.LabelsEntry + (*timestamppb.Timestamp)(nil), // 42: google.protobuf.Timestamp } var file_ateapi_proto_depIdxs = []int32{ - 1, // 0: ateapi.SnapshotInfo.external:type_name -> ateapi.ExternalSnapshotInfo - 2, // 1: ateapi.SnapshotInfo.local:type_name -> ateapi.LocalSnapshotInfo - 38, // 2: ateapi.Selector.match_labels:type_name -> ateapi.Selector.MatchLabelsEntry - 40, // 3: ateapi.ResourceMetadata.create_time:type_name -> google.protobuf.Timestamp - 40, // 4: ateapi.ResourceMetadata.update_time:type_name -> google.protobuf.Timestamp - 5, // 5: ateapi.Actor.metadata:type_name -> ateapi.ResourceMetadata - 0, // 6: ateapi.Actor.status:type_name -> ateapi.Actor.Status - 3, // 7: ateapi.Actor.latest_snapshot_info:type_name -> ateapi.SnapshotInfo - 4, // 8: ateapi.Actor.worker_selector:type_name -> ateapi.Selector - 5, // 9: ateapi.Atespace.metadata:type_name -> ateapi.ResourceMetadata - 7, // 10: ateapi.CreateAtespaceRequest.atespace:type_name -> ateapi.Atespace - 8, // 11: ateapi.GetAtespaceRequest.atespace:type_name -> ateapi.ObjectRef - 7, // 12: ateapi.ListAtespacesResponse.atespaces:type_name -> ateapi.Atespace - 8, // 13: ateapi.DeleteAtespaceRequest.atespace:type_name -> ateapi.ObjectRef - 8, // 14: ateapi.GetActorRequest.actor:type_name -> ateapi.ObjectRef - 6, // 15: ateapi.CreateActorRequest.actor:type_name -> ateapi.Actor - 8, // 16: ateapi.UpdateActorRequest.actor:type_name -> ateapi.ObjectRef - 4, // 17: ateapi.UpdateActorRequest.worker_selector:type_name -> ateapi.Selector - 6, // 18: ateapi.UpdateActorResponse.actor:type_name -> ateapi.Actor - 8, // 19: ateapi.SuspendActorRequest.actor:type_name -> ateapi.ObjectRef - 6, // 20: ateapi.SuspendActorResponse.actor:type_name -> ateapi.Actor - 8, // 21: ateapi.PauseActorRequest.actor:type_name -> ateapi.ObjectRef - 6, // 22: ateapi.PauseActorResponse.actor:type_name -> ateapi.Actor - 8, // 23: ateapi.ResumeActorRequest.actor:type_name -> ateapi.ObjectRef - 6, // 24: ateapi.ResumeActorResponse.actor:type_name -> ateapi.Actor - 8, // 25: ateapi.DeleteActorRequest.actor:type_name -> ateapi.ObjectRef - 29, // 26: ateapi.ListWorkersResponse.workers:type_name -> ateapi.Worker - 6, // 27: ateapi.ListActorsResponse.actors:type_name -> ateapi.Actor - 30, // 28: ateapi.Worker.assignment:type_name -> ateapi.Assignment - 39, // 29: ateapi.Worker.labels:type_name -> ateapi.Worker.LabelsEntry - 31, // 30: ateapi.Assignment.actor_template:type_name -> ateapi.KubeNamespacedObjectRef - 8, // 31: ateapi.Assignment.actor:type_name -> ateapi.ObjectRef - 14, // 32: ateapi.Control.GetActor:input_type -> ateapi.GetActorRequest - 15, // 33: ateapi.Control.CreateActor:input_type -> ateapi.CreateActorRequest - 16, // 34: ateapi.Control.UpdateActor:input_type -> ateapi.UpdateActorRequest - 18, // 35: ateapi.Control.SuspendActor:input_type -> ateapi.SuspendActorRequest - 20, // 36: ateapi.Control.PauseActor:input_type -> ateapi.PauseActorRequest - 22, // 37: ateapi.Control.ResumeActor:input_type -> ateapi.ResumeActorRequest - 24, // 38: ateapi.Control.DeleteActor:input_type -> ateapi.DeleteActorRequest - 25, // 39: ateapi.Control.ListWorkers:input_type -> ateapi.ListWorkersRequest - 27, // 40: ateapi.Control.ListActors:input_type -> ateapi.ListActorsRequest - 9, // 41: ateapi.Control.CreateAtespace:input_type -> ateapi.CreateAtespaceRequest - 10, // 42: ateapi.Control.GetAtespace:input_type -> ateapi.GetAtespaceRequest - 11, // 43: ateapi.Control.ListAtespaces:input_type -> ateapi.ListAtespacesRequest - 13, // 44: ateapi.Control.DeleteAtespace:input_type -> ateapi.DeleteAtespaceRequest - 32, // 45: ateapi.Control.DebugClear:input_type -> ateapi.DebugClearRequest - 34, // 46: ateapi.SessionIdentity.MintJWT:input_type -> ateapi.MintJWTRequest - 36, // 47: ateapi.SessionIdentity.MintCert:input_type -> ateapi.MintCertRequest - 6, // 48: ateapi.Control.GetActor:output_type -> ateapi.Actor - 6, // 49: ateapi.Control.CreateActor:output_type -> ateapi.Actor - 17, // 50: ateapi.Control.UpdateActor:output_type -> ateapi.UpdateActorResponse - 19, // 51: ateapi.Control.SuspendActor:output_type -> ateapi.SuspendActorResponse - 21, // 52: ateapi.Control.PauseActor:output_type -> ateapi.PauseActorResponse - 23, // 53: ateapi.Control.ResumeActor:output_type -> ateapi.ResumeActorResponse - 6, // 54: ateapi.Control.DeleteActor:output_type -> ateapi.Actor - 26, // 55: ateapi.Control.ListWorkers:output_type -> ateapi.ListWorkersResponse - 28, // 56: ateapi.Control.ListActors:output_type -> ateapi.ListActorsResponse - 7, // 57: ateapi.Control.CreateAtespace:output_type -> ateapi.Atespace - 7, // 58: ateapi.Control.GetAtespace:output_type -> ateapi.Atespace - 12, // 59: ateapi.Control.ListAtespaces:output_type -> ateapi.ListAtespacesResponse - 7, // 60: ateapi.Control.DeleteAtespace:output_type -> ateapi.Atespace - 33, // 61: ateapi.Control.DebugClear:output_type -> ateapi.DebugClearResponse - 35, // 62: ateapi.SessionIdentity.MintJWT:output_type -> ateapi.MintJWTResponse - 37, // 63: ateapi.SessionIdentity.MintCert:output_type -> ateapi.MintCertResponse - 48, // [48:64] is the sub-list for method output_type - 32, // [32:48] is the sub-list for method input_type - 32, // [32:32] is the sub-list for extension type_name - 32, // [32:32] is the sub-list for extension extendee - 0, // [0:32] is the sub-list for field type_name + 2, // 0: ateapi.SnapshotInfo.external:type_name -> ateapi.ExternalSnapshotInfo + 3, // 1: ateapi.SnapshotInfo.local:type_name -> ateapi.LocalSnapshotInfo + 40, // 2: ateapi.Selector.match_labels:type_name -> ateapi.Selector.MatchLabelsEntry + 42, // 3: ateapi.ResourceMetadata.create_time:type_name -> google.protobuf.Timestamp + 42, // 4: ateapi.ResourceMetadata.update_time:type_name -> google.protobuf.Timestamp + 0, // 5: ateapi.ExternalVolume.status:type_name -> ateapi.ExternalVolume.Status + 6, // 6: ateapi.Actor.metadata:type_name -> ateapi.ResourceMetadata + 1, // 7: ateapi.Actor.status:type_name -> ateapi.Actor.Status + 4, // 8: ateapi.Actor.latest_snapshot_info:type_name -> ateapi.SnapshotInfo + 5, // 9: ateapi.Actor.worker_selector:type_name -> ateapi.Selector + 7, // 10: ateapi.Actor.actor_volumes:type_name -> ateapi.ExternalVolume + 6, // 11: ateapi.Atespace.metadata:type_name -> ateapi.ResourceMetadata + 9, // 12: ateapi.CreateAtespaceRequest.atespace:type_name -> ateapi.Atespace + 10, // 13: ateapi.GetAtespaceRequest.atespace:type_name -> ateapi.ObjectRef + 9, // 14: ateapi.ListAtespacesResponse.atespaces:type_name -> ateapi.Atespace + 10, // 15: ateapi.DeleteAtespaceRequest.atespace:type_name -> ateapi.ObjectRef + 10, // 16: ateapi.GetActorRequest.actor:type_name -> ateapi.ObjectRef + 8, // 17: ateapi.CreateActorRequest.actor:type_name -> ateapi.Actor + 10, // 18: ateapi.UpdateActorRequest.actor:type_name -> ateapi.ObjectRef + 5, // 19: ateapi.UpdateActorRequest.worker_selector:type_name -> ateapi.Selector + 8, // 20: ateapi.UpdateActorResponse.actor:type_name -> ateapi.Actor + 10, // 21: ateapi.SuspendActorRequest.actor:type_name -> ateapi.ObjectRef + 8, // 22: ateapi.SuspendActorResponse.actor:type_name -> ateapi.Actor + 10, // 23: ateapi.PauseActorRequest.actor:type_name -> ateapi.ObjectRef + 8, // 24: ateapi.PauseActorResponse.actor:type_name -> ateapi.Actor + 10, // 25: ateapi.ResumeActorRequest.actor:type_name -> ateapi.ObjectRef + 8, // 26: ateapi.ResumeActorResponse.actor:type_name -> ateapi.Actor + 10, // 27: ateapi.DeleteActorRequest.actor:type_name -> ateapi.ObjectRef + 31, // 28: ateapi.ListWorkersResponse.workers:type_name -> ateapi.Worker + 8, // 29: ateapi.ListActorsResponse.actors:type_name -> ateapi.Actor + 32, // 30: ateapi.Worker.assignment:type_name -> ateapi.Assignment + 41, // 31: ateapi.Worker.labels:type_name -> ateapi.Worker.LabelsEntry + 33, // 32: ateapi.Assignment.actor_template:type_name -> ateapi.KubeNamespacedObjectRef + 10, // 33: ateapi.Assignment.actor:type_name -> ateapi.ObjectRef + 16, // 34: ateapi.Control.GetActor:input_type -> ateapi.GetActorRequest + 17, // 35: ateapi.Control.CreateActor:input_type -> ateapi.CreateActorRequest + 18, // 36: ateapi.Control.UpdateActor:input_type -> ateapi.UpdateActorRequest + 20, // 37: ateapi.Control.SuspendActor:input_type -> ateapi.SuspendActorRequest + 22, // 38: ateapi.Control.PauseActor:input_type -> ateapi.PauseActorRequest + 24, // 39: ateapi.Control.ResumeActor:input_type -> ateapi.ResumeActorRequest + 26, // 40: ateapi.Control.DeleteActor:input_type -> ateapi.DeleteActorRequest + 27, // 41: ateapi.Control.ListWorkers:input_type -> ateapi.ListWorkersRequest + 29, // 42: ateapi.Control.ListActors:input_type -> ateapi.ListActorsRequest + 11, // 43: ateapi.Control.CreateAtespace:input_type -> ateapi.CreateAtespaceRequest + 12, // 44: ateapi.Control.GetAtespace:input_type -> ateapi.GetAtespaceRequest + 13, // 45: ateapi.Control.ListAtespaces:input_type -> ateapi.ListAtespacesRequest + 15, // 46: ateapi.Control.DeleteAtespace:input_type -> ateapi.DeleteAtespaceRequest + 34, // 47: ateapi.Control.DebugClear:input_type -> ateapi.DebugClearRequest + 36, // 48: ateapi.SessionIdentity.MintJWT:input_type -> ateapi.MintJWTRequest + 38, // 49: ateapi.SessionIdentity.MintCert:input_type -> ateapi.MintCertRequest + 8, // 50: ateapi.Control.GetActor:output_type -> ateapi.Actor + 8, // 51: ateapi.Control.CreateActor:output_type -> ateapi.Actor + 19, // 52: ateapi.Control.UpdateActor:output_type -> ateapi.UpdateActorResponse + 21, // 53: ateapi.Control.SuspendActor:output_type -> ateapi.SuspendActorResponse + 23, // 54: ateapi.Control.PauseActor:output_type -> ateapi.PauseActorResponse + 25, // 55: ateapi.Control.ResumeActor:output_type -> ateapi.ResumeActorResponse + 8, // 56: ateapi.Control.DeleteActor:output_type -> ateapi.Actor + 28, // 57: ateapi.Control.ListWorkers:output_type -> ateapi.ListWorkersResponse + 30, // 58: ateapi.Control.ListActors:output_type -> ateapi.ListActorsResponse + 9, // 59: ateapi.Control.CreateAtespace:output_type -> ateapi.Atespace + 9, // 60: ateapi.Control.GetAtespace:output_type -> ateapi.Atespace + 14, // 61: ateapi.Control.ListAtespaces:output_type -> ateapi.ListAtespacesResponse + 9, // 62: ateapi.Control.DeleteAtespace:output_type -> ateapi.Atespace + 35, // 63: ateapi.Control.DebugClear:output_type -> ateapi.DebugClearResponse + 37, // 64: ateapi.SessionIdentity.MintJWT:output_type -> ateapi.MintJWTResponse + 39, // 65: ateapi.SessionIdentity.MintCert:output_type -> ateapi.MintCertResponse + 50, // [50:66] is the sub-list for method output_type + 34, // [34:50] is the sub-list for method input_type + 34, // [34:34] is the sub-list for extension type_name + 34, // [34:34] is the sub-list for extension extendee + 0, // [0:34] is the sub-list for field type_name } func init() { file_ateapi_proto_init() } @@ -2445,8 +2590,8 @@ func file_ateapi_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_ateapi_proto_rawDesc), len(file_ateapi_proto_rawDesc)), - NumEnums: 1, - NumMessages: 39, + NumEnums: 2, + NumMessages: 40, NumExtensions: 0, NumServices: 2, }, diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 7a8af2280..02d055cb0 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -118,6 +118,24 @@ message ResourceMetadata { google.protobuf.Timestamp update_time = 6; } +message ExternalVolume { + // actor_id + the volume name specified in the actor template. + string actor_volume_id = 1; + + // The volume_id returned from the storage system. + string storage_volume_id = 2; + + // Internal volume plugin name or CSI driver name. + string volume_type = 3; + + enum Status { + PROVISIONING = 0; + CREATED = 1; + DELETING = 2; + } + Status status = 4; +} + message Actor { // Common resource metadata: atespace, name, uid, version, timestamps. ResourceMetadata metadata = 1; @@ -158,6 +176,10 @@ message Actor { // suspend/pause since eligibility is no longer a single fixed pool // reference on the ActorTemplate. string worker_pool_name = 12; + + // Volumes attached to the actor. These volumes only live as long as the actor. + // They are deleted when the actor is deleted. + repeated ExternalVolume actor_volumes = 16; } // Atespace is the isolation boundary an Actor is created into. Global-scoped: From 79bd68940fc9ec2cbad8196b51cbbb02f9697a61 Mon Sep 17 00:00:00 2001 From: msau42 Date: Tue, 7 Jul 2026 23:39:52 +0000 Subject: [PATCH 3/6] atelet handling of external volumes. Volume operations are integrated into the actor workflows: - Run/Restore: MountVolume - Checkpoint: UnmountVolume Volumes are mounted under the actor directory and passed as bind mounts to OCI. --- cmd/atelet/main.go | 58 ++++-- cmd/atelet/oci.go | 35 +++- cmd/atelet/oci_test.go | 8 +- cmd/atelet/volumes.go | 74 ++++++++ internal/ateompath/ateompath.go | 14 ++ internal/proto/ateletpb/atelet.pb.go | 268 +++++++++++++++++---------- internal/proto/ateletpb/atelet.proto | 13 +- 7 files changed, 354 insertions(+), 116 deletions(-) create mode 100644 cmd/atelet/volumes.go diff --git a/cmd/atelet/main.go b/cmd/atelet/main.go index 4071ee645..9ccd20ec8 100644 --- a/cmd/atelet/main.go +++ b/cmd/atelet/main.go @@ -212,7 +212,7 @@ func NewService( return wms } -func (s *AteomHerder) Run(ctx context.Context, req *ateletpb.RunRequest) (*ateletpb.RunResponse, error) { +func (s *AteomHerder) Run(ctx context.Context, req *ateletpb.RunRequest) (resp *ateletpb.RunResponse, err error) { if err := validateRunRequest(req); err != nil { // status.Error so the interceptor surfaces InvalidArgument and the // message instead of masking both as Internal. @@ -234,6 +234,16 @@ func (s *AteomHerder) Run(ctx context.Context, req *ateletpb.RunRequest) (*atele return nil, fmt.Errorf("while resetting actor dirs: %w", err) } + if err := s.mountExternalVolumes(ctx, atespace, actorID, req.GetSpec().GetVolumes()); err != nil { + return nil, err + } + defer func() { + if err != nil { + // TODO cleanup orphaned volumes + _ = s.unmountExternalVolumes(ctx, atespace, actorID, req.GetSpec().GetVolumes()) + } + }() + // Record the sandbox binaries this actor is running so a later Checkpoint // (whose request no longer carries the sandbox config) can re-fetch the same // version and pin it into the snapshot manifest. @@ -350,6 +360,7 @@ func (s *AteomHerder) Checkpoint(ctx context.Context, req *ateletpb.CheckpointRe // in the metadata if the error is not retriable. return nil, fmt.Errorf("while calling ateom.CheckpointWorkload: %w", err) } + sandboxRec.SnapshotFiles = resp.GetSnapshotFiles() if len(sandboxRec.SnapshotFiles) == 0 { return nil, ateerrors.NewGRPCError(ctx, codes.DataLoss, ateerrors.ReasonInvalidCheckpointResult, ateerrors.ActorCrashedMetadata(), errors.New("ateom reported no snapshot files for checkpoint")) @@ -369,6 +380,9 @@ func (s *AteomHerder) Checkpoint(ctx context.Context, req *ateletpb.CheckpointRe return nil, fmt.Errorf("unexpected checkpoint type: %v", req.GetType()) } + // TODO cleanup orphaned volumes + _ = s.unmountExternalVolumes(ctx, atespace, actorID, req.GetSpec().GetVolumes()) + // Note: we do not crash the actor if resetting the directory fails. if err := resetActorDirs(atespace, actorID); err != nil { return nil, fmt.Errorf("while resetting actor dirs: %w", err) @@ -449,7 +463,7 @@ func (s *AteomHerder) uploadExternalCheckpoint(ctx context.Context, req *ateletp return nil } -func (s *AteomHerder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) (*ateletpb.RestoreResponse, error) { +func (s *AteomHerder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) (resp *ateletpb.RestoreResponse, err error) { if err := validateRestoreRequest(req); err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -462,6 +476,16 @@ func (s *AteomHerder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) return nil, fmt.Errorf("while resetting actor dirs: %w", err) } + if err := s.mountExternalVolumes(ctx, atespace, actorID, req.GetSpec().GetVolumes()); err != nil { + return nil, err + } + defer func() { + if err != nil { + // TODO cleanup orphaned mounts + _ = s.unmountExternalVolumes(ctx, atespace, actorID, req.GetSpec().GetVolumes()) + } + }() + checkpointDir := ateompath.RestoreStateDir(atespace, actorID) // Per-step timing so we can attribute resume latency between the rustfs @@ -657,12 +681,9 @@ func (s *AteomHerder) prepareOCIBundles( if err := writeFileAtomic(filepath.Join(identityDir, ActorIDFileName), []byte(actorID), 0o644); err != nil { return fmt.Errorf("while writing actor identity file: %w", err) } - - ddVolumes := make(map[string]bool) // make directories for all durable-dir volumes for _, vol := range spec.GetVolumes() { if vol.GetType() == ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR { - ddVolumes[vol.GetName()] = true volPath := ateompath.DurableDirVolumeMountPoint(atespace, actorID, vol.GetName()) if err := os.MkdirAll(volPath, 0o700); err != nil { return fmt.Errorf("while creating %q: %w", volPath, err) @@ -700,6 +721,7 @@ func (s *AteomHerder) prepareOCIBundles( ateompath.AteomNetNSPath(targetAteomUid), "", // pause is sandbox infra; it gets no actor identity mount. nil, + nil, ); err != nil { return wrapFileSystemErr("while creating pause OCI bundle", err) } @@ -713,12 +735,6 @@ func (s *AteomHerder) prepareOCIBundles( for _, env := range ctr.GetEnv() { envs = append(envs, fmt.Sprintf("%s=%s", env.GetName(), env.GetValue())) } - var ddMounts []*ateletpb.VolumeMount - for _, vm := range ctr.GetVolumeMounts() { - if ddVolumes[vm.GetName()] { - ddMounts = append(ddMounts, vm) - } - } g.Go(func() error { if err := prepareOCIDirectory( gCtx, @@ -735,7 +751,8 @@ func (s *AteomHerder) prepareOCIBundles( }, ateompath.AteomNetNSPath(targetAteomUid), identityDir, - ddMounts, + spec.GetVolumes(), + ctr.GetVolumeMounts(), ); err != nil { return wrapFileSystemErr(fmt.Sprintf("while creating %q OCI bundle", ctr.GetName()), err) } @@ -1058,5 +1075,22 @@ func resetActorDirs(atespace, actorID string) error { return wrapFileSystemErr("while creating durable-dir volumes mount 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(atespace, actorID) + entries, err := os.ReadDir(volumesDir) + if err != nil && !os.IsNotExist(err) { + return wrapFileSystemErr("while reading volumes dir: %w", err) + } + for _, entry := range entries { + volPath := filepath.Join(volumesDir, entry.Name()) + if err := os.Remove(volPath); err != nil { + return wrapFileSystemErr("while removing volume dir: %w", err) + } + } + if err := os.MkdirAll(volumesDir, 0o755); err != nil { + return wrapFileSystemErr("while creating volumes dir: %w", err) + } + return nil } diff --git a/cmd/atelet/oci.go b/cmd/atelet/oci.go index 68b6f34a5..a5b4a0980 100644 --- a/cmd/atelet/oci.go +++ b/cmd/atelet/oci.go @@ -29,6 +29,7 @@ import ( "github.com/agent-substrate/substrate/cmd/atelet/internal/memorypullcache" "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/opencontainers/runtime-spec/specs-go" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" @@ -53,7 +54,7 @@ const ( ActorIDFileName = "actor-id" ) -func prepareOCIDirectory(ctx context.Context, pullCache *memorypullcache.MemoryPullCache, atespace, actorID, containerName, ref string, args []string, env []string, annotations map[string]string, netns string, identityDir string, durableDirVolumeMounts []*ateletpb.VolumeMount) error { +func prepareOCIDirectory(ctx context.Context, pullCache *memorypullcache.MemoryPullCache, atespace, actorID, containerName, ref string, args []string, env []string, annotations map[string]string, netns string, identityDir string, volumes []*ateletpb.Volume, volumeMounts []*ateletpb.VolumeMount) error { tracer := otel.Tracer("prepareOCIDirectory") ctx, span := tracer.Start(ctx, "prepareOCIDirectory") @@ -90,7 +91,14 @@ func prepareOCIDirectory(ctx context.Context, pullCache *memorypullcache.MemoryP } } - ociSpec := buildActorOCISpec(atespace, actorID, args, env, annotations, netns, identityDir, durableDirVolumeMounts) + // Create directories in rootfs to mount all volumes. + for _, vm := range volumeMounts { + if err := createMountPoint(rootPath, vm.GetMountPath()); err != nil { + return fmt.Errorf("while creating volume mount point %q: %w", vm.GetMountPath(), err) + } + } + + ociSpec := buildActorOCISpec(atespace, actorID, args, env, annotations, netns, identityDir, volumes, volumeMounts) ociSpecBytes, err := json.MarshalIndent(ociSpec, "", " ") if err != nil { return fmt.Errorf("while marshaling OCI spec: %w", err) @@ -107,7 +115,7 @@ func prepareOCIDirectory(ctx context.Context, pullCache *memorypullcache.MemoryP // 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(atespace string, actorID string, args []string, env []string, annotations map[string]string, netns string, identityDir string, durableDirVolumeMounts []*ateletpb.VolumeMount) *specs.Spec { +func buildActorOCISpec(atespace string, actorID string, args []string, env []string, annotations map[string]string, netns string, identityDir string, volumes []*ateletpb.Volume, volumeMounts []*ateletpb.VolumeMount) *specs.Spec { envVars := []string{ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", } @@ -220,12 +228,27 @@ func buildActorOCISpec(atespace string, actorID string, args []string, env []str Annotations: annotations, } - // Prepare and mount durable-dir volumes. - for _, vm := range durableDirVolumeMounts { + // Prepare and mount all volumes. + volumeTypes := make(map[string]ateletpb.VolumeType) + for _, vol := range volumes { + volumeTypes[vol.GetName()] = vol.GetType() + } + + for _, vm := range volumeMounts { + var srcPath string + switch volumeTypes[vm.GetName()] { + case ateletpb.VolumeType_VOLUME_TYPE_DURABLE_DIR: + srcPath = ateompath.DurableDirVolumeMountPoint(atespace, actorID, vm.GetName()) + case ateletpb.VolumeType_VOLUME_TYPE_EXTERNAL: + srcPath = ateompath.VolumeHostPath(atespace, actorID, vm.GetName()) + default: + continue + } spec.Mounts = append(spec.Mounts, specs.Mount{ Destination: vm.GetMountPath(), Type: "bind", - Source: ateompath.DurableDirVolumeMountPoint(atespace, actorID, vm.GetName()), + Source: srcPath, + Options: []string{"bind", "rw"}, }) } diff --git a/cmd/atelet/oci_test.go b/cmd/atelet/oci_test.go index 737f5c0db..28b02b499 100644 --- a/cmd/atelet/oci_test.go +++ b/cmd/atelet/oci_test.go @@ -94,6 +94,7 @@ func TestBuildActorOCISpec_IdentityMount(t *testing.T) { "/run/netns/x", "/host/actors/atespace:id/identity", nil, + nil, ) found := false for _, m := range spec.Mounts { @@ -118,7 +119,7 @@ func TestBuildActorOCISpec_IdentityMount(t *testing.T) { // Without an identity dir (the pause container), no identity mount appears. func TestBuildActorOCISpec_NoIdentityMountForPause(t *testing.T) { - bare := buildActorOCISpec("atespace", "id", []string{"/pause"}, nil, nil, "/run/netns/x", "", nil) + bare := buildActorOCISpec("atespace", "id", []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") @@ -134,11 +135,16 @@ func TestBuildActorOCISpec_DurableDirVolumeMounts(t *testing.T) { {Name: "data", MountPath: "/var/data"}, {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}, + } spec := buildActorOCISpec( atespace, id, []string{"/app"}, nil, nil, "/run/netns/x", "", + volumes, durableDirs, ) diff --git a/cmd/atelet/volumes.go b/cmd/atelet/volumes.go new file mode 100644 index 000000000..70e6eda35 --- /dev/null +++ b/cmd/atelet/volumes.go @@ -0,0 +1,74 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "log/slog" + "os" + + "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "github.com/agent-substrate/substrate/internal/volume" +) + +var ( + globalVolumePlugin = volume.NewMockVolumePlugin() +) + +// TODO: Replace with actual volume plugin search +func getVolumePlugin() volume.VolumePlugin { + return globalVolumePlugin +} + +func (s *AteomHerder) mountExternalVolumes(ctx context.Context, atespace, actorID 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 + } + hostPath := ateompath.VolumeHostPath(atespace, actorID, vol.GetName()) + if err := os.MkdirAll(hostPath, 0o750); err != nil { + return fmt.Errorf("failed to create mount point %q: %w", hostPath, err) + } + slog.InfoContext(ctx, "Mounting volume", slog.String("volume_id", ext.GetStorageVolumeId()), slog.String("host_path", hostPath)) + if err := getVolumePlugin().MountVolume(ctx, ext.GetStorageVolumeId(), hostPath); err != nil { + return fmt.Errorf("failed to mount volume %q to %q: %w", ext.GetStorageVolumeId(), hostPath, err) + } + } + return nil +} + +func (s *AteomHerder) unmountExternalVolumes(ctx context.Context, atespace, actorID 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 + } + hostPath := ateompath.VolumeHostPath(atespace, actorID, vol.GetName()) + slog.InfoContext(ctx, "Unmounting volume", slog.String("volume_id", ext.GetStorageVolumeId()), slog.String("host_path", hostPath)) + if err := getVolumePlugin().UnmountVolume(ctx, ext.GetStorageVolumeId(), hostPath); err != nil { + slog.ErrorContext(ctx, "failed to unmount volume", slog.String("volume_id", ext.GetStorageVolumeId()), slog.String("host_path", hostPath), slog.Any("error", err)) + } + } + return nil +} diff --git a/internal/ateompath/ateompath.go b/internal/ateompath/ateompath.go index f72091080..0c7ec8251 100644 --- a/internal/ateompath/ateompath.go +++ b/internal/ateompath/ateompath.go @@ -184,3 +184,17 @@ func PIDFilePath(atespace, actorID, containerName string) string { containerName+".pid", ) } + +func VolumesDir(atespace, actorID string) string { + return filepath.Join( + ActorPath(atespace, actorID), + "volumes", + ) +} + +func VolumeHostPath(atespace, actorID, volumeName string) string { + return filepath.Join( + VolumesDir(atespace, actorID), + volumeName, + ) +} diff --git a/internal/proto/ateletpb/atelet.pb.go b/internal/proto/ateletpb/atelet.pb.go index 3aadf65eb..509552234 100644 --- a/internal/proto/ateletpb/atelet.pb.go +++ b/internal/proto/ateletpb/atelet.pb.go @@ -40,6 +40,7 @@ type VolumeType int32 const ( VolumeType_VOLUME_TYPE_UNSPECIFIED VolumeType = 0 VolumeType_VOLUME_TYPE_DURABLE_DIR VolumeType = 1 + VolumeType_VOLUME_TYPE_EXTERNAL VolumeType = 2 ) // Enum value maps for VolumeType. @@ -47,10 +48,12 @@ 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, } ) @@ -538,6 +541,58 @@ func (*DurableDirVolume) Descriptor() ([]byte, []int) { return file_atelet_proto_rawDescGZIP(), []int{5} } +type ExternalVolumeSource struct { + state protoimpl.MessageState `protogen:"open.v1"` + StorageVolumeId string `protobuf:"bytes,1,opt,name=storage_volume_id,json=storageVolumeId,proto3" json:"storage_volume_id,omitempty"` + VolumeType string `protobuf:"bytes,2,opt,name=volume_type,json=volumeType,proto3" json:"volume_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExternalVolumeSource) Reset() { + *x = ExternalVolumeSource{} + mi := &file_atelet_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExternalVolumeSource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExternalVolumeSource) ProtoMessage() {} + +func (x *ExternalVolumeSource) ProtoReflect() protoreflect.Message { + mi := &file_atelet_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 ExternalVolumeSource.ProtoReflect.Descriptor instead. +func (*ExternalVolumeSource) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{6} +} + +func (x *ExternalVolumeSource) GetStorageVolumeId() string { + if x != nil { + return x.StorageVolumeId + } + return "" +} + +func (x *ExternalVolumeSource) GetVolumeType() string { + if x != nil { + return x.VolumeType + } + return "" +} + type Volume struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -545,6 +600,7 @@ type Volume struct { // Types that are valid to be assigned to Source: // // *Volume_DurableDir + // *Volume_External Source isVolume_Source `protobuf_oneof:"source"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -552,7 +608,7 @@ type Volume struct { func (x *Volume) Reset() { *x = Volume{} - mi := &file_atelet_proto_msgTypes[6] + mi := &file_atelet_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -564,7 +620,7 @@ func (x *Volume) String() string { func (*Volume) ProtoMessage() {} func (x *Volume) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[6] + mi := &file_atelet_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -577,7 +633,7 @@ func (x *Volume) ProtoReflect() protoreflect.Message { // Deprecated: Use Volume.ProtoReflect.Descriptor instead. func (*Volume) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{6} + return file_atelet_proto_rawDescGZIP(), []int{7} } func (x *Volume) GetName() string { @@ -610,6 +666,15 @@ func (x *Volume) GetDurableDir() *DurableDirVolume { return nil } +func (x *Volume) GetExternal() *ExternalVolumeSource { + if x != nil { + if x, ok := x.Source.(*Volume_External); ok { + return x.External + } + } + return nil +} + type isVolume_Source interface { isVolume_Source() } @@ -618,8 +683,14 @@ type Volume_DurableDir struct { DurableDir *DurableDirVolume `protobuf:"bytes,3,opt,name=durable_dir,json=durableDir,proto3,oneof"` } +type Volume_External struct { + External *ExternalVolumeSource `protobuf:"bytes,4,opt,name=external,proto3,oneof"` +} + func (*Volume_DurableDir) isVolume_Source() {} +func (*Volume_External) isVolume_Source() {} + type VolumeMount struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -630,7 +701,7 @@ type VolumeMount struct { func (x *VolumeMount) Reset() { *x = VolumeMount{} - mi := &file_atelet_proto_msgTypes[7] + mi := &file_atelet_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -642,7 +713,7 @@ func (x *VolumeMount) String() string { func (*VolumeMount) ProtoMessage() {} func (x *VolumeMount) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[7] + mi := &file_atelet_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -655,7 +726,7 @@ func (x *VolumeMount) ProtoReflect() protoreflect.Message { // Deprecated: Use VolumeMount.ProtoReflect.Descriptor instead. func (*VolumeMount) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{7} + return file_atelet_proto_rawDescGZIP(), []int{8} } func (x *VolumeMount) GetName() string { @@ -686,7 +757,7 @@ type Container struct { func (x *Container) Reset() { *x = Container{} - mi := &file_atelet_proto_msgTypes[8] + mi := &file_atelet_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -698,7 +769,7 @@ func (x *Container) String() string { func (*Container) ProtoMessage() {} func (x *Container) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[8] + mi := &file_atelet_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -711,7 +782,7 @@ func (x *Container) ProtoReflect() protoreflect.Message { // Deprecated: Use Container.ProtoReflect.Descriptor instead. func (*Container) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{8} + return file_atelet_proto_rawDescGZIP(), []int{9} } func (x *Container) GetName() string { @@ -766,7 +837,7 @@ type EnvEntry struct { func (x *EnvEntry) Reset() { *x = EnvEntry{} - mi := &file_atelet_proto_msgTypes[9] + mi := &file_atelet_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -778,7 +849,7 @@ func (x *EnvEntry) String() string { func (*EnvEntry) ProtoMessage() {} func (x *EnvEntry) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[9] + mi := &file_atelet_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -791,7 +862,7 @@ func (x *EnvEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use EnvEntry.ProtoReflect.Descriptor instead. func (*EnvEntry) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{9} + return file_atelet_proto_rawDescGZIP(), []int{10} } func (x *EnvEntry) GetName() string { @@ -819,7 +890,7 @@ type Readyz struct { func (x *Readyz) Reset() { *x = Readyz{} - mi := &file_atelet_proto_msgTypes[10] + mi := &file_atelet_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -831,7 +902,7 @@ func (x *Readyz) String() string { func (*Readyz) ProtoMessage() {} func (x *Readyz) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[10] + mi := &file_atelet_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -844,7 +915,7 @@ func (x *Readyz) ProtoReflect() protoreflect.Message { // Deprecated: Use Readyz.ProtoReflect.Descriptor instead. func (*Readyz) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{10} + return file_atelet_proto_rawDescGZIP(), []int{11} } func (x *Readyz) GetHttpGet() *HTTPGetAction { @@ -867,7 +938,7 @@ type HTTPGetAction struct { func (x *HTTPGetAction) Reset() { *x = HTTPGetAction{} - mi := &file_atelet_proto_msgTypes[11] + mi := &file_atelet_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -879,7 +950,7 @@ func (x *HTTPGetAction) String() string { func (*HTTPGetAction) ProtoMessage() {} func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[11] + mi := &file_atelet_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -892,7 +963,7 @@ func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { // Deprecated: Use HTTPGetAction.ProtoReflect.Descriptor instead. func (*HTTPGetAction) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{11} + return file_atelet_proto_rawDescGZIP(), []int{12} } func (x *HTTPGetAction) GetPath() string { @@ -917,7 +988,7 @@ type RunResponse struct { func (x *RunResponse) Reset() { *x = RunResponse{} - mi := &file_atelet_proto_msgTypes[12] + mi := &file_atelet_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -929,7 +1000,7 @@ func (x *RunResponse) String() string { func (*RunResponse) ProtoMessage() {} func (x *RunResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[12] + mi := &file_atelet_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -942,7 +1013,7 @@ func (x *RunResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RunResponse.ProtoReflect.Descriptor instead. func (*RunResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{12} + return file_atelet_proto_rawDescGZIP(), []int{13} } type LocalCheckpointConfiguration struct { @@ -956,7 +1027,7 @@ type LocalCheckpointConfiguration struct { func (x *LocalCheckpointConfiguration) Reset() { *x = LocalCheckpointConfiguration{} - mi := &file_atelet_proto_msgTypes[13] + mi := &file_atelet_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -968,7 +1039,7 @@ func (x *LocalCheckpointConfiguration) String() string { func (*LocalCheckpointConfiguration) ProtoMessage() {} func (x *LocalCheckpointConfiguration) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[13] + mi := &file_atelet_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -981,7 +1052,7 @@ func (x *LocalCheckpointConfiguration) ProtoReflect() protoreflect.Message { // Deprecated: Use LocalCheckpointConfiguration.ProtoReflect.Descriptor instead. func (*LocalCheckpointConfiguration) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{13} + return file_atelet_proto_rawDescGZIP(), []int{14} } func (x *LocalCheckpointConfiguration) GetSnapshotPrefix() string { @@ -1010,7 +1081,7 @@ type ExternalCheckpointConfiguration struct { func (x *ExternalCheckpointConfiguration) Reset() { *x = ExternalCheckpointConfiguration{} - mi := &file_atelet_proto_msgTypes[14] + mi := &file_atelet_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1022,7 +1093,7 @@ func (x *ExternalCheckpointConfiguration) String() string { func (*ExternalCheckpointConfiguration) ProtoMessage() {} func (x *ExternalCheckpointConfiguration) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[14] + mi := &file_atelet_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1035,7 +1106,7 @@ func (x *ExternalCheckpointConfiguration) ProtoReflect() protoreflect.Message { // Deprecated: Use ExternalCheckpointConfiguration.ProtoReflect.Descriptor instead. func (*ExternalCheckpointConfiguration) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{14} + return file_atelet_proto_rawDescGZIP(), []int{15} } func (x *ExternalCheckpointConfiguration) GetSnapshotUriPrefix() string { @@ -1072,7 +1143,7 @@ type CheckpointRequest struct { func (x *CheckpointRequest) Reset() { *x = CheckpointRequest{} - mi := &file_atelet_proto_msgTypes[15] + mi := &file_atelet_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1084,7 +1155,7 @@ func (x *CheckpointRequest) String() string { func (*CheckpointRequest) ProtoMessage() {} func (x *CheckpointRequest) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[15] + mi := &file_atelet_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1097,7 +1168,7 @@ func (x *CheckpointRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointRequest.ProtoReflect.Descriptor instead. func (*CheckpointRequest) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{15} + return file_atelet_proto_rawDescGZIP(), []int{16} } func (x *CheckpointRequest) GetTargetAteomUid() string { @@ -1205,7 +1276,7 @@ type CheckpointResponse struct { func (x *CheckpointResponse) Reset() { *x = CheckpointResponse{} - mi := &file_atelet_proto_msgTypes[16] + mi := &file_atelet_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1217,7 +1288,7 @@ func (x *CheckpointResponse) String() string { func (*CheckpointResponse) ProtoMessage() {} func (x *CheckpointResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[16] + mi := &file_atelet_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1230,7 +1301,7 @@ func (x *CheckpointResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointResponse.ProtoReflect.Descriptor instead. func (*CheckpointResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{16} + return file_atelet_proto_rawDescGZIP(), []int{17} } type RestoreRequest struct { @@ -1260,7 +1331,7 @@ type RestoreRequest struct { func (x *RestoreRequest) Reset() { *x = RestoreRequest{} - mi := &file_atelet_proto_msgTypes[17] + mi := &file_atelet_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1272,7 +1343,7 @@ func (x *RestoreRequest) String() string { func (*RestoreRequest) ProtoMessage() {} func (x *RestoreRequest) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[17] + mi := &file_atelet_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1285,7 +1356,7 @@ func (x *RestoreRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreRequest.ProtoReflect.Descriptor instead. func (*RestoreRequest) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{17} + return file_atelet_proto_rawDescGZIP(), []int{18} } func (x *RestoreRequest) GetTargetAteomUid() string { @@ -1393,7 +1464,7 @@ type RestoreResponse struct { func (x *RestoreResponse) Reset() { *x = RestoreResponse{} - mi := &file_atelet_proto_msgTypes[18] + mi := &file_atelet_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1405,7 +1476,7 @@ func (x *RestoreResponse) String() string { func (*RestoreResponse) ProtoMessage() {} func (x *RestoreResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[18] + mi := &file_atelet_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1418,7 +1489,7 @@ func (x *RestoreResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreResponse.ProtoReflect.Descriptor instead. func (*RestoreResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{18} + return file_atelet_proto_rawDescGZIP(), []int{19} } var File_atelet_proto protoreflect.FileDescriptor @@ -1458,12 +1529,17 @@ const file_atelet_proto_rawDesc = "" + "\vpause_image\x18\x02 \x01(\tR\n" + "pauseImage\x12(\n" + "\avolumes\x18\x03 \x03(\v2\x0e.atelet.VolumeR\avolumes\"\x12\n" + - "\x10DurableDirVolume\"\x8b\x01\n" + + "\x10DurableDirVolume\"c\n" + + "\x14ExternalVolumeSource\x12*\n" + + "\x11storage_volume_id\x18\x01 \x01(\tR\x0fstorageVolumeId\x12\x1f\n" + + "\vvolume_type\x18\x02 \x01(\tR\n" + + "volumeType\"\xc7\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" + - "durableDirB\b\n" + + "durableDir\x12:\n" + + "\bexternal\x18\x04 \x01(\v2\x1c.atelet.ExternalVolumeSourceH\x00R\bexternalB\b\n" + "\x06source\"@\n" + "\vVolumeMount\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" + @@ -1516,11 +1592,12 @@ const file_atelet_proto_rawDesc = "" + "\x05scope\x18\n" + " \x01(\x0e2\x15.atelet.SnapshotScopeR\x05scopeB\b\n" + "\x06config\"\x11\n" + - "\x0fRestoreResponse*F\n" + + "\x0fRestoreResponse*`\n" + "\n" + "VolumeType\x12\x1b\n" + "\x17VOLUME_TYPE_UNSPECIFIED\x10\x00\x12\x1b\n" + - "\x17VOLUME_TYPE_DURABLE_DIR\x10\x01*j\n" + + "\x17VOLUME_TYPE_DURABLE_DIR\x10\x01\x12\x18\n" + + "\x14VOLUME_TYPE_EXTERNAL\x10\x02*j\n" + "\x0eCheckpointType\x12\x1f\n" + "\x1bCHECKPOINT_TYPE_UNSPECIFIED\x10\x00\x12\x19\n" + "\x15CHECKPOINT_TYPE_LOCAL\x10\x01\x12\x1c\n" + @@ -1548,7 +1625,7 @@ func file_atelet_proto_rawDescGZIP() []byte { } var file_atelet_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_atelet_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_atelet_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_atelet_proto_goTypes = []any{ (VolumeType)(0), // 0: atelet.VolumeType (CheckpointType)(0), // 1: atelet.CheckpointType @@ -1559,58 +1636,60 @@ var file_atelet_proto_goTypes = []any{ (*SandboxAssets)(nil), // 6: atelet.SandboxAssets (*WorkloadSpec)(nil), // 7: atelet.WorkloadSpec (*DurableDirVolume)(nil), // 8: atelet.DurableDirVolume - (*Volume)(nil), // 9: atelet.Volume - (*VolumeMount)(nil), // 10: atelet.VolumeMount - (*Container)(nil), // 11: atelet.Container - (*EnvEntry)(nil), // 12: atelet.EnvEntry - (*Readyz)(nil), // 13: atelet.Readyz - (*HTTPGetAction)(nil), // 14: atelet.HTTPGetAction - (*RunResponse)(nil), // 15: atelet.RunResponse - (*LocalCheckpointConfiguration)(nil), // 16: atelet.LocalCheckpointConfiguration - (*ExternalCheckpointConfiguration)(nil), // 17: atelet.ExternalCheckpointConfiguration - (*CheckpointRequest)(nil), // 18: atelet.CheckpointRequest - (*CheckpointResponse)(nil), // 19: atelet.CheckpointResponse - (*RestoreRequest)(nil), // 20: atelet.RestoreRequest - (*RestoreResponse)(nil), // 21: atelet.RestoreResponse - nil, // 22: atelet.ArchAssets.FilesEntry - nil, // 23: atelet.SandboxAssets.AssetsEntry + (*ExternalVolumeSource)(nil), // 9: atelet.ExternalVolumeSource + (*Volume)(nil), // 10: atelet.Volume + (*VolumeMount)(nil), // 11: atelet.VolumeMount + (*Container)(nil), // 12: atelet.Container + (*EnvEntry)(nil), // 13: atelet.EnvEntry + (*Readyz)(nil), // 14: atelet.Readyz + (*HTTPGetAction)(nil), // 15: atelet.HTTPGetAction + (*RunResponse)(nil), // 16: atelet.RunResponse + (*LocalCheckpointConfiguration)(nil), // 17: atelet.LocalCheckpointConfiguration + (*ExternalCheckpointConfiguration)(nil), // 18: atelet.ExternalCheckpointConfiguration + (*CheckpointRequest)(nil), // 19: atelet.CheckpointRequest + (*CheckpointResponse)(nil), // 20: atelet.CheckpointResponse + (*RestoreRequest)(nil), // 21: atelet.RestoreRequest + (*RestoreResponse)(nil), // 22: atelet.RestoreResponse + nil, // 23: atelet.ArchAssets.FilesEntry + nil, // 24: atelet.SandboxAssets.AssetsEntry } var file_atelet_proto_depIdxs = []int32{ 7, // 0: atelet.RunRequest.spec:type_name -> atelet.WorkloadSpec 6, // 1: atelet.RunRequest.sandbox_assets:type_name -> atelet.SandboxAssets - 22, // 2: atelet.ArchAssets.files:type_name -> atelet.ArchAssets.FilesEntry - 23, // 3: atelet.SandboxAssets.assets:type_name -> atelet.SandboxAssets.AssetsEntry - 11, // 4: atelet.WorkloadSpec.containers:type_name -> atelet.Container - 9, // 5: atelet.WorkloadSpec.volumes:type_name -> atelet.Volume + 23, // 2: atelet.ArchAssets.files:type_name -> atelet.ArchAssets.FilesEntry + 24, // 3: atelet.SandboxAssets.assets:type_name -> atelet.SandboxAssets.AssetsEntry + 12, // 4: atelet.WorkloadSpec.containers:type_name -> atelet.Container + 10, // 5: atelet.WorkloadSpec.volumes:type_name -> atelet.Volume 0, // 6: atelet.Volume.type:type_name -> atelet.VolumeType 8, // 7: atelet.Volume.durable_dir:type_name -> atelet.DurableDirVolume - 12, // 8: atelet.Container.env:type_name -> atelet.EnvEntry - 13, // 9: atelet.Container.readyz:type_name -> atelet.Readyz - 10, // 10: atelet.Container.volume_mounts:type_name -> atelet.VolumeMount - 14, // 11: atelet.Readyz.http_get:type_name -> atelet.HTTPGetAction - 7, // 12: atelet.CheckpointRequest.spec:type_name -> atelet.WorkloadSpec - 1, // 13: atelet.CheckpointRequest.type:type_name -> atelet.CheckpointType - 16, // 14: atelet.CheckpointRequest.local_config:type_name -> atelet.LocalCheckpointConfiguration - 17, // 15: atelet.CheckpointRequest.external_config:type_name -> atelet.ExternalCheckpointConfiguration - 2, // 16: atelet.CheckpointRequest.scope:type_name -> atelet.SnapshotScope - 7, // 17: atelet.RestoreRequest.spec:type_name -> atelet.WorkloadSpec - 1, // 18: atelet.RestoreRequest.type:type_name -> atelet.CheckpointType - 16, // 19: atelet.RestoreRequest.local_config:type_name -> atelet.LocalCheckpointConfiguration - 17, // 20: atelet.RestoreRequest.external_config:type_name -> atelet.ExternalCheckpointConfiguration - 2, // 21: atelet.RestoreRequest.scope:type_name -> atelet.SnapshotScope - 4, // 22: atelet.ArchAssets.FilesEntry.value:type_name -> atelet.AssetFile - 5, // 23: atelet.SandboxAssets.AssetsEntry.value:type_name -> atelet.ArchAssets - 3, // 24: atelet.AteomHerder.Run:input_type -> atelet.RunRequest - 18, // 25: atelet.AteomHerder.Checkpoint:input_type -> atelet.CheckpointRequest - 20, // 26: atelet.AteomHerder.Restore:input_type -> atelet.RestoreRequest - 15, // 27: atelet.AteomHerder.Run:output_type -> atelet.RunResponse - 19, // 28: atelet.AteomHerder.Checkpoint:output_type -> atelet.CheckpointResponse - 21, // 29: atelet.AteomHerder.Restore:output_type -> atelet.RestoreResponse - 27, // [27:30] is the sub-list for method output_type - 24, // [24:27] is the sub-list for method input_type - 24, // [24:24] is the sub-list for extension type_name - 24, // [24:24] is the sub-list for extension extendee - 0, // [0:24] is the sub-list for field type_name + 9, // 8: atelet.Volume.external:type_name -> atelet.ExternalVolumeSource + 13, // 9: atelet.Container.env:type_name -> atelet.EnvEntry + 14, // 10: atelet.Container.readyz:type_name -> atelet.Readyz + 11, // 11: atelet.Container.volume_mounts:type_name -> atelet.VolumeMount + 15, // 12: atelet.Readyz.http_get:type_name -> atelet.HTTPGetAction + 7, // 13: atelet.CheckpointRequest.spec:type_name -> atelet.WorkloadSpec + 1, // 14: atelet.CheckpointRequest.type:type_name -> atelet.CheckpointType + 17, // 15: atelet.CheckpointRequest.local_config:type_name -> atelet.LocalCheckpointConfiguration + 18, // 16: atelet.CheckpointRequest.external_config:type_name -> atelet.ExternalCheckpointConfiguration + 2, // 17: atelet.CheckpointRequest.scope:type_name -> atelet.SnapshotScope + 7, // 18: atelet.RestoreRequest.spec:type_name -> atelet.WorkloadSpec + 1, // 19: atelet.RestoreRequest.type:type_name -> atelet.CheckpointType + 17, // 20: atelet.RestoreRequest.local_config:type_name -> atelet.LocalCheckpointConfiguration + 18, // 21: atelet.RestoreRequest.external_config:type_name -> atelet.ExternalCheckpointConfiguration + 2, // 22: atelet.RestoreRequest.scope:type_name -> atelet.SnapshotScope + 4, // 23: atelet.ArchAssets.FilesEntry.value:type_name -> atelet.AssetFile + 5, // 24: atelet.SandboxAssets.AssetsEntry.value:type_name -> atelet.ArchAssets + 3, // 25: atelet.AteomHerder.Run:input_type -> atelet.RunRequest + 19, // 26: atelet.AteomHerder.Checkpoint:input_type -> atelet.CheckpointRequest + 21, // 27: atelet.AteomHerder.Restore:input_type -> atelet.RestoreRequest + 16, // 28: atelet.AteomHerder.Run:output_type -> atelet.RunResponse + 20, // 29: atelet.AteomHerder.Checkpoint:output_type -> atelet.CheckpointResponse + 22, // 30: atelet.AteomHerder.Restore:output_type -> atelet.RestoreResponse + 28, // [28:31] is the sub-list for method output_type + 25, // [25:28] is the sub-list for method input_type + 25, // [25:25] is the sub-list for extension type_name + 25, // [25:25] is the sub-list for extension extendee + 0, // [0:25] is the sub-list for field type_name } func init() { file_atelet_proto_init() } @@ -1618,14 +1697,15 @@ func file_atelet_proto_init() { if File_atelet_proto != nil { return } - file_atelet_proto_msgTypes[6].OneofWrappers = []any{ + file_atelet_proto_msgTypes[7].OneofWrappers = []any{ (*Volume_DurableDir)(nil), + (*Volume_External)(nil), } - file_atelet_proto_msgTypes[15].OneofWrappers = []any{ + file_atelet_proto_msgTypes[16].OneofWrappers = []any{ (*CheckpointRequest_LocalConfig)(nil), (*CheckpointRequest_ExternalConfig)(nil), } - file_atelet_proto_msgTypes[17].OneofWrappers = []any{ + file_atelet_proto_msgTypes[18].OneofWrappers = []any{ (*RestoreRequest_LocalConfig)(nil), (*RestoreRequest_ExternalConfig)(nil), } @@ -1635,7 +1715,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: 21, + NumMessages: 22, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/proto/ateletpb/atelet.proto b/internal/proto/ateletpb/atelet.proto index c2fe29bb7..ff23263e9 100644 --- a/internal/proto/ateletpb/atelet.proto +++ b/internal/proto/ateletpb/atelet.proto @@ -83,22 +83,29 @@ message WorkloadSpec { enum VolumeType { VOLUME_TYPE_UNSPECIFIED = 0; VOLUME_TYPE_DURABLE_DIR = 1; + VOLUME_TYPE_EXTERNAL = 2; } message DurableDirVolume { } +message ExternalVolumeSource { + string storage_volume_id = 1; + string volume_type = 2; +} + message Volume { string name = 1; VolumeType type = 2; - oneof source{ + oneof source { DurableDirVolume durable_dir = 3; + ExternalVolumeSource external = 4; } } -message VolumeMount{ +message VolumeMount { string name = 1; string mount_path = 2; } @@ -108,7 +115,7 @@ message Container { string image = 2; repeated string command = 3; repeated EnvEntry env = 4; - Readyz readyz = 5; + Readyz readyz = 5; repeated VolumeMount volume_mounts = 6; } From ff871f721f6be43f2796fb362a191496ab2aaa0e Mon Sep 17 00:00:00 2001 From: msau42 Date: Tue, 7 Jul 2026 23:35:27 +0000 Subject: [PATCH 4/6] Allow ateom to see host mounts --- cmd/atecontroller/internal/controllers/workerpool_apply.go | 3 ++- .../internal/controllers/workerpool_apply_test.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/atecontroller/internal/controllers/workerpool_apply.go b/cmd/atecontroller/internal/controllers/workerpool_apply.go index faa94f572..9f2983e20 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_apply.go +++ b/cmd/atecontroller/internal/controllers/workerpool_apply.go @@ -47,7 +47,8 @@ func buildDeploymentApplyConfig(wp *atev1alpha1.WorkerPool) *appsv1ac.Deployment ). WithVolumeMounts(corev1ac.VolumeMount(). WithName("run-ateom"). - WithMountPath(ateompath.BasePath)) + WithMountPath(ateompath.BasePath). + WithMountPropagation(corev1.MountPropagationHostToContainer)) podSpecAC := corev1ac.PodSpec(). WithSecurityContext(corev1ac.PodSecurityContext(). diff --git a/cmd/atecontroller/internal/controllers/workerpool_apply_test.go b/cmd/atecontroller/internal/controllers/workerpool_apply_test.go index 1cdc972d6..f2ad7e5c9 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_apply_test.go +++ b/cmd/atecontroller/internal/controllers/workerpool_apply_test.go @@ -299,7 +299,8 @@ func expectedDeploymentApplyConfig(mutatePodSpec func(*corev1ac.PodSpecApplyConf WithFieldPath("metadata.uid")))). WithVolumeMounts(corev1ac.VolumeMount(). WithName("run-ateom"). - WithMountPath(ateompath.BasePath)). + WithMountPath(ateompath.BasePath). + WithMountPropagation(corev1.MountPropagationHostToContainer)). WithResources(corev1ac.ResourceRequirements())) podSpecAC.NodeSelector = map[string]string{} From 8e6961154fa5ad247fb1384fc1e33d6c83b7f743 Mon Sep 17 00:00:00 2001 From: msau42 Date: Tue, 7 Jul 2026 23:37:18 +0000 Subject: [PATCH 5/6] Volume plugin interface and mock volume implementation. This allows for different storage backends to be supported. Right now it only contains the mock volume plugin, which is only intended for testing purposes right now. --- internal/volume/mock.go | 181 ++++++++++++++++++++++++++++++++++++++ internal/volume/plugin.go | 29 ++++++ 2 files changed, 210 insertions(+) create mode 100644 internal/volume/mock.go create mode 100644 internal/volume/plugin.go diff --git a/internal/volume/mock.go b/internal/volume/mock.go new file mode 100644 index 000000000..d9e3aed6b --- /dev/null +++ b/internal/volume/mock.go @@ -0,0 +1,181 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package volume + +import ( + "context" + "fmt" + "log/slog" + "os" + "path/filepath" + "sync" + + "github.com/agent-substrate/substrate/internal/ateompath" +) + +// Use a directory that is shared between atelet and ateom but not cleaned up by atelet +var mockVolumeDirectories string = filepath.Join(ateompath.BasePath, "mockvolumes") + +// MockVolumePlugin is a simple implementation of VolumePlugin for testing purposes. +// +// It creates a subdirectory on the host for each actor. This only persists data if the actor +// is scheduled to the same host. +// +// This plugin also does not cleanup the subdirectories, so that has to be done by the test infrastructure. +type MockVolumePlugin struct { + mu sync.Mutex + volumes map[string]*MockVolumeState + counter int +} + +// MockVolumeState tracks the state of a mock volume. +type MockVolumeState struct { + ID string + Name string + Capacity string + StorageClass string + Node string + Mounts map[string]bool // targetPath -> mounted +} + +// NewMockVolumePlugin creates a new MockVolumePlugin. +func NewMockVolumePlugin() *MockVolumePlugin { + return &MockVolumePlugin{ + volumes: make(map[string]*MockVolumeState), + } +} + +// CreateVolume simulates volume provisioning. +func (p *MockVolumePlugin) CreateVolume(ctx context.Context, name string, capacity string, storageClass string) (string, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.counter++ + volumeID := fmt.Sprintf("mock-vol-%d", p.counter) + slog.InfoContext(ctx, "MockVolumePlugin.CreateVolume", slog.String("name", name), slog.String("capacity", capacity), slog.String("storageClass", storageClass), slog.String("volumeID", volumeID)) + p.volumes[volumeID] = &MockVolumeState{ + ID: volumeID, + Name: name, + Capacity: capacity, + StorageClass: storageClass, + Mounts: make(map[string]bool), + } + return volumeID, nil +} + +// DeleteVolume simulates volume deletion. +func (p *MockVolumePlugin) DeleteVolume(ctx context.Context, volumeID string) error { + p.mu.Lock() + defer p.mu.Unlock() + slog.InfoContext(ctx, "MockVolumePlugin.DeleteVolume", slog.String("volumeID", volumeID)) + if _, ok := p.volumes[volumeID]; !ok { + slog.ErrorContext(ctx, "MockVolumePlugin.DeleteVolume failed: volume not found", slog.String("volumeID", volumeID)) + return fmt.Errorf("volume %s not found", volumeID) + } + delete(p.volumes, volumeID) + return nil +} + +// AttachVolume simulates volume attachment to a node. +func (p *MockVolumePlugin) AttachVolume(ctx context.Context, volumeID string, node string) error { + p.mu.Lock() + defer p.mu.Unlock() + slog.InfoContext(ctx, "MockVolumePlugin.AttachVolume", slog.String("volumeID", volumeID), slog.String("node", node)) + vol, ok := p.volumes[volumeID] + if !ok { + slog.ErrorContext(ctx, "MockVolumePlugin.AttachVolume failed: volume not found", slog.String("volumeID", volumeID)) + return fmt.Errorf("volume %s not found", volumeID) + } + vol.Node = node + return nil +} + +// DetachVolume simulates volume detachment from a node. +func (p *MockVolumePlugin) DetachVolume(ctx context.Context, volumeID string, node string) error { + p.mu.Lock() + defer p.mu.Unlock() + slog.InfoContext(ctx, "MockVolumePlugin.DetachVolume", slog.String("volumeID", volumeID), slog.String("node", node)) + vol, ok := p.volumes[volumeID] + if !ok { + slog.ErrorContext(ctx, "MockVolumePlugin.DetachVolume failed: volume not found", slog.String("volumeID", volumeID)) + return fmt.Errorf("volume %s not found", volumeID) + } + if vol.Node != node { + slog.ErrorContext(ctx, "MockVolumePlugin.DetachVolume failed: volume not attached to node", slog.String("volumeID", volumeID), slog.String("node", node), slog.String("attachedNode", vol.Node)) + return fmt.Errorf("volume %s not attached to node %s", volumeID, node) + } + vol.Node = "" + return nil +} + +// MountVolume simulates mounting volume on the host. +func (p *MockVolumePlugin) MountVolume(ctx context.Context, volumeID string, targetPath string) error { + slog.InfoContext(ctx, "MockVolumePlugin.MountVolume", slog.String("volumeID", volumeID), slog.String("targetPath", targetPath)) + + volumeDir := filepath.Join(mockVolumeDirectories, volumeID) + if err := os.MkdirAll(volumeDir, 0755); err != nil { + slog.ErrorContext(ctx, "MockVolumePlugin.MountVolume failed: mkdir error", slog.String("volumeID", volumeID), slog.Any("error", err)) + return fmt.Errorf("failed to create mock volume directory %q: %w", volumeDir, err) + } + + testFilePath := filepath.Join(volumeDir, "test.txt") + if err := os.WriteFile(testFilePath, []byte("test content\n"), 0644); err != nil { + slog.ErrorContext(ctx, "MockVolumePlugin.MountVolume failed: create test file error", slog.String("volumeID", volumeID), slog.Any("error", err)) + return fmt.Errorf("failed to create test file in %q: %w", volumeDir, err) + } + + // Use symlink instead of bind mount to avoid atelet requiring bidirectional mount propagation. + _ = os.Remove(targetPath) + if err := os.Symlink(volumeDir, targetPath); err != nil { + return fmt.Errorf("failed to symlink %q to %q: %w", volumeDir, targetPath, err) + } + return nil +} + +// UnmountVolume simulates unmounting volume from the host. +func (p *MockVolumePlugin) UnmountVolume(ctx context.Context, volumeID string, targetPath string) error { + slog.InfoContext(ctx, "MockVolumePlugin.UnmountVolume", slog.String("volumeID", volumeID), slog.String("targetPath", targetPath)) + + if err := os.Remove(targetPath); err != nil && !os.IsNotExist(err) { + slog.ErrorContext(ctx, "MockVolumePlugin.UnmountVolume failed: remove error", slog.String("volumeID", volumeID), slog.String("targetPath", targetPath), slog.Any("error", err)) + return fmt.Errorf("failed to remove target path %q: %w", targetPath, err) + } + return nil +} + +// GetVolumeState returns the state of a mock volume for verification in tests. +// This only works for controller methods. +func (p *MockVolumePlugin) GetVolumeState(volumeID string) (*MockVolumeState, error) { + p.mu.Lock() + defer p.mu.Unlock() + slog.Info("MockVolumePlugin.GetVolumeState", slog.String("volumeID", volumeID)) + vol, ok := p.volumes[volumeID] + if !ok { + slog.Error("MockVolumePlugin.GetVolumeState failed: volume not found", slog.String("volumeID", volumeID)) + return nil, fmt.Errorf("volume %s not found", volumeID) + } + // Return a copy to avoid concurrent access issues in tests + mountsCopy := make(map[string]bool) + for k, v := range vol.Mounts { + mountsCopy[k] = v + } + return &MockVolumeState{ + ID: vol.ID, + Name: vol.Name, + Capacity: vol.Capacity, + StorageClass: vol.StorageClass, + Node: vol.Node, + Mounts: mountsCopy, + }, nil +} diff --git a/internal/volume/plugin.go b/internal/volume/plugin.go new file mode 100644 index 000000000..8313664df --- /dev/null +++ b/internal/volume/plugin.go @@ -0,0 +1,29 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package volume + +import ( + "context" +) + +// VolumePlugin abstracts storage operations. +type VolumePlugin interface { + CreateVolume(ctx context.Context, name string, capacity string, storageClass string) (volumeID string, err error) + DeleteVolume(ctx context.Context, volumeID string) error + AttachVolume(ctx context.Context, volumeID string, node string) error + DetachVolume(ctx context.Context, volumeID string, node string) error + MountVolume(ctx context.Context, volumeID string, targetPath string) error + UnmountVolume(ctx context.Context, volumeID string, targetPath string) error +} From 4890a34ba9284855e2183298b8398a90b2e486cb Mon Sep 17 00:00:00 2001 From: msau42 Date: Wed, 8 Jul 2026 00:53:12 +0000 Subject: [PATCH 6/6] Update counter demo with external volume using mock volume plugin. It makes the file counter path configurable so we can test it against different volume types. And adds an optional file verification step to test the presence of a file on an existing volume. --- demos/counter/counter.go | 29 +++++++++---- demos/counter/counter.yaml.tmpl | 8 +++- hack/run-demo-counter-kind.sh | 74 +++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 9 deletions(-) create mode 100755 hack/run-demo-counter-kind.sh diff --git a/demos/counter/counter.go b/demos/counter/counter.go index bd5b90848..0b18f0ff3 100644 --- a/demos/counter/counter.go +++ b/demos/counter/counter.go @@ -27,6 +27,7 @@ import ( "net" "net/http" "os" + "path/filepath" "strconv" "sync" "sync/atomic" @@ -41,20 +42,18 @@ var ( fileMutex sync.Mutex ) -const fileCounterPath = "/home/counter/a.txt" - -func incrementFileCounter() int { +func incrementFileCounter(filePath string) int { fileMutex.Lock() defer fileMutex.Unlock() counter := 0 - data, err := os.ReadFile(fileCounterPath) + data, err := os.ReadFile(filePath) if err == nil { if i, err := strconv.Atoi(string(data)); err == nil { counter = i } } counter++ - err = os.WriteFile(fileCounterPath, []byte(strconv.Itoa(counter)), 0o644) + err = os.WriteFile(filePath, []byte(strconv.Itoa(counter)), 0o644) if err != nil { return -1 } @@ -62,6 +61,8 @@ func incrementFileCounter() int { } func main() { + fileCounterDirectory := pflag.String("file-counter-directory", "/home/counter", "Directory for file counter") + validateExistingFilePath := pflag.String("validate-existing-file-path", "", "Path to existing file to validate reading") pflag.Parse() ctx := context.Background() @@ -70,11 +71,23 @@ func main() { defaultMux := http.NewServeMux() defaultMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - fileCounter := incrementFileCounter() - + fileCounter := incrementFileCounter(filepath.Join(*fileCounterDirectory, "a.txt")) memoryCounter := atomic.AddUint64(&requestCount, 1) currentIP := getCurrentIP() - response := fmt.Sprintf("hello from: %s | preserved memory count: %d | preserved file counter: %d\n", currentIP, memoryCounter, fileCounter) + + fileContentStr := "" + if *validateExistingFilePath != "" { + fileContent, err := os.ReadFile(*validateExistingFilePath) + if err != nil { + fileResponse := fmt.Sprintf("failed to read test file: %s\n", err.Error()) + w.WriteHeader(http.StatusOK) + w.Write([]byte(fileResponse)) + return + } + fileContentStr = fmt.Sprintf(" | file content: %s", string(fileContent)) + } + + response := fmt.Sprintf("hello from: %s | preserved memory count: %d | preserved file counter: %d%s\n", currentIP, memoryCounter, fileCounter, fileContentStr) slog.InfoContext(ctx, "Handled request", slog.String("response", response)) w.WriteHeader(http.StatusOK) diff --git a/demos/counter/counter.yaml.tmpl b/demos/counter/counter.yaml.tmpl index f1dc416b7..99d139455 100644 --- a/demos/counter/counter.yaml.tmpl +++ b/demos/counter/counter.yaml.tmpl @@ -42,7 +42,7 @@ spec: containers: - name: counter image: ko://github.com/agent-substrate/substrate/demos/counter - command: ["/ko-app/counter"] + command: ["/ko-app/counter", "--validate-existing-file-path=/external-data/test.txt"] readyz: httpGet: path: /readyz @@ -50,6 +50,8 @@ spec: volumeMounts: - name: data mountPath: /home/counter + - name: external-data + mountPath: /external-data workerSelector: matchLabels: workload: counter @@ -60,3 +62,7 @@ spec: volumes: - name: data durableDir: {} + - name: external-data + externalVolumeTemplate: + capacity: 1Gi + storageClassName: standard diff --git a/hack/run-demo-counter-kind.sh b/hack/run-demo-counter-kind.sh new file mode 100755 index 000000000..5b5f6d502 --- /dev/null +++ b/hack/run-demo-counter-kind.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash + +# 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. + +set -o errexit -o nounset -o pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "${ROOT}" + +# ANSI color codes for pretty output +COLOR_CYAN='\033[1;36m' +COLOR_GREEN='\033[1;32m' +COLOR_YELLOW='\033[1;33m' +COLOR_RED='\033[1;31m' +COLOR_RESET='\033[0m' + +function log_step() { + echo -e "${COLOR_CYAN}[step]: $1${COLOR_RESET}" +} + +function log_success() { + echo -e "${COLOR_GREEN}[success]: $1${COLOR_RESET}" +} + +function log_warn() { + echo -e "${COLOR_YELLOW}[warning]: $1${COLOR_RESET}" +} + +function log_error() { + echo -e "${COLOR_RED}[error]: $1${COLOR_RESET}" +} + +log_step "Cleaning up previous test" +./hack/delete-kind-cluster.sh || true + +log_step "Installing kind cluster" +./hack/create-kind-cluster.sh + +log_step "Installing ATE control plane, Valkey, and RustFS..." +./hack/install-ate-kind.sh --deploy-ate-system + +log_step "Installing counter demo..." +./hack/install-ate-kind.sh --deploy-demo-counter + +log_step "Installing kubectl-ate CLI..." +go install ./cmd/kubectl-ate + +log_step "Creating atespace (demo)..." +kubectl ate create atespace demo + +log_step "Creating counter actor (my-counter-1)..." +kubectl ate create actor my-counter-1 --template ate-demo-counter/counter --atespace demo + +log_success "Counter actor my-counter-1 created" +echo "" +echo -e "${COLOR_YELLOW}========================================================================${COLOR_RESET}" +echo -e "To interact with the counter actor, open a separate terminal and run:" +echo -e " curl -X POST -H \"Host: my-counter-1.demo.actors.resources.substrate.ate.dev\" -i http://localhost:8000/" +echo -e "${COLOR_YELLOW}========================================================================${COLOR_RESET}" +echo "" +log_step "Starting port-forwarding for the network router (press Ctrl+C to stop)..." +kubectl port-forward -n ate-system svc/atenet-router 8000:80