diff --git a/.github/workflows/pr-workflow.yaml b/.github/workflows/pr-workflow.yaml index 0c90cf6afe..f65ae14d23 100644 --- a/.github/workflows/pr-workflow.yaml +++ b/.github/workflows/pr-workflow.yaml @@ -164,6 +164,8 @@ jobs: # features", not an unset value. The e2e steps pass it to the suites as # --preview, so tests of a disabled gate skip themselves (see # internal/e2e/preview.go). + GOFLAGS: -tags=e2e_runtime_identity + E2E_RUNTIME_GOLDEN_REQUIRED: ${{ matrix.preview == 'on' && 'true' || 'false' }} ATE_PREVIEW: ${{ matrix.preview == 'on' && 'ALL' || '' }} # Appended to the lane and artifact names: e2e-gvisor runs without # preview features, e2e-gvisor-preview with them. @@ -272,6 +274,8 @@ jobs: run: hack/e2e-focus-sandbox-class.sh gvisor - name: Run E2E tests (gVisor) env: + E2E_SANDBOX_CLASS: gvisor + E2E_RUNTIME_LOG_DIR: ${{ github.workspace }}/_artifacts/runtime-golden-gvisor E2E_PARALLELISM: 8 E2E_JUNIT_FILE: ${{ github.workspace }}/_artifacts/e2e-gvisor${{ env.LANE_SUFFIX }}.xml run: hack/run-e2e-kind.sh -v -args --no-color --preview="${ATE_PREVIEW}" @@ -282,6 +286,7 @@ jobs: # variant by the single E2E_SANDBOX_CLASS knob (see internal/e2e/sandbox.go). env: E2E_SANDBOX_CLASS: microvm + E2E_RUNTIME_LOG_DIR: ${{ github.workspace }}/_artifacts/runtime-golden E2E_PARALLELISM: 8 E2E_JUNIT_FILE: ${{ github.workspace }}/_artifacts/e2e-microvm${{ env.LANE_SUFFIX }}.xml run: hack/run-e2e-kind.sh -v -args --no-color --preview="${ATE_PREVIEW}" diff --git a/benchmarking/workloads/manifests/glutton-storage-template.yaml.tmpl b/benchmarking/workloads/manifests/glutton-storage-template.yaml.tmpl index fee943693e..c4b68e1f14 100644 --- a/benchmarking/workloads/manifests/glutton-storage-template.yaml.tmpl +++ b/benchmarking/workloads/manifests/glutton-storage-template.yaml.tmpl @@ -24,9 +24,10 @@ workerSelector: workload: benchmark-worker volumes: - name: external-storage - externalVolumeTemplate: - capacity: 1Gi - storageClassName: ${STORAGE_CLASS_NAME} + external: + template: + capacity: 1Gi + storageClassName: ${STORAGE_CLASS_NAME} containers: - name: glutton-storage image: ko://github.com/agent-substrate/substrate/cmd/benchmarking/glutton diff --git a/cmd/ate-setup/internal/demos/counter/counter.go b/cmd/ate-setup/internal/demos/counter/counter.go index 39f5848036..fc58142853 100644 --- a/cmd/ate-setup/internal/demos/counter/counter.go +++ b/cmd/ate-setup/internal/demos/counter/counter.go @@ -40,9 +40,10 @@ func (d *demo) externalVolumeValues(e *steps.Env) map[string]string { "EXTERNAL_VOLUME_MOUNTS": " - name: external-data\n" + " mountPath: /external-data", "EXTERNAL_VOLUMES": "- name: external-data\n" + - " externalVolumeTemplate:\n" + - " capacity: 1Gi\n" + - " storageClassName: " + sc, + " external:\n" + + " template:\n" + + " capacity: 1Gi\n" + + " storageClassName: " + sc, } } diff --git a/cmd/ate-setup/internal/demos/counter/counter_test.go b/cmd/ate-setup/internal/demos/counter/counter_test.go index ada06a8977..d1fb4df85e 100644 --- a/cmd/ate-setup/internal/demos/counter/counter_test.go +++ b/cmd/ate-setup/internal/demos/counter/counter_test.go @@ -69,7 +69,8 @@ func TestExternalVolumeRenders(t *testing.T) { for _, want := range []string{ "--validate-existing-file-path=/external-data/test.txt", "mountPath: /external-data", - "externalVolumeTemplate:", + "external:", + "template:", tc.wantSC, } { if !strings.Contains(string(manifest), want) { diff --git a/cmd/ateapi/internal/apivalidation/actor.go b/cmd/ateapi/internal/apivalidation/actor.go index a77942fcd3..4981dbaf74 100644 --- a/cmd/ateapi/internal/apivalidation/actor.go +++ b/cmd/ateapi/internal/apivalidation/actor.go @@ -138,14 +138,14 @@ func ValidateCustom_WorkerAssignment_WorkerPodIps(_ context.Context, _ operation return validateDualStackIPs(fldPath, value) } -func ValidateCustom_ExternalVolume_VolumeType(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { +func ValidateCustom_ExternalVolumeRef_VolumeType(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { return resources.ValidateVolumeType(fldPath, *value) } -func ValidateCustom_ExternalVolume_StorageVolumeId(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { +func ValidateCustom_ExternalVolumeRef_StorageVolumeId(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { return resources.ValidateStorageVolumeID(fldPath, *value) } -func ValidateCustom_ExternalVolume_VolumeContext(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[string]string) field.ErrorList { +func ValidateCustom_ExternalVolumeRef_VolumeContext(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[string]string) field.ErrorList { return resources.ValidateCSIMap(fldPath, value) } diff --git a/cmd/ateapi/internal/apivalidation/actor_template_test.go b/cmd/ateapi/internal/apivalidation/actor_template_test.go index ebbba44356..2ca865b0f8 100644 --- a/cmd/ateapi/internal/apivalidation/actor_template_test.go +++ b/cmd/ateapi/internal/apivalidation/actor_template_test.go @@ -35,6 +35,18 @@ func TestValidateCreateActorTemplateRequest(t *testing.T) { "valid", &ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate()}, nil, + }, { + "runtime golden gvisor", + &ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(x *ateapipb.ActorTemplate) { + x.SnapshotConfig.GoldenSnapshotConfig = &ateapipb.GoldenSnapshotConfig{Mode: ateapipb.GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_RUNTIME} + })}, + nil, + }, { + "runtime golden microvm", + &ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(x *ateapipb.ActorTemplate) { + x.SnapshotConfig.GoldenSnapshotConfig = &ateapipb.GoldenSnapshotConfig{Mode: ateapipb.GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_RUNTIME} + x.SandboxConfig.SandboxClass = "microvm" + })}, nil, }, { "missing actor_template", &ateapipb.CreateActorTemplateRequest{}, @@ -217,7 +229,7 @@ func TestValidateCreateActorTemplateRequest(t *testing.T) { func TestValidateCreateActorTemplateRequest_PreviewRestricted(t *testing.T) { setPreviewForTest(t, "ALL") req := &ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(tmpl *ateapipb.ActorTemplate) { - tmpl.Volumes = []*ateapipb.Volume{{Name: "data", ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{Capacity: "10Gi", StorageClassName: "fast-ssd"}}} + tmpl.Volumes = []*ateapipb.Volume{{Name: "data", External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{Capacity: "10Gi", StorageClassName: "fast-ssd"}}}} })} t.Run("allowed", func(t *testing.T) { @@ -225,7 +237,7 @@ func TestValidateCreateActorTemplateRequest_PreviewRestricted(t *testing.T) { }) t.Run("restricted", func(t *testing.T) { ctx := preview.ContextWithOptOut(context.Background()) - want := field.ErrorList{field.Forbidden(field.NewPath("actor_template", "volumes").Index(0).Child("external_volume_template"), "")} + want := field.ErrorList{field.Forbidden(field.NewPath("actor_template", "volumes").Index(0).Child("external"), "")} assertValidateErr(t, ValidateCreateActorTemplateRequest(ctx, req), want) }) } @@ -360,7 +372,7 @@ func TestValidateActorTemplate(t *testing.T) { }, { name: "golden snapshot mode unknown", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.SnapshotConfig.GoldenSnapshotConfig = &ateapipb.GoldenSnapshotConfig{Mode: ateapipb.GoldenSnapshotMode(3)} + tmpl.SnapshotConfig.GoldenSnapshotConfig = &ateapipb.GoldenSnapshotConfig{Mode: ateapipb.GoldenSnapshotMode(4)} }, want: field.ErrorList{field.Invalid(field.NewPath("snapshot_config", "golden_snapshot_config", "mode"), nil, "").WithOrigin("maximum")}, }, { @@ -832,52 +844,52 @@ func TestValidateActorTemplate(t *testing.T) { }, { name: "valid external volume template", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Volumes = []*ateapipb.Volume{{Name: "data", ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{Capacity: "10Gi", StorageClassName: "fast-ssd"}}} + tmpl.Volumes = []*ateapipb.Volume{{Name: "data", External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{Capacity: "10Gi", StorageClassName: "fast-ssd"}}}} }, - want: field.ErrorList{field.Forbidden(field.NewPath("volumes").Index(0).Child("external_volume_template"), "")}, + want: field.ErrorList{field.Forbidden(field.NewPath("volumes").Index(0).Child("external"), "")}, wantWithPreview: map[string]field.ErrorList{"ALL": nil}, }, { name: "external volume template missing capacity", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Volumes = []*ateapipb.Volume{{Name: "data", ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{StorageClassName: "fast-ssd"}}} + tmpl.Volumes = []*ateapipb.Volume{{Name: "data", External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{StorageClassName: "fast-ssd"}}}} }, - want: field.ErrorList{field.Forbidden(field.NewPath("volumes").Index(0).Child("external_volume_template"), "")}, - wantWithPreview: map[string]field.ErrorList{"ALL": {field.Required(field.NewPath("volumes").Index(0).Child("external_volume_template", "capacity"), "")}}, + want: field.ErrorList{field.Forbidden(field.NewPath("volumes").Index(0).Child("external"), "")}, + wantWithPreview: map[string]field.ErrorList{"ALL": {field.Required(field.NewPath("volumes").Index(0).Child("external", "template", "capacity"), "")}}, }, { name: "external volume template malformed capacity", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Volumes = []*ateapipb.Volume{{Name: "data", ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{Capacity: "ten gigs", StorageClassName: "fast-ssd"}}} + tmpl.Volumes = []*ateapipb.Volume{{Name: "data", External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{Capacity: "ten gigs", StorageClassName: "fast-ssd"}}}} }, - want: field.ErrorList{field.Forbidden(field.NewPath("volumes").Index(0).Child("external_volume_template"), "")}, - wantWithPreview: map[string]field.ErrorList{"ALL": {field.Invalid(field.NewPath("volumes").Index(0).Child("external_volume_template", "capacity"), nil, "")}}, + want: field.ErrorList{field.Forbidden(field.NewPath("volumes").Index(0).Child("external"), "")}, + wantWithPreview: map[string]field.ErrorList{"ALL": {field.Invalid(field.NewPath("volumes").Index(0).Child("external", "template", "capacity"), nil, "")}}, }, { name: "external volume template capacity at the length bound", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Volumes = []*ateapipb.Volume{{Name: "data", ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{Capacity: strings.Repeat("1", 30) + "Gi", StorageClassName: "fast-ssd"}}} + tmpl.Volumes = []*ateapipb.Volume{{Name: "data", External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{Capacity: strings.Repeat("1", 30) + "Gi", StorageClassName: "fast-ssd"}}}} }, - want: field.ErrorList{field.Forbidden(field.NewPath("volumes").Index(0).Child("external_volume_template"), "")}, + want: field.ErrorList{field.Forbidden(field.NewPath("volumes").Index(0).Child("external"), "")}, wantWithPreview: map[string]field.ErrorList{"ALL": nil}, }, { name: "external volume template capacity too long", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Volumes = []*ateapipb.Volume{{Name: "data", ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{Capacity: strings.Repeat("1", 31) + "Gi", StorageClassName: "fast-ssd"}}} + tmpl.Volumes = []*ateapipb.Volume{{Name: "data", External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{Capacity: strings.Repeat("1", 31) + "Gi", StorageClassName: "fast-ssd"}}}} }, - want: field.ErrorList{field.Forbidden(field.NewPath("volumes").Index(0).Child("external_volume_template"), "")}, - wantWithPreview: map[string]field.ErrorList{"ALL": {field.TooLong(field.NewPath("volumes").Index(0).Child("external_volume_template", "capacity"), nil, 32).WithOrigin("maxLength")}}, + want: field.ErrorList{field.Forbidden(field.NewPath("volumes").Index(0).Child("external"), "")}, + wantWithPreview: map[string]field.ErrorList{"ALL": {field.TooLong(field.NewPath("volumes").Index(0).Child("external", "template", "capacity"), nil, 32).WithOrigin("maxLength")}}, }, { name: "external volume template missing storage_class_name", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Volumes = []*ateapipb.Volume{{Name: "data", ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{Capacity: "10Gi"}}} + tmpl.Volumes = []*ateapipb.Volume{{Name: "data", External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{Capacity: "10Gi"}}}} }, - want: field.ErrorList{field.Forbidden(field.NewPath("volumes").Index(0).Child("external_volume_template"), "")}, - wantWithPreview: map[string]field.ErrorList{"ALL": {field.Required(field.NewPath("volumes").Index(0).Child("external_volume_template", "storage_class_name"), "")}}, + want: field.ErrorList{field.Forbidden(field.NewPath("volumes").Index(0).Child("external"), "")}, + wantWithPreview: map[string]field.ErrorList{"ALL": {field.Required(field.NewPath("volumes").Index(0).Child("external", "template", "storage_class_name"), "")}}, }, { name: "external volume template invalid storage_class_name", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Volumes = []*ateapipb.Volume{{Name: "data", ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{Capacity: "10Gi", StorageClassName: "Fast SSD"}}} + tmpl.Volumes = []*ateapipb.Volume{{Name: "data", External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{Capacity: "10Gi", StorageClassName: "Fast SSD"}}}} }, - want: field.ErrorList{field.Forbidden(field.NewPath("volumes").Index(0).Child("external_volume_template"), "")}, - wantWithPreview: map[string]field.ErrorList{"ALL": {field.Invalid(field.NewPath("volumes").Index(0).Child("external_volume_template", "storage_class_name"), nil, "").WithOrigin("format=k8s-long-name")}}, + want: field.ErrorList{field.Forbidden(field.NewPath("volumes").Index(0).Child("external"), "")}, + wantWithPreview: map[string]field.ErrorList{"ALL": {field.Invalid(field.NewPath("volumes").Index(0).Child("external", "template", "storage_class_name"), nil, "").WithOrigin("format=k8s-long-name")}}, }, { name: "valid resources", mutate: func(tmpl *ateapipb.ActorTemplate) { diff --git a/cmd/ateapi/internal/apivalidation/actor_test.go b/cmd/ateapi/internal/apivalidation/actor_test.go index dea6ab5882..f5d53dbcde 100644 --- a/cmd/ateapi/internal/apivalidation/actor_test.go +++ b/cmd/ateapi/internal/apivalidation/actor_test.go @@ -605,7 +605,7 @@ func TestValidateActorUpdate(t *testing.T) { name: "adding a volume", oldVal: validActor(withActorStatus()), newVal: validActor(withActorStatus(func(s *ateapipb.ActorStatus) { - s.ExternalVolumes = []*ateapipb.ExternalVolume{{Name: "vol-a", VolumeType: "substrate.io/mock"}} + s.ExternalVolumes = []*ateapipb.ExternalVolumeRef{{Name: "vol-a", VolumeType: "substrate.io/mock"}} })), want: nil, wantWithPreview: map[string]field.ErrorList{"ALL": nil}, @@ -613,17 +613,17 @@ func TestValidateActorUpdate(t *testing.T) { name: "adding a volume to an actor with no status", oldVal: validActor(), newVal: validActor(withActorStatus(func(s *ateapipb.ActorStatus) { - s.ExternalVolumes = []*ateapipb.ExternalVolume{{Name: "vol-a", VolumeType: "substrate.io/mock"}} + s.ExternalVolumes = []*ateapipb.ExternalVolumeRef{{Name: "vol-a", VolumeType: "substrate.io/mock"}} })), want: nil, wantWithPreview: map[string]field.ErrorList{"ALL": nil}, }, { name: "provisioning transition on an existing volume", oldVal: validInput(withStatus(func(s *ateapipb.ActorStatus) { - s.ExternalVolumes = []*ateapipb.ExternalVolume{{Name: "vol-a", VolumeType: "substrate.io/mock"}} + s.ExternalVolumes = []*ateapipb.ExternalVolumeRef{{Name: "vol-a", VolumeType: "substrate.io/mock"}} })), newVal: validActor(withActorStatus(func(s *ateapipb.ActorStatus) { - s.ExternalVolumes = []*ateapipb.ExternalVolume{{ + s.ExternalVolumes = []*ateapipb.ExternalVolumeRef{{ Name: "vol-a", VolumeType: "substrate.io/mock", StorageVolumeId: "csi-426d29b7", @@ -636,7 +636,7 @@ func TestValidateActorUpdate(t *testing.T) { name: "duplicate external_volumes volume_name", oldVal: validActor(withActorStatus()), newVal: validActor(withActorStatus(func(s *ateapipb.ActorStatus) { - s.ExternalVolumes = []*ateapipb.ExternalVolume{ + s.ExternalVolumes = []*ateapipb.ExternalVolumeRef{ {Name: "vol-a", VolumeType: "substrate.io/mock"}, {Name: "vol-a", VolumeType: "substrate.io/mock"}, } @@ -649,9 +649,9 @@ func TestValidateActorUpdate(t *testing.T) { name: "too many external_volumes", oldVal: validActor(withActorStatus()), newVal: validActor(withActorStatus(func(s *ateapipb.ActorStatus) { - vols := make([]*ateapipb.ExternalVolume, 33) + vols := make([]*ateapipb.ExternalVolumeRef, 33) for i := range vols { - vols[i] = &ateapipb.ExternalVolume{Name: fmt.Sprintf("vol-%d", i), VolumeType: "substrate.io/mock"} + vols[i] = &ateapipb.ExternalVolumeRef{Name: fmt.Sprintf("vol-%d", i), VolumeType: "substrate.io/mock"} } s.ExternalVolumes = vols })), diff --git a/cmd/ateapi/internal/apivalidation/shared_volume.go b/cmd/ateapi/internal/apivalidation/shared_volume.go new file mode 100644 index 0000000000..4d852b51cf --- /dev/null +++ b/cmd/ateapi/internal/apivalidation/shared_volume.go @@ -0,0 +1,124 @@ +// 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 apivalidation + +import ( + "context" + "fmt" + + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "k8s.io/apimachinery/pkg/api/operation" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/api/validate" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func ValidateCreateSharedVolumeRequest(ctx context.Context, req *ateapipb.CreateSharedVolumeRequest) field.ErrorList { + op := operation.Operation{Type: operation.Create} + return Validate_CreateSharedVolumeRequest(ctx, op, nil, req, nil) +} + +func ValidateGetSharedVolumeRequest(ctx context.Context, req *ateapipb.GetSharedVolumeRequest) field.ErrorList { + op := operation.Operation{Type: operation.Create} + return Validate_GetSharedVolumeRequest(ctx, op, nil, req, nil) +} + +func ValidateListSharedVolumesRequest(ctx context.Context, req *ateapipb.ListSharedVolumesRequest) field.ErrorList { + op := operation.Operation{Type: operation.Create} + return Validate_ListSharedVolumesRequest(ctx, op, nil, req, nil) +} + +// The request is validated as a create; the result is validated as an update +// once the stored value is known, like UpdateTagRequest. +func ValidateUpdateSharedVolumeRequest(ctx context.Context, req *ateapipb.UpdateSharedVolumeRequest) field.ErrorList { + op := operation.Operation{Type: operation.Create} + return Validate_UpdateSharedVolumeRequest(ctx, op, nil, req, nil) +} + +func ValidateDeleteSharedVolumeRequest(ctx context.Context, req *ateapipb.DeleteSharedVolumeRequest) field.ErrorList { + op := operation.Operation{Type: operation.Create} + return Validate_DeleteSharedVolumeRequest(ctx, op, nil, req, nil) +} + +func ValidateSharedVolumeUpdate(ctx context.Context, fldPath *field.Path, newVal, oldVal *ateapipb.SharedVolume) field.ErrorList { + op := operation.Operation{Type: operation.Update} + return Validate_SharedVolume(ctx, op, fldPath, newVal, oldVal) +} + +func ValidateCustom_CreateSharedVolumeRequest(_ context.Context, _ operation.Operation, p *field.Path, req, _ *ateapipb.CreateSharedVolumeRequest) field.ErrorList { + volume := req.GetSharedVolume() + if volume == nil { + return nil + } + volumePath := p.Child("shared_volume") + storageClassName, volumeID := volume.GetStorageClassName(), volume.GetVolumeId() + + var errs field.ErrorList + // TODO: accept READ_WRITE_MANY once concurrent holders are supported. + if mode := volume.GetAccessMode(); mode != ateapipb.AccessMode_ACCESS_MODE_UNSPECIFIED && mode != ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_ONCE { + errs = append(errs, field.NotSupported(volumePath.Child("access_mode"), mode.String(), []string{ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_ONCE.String()})) + } + switch { + case storageClassName == "" && volumeID == "": + errs = append(errs, field.Required(volumePath.Child("storage_class_name"), "one of storage_class_name or volume_id is required")) + case volumeID != "": + if volume.GetCapacity() != "" { + errs = append(errs, field.Forbidden(volumePath.Child("capacity"), "must not be set with volume_id")) + } + if volume.GetVolumeType() == "" && storageClassName == "" { + errs = append(errs, field.Required(volumePath.Child("volume_type"), "required with volume_id unless storage_class_name is set")) + } + default: + if volume.GetVolumeType() != "" { + errs = append(errs, field.Forbidden(volumePath.Child("volume_type"), "must not be set with storage_class_name; it is taken from the class's provisioner")) + } + if len(volume.GetVolumeContext()) > 0 { + errs = append(errs, field.Forbidden(volumePath.Child("volume_context"), "must not be set with storage_class_name; it is returned by the storage system")) + } + if volume.GetCapacity() == "" { + errs = append(errs, field.Required(volumePath.Child("capacity"), "required with storage_class_name")) + } + } + return errs +} + +func ValidateCustom_SharedVolume_Capacity(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + if value == nil || *value == "" { + return nil + } + if _, err := resource.ParseQuantity(*value); err != nil { + return field.ErrorList{field.Invalid(fldPath, *value, fmt.Sprintf("must be a Kubernetes resource quantity: %v", err))} + } + return nil +} + +// This exists only because nested subfield tags are not supported yet. +func ValidateCustom_UpdateSharedVolumeRequest_SharedVolume(ctx context.Context, op operation.Operation, fldPath *field.Path, volume, _ *ateapipb.SharedVolume) field.ErrorList { + if volume == nil || volume.Metadata == nil { + return nil + } + errs := Validate_ResourceMetadata(ctx, op, fldPath.Child("metadata"), volume.Metadata, nil) + errs = append(errs, validate.RequiredValue(ctx, op, fldPath.Child("metadata", "atespace"), &volume.Metadata.Atespace, nil)...) + return errs +} + +// An SharedVolume names its storage the same way an ExternalVolumeRef does. +func ValidateCustom_SharedVolume_VolumeType(ctx context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *string) field.ErrorList { + return ValidateCustom_ExternalVolumeRef_VolumeType(ctx, op, fldPath, value, oldValue) +} + +func ValidateCustom_SharedVolume_VolumeId(ctx context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *string) field.ErrorList { + return ValidateCustom_ExternalVolumeRef_StorageVolumeId(ctx, op, fldPath, value, oldValue) +} diff --git a/cmd/ateapi/internal/apivalidation/shared_volume_test.go b/cmd/ateapi/internal/apivalidation/shared_volume_test.go new file mode 100644 index 0000000000..c1554805a7 --- /dev/null +++ b/cmd/ateapi/internal/apivalidation/shared_volume_test.go @@ -0,0 +1,117 @@ +// 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 apivalidation + +import ( + "context" + "testing" + + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "github.com/google/go-cmp/cmp" + "k8s.io/apimachinery/pkg/api/operation" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestValidateCustomCreateSharedVolumeRequest(t *testing.T) { + tests := []struct { + name string + volume *ateapipb.SharedVolume + wantFields []string + }{ + { + name: "provisioning from a class", + volume: &ateapipb.SharedVolume{StorageClassName: "standard", Capacity: "10Gi"}, + }, + { + name: "registering an existing volume", + volume: &ateapipb.SharedVolume{VolumeId: "disk-1", VolumeType: "mock"}, + }, + { + name: "registering an existing volume under a named class", + volume: &ateapipb.SharedVolume{VolumeId: "disk-1", StorageClassName: "standard"}, + }, + { + name: "neither a class nor an ID", + volume: &ateapipb.SharedVolume{}, + wantFields: []string{"shared_volume.storage_class_name"}, + }, + { + name: "a size for a volume that already exists", + volume: &ateapipb.SharedVolume{VolumeId: "disk-1", VolumeType: "mock", Capacity: "10Gi"}, + wantFields: []string{"shared_volume.capacity"}, + }, + { + name: "an ID with no driver to reach it through", + volume: &ateapipb.SharedVolume{VolumeId: "disk-1"}, + wantFields: []string{"shared_volume.volume_type"}, + }, + { + name: "a driver alongside the class that supplies one", + volume: &ateapipb.SharedVolume{StorageClassName: "standard", VolumeType: "mock", Capacity: "10Gi"}, + wantFields: []string{"shared_volume.volume_type"}, + }, + { + name: "driver metadata for a volume that does not exist yet", + volume: &ateapipb.SharedVolume{StorageClassName: "standard", Capacity: "10Gi", VolumeContext: map[string]string{"fsType": "ext4"}}, + wantFields: []string{"shared_volume.volume_context"}, + }, + { + name: "provisioning without a size", + volume: &ateapipb.SharedVolume{StorageClassName: "standard"}, + wantFields: []string{"shared_volume.capacity"}, + }, + { + name: "read write many is not supported yet", + volume: &ateapipb.SharedVolume{StorageClassName: "standard", Capacity: "10Gi", AccessMode: ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_MANY}, + wantFields: []string{"shared_volume.access_mode"}, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + req := &ateapipb.CreateSharedVolumeRequest{SharedVolume: tc.volume} + errs := ValidateCustom_CreateSharedVolumeRequest(context.Background(), operation.Operation{Type: operation.Create}, nil, req, nil) + + var gotFields []string + for _, err := range errs { + gotFields = append(gotFields, err.Field) + } + if diff := cmp.Diff(tc.wantFields, gotFields); diff != "" { + t.Errorf("offending fields mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestValidateCustomSharedVolumeCapacity(t *testing.T) { + tests := []struct { + value string + wantErr bool + }{ + {value: ""}, + {value: "10Gi"}, + {value: "1500m"}, + {value: "ten gigs", wantErr: true}, + {value: "10GiB", wantErr: true}, + } + for _, tc := range tests { + t.Run(tc.value, func(t *testing.T) { + value := tc.value + errs := ValidateCustom_SharedVolume_Capacity(context.Background(), operation.Operation{Type: operation.Create}, field.NewPath("capacity"), &value, nil) + if gotErr := len(errs) > 0; gotErr != tc.wantErr { + t.Errorf("ValidateCustom_SharedVolume_Capacity(%q) errors = %v, wantErr %v", tc.value, errs, tc.wantErr) + } + }) + } +} diff --git a/cmd/ateapi/internal/apivalidation/validation_test.go b/cmd/ateapi/internal/apivalidation/validation_test.go index 120335bb21..e6c7d50ab7 100644 --- a/cmd/ateapi/internal/apivalidation/validation_test.go +++ b/cmd/ateapi/internal/apivalidation/validation_test.go @@ -871,8 +871,8 @@ func TestValidateTrustBundleDataSource(t *testing.T) { } } -func validExternalVolume(mutate ...func(*ateapipb.ExternalVolume)) *ateapipb.ExternalVolume { - v := &ateapipb.ExternalVolume{ +func validExternalVolume(mutate ...func(*ateapipb.ExternalVolumeRef)) *ateapipb.ExternalVolumeRef { + v := &ateapipb.ExternalVolumeRef{ Name: "my-vol", StorageVolumeId: "valid-storage-id", VolumeType: "mock", @@ -888,57 +888,57 @@ func TestValidateExternalVolume(t *testing.T) { tests := []struct { name string - obj *ateapipb.ExternalVolume + obj *ateapipb.ExternalVolumeRef want field.ErrorList }{{ name: "valid external volume", obj: valid(), }, { name: "missing volume name", - obj: valid(func(v *ateapipb.ExternalVolume) { v.Name = "" }), + obj: valid(func(v *ateapipb.ExternalVolumeRef) { v.Name = "" }), want: field.ErrorList{field.Required(field.NewPath("name"), "")}, }, { name: "invalid volume name", - obj: valid(func(v *ateapipb.ExternalVolume) { v.Name = "NOT A VOLUME" }), + obj: valid(func(v *ateapipb.ExternalVolumeRef) { v.Name = "NOT A VOLUME" }), want: field.ErrorList{field.Invalid(field.NewPath("name"), nil, "").WithOrigin("format=k8s-short-name")}, }, { name: "valid external volume with empty storage volume id", - obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "" }), + obj: valid(func(v *ateapipb.ExternalVolumeRef) { v.StorageVolumeId = "" }), }, { name: "invalid storage volume id with null U+0000", - obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol\x00id" }), + obj: valid(func(v *ateapipb.ExternalVolumeRef) { v.StorageVolumeId = "vol\x00id" }), want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "")}, }, { name: "storage volume id too long", - obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = strings.Repeat("x", 257) }), + obj: valid(func(v *ateapipb.ExternalVolumeRef) { v.StorageVolumeId = strings.Repeat("x", 257) }), want: field.ErrorList{field.TooLong(field.NewPath("storage_volume_id"), nil, 256).WithOrigin("maxLength")}, }, { name: "missing volume type", - obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "" }), + obj: valid(func(v *ateapipb.ExternalVolumeRef) { v.VolumeType = "" }), want: field.ErrorList{field.Required(field.NewPath("volume_type"), "")}, }, { name: "invalid volume type with uppercase", - obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "MockPlugin" }), + obj: valid(func(v *ateapipb.ExternalVolumeRef) { v.VolumeType = "MockPlugin" }), want: field.ErrorList{field.Invalid(field.NewPath("volume_type"), nil, "")}, }, { name: "valid volume type with 253 characters", - obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = strings.Repeat("a", 253) }), + obj: valid(func(v *ateapipb.ExternalVolumeRef) { v.VolumeType = strings.Repeat("a", 253) }), }, { name: "invalid volume type exceeding 253 characters", - obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = strings.Repeat("a", 254) }), + obj: valid(func(v *ateapipb.ExternalVolumeRef) { v.VolumeType = strings.Repeat("a", 254) }), want: field.ErrorList{ field.Invalid(field.NewPath("volume_type"), nil, ""), field.TooLong(field.NewPath("volume_type"), nil, 253).WithOrigin("maxLength"), }, }, { name: "valid volume with substrate.io prefixed volume type", - obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "substrate.io/mock" }), + obj: valid(func(v *ateapipb.ExternalVolumeRef) { v.VolumeType = "substrate.io/mock" }), }, { name: "storage volume id at the bound", - obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = strings.Repeat("x", 256) }), + obj: valid(func(v *ateapipb.ExternalVolumeRef) { v.StorageVolumeId = strings.Repeat("x", 256) }), }, { name: "volume_context with many entries, as a CSI driver may return", - obj: valid(func(v *ateapipb.ExternalVolume) { + obj: valid(func(v *ateapipb.ExternalVolumeRef) { ctxMap := make(map[string]string, 100) for i := range 100 { ctxMap[fmt.Sprintf("key-%03d", i)] = "v" @@ -947,12 +947,12 @@ func TestValidateExternalVolume(t *testing.T) { }), }, { name: "volume_context with one long value, as a CSI driver may return", - obj: valid(func(v *ateapipb.ExternalVolume) { + obj: valid(func(v *ateapipb.ExternalVolumeRef) { v.VolumeContext = map[string]string{"attachment": strings.Repeat("v", 4000)} }), }, { name: "volume_context over the size bound", - obj: valid(func(v *ateapipb.ExternalVolume) { + obj: valid(func(v *ateapipb.ExternalVolumeRef) { v.VolumeContext = map[string]string{"attachment": strings.Repeat("v", 40*1024)} }), want: field.ErrorList{field.TooLong(field.NewPath("volume_context"), nil, 40*1024)}, @@ -960,7 +960,7 @@ func TestValidateExternalVolume(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { op := MakeCreateOp(context.Background()) - assertValidateErr(t, Validate_ExternalVolume(context.Background(), op, nil, tt.obj, nil), tt.want) + assertValidateErr(t, Validate_ExternalVolumeRef(context.Background(), op, nil, tt.obj, nil), tt.want) }) } } @@ -970,8 +970,8 @@ func TestValidateExternalVolume_Update(t *testing.T) { tests := []struct { name string - oldObj *ateapipb.ExternalVolume - newObj *ateapipb.ExternalVolume + oldObj *ateapipb.ExternalVolumeRef + newObj *ateapipb.ExternalVolumeRef want field.ErrorList }{{ name: "unchanged volume is valid", @@ -979,37 +979,37 @@ func TestValidateExternalVolume_Update(t *testing.T) { newObj: valid(), }, { name: "name changed is invalid", - oldObj: valid(func(v *ateapipb.ExternalVolume) { v.Name = "vol1" }), - newObj: valid(func(v *ateapipb.ExternalVolume) { v.Name = "vol2" }), + oldObj: valid(func(v *ateapipb.ExternalVolumeRef) { v.Name = "vol1" }), + newObj: valid(func(v *ateapipb.ExternalVolumeRef) { v.Name = "vol2" }), want: field.ErrorList{field.Invalid(field.NewPath("name"), nil, "").WithOrigin("update")}, }, { name: "storage_volume_id transition from empty to non-empty is valid", - oldObj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "" }), - newObj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol-id-1" }), + oldObj: valid(func(v *ateapipb.ExternalVolumeRef) { v.StorageVolumeId = "" }), + newObj: valid(func(v *ateapipb.ExternalVolumeRef) { v.StorageVolumeId = "vol-id-1" }), }, { name: "storage_volume_id changed once set is invalid", - oldObj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol-id-1" }), - newObj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol-id-2" }), + oldObj: valid(func(v *ateapipb.ExternalVolumeRef) { v.StorageVolumeId = "vol-id-1" }), + newObj: valid(func(v *ateapipb.ExternalVolumeRef) { v.StorageVolumeId = "vol-id-2" }), want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "").WithOrigin("update")}, }, { name: "storage_volume_id unset once set is invalid", - oldObj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol-id-1" }), - newObj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "" }), + oldObj: valid(func(v *ateapipb.ExternalVolumeRef) { v.StorageVolumeId = "vol-id-1" }), + newObj: valid(func(v *ateapipb.ExternalVolumeRef) { v.StorageVolumeId = "" }), want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "").WithOrigin("update")}, }, { name: "volume_type changed is invalid", - oldObj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "mock" }), - newObj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "pd.csi.storage.gke.io" }), + oldObj: valid(func(v *ateapipb.ExternalVolumeRef) { v.VolumeType = "mock" }), + newObj: valid(func(v *ateapipb.ExternalVolumeRef) { v.VolumeType = "pd.csi.storage.gke.io" }), want: field.ErrorList{field.Invalid(field.NewPath("volume_type"), nil, "").WithOrigin("update")}, }, { name: "volume_context changed is valid", - oldObj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeContext = nil }), - newObj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeContext = map[string]string{"foo": "bar"} }), + oldObj: valid(func(v *ateapipb.ExternalVolumeRef) { v.VolumeContext = nil }), + newObj: valid(func(v *ateapipb.ExternalVolumeRef) { v.VolumeContext = map[string]string{"foo": "bar"} }), }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { op := MakeUpdateOp(context.Background()) - assertValidateErr(t, Validate_ExternalVolume(context.Background(), op, nil, tt.newObj, tt.oldObj), tt.want) + assertValidateErr(t, Validate_ExternalVolumeRef(context.Background(), op, nil, tt.newObj, tt.oldObj), tt.want) }) } } diff --git a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go index d6e8ce53b7..ada0b99753 100644 --- a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go +++ b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go @@ -325,6 +325,48 @@ func Validate_Actor( errs = append(errs, fn(fldPath.Child("status"), obj.Status, oldVal, oldObj != nil)...) } + { // field ateapipb.Actor.SharedVolumeBindings + fn := func( + fldPath *field.Path, + obj, oldObj map[string]string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.Immutable(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxProperties(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalMap(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.EachMapKey(ctx, op, fldPath, obj, oldObj, validate.ShortName); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.EachMapVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, validate.ShortName); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.Actor) map[string]string { + return oldObj.SharedVolumeBindings + }) + errs = append(errs, fn(fldPath.Child("shared_volume_bindings"), obj.SharedVolumeBindings, oldVal, oldObj != nil)...) + } + return errs } @@ -631,6 +673,107 @@ func Validate_ActorMetadataItem( return errs } +// Validate_ActorRef validates an instance of ActorRef according +// to declarative validation rules in the API schema. +func Validate_ActorRef( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.ActorRef) (errs field.ErrorList) { + + { // field ateapipb.ActorRef.ActorUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 253); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ActorRef) *string { + return &oldObj.ActorUid + }) + errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ActorRef.ActorName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ActorRef) *string { + return &oldObj.ActorName + }) + errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ActorRef.Active + fn := func( + fldPath *field.Path, + obj, oldObj *bool, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ActorRef) *bool { + return &oldObj.Active + }) + errs = append(errs, fn(fldPath.Child("active"), &obj.Active, oldVal, oldObj != nil)...) + } + + return errs +} + // Validate_ActorSnapshotState validates an instance of ActorSnapshotState according // to declarative validation rules in the API schema. func Validate_ActorSnapshotState( @@ -836,7 +979,7 @@ func Validate_ActorStatus( { // field ateapipb.ActorStatus.ExternalVolumes fn := func( fldPath *field.Path, - obj, oldObj []*ateapipb.ExternalVolume, + obj, oldObj []*ateapipb.ExternalVolumeRef, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -846,7 +989,7 @@ func Validate_ActorStatus( } // call field-attached validations earlyReturn := false - if e := validate.PtrSliceNoNils[ateapipb.ExternalVolume](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.PtrSliceNoNils[ateapipb.ExternalVolumeRef](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } @@ -862,18 +1005,18 @@ func Validate_ActorStatus( } // lists with map semantics require unique keys if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, - func(a *ateapipb.ExternalVolume, b *ateapipb.ExternalVolume) bool { return a.Name == b.Name }); len(e) != 0 { + func(a *ateapipb.ExternalVolumeRef, b *ateapipb.ExternalVolumeRef) bool { return a.Name == b.Name }); len(e) != 0 { errs = append(errs, e...) } // iterate the list and call the type's validation function if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, - func(a *ateapipb.ExternalVolume, b *ateapipb.ExternalVolume) bool { return a.Name == b.Name }, ateDeepEqual, Validate_ExternalVolume); len(e) != 0 { + func(a *ateapipb.ExternalVolumeRef, b *ateapipb.ExternalVolumeRef) bool { return a.Name == b.Name }, ateDeepEqual, Validate_ExternalVolumeRef); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.ActorStatus) []*ateapipb.ExternalVolume { + func(oldObj *ateapipb.ActorStatus) []*ateapipb.ExternalVolumeRef { return oldObj.ExternalVolumes }) errs = append(errs, fn(fldPath.Child("external_volumes"), obj.ExternalVolumes, oldVal, oldObj != nil)...) @@ -2369,6 +2512,51 @@ func Validate_CreateGlobalAccessPolicyRequest( return errs } +// Validate_CreateSharedVolumeRequest validates an instance of CreateSharedVolumeRequest according +// to declarative validation rules in the API schema. +func Validate_CreateSharedVolumeRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.CreateSharedVolumeRequest) (errs field.ErrorList) { + + // custom validation + if e := ValidateCustom_CreateSharedVolumeRequest(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + + { // field ateapipb.CreateSharedVolumeRequest.SharedVolume + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.SharedVolume, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_SharedVolume(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.CreateSharedVolumeRequest) *ateapipb.SharedVolume { + return oldObj.SharedVolume + }) + errs = append(errs, fn(fldPath.Child("shared_volume"), obj.SharedVolume, oldVal, oldObj != nil)...) + } + + return errs +} + // Validate_CreateTagRequest validates an instance of CreateTagRequest according // to declarative validation rules in the API schema. func Validate_CreateTagRequest( @@ -3213,6 +3401,91 @@ func Validate_DeleteOptions( return errs } +// Validate_DeleteSharedVolumeRequest validates an instance of DeleteSharedVolumeRequest according +// to declarative validation rules in the API schema. +func Validate_DeleteSharedVolumeRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.DeleteSharedVolumeRequest) (errs field.ErrorList) { + + { // field ateapipb.DeleteSharedVolumeRequest.SharedVolume + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.ObjectRef, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + func() { // cohort = "atespace" + earlyReturn := false + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ObjectRef) *string { return &o.Atespace }, validate.DirectEqual, validate.RequiredValue).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ObjectRef) *string { return &o.Atespace }, validate.DirectEqual, validate.OptionalValue).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + }() + // call the type's validation function + errs = append(errs, Validate_ObjectRef(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.DeleteSharedVolumeRequest) *ateapipb.ObjectRef { + return oldObj.SharedVolume + }) + errs = append(errs, fn(fldPath.Child("shared_volume"), obj.SharedVolume, oldVal, oldObj != nil)...) + } + + { // field ateapipb.DeleteSharedVolumeRequest.Options + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.DeleteOptions, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_DeleteOptions(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.DeleteSharedVolumeRequest) *ateapipb.DeleteOptions { + return oldObj.Options + }) + errs = append(errs, fn(fldPath.Child("options"), obj.Options, oldVal, oldObj != nil)...) + } + + return errs +} + // Validate_DeleteTagRequest validates an instance of DeleteTagRequest according // to declarative validation rules in the API schema. func Validate_DeleteTagRequest( @@ -3752,13 +4025,13 @@ func Validate_EnvVar( return errs } -// Validate_ExternalVolume validates an instance of ExternalVolume according +// Validate_ExternalVolumeRef validates an instance of ExternalVolumeRef according // to declarative validation rules in the API schema. -func Validate_ExternalVolume( +func Validate_ExternalVolumeRef( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.ExternalVolume) (errs field.ErrorList) { + obj, oldObj *ateapipb.ExternalVolumeRef) (errs field.ErrorList) { - { // field ateapipb.ExternalVolume.Name + { // field ateapipb.ExternalVolumeRef.Name fn := func( fldPath *field.Path, obj, oldObj *string, @@ -3789,13 +4062,13 @@ func Validate_ExternalVolume( return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.ExternalVolume) *string { + func(oldObj *ateapipb.ExternalVolumeRef) *string { return &oldObj.Name }) errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) } - { // field ateapipb.ExternalVolume.StorageVolumeId + { // field ateapipb.ExternalVolumeRef.StorageVolumeId fn := func( fldPath *field.Path, obj, oldObj *string, @@ -3820,7 +4093,7 @@ func Validate_ExternalVolume( return // do not proceed } // custom validation - if e := ValidateCustom_ExternalVolume_StorageVolumeId(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + if e := ValidateCustom_ExternalVolumeRef_StorageVolumeId(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256); len(e) != 0 { @@ -3829,13 +4102,13 @@ func Validate_ExternalVolume( return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.ExternalVolume) *string { + func(oldObj *ateapipb.ExternalVolumeRef) *string { return &oldObj.StorageVolumeId }) errs = append(errs, fn(fldPath.Child("storage_volume_id"), &obj.StorageVolumeId, oldVal, oldObj != nil)...) } - { // field ateapipb.ExternalVolume.VolumeType + { // field ateapipb.ExternalVolumeRef.VolumeType fn := func( fldPath *field.Path, obj, oldObj *string, @@ -3861,7 +4134,7 @@ func Validate_ExternalVolume( return // do not proceed } // custom validation - if e := ValidateCustom_ExternalVolume_VolumeType(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + if e := ValidateCustom_ExternalVolumeRef_VolumeType(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 253); len(e) != 0 { @@ -3870,13 +4143,13 @@ func Validate_ExternalVolume( return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.ExternalVolume) *string { + func(oldObj *ateapipb.ExternalVolumeRef) *string { return &oldObj.VolumeType }) errs = append(errs, fn(fldPath.Child("volume_type"), &obj.VolumeType, oldVal, oldObj != nil)...) } - { // field ateapipb.ExternalVolume.VolumeContext + { // field ateapipb.ExternalVolumeRef.VolumeContext fn := func( fldPath *field.Path, obj, oldObj map[string]string, @@ -3896,28 +4169,19 @@ func Validate_ExternalVolume( return // do not proceed } // custom validation - if e := ValidateCustom_ExternalVolume_VolumeContext(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + if e := ValidateCustom_ExternalVolumeRef_VolumeContext(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.ExternalVolume) map[string]string { + func(oldObj *ateapipb.ExternalVolumeRef) map[string]string { return oldObj.VolumeContext }) errs = append(errs, fn(fldPath.Child("volume_context"), obj.VolumeContext, oldVal, oldObj != nil)...) } - return errs -} - -// Validate_ExternalVolumeTemplate validates an instance of ExternalVolumeTemplate according -// to declarative validation rules in the API schema. -func Validate_ExternalVolumeTemplate( - ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.ExternalVolumeTemplate) (errs field.ErrorList) { - - { // field ateapipb.ExternalVolumeTemplate.Capacity + { // field ateapipb.ExternalVolumeRef.SharedVolumeName fn := func( fldPath *field.Path, obj, oldObj *string, @@ -3930,18 +4194,150 @@ func Validate_ExternalVolumeTemplate( } // call field-attached validations earlyReturn := false - if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if e := validate.UpdateValue(ctx, op, fldPath, obj, oldObj, + func(a string, b string) bool { return a == b }, validate.NoUnset, validate.NoModify).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } if earlyReturn { return // do not proceed } - // custom validation - if e := ValidateCustom_ExternalVolumeTemplate_Capacity(ctx, op, fldPath, obj, oldObj); len(e) != 0 { - errs = append(errs, e...) - } - if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 32); len(e) != 0 { + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ExternalVolumeRef) *string { + return &oldObj.SharedVolumeName + }) + errs = append(errs, fn(fldPath.Child("shared_volume_name"), &obj.SharedVolumeName, oldVal, oldObj != nil)...) + } + + return errs +} + +var unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_ExternalVolumeSource_ = validate.NewUnionMembership(validate.NewUnionMember("template"), validate.NewUnionMember("shared_volume")) + +// Validate_ExternalVolumeSource validates an instance of ExternalVolumeSource according +// to declarative validation rules in the API schema. +func Validate_ExternalVolumeSource( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.ExternalVolumeSource) (errs field.ErrorList) { + + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_ExternalVolumeSource_, + func(obj *ateapipb.ExternalVolumeSource) bool { + if obj == nil { + return false + } + return obj.Template != nil + }, + func(obj *ateapipb.ExternalVolumeSource) bool { + if obj == nil { + return false + } + return obj.SharedVolume != nil + }); len(e) != 0 { + errs = append(errs, e...) + } + + { // field ateapipb.ExternalVolumeSource.Template + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.ExternalVolumeTemplate, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_ExternalVolumeTemplate(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ExternalVolumeSource) *ateapipb.ExternalVolumeTemplate { + return oldObj.Template + }) + errs = append(errs, fn(fldPath.Child("template"), obj.Template, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ExternalVolumeSource.SharedVolume + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.SharedVolumeRef, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_SharedVolumeRef(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ExternalVolumeSource) *ateapipb.SharedVolumeRef { + return oldObj.SharedVolume + }) + errs = append(errs, fn(fldPath.Child("shared_volume"), obj.SharedVolume, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_ExternalVolumeTemplate validates an instance of ExternalVolumeTemplate according +// to declarative validation rules in the API schema. +func Validate_ExternalVolumeTemplate( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.ExternalVolumeTemplate) (errs field.ErrorList) { + + { // field ateapipb.ExternalVolumeTemplate.Capacity + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_ExternalVolumeTemplate_Capacity(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 32); len(e) != 0 { errs = append(errs, e...) } return @@ -4388,6 +4784,61 @@ func Validate_GetGlobalAccessPolicyRequest( return errs } +// Validate_GetSharedVolumeRequest validates an instance of GetSharedVolumeRequest according +// to declarative validation rules in the API schema. +func Validate_GetSharedVolumeRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.GetSharedVolumeRequest) (errs field.ErrorList) { + + { // field ateapipb.GetSharedVolumeRequest.SharedVolume + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.ObjectRef, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + func() { // cohort = "atespace" + earlyReturn := false + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ObjectRef) *string { return &o.Atespace }, validate.DirectEqual, validate.RequiredValue).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ObjectRef) *string { return &o.Atespace }, validate.DirectEqual, validate.OptionalValue).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + }() + // call the type's validation function + errs = append(errs, Validate_ObjectRef(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.GetSharedVolumeRequest) *ateapipb.ObjectRef { + return oldObj.SharedVolume + }) + errs = append(errs, fn(fldPath.Child("shared_volume"), obj.SharedVolume, oldVal, oldObj != nil)...) + } + + return errs +} + // Validate_GetTagRequest validates an instance of GetTagRequest according // to declarative validation rules in the API schema. func Validate_GetTagRequest( @@ -4528,7 +4979,7 @@ func Validate_GoldenSnapshotConfig( if earlyReturn { return // do not proceed } - if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 2); len(e) != 0 { + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 3); len(e) != 0 { errs = append(errs, e...) } if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { @@ -5392,6 +5843,108 @@ func Validate_ListOptions( return errs } +// Validate_ListSharedVolumesRequest validates an instance of ListSharedVolumesRequest according +// to declarative validation rules in the API schema. +func Validate_ListSharedVolumesRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.ListSharedVolumesRequest) (errs field.ErrorList) { + + { // field ateapipb.ListSharedVolumesRequest.Atespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ListSharedVolumesRequest) *string { + return &oldObj.Atespace + }) + errs = append(errs, fn(fldPath.Child("atespace"), &obj.Atespace, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ListSharedVolumesRequest.PageSize + fn := func( + fldPath *field.Path, + obj, oldObj *int32, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ListSharedVolumesRequest) *int32 { + return &oldObj.PageSize + }) + errs = append(errs, fn(fldPath.Child("page_size"), &obj.PageSize, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ListSharedVolumesRequest.PageToken + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ListSharedVolumesRequest) *string { + return &oldObj.PageToken + }) + errs = append(errs, fn(fldPath.Child("page_token"), &obj.PageToken, oldVal, oldObj != nil)...) + } + + return errs +} + // Validate_ListTagsRequest validates an instance of ListTagsRequest according // to declarative validation rules in the API schema. func Validate_ListTagsRequest( @@ -7514,11 +8067,502 @@ func Validate_Selector( return errs } -// Validate_Snapshot validates an instance of Snapshot according +// Validate_SharedVolume validates an instance of SharedVolume according // to declarative validation rules in the API schema. -func Validate_Snapshot( +func Validate_SharedVolume( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.Snapshot) (errs field.ErrorList) { + obj, oldObj *ateapipb.SharedVolume) (errs field.ErrorList) { + + { // field ateapipb.SharedVolume.Metadata + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.ResourceMetadata, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + func() { // cohort = "atespace" + earlyReturn := false + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ResourceMetadata) *string { return &o.Atespace }, validate.DirectEqual, validate.RequiredValue).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ResourceMetadata) *string { return &o.Atespace }, validate.DirectEqual, validate.Immutable).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ResourceMetadata) *string { return &o.Atespace }, validate.DirectEqual, validate.OptionalValue).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + }() + // call the type's validation function + errs = append(errs, Validate_ResourceMetadata(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.SharedVolume) *ateapipb.ResourceMetadata { + return oldObj.Metadata + }) + errs = append(errs, fn(fldPath.Child("metadata"), obj.Metadata, oldVal, oldObj != nil)...) + } + + { // field ateapipb.SharedVolume.DeleteTrigger + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.DeleteTrigger, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 2); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.SharedVolume) *ateapipb.DeleteTrigger { + return &oldObj.DeleteTrigger + }) + errs = append(errs, fn(fldPath.Child("delete_trigger"), &obj.DeleteTrigger, oldVal, oldObj != nil)...) + } + + { // field ateapipb.SharedVolume.StorageClassName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if e := validate.UpdateValue(ctx, op, fldPath, obj, oldObj, + func(a string, b string) bool { return a == b }, validate.NoUnset, validate.NoModify).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.LongName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.SharedVolume) *string { + return &oldObj.StorageClassName + }) + errs = append(errs, fn(fldPath.Child("storage_class_name"), &obj.StorageClassName, oldVal, oldObj != nil)...) + } + + { // field ateapipb.SharedVolume.VolumeId + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if e := validate.UpdateValue(ctx, op, fldPath, obj, oldObj, + func(a string, b string) bool { return a == b }, validate.NoUnset, validate.NoModify).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_SharedVolume_VolumeId(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.SharedVolume) *string { + return &oldObj.VolumeId + }) + errs = append(errs, fn(fldPath.Child("volume_id"), &obj.VolumeId, oldVal, oldObj != nil)...) + } + + { // field ateapipb.SharedVolume.VolumeType + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if e := validate.UpdateValue(ctx, op, fldPath, obj, oldObj, + func(a string, b string) bool { return a == b }, validate.NoUnset, validate.NoModify).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_SharedVolume_VolumeType(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 253); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.SharedVolume) *string { + return &oldObj.VolumeType + }) + errs = append(errs, fn(fldPath.Child("volume_type"), &obj.VolumeType, oldVal, oldObj != nil)...) + } + + { // field ateapipb.SharedVolume.VolumeContext + fn := func( + fldPath *field.Path, + obj, oldObj map[string]string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.MaxProperties(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalMap(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.EachMapKey(ctx, op, fldPath, obj, oldObj, + func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 128) + }); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.EachMapVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, + func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256) + }); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.SharedVolume) map[string]string { + return oldObj.VolumeContext + }) + errs = append(errs, fn(fldPath.Child("volume_context"), obj.VolumeContext, oldVal, oldObj != nil)...) + } + + { // field ateapipb.SharedVolume.Status + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.SharedVolumeStatus, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_SharedVolumeStatus(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.SharedVolume) *ateapipb.SharedVolumeStatus { + return oldObj.Status + }) + errs = append(errs, fn(fldPath.Child("status"), obj.Status, oldVal, oldObj != nil)...) + } + + { // field ateapipb.SharedVolume.Capacity + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if e := validate.UpdateValue(ctx, op, fldPath, obj, oldObj, + func(a string, b string) bool { return a == b }, validate.NoUnset, validate.NoModify).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_SharedVolume_Capacity(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 64); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.SharedVolume) *string { + return &oldObj.Capacity + }) + errs = append(errs, fn(fldPath.Child("capacity"), &obj.Capacity, oldVal, oldObj != nil)...) + } + + { // field ateapipb.SharedVolume.AccessMode + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.AccessMode, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if e := validate.UpdateValue(ctx, op, fldPath, obj, oldObj, + func(a ateapipb.AccessMode, b ateapipb.AccessMode) bool { return a == b }, validate.NoUnset, validate.NoModify).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 2); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.SharedVolume) *ateapipb.AccessMode { + return &oldObj.AccessMode + }) + errs = append(errs, fn(fldPath.Child("access_mode"), &obj.AccessMode, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_SharedVolumeRef validates an instance of SharedVolumeRef according +// to declarative validation rules in the API schema. +func Validate_SharedVolumeRef( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.SharedVolumeRef) (errs field.ErrorList) { + + { // field ateapipb.SharedVolumeRef.Name + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.SharedVolumeRef) *string { + return &oldObj.Name + }) + errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_SharedVolumeStatus validates an instance of SharedVolumeStatus according +// to declarative validation rules in the API schema. +func Validate_SharedVolumeStatus( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.SharedVolumeStatus) (errs field.ErrorList) { + + { // field ateapipb.SharedVolumeStatus.State + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.SharedVolumeState, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 3); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.SharedVolumeStatus) *ateapipb.SharedVolumeState { + return &oldObj.State + }) + errs = append(errs, fn(fldPath.Child("state"), &obj.State, oldVal, oldObj != nil)...) + } + + { // field ateapipb.SharedVolumeStatus.Refs + fn := func( + fldPath *field.Path, + obj, oldObj []*ateapipb.ActorRef, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrSliceNoNils[ateapipb.ActorRef](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 256).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // lists with map semantics require unique keys + if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *ateapipb.ActorRef, b *ateapipb.ActorRef) bool { return a.ActorUid == b.ActorUid }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *ateapipb.ActorRef, b *ateapipb.ActorRef) bool { return a.ActorUid == b.ActorUid }, ateDeepEqual, Validate_ActorRef); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.SharedVolumeStatus) []*ateapipb.ActorRef { + return oldObj.Refs + }) + errs = append(errs, fn(fldPath.Child("refs"), obj.Refs, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_Snapshot validates an instance of Snapshot according +// to declarative validation rules in the API schema. +func Validate_Snapshot( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.Snapshot) (errs field.ErrorList) { { // field ateapipb.Snapshot.Uuid fn := func( @@ -9069,6 +10113,59 @@ func Validate_UpdateGlobalAccessPolicyRequest( return errs } +// Validate_UpdateSharedVolumeRequest validates an instance of UpdateSharedVolumeRequest according +// to declarative validation rules in the API schema. +func Validate_UpdateSharedVolumeRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.UpdateSharedVolumeRequest) (errs field.ErrorList) { + + { // field ateapipb.UpdateSharedVolumeRequest.SharedVolume + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.SharedVolume, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_UpdateSharedVolumeRequest_SharedVolume(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + func() { // cohort = "metadata" + earlyReturn := false + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "metadata", + func(o *ateapipb.SharedVolume) *ateapipb.ResourceMetadata { return o.Metadata }, ateDeepEqual, validate.RequiredPointer).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + }() + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.UpdateSharedVolumeRequest) *ateapipb.SharedVolume { + return oldObj.SharedVolume + }) + errs = append(errs, fn(fldPath.Child("shared_volume"), obj.SharedVolume, oldVal, oldObj != nil)...) + } + + return errs +} + // Validate_UpdateTagRequest validates an instance of UpdateTagRequest according // to declarative validation rules in the API schema. func Validate_UpdateTagRequest( @@ -9263,7 +10360,7 @@ func Validate_VersionedSandboxCompat( return errs } -var unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_Volume_ = validate.NewUnionMembership(validate.NewUnionMember("durable_dir"), validate.NewUnionMember("external_volume_template"), validate.NewUnionMember("system_info"), validate.NewUnionMember("image")) +var unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_Volume_ = validate.NewUnionMembership(validate.NewUnionMember("durable_dir"), validate.NewUnionMember("external"), validate.NewUnionMember("system_info"), validate.NewUnionMember("image")) // Validate_Volume validates an instance of Volume according // to declarative validation rules in the API schema. @@ -9282,7 +10379,7 @@ func Validate_Volume( if obj == nil { return false } - return obj.ExternalVolumeTemplate != nil + return obj.External != nil }, func(obj *ateapipb.Volume) bool { if obj == nil { @@ -9359,10 +10456,10 @@ func Validate_Volume( errs = append(errs, fn(fldPath.Child("durable_dir"), obj.DurableDir, oldVal, oldObj != nil)...) } - { // field ateapipb.Volume.ExternalVolumeTemplate + { // field ateapipb.Volume.External fn := func( fldPath *field.Path, - obj, oldObj *ateapipb.ExternalVolumeTemplate, + obj, oldObj *ateapipb.ExternalVolumeSource, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -9386,14 +10483,14 @@ func Validate_Volume( return // do not proceed } // call the type's validation function - errs = append(errs, Validate_ExternalVolumeTemplate(ctx, op, fldPath, obj, oldObj)...) + errs = append(errs, Validate_ExternalVolumeSource(ctx, op, fldPath, obj, oldObj)...) return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.Volume) *ateapipb.ExternalVolumeTemplate { - return oldObj.ExternalVolumeTemplate + func(oldObj *ateapipb.Volume) *ateapipb.ExternalVolumeSource { + return oldObj.External }) - errs = append(errs, fn(fldPath.Child("external_volume_template"), obj.ExternalVolumeTemplate, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("external"), obj.External, oldVal, oldObj != nil)...) } { // field ateapipb.Volume.SystemInfo diff --git a/cmd/ateapi/internal/authz/authz_util.go b/cmd/ateapi/internal/authz/authz_util.go index 608d660469..c7c912855c 100644 --- a/cmd/ateapi/internal/authz/authz_util.go +++ b/cmd/ateapi/internal/authz/authz_util.go @@ -80,6 +80,12 @@ const ( RelationCanMintActorJWT = "can_mint_actor_jwt" RelationCanMintActorCertificate = "can_mint_actor_certificate" + RelationCanCreateSharedVolume = "can_create_shared_volume" + RelationCanGetSharedVolume = "can_get_shared_volume" + RelationCanUpdateSharedVolume = "can_update_shared_volume" + RelationCanDeleteSharedVolume = "can_delete_shared_volume" + RelationCanListSharedVolumes = "can_list_shared_volumes" + // maxTuplesPerWrite is OpenFGA's default maximum number of tuples allowed in a single Write request. maxTuplesPerWrite = 100 ) diff --git a/cmd/ateapi/internal/authz/interceptor_test.go b/cmd/ateapi/internal/authz/interceptor_test.go index f7bf2c4fb6..379d1d7149 100644 --- a/cmd/ateapi/internal/authz/interceptor_test.go +++ b/cmd/ateapi/internal/authz/interceptor_test.go @@ -657,6 +657,36 @@ func TestUnaryServerInterceptor_LifecycleEgressWorkerAndTagChecks(t *testing.T) {"bootstrap owner cannot mint actor certificate", "bootstrap-owner", mintCert, codes.PermissionDenied}, } + // SharedVolume permissions must enforce the owning atespace across all RPCs. + for _, call := range []struct { + method string + req any + write, global bool + }{ + {ateapipb.Control_CreateSharedVolume_FullMethodName, &ateapipb.CreateSharedVolumeRequest{SharedVolume: &ateapipb.SharedVolume{Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "data"}}}, true, false}, + {ateapipb.Control_GetSharedVolume_FullMethodName, &ateapipb.GetSharedVolumeRequest{SharedVolume: actor}, false, false}, + {ateapipb.Control_UpdateSharedVolume_FullMethodName, &ateapipb.UpdateSharedVolumeRequest{SharedVolume: &ateapipb.SharedVolume{Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "data"}}}, true, false}, + {ateapipb.Control_DeleteSharedVolume_FullMethodName, &ateapipb.DeleteSharedVolumeRequest{SharedVolume: actor}, true, false}, + {ateapipb.Control_ListSharedVolumes_FullMethodName, &ateapipb.ListSharedVolumesRequest{Atespace: "team-a"}, false, false}, + {ateapipb.Control_ListSharedVolumes_FullMethodName, &ateapipb.ListSharedVolumesRequest{}, false, true}, + } { + for _, user := range []string{"global-owner", "global-viewer", "editor", "viewer", "outsider"} { + t.Run(call.method+"/"+user, func(t *testing.T) { + allowed := user == "global-owner" || (!call.write && user == "global-viewer") || (!call.global && (user == "editor" || (!call.write && user == "viewer"))) + userCtx := principal.InjectContext(ctx, principal.PrincipalInfo{ID: user, Kind: principal.KindJWT}) + called := false + _, err := UnaryServerInterceptor(authorizer, true)(userCtx, call.req, &grpc.UnaryServerInfo{FullMethod: call.method}, func(context.Context, any) (any, error) { called = true; return nil, nil }) + if allowed { + if err != nil || !called { + t.Fatalf("allowed call rejected: %v", err) + } + } else if apierror.Code(err) != codes.PermissionDenied || called { + t.Fatalf("unauthorized call reached handler: %v", err) + } + }) + } + } + interceptor := UnaryServerInterceptor(authorizer, true) for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { diff --git a/cmd/ateapi/internal/authz/model.fga b/cmd/ateapi/internal/authz/model.fga index ccd8242108..1fb536136f 100644 --- a/cmd/ateapi/internal/authz/model.fga +++ b/cmd/ateapi/internal/authz/model.fga @@ -62,6 +62,7 @@ type global define can_list_actors: viewer or ingress_router or egress_gateway define can_list_actor_templates: viewer define can_list_tags: viewer + define can_list_shared_volumes: viewer # --- Policy Governance --- define can_create_access_policy: owner @@ -94,7 +95,12 @@ type atespace define can_list_actors: viewer define can_list_actor_templates: viewer define can_create_tag: editor + define can_create_shared_volume: editor + define can_get_shared_volume: viewer + define can_update_shared_volume: editor + define can_delete_shared_volume: editor define can_list_tags: viewer + define can_list_shared_volumes: viewer define can_get: viewer define can_delete: owner diff --git a/cmd/ateapi/internal/authz/registry.go b/cmd/ateapi/internal/authz/registry.go index 65231d9e9c..c44f3e78e7 100644 --- a/cmd/ateapi/internal/authz/registry.go +++ b/cmd/ateapi/internal/authz/registry.go @@ -336,6 +336,39 @@ var defaultRPCPermissions = map[string]rpcRule{ return onTag(RelationCanDelete, r.GetTag(), field.NewPath("tag")) })), + // Shared volumes are governed by their owning atespace. + ateapipb.Control_CreateSharedVolume_FullMethodName: rule(checksOf(func(r *ateapipb.CreateSharedVolumeRequest) ([]check, field.ErrorList) { + ref := r.GetSharedVolume().GetMetadata() + if errs := validateRef(ref, field.NewPath("shared_volume", "metadata")); len(errs) > 0 { + return nil, errs + } + return onAtespace(RelationCanCreateSharedVolume, ref.GetAtespace(), field.NewPath("shared_volume", "metadata", "atespace")) + })), + ateapipb.Control_GetSharedVolume_FullMethodName: rule(checksOf(func(r *ateapipb.GetSharedVolumeRequest) ([]check, field.ErrorList) { + ref := r.GetSharedVolume() + if errs := validateRef(ref, field.NewPath("shared_volume")); len(errs) > 0 { + return nil, errs + } + return onAtespace(RelationCanGetSharedVolume, ref.GetAtespace(), field.NewPath("shared_volume", "atespace")) + })), + ateapipb.Control_UpdateSharedVolume_FullMethodName: rule(checksOf(func(r *ateapipb.UpdateSharedVolumeRequest) ([]check, field.ErrorList) { + ref := r.GetSharedVolume().GetMetadata() + if errs := validateRef(ref, field.NewPath("shared_volume", "metadata")); len(errs) > 0 { + return nil, errs + } + return onAtespace(RelationCanUpdateSharedVolume, ref.GetAtespace(), field.NewPath("shared_volume", "metadata", "atespace")) + })), + ateapipb.Control_DeleteSharedVolume_FullMethodName: rule(checksOf(func(r *ateapipb.DeleteSharedVolumeRequest) ([]check, field.ErrorList) { + ref := r.GetSharedVolume() + if errs := validateRef(ref, field.NewPath("shared_volume")); len(errs) > 0 { + return nil, errs + } + return onAtespace(RelationCanDeleteSharedVolume, ref.GetAtespace(), field.NewPath("shared_volume", "atespace")) + })), + ateapipb.Control_ListSharedVolumes_FullMethodName: rule(checksOf(func(r *ateapipb.ListSharedVolumesRequest) ([]check, field.ErrorList) { + return onAtespaceOrGlobal(RelationCanListSharedVolumes, r.GetAtespace(), field.NewPath("atespace")) + })), + // WorkerService is served only to atelet, which each handler // authenticates by its mTLS SPIFFE ID. ateapipb.WorkerService_RegisterWorker_FullMethodName: inHandler("ateletauth: caller is atelet on the Worker's node"), diff --git a/cmd/ateapi/internal/authz/registry_test.go b/cmd/ateapi/internal/authz/registry_test.go index ce3f8ceaf6..ee87a27674 100644 --- a/cmd/ateapi/internal/authz/registry_test.go +++ b/cmd/ateapi/internal/authz/registry_test.go @@ -64,6 +64,14 @@ func TestDefaultRPCPermissions(t *testing.T) { want: []check{{RelationCanDelete, "atespace:team-a"}}, }, + // Shared-volume permissions are scoped to the owning atespace. + {name: "CreateSharedVolume", fullMethod: ateapipb.Control_CreateSharedVolume_FullMethodName, req: &ateapipb.CreateSharedVolumeRequest{SharedVolume: &ateapipb.SharedVolume{Metadata: actorMeta}}, want: []check{{RelationCanCreateSharedVolume, "atespace:team-a"}}}, + {name: "GetSharedVolume", fullMethod: ateapipb.Control_GetSharedVolume_FullMethodName, req: &ateapipb.GetSharedVolumeRequest{SharedVolume: actorRef}, want: []check{{RelationCanGetSharedVolume, "atespace:team-a"}}}, + {name: "UpdateSharedVolume", fullMethod: ateapipb.Control_UpdateSharedVolume_FullMethodName, req: &ateapipb.UpdateSharedVolumeRequest{SharedVolume: &ateapipb.SharedVolume{Metadata: actorMeta}}, want: []check{{RelationCanUpdateSharedVolume, "atespace:team-a"}}}, + {name: "DeleteSharedVolume", fullMethod: ateapipb.Control_DeleteSharedVolume_FullMethodName, req: &ateapipb.DeleteSharedVolumeRequest{SharedVolume: actorRef}, want: []check{{RelationCanDeleteSharedVolume, "atespace:team-a"}}}, + {name: "ListSharedVolumes atespace:team-a", fullMethod: ateapipb.Control_ListSharedVolumes_FullMethodName, req: &ateapipb.ListSharedVolumesRequest{Atespace: "team-a"}, want: []check{{RelationCanListSharedVolumes, "atespace:team-a"}}}, + {name: "ListSharedVolumes global:root", fullMethod: ateapipb.Control_ListSharedVolumes_FullMethodName, req: &ateapipb.ListSharedVolumesRequest{Atespace: ""}, want: []check{{RelationCanListSharedVolumes, "global:root"}}}, + // Actor templates. { name: "CreateActorTemplate", diff --git a/cmd/ateapi/internal/controlapi/actor.go b/cmd/ateapi/internal/controlapi/actor.go index b0591d18e0..7f9d5c67b9 100644 --- a/cmd/ateapi/internal/controlapi/actor.go +++ b/cmd/ateapi/internal/controlapi/actor.go @@ -90,17 +90,21 @@ func (s *ServiceImpl) CreateActor(ctx context.Context, inActor *ateapipb.Actor) // Resolve the explicit tag, or freeze the template's current golden default. tagRef := inActor.GetSourceTag() - if tagRef == nil { - tagRef = template.GetStatus().GetGoldenSnapshotStatus().GetGoldenTag() - } else { - // NOTE: whether the ExternalVolumes gate is enabled or not, we do not - // support cloning from a tag that has external volumes. + switch { + case tagRef != nil: for _, volume := range template.GetVolumes() { - if volume.GetExternalVolumeTemplate() != nil { + if volume.GetExternal().GetTemplate() != nil { // TODO: Permit cloning after CSI volume snapshots are supported. return nil, apierror.FailedPrecondition("Tag cloning does not support ActorTemplates with external volumes") } + if volume.GetExternal().GetSharedVolume() != nil { + return nil, apierror.FailedPrecondition("Tag cloning does not support ActorTemplates that reference a SharedVolume") + } } + case templateHasSharedVolumeRef(template) && template.GetSnapshotConfig().GetGoldenSnapshotConfig().GetMode() != ateapipb.GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_RUNTIME: + // TODO(shared-volumes): restore from the golden once checkpoint and restore mount options agree for external volume refs; see docs/csi-volumes.md. + default: + tagRef = template.GetStatus().GetGoldenSnapshotStatus().GetGoldenTag() } var sourceTag *ateapipb.Tag if tagRef != nil { @@ -119,8 +123,8 @@ func (s *ServiceImpl) CreateActor(ctx context.Context, inActor *ateapipb.Actor) name := inActor.GetMetadata().GetName() // Volume creation is completed asynchronously after the actor is recorded. - var initVols []*ateapipb.ExternalVolume - if iv, err := initialExternalVolumes(ctx, s.storageClassLister, template); err != nil { + var initVols []*ateapipb.ExternalVolumeRef + if iv, err := initialActorVolumes(ctx, s.storageClassLister, template); err != nil { return nil, err } else if preview.IsEnabledFor(ctx, preview.GateExternalVolumes) { initVols = iv @@ -131,6 +135,13 @@ func (s *ServiceImpl) CreateActor(ctx context.Context, inActor *ateapipb.Actor) } return nil, resources.ToAPIError(errs) } + if !isGoldenActor(inActor) { + refVols, err := resolveSharedVolumeRefs(ctx, s.store, inActor, template) + if err != nil { + return nil, err + } + initVols = append(initVols, refVols...) + } // Verify that the result is properly valid before storing it. outActor := proto.CloneOf(inActor) @@ -164,6 +175,9 @@ func (s *ServiceImpl) CreateActor(ctx context.Context, inActor *ateapipb.Actor) return nil, apierror.AlreadyExists("Actor %s already exists", name) } if errors.Is(err, store.ErrFailedPrecondition) { + if templateHasSharedVolumeRef(template) { + return nil, apierror.FailedPrecondition("Atespace %s or a referenced SharedVolume is unavailable", atespace) + } return nil, apierror.FailedPrecondition("Atespace %s not found", atespace) } return nil, fmt.Errorf("while recording actor: %w", err) @@ -176,6 +190,10 @@ func (s *ServiceImpl) CreateActor(ctx context.Context, inActor *ateapipb.Actor) return stored, nil } +func isGoldenActor(actor *ateapipb.Actor) bool { + return actor.GetMetadata().GetAtespace() == resources.GoldenActorAtespace +} + // resolveTagSource resolves a CreateActor request's source tag // and checks that the tag is usable for creating an Actor in actorAtespace // from template. diff --git a/cmd/ateapi/internal/controlapi/functionaltest/actor_template_test.go b/cmd/ateapi/internal/controlapi/functionaltest/actor_template_test.go index 2ff8dbdd28..037397872b 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/actor_template_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/actor_template_test.go @@ -428,8 +428,8 @@ func TestCreateActorTemplate_ExternalVolumesPreview(t *testing.T) { VolumeMounts: []*ateapipb.VolumeMount{{Name: "data", MountPath: "/data"}}, }}, Volumes: []*ateapipb.Volume{{ - Name: "data", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{StorageClassName: "standard", Capacity: "1Gi"}, + Name: "data", + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{StorageClassName: "standard", Capacity: "1Gi"}}, }}, Resources: &ateapipb.Resources{Limits: []*ateapipb.Limits{{Name: "cpu", Quantity: "1"}, {Name: "memory", Quantity: "1Gi"}}}, SnapshotConfig: &ateapipb.SnapshotConfig{StorageLocation: "gs://my-bucket/snapshots"}, diff --git a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go index 02f6acd32d..87a16d2aae 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go @@ -121,10 +121,10 @@ func TestCreateActor_WithExternalVolumes(t *testing.T) { volumes := []*ateapipb.Volume{ { Name: "ext-vol-1", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", Capacity: "10Gi", - }, + }}, }, } mounts := []*ateapipb.VolumeMount{ @@ -183,8 +183,8 @@ func TestCreateActor_ExternalVolumesWithoutOptIn(t *testing.T) { createTemplateWithVolumes(t, tc, ns, []*ateapipb.Volume{{ - Name: "ext-vol-1", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{StorageClassName: "standard", Capacity: "10Gi"}, + Name: "ext-vol-1", + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{StorageClassName: "standard", Capacity: "10Gi"}}, }}, []*ateapipb.VolumeMount{{Name: "ext-vol-1", MountPath: "/data"}}) @@ -215,8 +215,8 @@ func TestActorLifecycle_ExternalVolumesWithoutOptIn(t *testing.T) { createTemplateWithVolumes(t, tc, ns, []*ateapipb.Volume{{ - Name: "data-vol", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{StorageClassName: "fast", Capacity: "20Gi"}, + Name: "data-vol", + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{StorageClassName: "fast", Capacity: "20Gi"}}, }}, []*ateapipb.VolumeMount{{Name: "data-vol", MountPath: "/mnt/data"}}) createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") @@ -413,9 +413,9 @@ func TestCreateActor_RejectsSnapshotWithExternalVolumes(t *testing.T) { }}, Volumes: []*ateapipb.Volume{{ Name: "data", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ Capacity: "1Gi", StorageClassName: "standard", - }, + }}, }}, Resources: resources.CPUMemory(1000, 512<<20), }, @@ -1507,7 +1507,7 @@ func TestDeleteActor_MultipleVolumeDeletionFailures(t *testing.T) { ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"}, Status: &ateapipb.ActorStatus{ State: ateapipb.ActorState_ACTOR_STATE_SUSPENDED, - ExternalVolumes: []*ateapipb.ExternalVolume{ + ExternalVolumes: []*ateapipb.ExternalVolumeRef{ {Name: "vol1", StorageVolumeId: "storage-vol-1", VolumeType: "substrate.io/mock"}, {Name: "vol2", StorageVolumeId: "storage-vol-2", VolumeType: "substrate.io/mock"}, }, @@ -1585,7 +1585,7 @@ func TestDeleteActor_VolumeDeletionFailure_RetrySuccess(t *testing.T) { ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"}, Status: &ateapipb.ActorStatus{ State: ateapipb.ActorState_ACTOR_STATE_SUSPENDED, - ExternalVolumes: []*ateapipb.ExternalVolume{ + ExternalVolumes: []*ateapipb.ExternalVolumeRef{ {Name: "vol1", StorageVolumeId: "storage-vol-1", VolumeType: "substrate.io/mock"}, }, }, @@ -1719,10 +1719,10 @@ func TestActorLifecycle_WithExternalVolumes(t *testing.T) { volumes := []*ateapipb.Volume{ { Name: "data-vol", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "fast", Capacity: "20Gi", - }, + }}, }, } mounts := []*ateapipb.VolumeMount{ @@ -1867,17 +1867,17 @@ func TestResumeActor_VolumeCreationFailure(t *testing.T) { volumes := []*ateapipb.Volume{ { Name: "succ-vol1", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", Capacity: "10Gi", - }, + }}, }, { Name: "fail-vol2", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", Capacity: "10Gi", - }, + }}, }, } mounts := []*ateapipb.VolumeMount{ @@ -1925,7 +1925,7 @@ func TestResumeActor_VolumeCreationFailure(t *testing.T) { if len(getResp.GetStatus().GetExternalVolumes()) != 2 { t.Fatalf("expected 2 volumes on actor, got %d", len(getResp.GetStatus().GetExternalVolumes())) } - volsByName := make(map[string]*ateapipb.ExternalVolume) + volsByName := make(map[string]*ateapipb.ExternalVolumeRef) for _, v := range getResp.GetStatus().GetExternalVolumes() { volsByName[v.GetName()] = v } @@ -2011,17 +2011,17 @@ func TestResumeActor_VolumeCreationRetrySuccess(t *testing.T) { volumes := []*ateapipb.Volume{ { Name: "succ-vol1", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", Capacity: "10Gi", - }, + }}, }, { Name: "retry-vol2", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", Capacity: "10Gi", - }, + }}, }, } retryMounts := []*ateapipb.VolumeMount{ @@ -2061,7 +2061,7 @@ func TestResumeActor_VolumeCreationRetrySuccess(t *testing.T) { t.Errorf("actor state after first resume = %v, want %v", getResp.GetStatus().GetState(), ateapipb.ActorState_ACTOR_STATE_SUSPENDED) } - volsByName := make(map[string]*ateapipb.ExternalVolume) + volsByName := make(map[string]*ateapipb.ExternalVolumeRef) for _, v := range getResp.GetStatus().GetExternalVolumes() { volsByName[v.GetName()] = v } @@ -2167,10 +2167,10 @@ func TestResumeActor_VolumeAttachFailureAndRetry(t *testing.T) { volumes := []*ateapipb.Volume{ { Name: "vol1", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", Capacity: "10Gi", - }, + }}, }, } mounts := []*ateapipb.VolumeMount{ @@ -2271,10 +2271,10 @@ func TestResumeActor_VolumeAttachFailure_DeleteActor(t *testing.T) { volumes := []*ateapipb.Volume{ { Name: "vol1", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", Capacity: "10Gi", - }, + }}, }, } mounts := []*ateapipb.VolumeMount{ @@ -2428,17 +2428,17 @@ func TestResumeActor_MultiVolumePartialAttachFailure_Retry(t *testing.T) { volumes := []*ateapipb.Volume{ { Name: "vol1", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", Capacity: "10Gi", - }, + }}, }, { Name: "vol2", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", Capacity: "10Gi", - }, + }}, }, } mounts := []*ateapipb.VolumeMount{ @@ -2600,10 +2600,10 @@ func TestSuspendActor_VolumeDetachFailure_RetrySuccess(t *testing.T) { volumes := []*ateapipb.Volume{ { Name: "vol1", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", Capacity: "10Gi", - }, + }}, }, } mounts := []*ateapipb.VolumeMount{ @@ -2712,10 +2712,10 @@ func TestSuspendActor_VolumeDetachFailure_DeleteActorAnyState(t *testing.T) { volumes := []*ateapipb.Volume{ { Name: "vol1", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", Capacity: "10Gi", - }, + }}, }, } mounts := []*ateapipb.VolumeMount{ @@ -2813,10 +2813,10 @@ func TestPauseActor_VolumeLifecycle_DetachAndResumeAttach(t *testing.T) { volumes := []*ateapipb.Volume{ { Name: "vol1", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", Capacity: "10Gi", - }, + }}, }, } mounts := []*ateapipb.VolumeMount{ @@ -2941,10 +2941,10 @@ func TestPauseActor_VolumeDetachFailure_RetrySuccess(t *testing.T) { volumes := []*ateapipb.Volume{ { Name: "vol1", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", Capacity: "10Gi", - }, + }}, }, } mounts := []*ateapipb.VolumeMount{ diff --git a/cmd/ateapi/internal/controlapi/functionaltest/common_test.go b/cmd/ateapi/internal/controlapi/functionaltest/common_test.go index e993d92472..3969b003c7 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/common_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/common_test.go @@ -468,7 +468,7 @@ func createTemplateWithVolumes(t *testing.T, tc *testContext, ns string, volumes // "tmpl1" in testAtespace, backed by a WorkerPool in ns whose labels match the // template's worker selector, and seeds its golden snapshot. ns keys the pool // labels, so each test's template still selects only its own pool. -func createTemplateWithContainersAndVolumes(t *testing.T, tc *testContext, ns string, containers []*ateapipb.Container, volumes []*ateapipb.Volume) *ateapipb.ActorTemplate { +func createTemplateWithContainersAndVolumes(t *testing.T, tc *testContext, ns string, containers []*ateapipb.Container, volumes []*ateapipb.Volume, mode ...ateapipb.GoldenSnapshotMode) *ateapipb.ActorTemplate { t.Helper() // Sandbox binaries live on a (cluster-scoped) SandboxConfig the template @@ -477,15 +477,17 @@ func createTemplateWithContainersAndVolumes(t *testing.T, tc *testContext, ns st ensureDefaultGvisorSandboxConfig(t, tc) createWorkerPool(t, tc, ns, "pool1", map[string]string{poolLabelKey: ns}) + snapshotConfig := &ateapipb.SnapshotConfig{StorageLocation: testStorageLocation} + if len(mode) > 0 { + snapshotConfig.GoldenSnapshotConfig = &ateapipb.GoldenSnapshotConfig{Mode: mode[0]} + } created, err := tc.client.CreateActorTemplate(context.Background(), &ateapipb.CreateActorTemplateRequest{ ActorTemplate: &ateapipb.ActorTemplate{ Metadata: &ateapipb.ResourceMetadata{ Atespace: testAtespace, Name: "tmpl1", }, - SnapshotConfig: &ateapipb.SnapshotConfig{ - StorageLocation: testStorageLocation, - }, + SnapshotConfig: snapshotConfig, SandboxConfig: &ateapipb.SandboxConfig{ SandboxClass: "gvisor", ConfigName: "gvisor-default", diff --git a/cmd/ateapi/internal/controlapi/functionaltest/shared_volume_golden_test.go b/cmd/ateapi/internal/controlapi/functionaltest/shared_volume_golden_test.go new file mode 100644 index 0000000000..555c25b36a --- /dev/null +++ b/cmd/ateapi/internal/controlapi/functionaltest/shared_volume_golden_test.go @@ -0,0 +1,182 @@ +// 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 functionaltest + +import ( + "context" + "testing" + + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "google.golang.org/grpc/codes" +) + +func resetAteletCalls(tc *testContext) { + tc.fakeAtelet.Lock.Lock() + defer tc.fakeAtelet.Lock.Unlock() + tc.fakeAtelet.RunCalled, tc.fakeAtelet.RunRequest = false, nil + tc.fakeAtelet.RestoreCalled, tc.fakeAtelet.RestoreRequest = false, nil +} + +func ateletCalls(tc *testContext) (run, restore bool) { + tc.fakeAtelet.Lock.Lock() + defer tc.fakeAtelet.Lock.Unlock() + return tc.fakeAtelet.RunCalled, tc.fakeAtelet.RestoreCalled +} + +func getActor(t *testing.T, tc *testContext, name string) *ateapipb.Actor { + t.Helper() + actor, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ + Actor: &ateapipb.ObjectRef{Atespace: testAtespace, Name: name}, + }) + if err != nil { + t.Fatalf("GetActor(%s) failed: %v", name, err) + } + return actor +} + +func TestSharedVolumeRef_ColdBootsThenRestoresOwnSnapshot(t *testing.T) { + ns := namespaceForTest("ns-extvol-coldboot") + tc, _ := setupSharedVolumeTest(t, ns) + defer tc.cleanup() + + createSharedVolume(t, tc, "shared", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL) + tmpl := createTemplateWithVolumes(t, tc, ns, + []*ateapipb.Volume{{Name: "work", External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{Name: "shared"}}}}, + []*ateapipb.VolumeMount{{Name: "work", MountPath: "/mnt/work"}}) + if tmpl.GetStatus().GetGoldenSnapshotStatus().GetGoldenTag() == nil { + t.Fatal("test template has no golden tag; the case needs one to prove it is bypassed") + } + workerName := createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") + ctx := context.Background() + ref := &ateapipb.ObjectRef{Atespace: testAtespace, Name: "borrower"} + + createBorrowingActor(t, tc, "borrower", nil) + if snaps := getActor(t, tc, "borrower").GetStatus().GetSnapshots(); len(snaps) != 0 { + t.Fatalf("new actor carries snapshots %v, want none: it must not seed from the golden", snaps) + } + + waitForWorkerAvailable(t, tc, workerName) + resetAteletCalls(tc) + if _, err := tc.client.ResumeActor(ctx, &ateapipb.ResumeActorRequest{Actor: ref}); err != nil { + t.Fatalf("first ResumeActor failed: %v", err) + } + if run, restore := ateletCalls(tc); !run || restore { + t.Fatalf("first resume: Run=%v Restore=%v, want a cold boot through Run", run, restore) + } + + if _, err := tc.client.SuspendActor(ctx, &ateapipb.SuspendActorRequest{Actor: ref}); err != nil { + t.Fatalf("SuspendActor failed: %v", err) + } + own := durableSnapshotURI(getActor(t, tc, "borrower").GetStatus()) + if own == "" || own == goldenSnapshotURI(t) { + t.Fatalf("snapshot after suspend = %q, want the actor's own", own) + } + + waitForWorkerAvailable(t, tc, workerName) + resetAteletCalls(tc) + if _, err := tc.client.ResumeActor(ctx, &ateapipb.ResumeActorRequest{Actor: ref}); err != nil { + t.Fatalf("second ResumeActor failed: %v", err) + } + if run, restore := ateletCalls(tc); run || !restore { + t.Fatalf("second resume: Run=%v Restore=%v, want a restore", run, restore) + } + if got := tc.fakeAtelet.lastRestoreRequest().GetSnapshot().GetObject().GetSnapshotUri(); got != own { + t.Errorf("second resume restored %q, want the actor's own snapshot %q", got, own) + } +} + +func TestSharedVolumeRef_RejectsSourceTag(t *testing.T) { + ns := namespaceForTest("ns-extvol-sourcetag") + tc, _ := setupSharedVolumeTest(t, ns) + defer tc.cleanup() + + createSharedVolume(t, tc, "shared", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL) + tmpl := createTemplateWithVolumes(t, tc, ns, + []*ateapipb.Volume{{Name: "work", External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{Name: "shared"}}}}, + []*ateapipb.VolumeMount{{Name: "work", MountPath: "/mnt/work"}}) + + _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "clone"}, + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"}, + SourceTag: tmpl.GetStatus().GetGoldenSnapshotStatus().GetGoldenTag(), + }}) + assertGrpcError(t, err, codes.FailedPrecondition, "Tag cloning does not support ActorTemplates that reference a SharedVolume") +} + +func TestExternalVolumeTemplate_FirstResumeRestoresGolden(t *testing.T) { + ns := namespaceForTest("ns-extvol-owned-golden") + tc, _ := setupSharedVolumeTest(t, ns) + defer tc.cleanup() + + createTemplateWithVolumes(t, tc, ns, + []*ateapipb.Volume{{Name: "work", External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{StorageClassName: "standard", Capacity: "1Gi"}}}}, + []*ateapipb.VolumeMount{{Name: "work", MountPath: "/mnt/work"}}) + workerName := createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") + ctx := context.Background() + ref := &ateapipb.ObjectRef{Atespace: testAtespace, Name: "owner"} + + if _, err := tc.client.CreateActor(ctx, &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "owner"}, + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"}, + }}); err != nil { + t.Fatalf("CreateActor failed: %v", err) + } + if got := durableSnapshotURI(getActor(t, tc, "owner").GetStatus()); got != goldenSnapshotURI(t) { + t.Fatalf("new actor snapshot = %q, want the golden %q", got, goldenSnapshotURI(t)) + } + + waitForWorkerAvailable(t, tc, workerName) + resetAteletCalls(tc) + if _, err := tc.client.ResumeActor(ctx, &ateapipb.ResumeActorRequest{Actor: ref}); err != nil { + t.Fatalf("ResumeActor failed: %v", err) + } + if run, restore := ateletCalls(tc); run || !restore { + t.Fatalf("first resume: Run=%v Restore=%v, want a restore from the golden", run, restore) + } + if got := tc.fakeAtelet.lastRestoreRequest().GetSnapshot().GetObject().GetSnapshotUri(); got != goldenSnapshotURI(t) { + t.Errorf("first resume restored %q, want the golden %q", got, goldenSnapshotURI(t)) + } +} + +func TestSharedVolumeRef_RuntimeGoldenSeedsAndRestores(t *testing.T) { + ns := namespaceForTest("ns-runtime-golden-create") + tc, _ := setupSharedVolumeTest(t, ns) + defer tc.cleanup() + createSharedVolume(t, tc, "shared", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL) + createTemplateWithContainersAndVolumes(t, tc, ns, + []*ateapipb.Container{{Name: "main", Image: "main@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Command: []string{"/main"}, VolumeMounts: []*ateapipb.VolumeMount{{Name: "work", MountPath: "/mnt/work"}}}}, + []*ateapipb.Volume{{Name: "work", External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{Name: "shared"}}}}, + ateapipb.GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_RUNTIME) + worker := createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") + createBorrowingActor(t, tc, "borrower", nil) + actor := getActor(t, tc, "borrower") + if got := durableSnapshotURI(actor.GetStatus()); got != goldenSnapshotURI(t) { + t.Fatalf("source = %q, want runtime golden", got) + } + if actor.GetStatus().GetSnapshots()[0].GetOwner() != ateapipb.SnapshotOwner_SNAPSHOT_OWNER_TAG { + t.Fatal("clone must borrow the golden snapshot") + } + waitForWorkerAvailable(t, tc, worker) + resetAteletCalls(tc) + if _, err := tc.client.ResumeActor(context.Background(), &ateapipb.ResumeActorRequest{Actor: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "borrower"}}); err != nil { + t.Fatal(err) + } + if run, restore := ateletCalls(tc); run || !restore { + t.Fatalf("Run=%v Restore=%v", run, restore) + } + if tc.fakeAtelet.lastRestoreRequest().GetSpec().GetRuntimeGolden() { + t.Fatal("ordinary clone marked for runtime-only capture") + } +} diff --git a/cmd/ateapi/internal/controlapi/functionaltest/shared_volume_test.go b/cmd/ateapi/internal/controlapi/functionaltest/shared_volume_test.go new file mode 100644 index 0000000000..57597d8f96 --- /dev/null +++ b/cmd/ateapi/internal/controlapi/functionaltest/shared_volume_test.go @@ -0,0 +1,592 @@ +// 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 functionaltest + +import ( + "context" + "errors" + "fmt" + "sync" + "testing" + + "github.com/agent-substrate/substrate/internal/preview" + "github.com/agent-substrate/substrate/internal/volume" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "github.com/google/go-cmp/cmp" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/testing/protocmp" +) + +// sharedVolumePlugin records every storage operation the control plane makes. +type sharedVolumePlugin struct { + volume.VolumePluginControlPlane + + mu sync.Mutex + detachErr error + created []string + deleted []string + attached []string + detached []string +} + +func (p *sharedVolumePlugin) CreateVolume(_ context.Context, req volume.CreateVolumeRequest) (volume.CreateVolumeResponse, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.created = append(p.created, req.Name) + return volume.CreateVolumeResponse{VolumeID: "disk/" + req.Name, VolumeContext: req.Parameters}, nil +} + +func (p *sharedVolumePlugin) DeleteVolume(_ context.Context, volumeID string) error { + p.mu.Lock() + defer p.mu.Unlock() + p.deleted = append(p.deleted, volumeID) + return nil +} + +func (p *sharedVolumePlugin) AttachVolume(_ context.Context, req volume.AttachVolumeRequest) (volume.AttachVolumeResponse, error) { + p.mu.Lock() + defer p.mu.Unlock() + p.attached = append(p.attached, req.VolumeID+"@"+req.Node) + return volume.AttachVolumeResponse{}, nil +} + +func (p *sharedVolumePlugin) DetachVolume(_ context.Context, volumeID, node string) error { + p.mu.Lock() + defer p.mu.Unlock() + if p.detachErr != nil { + return p.detachErr + } + p.detached = append(p.detached, volumeID+"@"+node) + return nil +} + +func (p *sharedVolumePlugin) snapshot() (created, deleted, attached, detached []string) { + p.mu.Lock() + defer p.mu.Unlock() + return append([]string(nil), p.created...), append([]string(nil), p.deleted...), + append([]string(nil), p.attached...), append([]string(nil), p.detached...) +} + +func setupSharedVolumeTest(t *testing.T, ns string) (*testContext, *sharedVolumePlugin) { + preview.InitForTest(t, "ExternalVolumes") + t.Helper() + plugin := &sharedVolumePlugin{} + tc := setupTestWithVolumePlugins(t, ns, map[string]volume.VolumePluginControlPlane{ + "substrate.io/mock": plugin, + }) + return tc, plugin +} + +func createSharedVolume(t *testing.T, tc *testContext, name string, trigger ateapipb.DeleteTrigger) *ateapipb.SharedVolume { + t.Helper() + created, err := tc.client.CreateSharedVolume(context.Background(), &ateapipb.CreateSharedVolumeRequest{ + SharedVolume: &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: name}, + StorageClassName: "standard", + Capacity: "10Gi", + DeleteTrigger: trigger, + }, + }) + if err != nil { + t.Fatalf("CreateSharedVolume(%s) failed: %v", name, err) + } + if got := created.GetStatus().GetState(); got != ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY { + t.Fatalf("CreateSharedVolume(%s) left the volume in %v, want READY", name, got) + } + return created +} + +func getSharedVolume(t *testing.T, tc *testContext, name string) *ateapipb.SharedVolume { + t.Helper() + stored, err := tc.client.GetSharedVolume(context.Background(), &ateapipb.GetSharedVolumeRequest{ + SharedVolume: &ateapipb.ObjectRef{Atespace: testAtespace, Name: name}, + }) + if err != nil { + t.Fatalf("GetSharedVolume(%s) failed: %v", name, err) + } + return stored +} + +func TestCreateSharedVolume_Provisioned(t *testing.T) { + ns := namespaceForTest("ns-extvol-provisioned") + tc, plugin := setupSharedVolumeTest(t, ns) + defer tc.cleanup() + + created := createSharedVolume(t, tc, "shared", ateapipb.DeleteTrigger_DELETE_TRIGGER_UNSPECIFIED) + + wantDiskName := "substrate-" + created.GetMetadata().GetUid() + gotCreated, _, _, _ := plugin.snapshot() + if diff := cmp.Diff([]string{wantDiskName}, gotCreated); diff != "" { + t.Errorf("provisioned disks mismatch (-want +got):\n%s", diff) + } + if got, want := created.GetVolumeId(), "disk/"+wantDiskName; got != want { + t.Errorf("volume_id = %q, want %q", got, want) + } + if got, want := created.GetVolumeType(), "substrate.io/mock"; got != want { + t.Errorf("volume_type = %q, want %q", got, want) + } + if got := created.GetDeleteTrigger(); got != ateapipb.DeleteTrigger_DELETE_TRIGGER_LAST_ACTOR { + t.Errorf("delete_trigger = %v, want LAST_ACTOR", got) + } + if got := created.GetAccessMode(); got != ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_ONCE { + t.Errorf("access_mode = %v, want READ_WRITE_ONCE", got) + } + + if diff := cmp.Diff(created, getSharedVolume(t, tc, "shared"), protocmp.Transform()); diff != "" { + t.Errorf("GetSharedVolume mismatch (-want +got):\n%s", diff) + } + listed, err := tc.client.ListSharedVolumes(context.Background(), &ateapipb.ListSharedVolumesRequest{Atespace: testAtespace}) + if err != nil { + t.Fatalf("ListSharedVolumes failed: %v", err) + } + if diff := cmp.Diff([]*ateapipb.SharedVolume{created}, listed.GetSharedVolumes(), protocmp.Transform()); diff != "" { + t.Errorf("ListSharedVolumes mismatch (-want +got):\n%s", diff) + } +} + +func TestCreateSharedVolume_Registered(t *testing.T) { + ns := namespaceForTest("ns-extvol-registered") + tc, plugin := setupSharedVolumeTest(t, ns) + defer tc.cleanup() + + created, err := tc.client.CreateSharedVolume(context.Background(), &ateapipb.CreateSharedVolumeRequest{ + SharedVolume: &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "adopted"}, + VolumeId: "disk/made-elsewhere", + VolumeType: "substrate.io/mock", + }, + }) + if err != nil { + t.Fatalf("CreateSharedVolume failed: %v", err) + } + if got := created.GetStatus().GetState(); got != ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY { + t.Errorf("state = %v, want READY", got) + } + if got := created.GetDeleteTrigger(); got != ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL { + t.Errorf("delete_trigger = %v, want MANUAL", got) + } + if gotCreated, _, _, _ := plugin.snapshot(); len(gotCreated) != 0 { + t.Errorf("provisioned %v, want nothing provisioned for a registered volume", gotCreated) + } +} + +func TestCreateSharedVolume_Rejected(t *testing.T) { + ns := namespaceForTest("ns-extvol-rejected") + tc, _ := setupSharedVolumeTest(t, ns) + defer tc.cleanup() + + first := createSharedVolume(t, tc, "shared", ateapipb.DeleteTrigger_DELETE_TRIGGER_UNSPECIFIED) + + _, err := tc.client.CreateSharedVolume(context.Background(), &ateapipb.CreateSharedVolumeRequest{ + SharedVolume: &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "shared"}, + StorageClassName: "fast", + Capacity: "20Gi", + }, + }) + assertGrpcError(t, err, codes.AlreadyExists, + fmt.Sprintf("SharedVolume %s/shared already exists; delete it and create it again to retry", testAtespace)) + if diff := cmp.Diff(first, getSharedVolume(t, tc, "shared"), protocmp.Transform()); diff != "" { + t.Errorf("the rejected create changed the stored volume (-want +got):\n%s", diff) + } + + _, err = tc.client.CreateSharedVolume(context.Background(), &ateapipb.CreateSharedVolumeRequest{ + SharedVolume: &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "many"}, + StorageClassName: "standard", + Capacity: "10Gi", + AccessMode: ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_MANY, + }, + }) + assertGrpcErrorRegex(t, err, codes.InvalidArgument, "access_mode") +} + +func TestUpdateSharedVolume(t *testing.T) { + ns := namespaceForTest("ns-extvol-update") + tc, _ := setupSharedVolumeTest(t, ns) + defer tc.cleanup() + + created := createSharedVolume(t, tc, "shared", ateapipb.DeleteTrigger_DELETE_TRIGGER_LAST_ACTOR) + toUpdate := func(in *ateapipb.SharedVolume) *ateapipb.SharedVolume { + return &ateapipb.SharedVolume{ + Metadata: in.GetMetadata(), + DeleteTrigger: in.GetDeleteTrigger(), + AccessMode: in.GetAccessMode(), + StorageClassName: in.GetStorageClassName(), + Capacity: in.GetCapacity(), + VolumeId: in.GetVolumeId(), + VolumeType: in.GetVolumeType(), + VolumeContext: in.GetVolumeContext(), + } + } + + req := toUpdate(created) + req.DeleteTrigger = ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL + updated, err := tc.client.UpdateSharedVolume(context.Background(), &ateapipb.UpdateSharedVolumeRequest{SharedVolume: req}) + if err != nil { + t.Fatalf("UpdateSharedVolume failed: %v", err) + } + if got := updated.GetDeleteTrigger(); got != ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL { + t.Errorf("delete_trigger = %v, want MANUAL", got) + } + + req = toUpdate(updated) + req.VolumeId = "disk/somewhere-else" + _, err = tc.client.UpdateSharedVolume(context.Background(), &ateapipb.UpdateSharedVolumeRequest{SharedVolume: req}) + assertGrpcErrorRegex(t, err, codes.InvalidArgument, "volume_id") + + req = toUpdate(updated) + req.AccessMode = ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_MANY + _, err = tc.client.UpdateSharedVolume(context.Background(), &ateapipb.UpdateSharedVolumeRequest{SharedVolume: req}) + assertGrpcErrorRegex(t, err, codes.InvalidArgument, "access_mode") +} + +func TestDeleteSharedVolume(t *testing.T) { + ns := namespaceForTest("ns-extvol-delete") + tc, plugin := setupSharedVolumeTest(t, ns) + defer tc.cleanup() + + created := createSharedVolume(t, tc, "shared", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL) + ref := &ateapipb.ObjectRef{Atespace: testAtespace, Name: "shared"} + + _, err := tc.client.DeleteSharedVolume(context.Background(), &ateapipb.DeleteSharedVolumeRequest{ + SharedVolume: ref, + Options: &ateapipb.DeleteOptions{Version: created.GetMetadata().GetVersion() + 1}, + }) + assertGrpcError(t, err, codes.Aborted, "concurrent update conflict, please retry") + + if _, err := tc.client.DeleteSharedVolume(context.Background(), &ateapipb.DeleteSharedVolumeRequest{SharedVolume: ref}); err != nil { + t.Fatalf("DeleteSharedVolume failed: %v", err) + } + if _, deleted, _, _ := plugin.snapshot(); !cmp.Equal([]string{created.GetVolumeId()}, deleted) { + t.Errorf("deleted disks = %v, want %v", deleted, []string{created.GetVolumeId()}) + } + _, err = tc.client.DeleteSharedVolume(context.Background(), &ateapipb.DeleteSharedVolumeRequest{SharedVolume: ref}) + assertGrpcError(t, err, codes.NotFound, fmt.Sprintf("SharedVolume %s/shared not found", testAtespace)) +} + +// refStates maps each actor referencing volumeName to whether its reference is active. +func refStates(t *testing.T, tc *testContext, volumeName string) map[string]bool { + t.Helper() + states := make(map[string]bool) + for _, ref := range getSharedVolume(t, tc, volumeName).GetStatus().GetRefs() { + states[ref.GetActorName()] = ref.GetActive() + } + return states +} + +func borrowedMount(t *testing.T, tc *testContext, actorName, volumeName string) *ateapipb.ExternalVolumeRef { + t.Helper() + actor, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ + Actor: &ateapipb.ObjectRef{Atespace: testAtespace, Name: actorName}, + }) + if err != nil { + t.Fatalf("GetActor(%s) failed: %v", actorName, err) + } + for _, vol := range actor.GetStatus().GetExternalVolumes() { + if vol.GetName() == volumeName { + return vol + } + } + return nil +} + +func createBorrowingActor(t *testing.T, tc *testContext, name string, bindings map[string]string) { + t.Helper() + if _, err := tc.client.CreateActor(context.Background(), &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: name}, + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"}, + SharedVolumeBindings: bindings, + }}); err != nil { + t.Fatalf("CreateActor(%s) failed: %v", name, err) + } +} + +func actorState(t *testing.T, tc *testContext, name string) ateapipb.ActorState { + t.Helper() + actor, err := tc.client.GetActor(context.Background(), &ateapipb.GetActorRequest{ + Actor: &ateapipb.ObjectRef{Atespace: testAtespace, Name: name}, + }) + if err != nil { + t.Fatalf("GetActor(%s) failed: %v", name, err) + } + return actor.GetStatus().GetState() +} + +func TestSharedVolume_SequentialHandoff(t *testing.T) { + ns := namespaceForTest("ns-extvol-handoff") + tc, plugin := setupSharedVolumeTest(t, ns) + defer tc.cleanup() + + shared := createSharedVolume(t, tc, "handoff", ateapipb.DeleteTrigger_DELETE_TRIGGER_LAST_ACTOR) + createTemplateWithVolumes(t, tc, ns, + []*ateapipb.Volume{{Name: "work", External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{Name: "handoff"}}}}, + []*ateapipb.VolumeMount{{Name: "work", MountPath: "/mnt/work"}}) + workerName := createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") + ctx := context.Background() + + createBorrowingActor(t, tc, "producer", nil) + createBorrowingActor(t, tc, "consumer", nil) + if diff := cmp.Diff(map[string]bool{"producer": false, "consumer": false}, refStates(t, tc, "handoff")); diff != "" { + t.Errorf("refs after create mismatch (-want +got):\n%s", diff) + } + + waitForWorkerAvailable(t, tc, workerName) + if _, err := tc.client.ResumeActor(ctx, &ateapipb.ResumeActorRequest{Actor: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "producer"}}); err != nil { + t.Fatalf("ResumeActor(producer) failed: %v", err) + } + if diff := cmp.Diff(map[string]bool{"producer": true, "consumer": false}, refStates(t, tc, "handoff")); diff != "" { + t.Errorf("refs while the producer runs mismatch (-want +got):\n%s", diff) + } + if _, err := tc.client.SuspendActor(ctx, &ateapipb.SuspendActorRequest{Actor: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "producer"}}); err != nil { + t.Fatalf("SuspendActor(producer) failed: %v", err) + } + if diff := cmp.Diff(map[string]bool{"producer": false, "consumer": false}, refStates(t, tc, "handoff")); diff != "" { + t.Errorf("refs after the producer stops mismatch (-want +got):\n%s", diff) + } + + waitForWorkerAvailable(t, tc, workerName) + if _, err := tc.client.ResumeActor(ctx, &ateapipb.ResumeActorRequest{Actor: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "consumer"}}); err != nil { + t.Fatalf("ResumeActor(consumer) failed: %v", err) + } + if diff := cmp.Diff(map[string]bool{"producer": false, "consumer": true}, refStates(t, tc, "handoff")); diff != "" { + t.Errorf("refs while the consumer runs mismatch (-want +got):\n%s", diff) + } + for _, name := range []string{"producer", "consumer"} { + if got, want := borrowedMount(t, tc, name, "work").GetStorageVolumeId(), shared.GetVolumeId(); got != want { + t.Errorf("actor %s mounted %q, want the shared volume %q", name, got, want) + } + } + created, deleted, attached, detached := plugin.snapshot() + if diff := cmp.Diff([]string{shared.GetVolumeId() + "@node1", shared.GetVolumeId() + "@node1"}, attached); diff != "" { + t.Errorf("attached volumes mismatch (-want +got):\n%s", diff) + } + if diff := cmp.Diff([]string{shared.GetVolumeId() + "@node1"}, detached); diff != "" { + t.Errorf("detached volumes mismatch (-want +got):\n%s", diff) + } + if len(created) != 1 || len(deleted) != 0 { + t.Errorf("created %v deleted %v, want one provisioning and no deletion", created, deleted) + } + + if _, err := tc.client.SuspendActor(ctx, &ateapipb.SuspendActorRequest{Actor: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "consumer"}}); err != nil { + t.Fatalf("SuspendActor(consumer) failed: %v", err) + } + for _, name := range []string{"producer", "consumer"} { + if _, err := tc.client.DeleteActor(ctx, &ateapipb.DeleteActorRequest{Actor: &ateapipb.ObjectRef{Atespace: testAtespace, Name: name}}); err != nil { + t.Fatalf("DeleteActor(%s) failed: %v", name, err) + } + } + _, err := tc.client.GetSharedVolume(ctx, &ateapipb.GetSharedVolumeRequest{SharedVolume: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "handoff"}}) + assertGrpcError(t, err, codes.NotFound, fmt.Sprintf("SharedVolume %s/handoff not found", testAtespace)) + if _, deleted, _, _ := plugin.snapshot(); !cmp.Equal([]string{shared.GetVolumeId()}, deleted) { + t.Errorf("deleted volumes = %v, want the LAST_ACTOR trigger to delete %q", deleted, shared.GetVolumeId()) + } +} + +func TestSharedVolume_HeldByAnotherActor(t *testing.T) { + ns := namespaceForTest("ns-extvol-held") + tc, plugin := setupSharedVolumeTest(t, ns) + defer tc.cleanup() + + shared := createSharedVolume(t, tc, "held", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL) + createTemplateWithVolumes(t, tc, ns, + []*ateapipb.Volume{{Name: "work", External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{Name: "held"}}}}, + []*ateapipb.VolumeMount{{Name: "work", MountPath: "/mnt/work"}}) + firstWorker := createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") + ctx := context.Background() + + createBorrowingActor(t, tc, "producer", nil) + createBorrowingActor(t, tc, "consumer", nil) + waitForWorkerAvailable(t, tc, firstWorker) + if _, err := tc.client.ResumeActor(ctx, &ateapipb.ResumeActorRequest{Actor: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "producer"}}); err != nil { + t.Fatalf("ResumeActor(producer) failed: %v", err) + } + + secondWorker := createWorkerPod(t, tc, ns, "worker-2", "node1", "pool1") + waitForWorkerAvailable(t, tc, secondWorker) + _, err := tc.client.ResumeActor(ctx, &ateapipb.ResumeActorRequest{Actor: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "consumer"}}) + assertGrpcErrorRegex(t, err, codes.FailedPrecondition, `SharedVolume .*held is already held by actor "producer"`) + if diff := cmp.Diff(map[string]bool{"producer": true, "consumer": false}, refStates(t, tc, "held")); diff != "" { + t.Errorf("refs after the refused resume mismatch (-want +got):\n%s", diff) + } + if got := actorState(t, tc, "consumer"); got != ateapipb.ActorState_ACTOR_STATE_RESUMING { + t.Errorf("consumer is in %v, want RESUMING with placement retained for retry", got) + } + if _, _, attached, _ := plugin.snapshot(); !cmp.Equal([]string{shared.GetVolumeId() + "@node1"}, attached) { + t.Errorf("attached volumes = %v, want only the producer's attach", attached) + } + + if _, err := tc.client.PauseActor(ctx, &ateapipb.PauseActorRequest{Actor: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "producer"}}); err != nil { + t.Fatalf("PauseActor(producer) failed: %v", err) + } + if _, err := tc.client.ResumeActor(ctx, &ateapipb.ResumeActorRequest{Actor: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "consumer"}}); err != nil { + t.Fatalf("ResumeActor(consumer) after the handoff failed: %v", err) + } + if diff := cmp.Diff(map[string]bool{"producer": false, "consumer": true}, refStates(t, tc, "held")); diff != "" { + t.Errorf("refs after the handoff mismatch (-want +got):\n%s", diff) + } +} + +func TestDeleteSharedVolume_Referenced(t *testing.T) { + ns := namespaceForTest("ns-extvol-referenced") + tc, plugin := setupSharedVolumeTest(t, ns) + defer tc.cleanup() + + createSharedVolume(t, tc, "shared", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL) + createTemplateWithVolumes(t, tc, ns, + []*ateapipb.Volume{{Name: "work", External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{Name: "shared"}}}}, + []*ateapipb.VolumeMount{{Name: "work", MountPath: "/mnt/work"}}) + ctx := context.Background() + ref := &ateapipb.ObjectRef{Atespace: testAtespace, Name: "shared"} + + createBorrowingActor(t, tc, "borrower", nil) + _, err := tc.client.DeleteSharedVolume(ctx, &ateapipb.DeleteSharedVolumeRequest{SharedVolume: ref}) + assertGrpcErrorRegex(t, err, codes.FailedPrecondition, `SharedVolume .*shared is still referenced by 1 actor\(s\), starting with "borrower"`) + + if _, err := tc.client.DeleteActor(ctx, &ateapipb.DeleteActorRequest{Actor: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "borrower"}}); err != nil { + t.Fatalf("DeleteActor failed: %v", err) + } + if got := refStates(t, tc, "shared"); len(got) != 0 { + t.Errorf("refs after the actor is deleted = %v, want none", got) + } + if _, deleted, _, _ := plugin.snapshot(); len(deleted) != 0 { + t.Errorf("deleted %v, want a MANUAL volume left in place", deleted) + } + if _, err := tc.client.DeleteSharedVolume(ctx, &ateapipb.DeleteSharedVolumeRequest{SharedVolume: ref}); err != nil { + t.Fatalf("DeleteSharedVolume failed: %v", err) + } +} + +func TestCreateActor_SharedVolumeBindings(t *testing.T) { + ns := namespaceForTest("ns-extvol-bindings") + tc, _ := setupSharedVolumeTest(t, ns) + defer tc.cleanup() + + shared := createSharedVolume(t, tc, "ctx-42", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL) + createTemplateWithVolumes(t, tc, ns, + []*ateapipb.Volume{{Name: "work", External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{}}}}, + []*ateapipb.VolumeMount{{Name: "work", MountPath: "/mnt/work"}}) + ctx := context.Background() + create := func(name string, bindings map[string]string) error { + _, err := tc.client.CreateActor(ctx, &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: name}, + ActorTemplate: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "tmpl1"}, + SharedVolumeBindings: bindings, + }}) + return err + } + + assertGrpcErrorRegex(t, create("unbound", nil), codes.InvalidArgument, `shared_volume_bindings must bind volume "work"`) + assertGrpcErrorRegex(t, create("stray", map[string]string{"work": "ctx-42", "other": "ctx-42"}), codes.InvalidArgument, `shared_volume_bindings\["other"\]`) + assertGrpcErrorRegex(t, create("missing", map[string]string{"work": "ctx-41"}), codes.FailedPrecondition, fmt.Sprintf("SharedVolume %s/ctx-41 not found", testAtespace)) + + if err := create("bound", map[string]string{"work": "ctx-42"}); err != nil { + t.Fatalf("CreateActor(bound) failed: %v", err) + } + if got, want := borrowedMount(t, tc, "bound", "work").GetStorageVolumeId(), shared.GetVolumeId(); got != want { + t.Errorf("bound actor mounts %q, want %q", got, want) + } + if diff := cmp.Diff(map[string]bool{"bound": false}, refStates(t, tc, "ctx-42")); diff != "" { + t.Errorf("refs mismatch (-want +got):\n%s", diff) + } +} + +func TestSharedVolumeDeleteRetainsReferencesUntilNodeCleanup(t *testing.T) { + for _, failure := range []string{"terminate", "detach"} { + t.Run(failure, func(t *testing.T) { + ctx := context.Background() + ns := namespaceForTest("ns-delete-failure-" + failure) + tc, plugin := setupSharedVolumeTest(t, ns) + defer tc.cleanup() + createSharedVolume(t, tc, "data", ateapipb.DeleteTrigger_DELETE_TRIGGER_LAST_ACTOR) + createTemplateWithVolumes(t, tc, ns, []*ateapipb.Volume{{Name: "work", External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{Name: "data"}}}}, []*ateapipb.VolumeMount{{Name: "work", MountPath: "/work"}}) + worker := createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") + createBorrowingActor(t, tc, "consumer", nil) + waitForWorkerAvailable(t, tc, worker) + ref := &ateapipb.ObjectRef{Atespace: testAtespace, Name: "consumer"} + if _, err := tc.client.ResumeActor(ctx, &ateapipb.ResumeActorRequest{Actor: ref}); err != nil { + t.Fatal(err) + } + if failure == "terminate" { + tc.fakeAtelet.Lock.Lock() + tc.fakeAtelet.FailTerminate = errors.New("injected termination failure") + tc.fakeAtelet.Lock.Unlock() + } else { + plugin.mu.Lock() + plugin.detachErr = errors.New("injected detach failure") + plugin.mu.Unlock() + } + req := &ateapipb.DeleteActorRequest{Actor: ref, AnyState: true} + if _, err := tc.client.DeleteActor(ctx, req); err == nil { + t.Fatal("expected cleanup failure") + } + if refs := refStates(t, tc, "data"); !refs["consumer"] { + t.Fatalf("lost active reference: %v", refs) + } + _, deleted, _, _ := plugin.snapshot() + if len(deleted) != 0 { + t.Fatal("deleted attached storage") + } + tc.fakeAtelet.Lock.Lock() + tc.fakeAtelet.FailTerminate = nil + tc.fakeAtelet.Lock.Unlock() + plugin.mu.Lock() + plugin.detachErr = nil + plugin.mu.Unlock() + if _, err := tc.client.DeleteActor(ctx, req); err != nil { + t.Fatalf("retry: %v", err) + } + _, deleted, _, _ = plugin.snapshot() + if len(deleted) != 1 { + t.Fatalf("deleted=%v", deleted) + } + }) + } +} + +func TestSharedVolumeFailedResumeReleasesClaimAfterCleanup(t *testing.T) { + ctx := context.Background() + ns := namespaceForTest("ns-resume-claim-cleanup") + tc, _ := setupSharedVolumeTest(t, ns) + defer tc.cleanup() + createSharedVolume(t, tc, "data", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL) + createTemplateWithVolumes(t, tc, ns, []*ateapipb.Volume{{Name: "work", External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{Name: "data"}}}}, []*ateapipb.VolumeMount{{Name: "work", MountPath: "/work"}}) + worker := createWorkerPod(t, tc, ns, "worker-1", "node1", "pool1") + createBorrowingActor(t, tc, "consumer", nil) + waitForWorkerAvailable(t, tc, worker) + current := getSharedVolume(t, tc, "data") + current.VolumeContext = map[string]string{"generation": "new-driver-context"} + if _, err := tc.client.UpdateSharedVolume(ctx, &ateapipb.UpdateSharedVolumeRequest{SharedVolume: current}); err != nil { + t.Fatal(err) + } + + tc.fakeAtelet.Lock.Lock() + tc.fakeAtelet.FailRun = errors.New("injected run failure") + tc.fakeAtelet.Lock.Unlock() + if _, err := tc.client.ResumeActor(ctx, &ateapipb.ResumeActorRequest{Actor: &ateapipb.ObjectRef{Atespace: testAtespace, Name: "consumer"}}); err == nil { + t.Fatal("expected run failure") + } + tc.fakeAtelet.Lock.Lock() + mounted := tc.fakeAtelet.RunRequest.GetSpec().GetVolumes()[0].GetExternal().GetVolumeContext()["generation"] + tc.fakeAtelet.Lock.Unlock() + if mounted != "new-driver-context" { + t.Fatalf("mounted stale driver context: %q", mounted) + } + + refs := refStates(t, tc, "data") + if active, ok := refs["consumer"]; !ok || active { + t.Fatalf("want retained inactive reference: %v", refs) + } +} diff --git a/cmd/ateapi/internal/controlapi/service.go b/cmd/ateapi/internal/controlapi/service.go index 0c3ae2fc85..aec129698f 100644 --- a/cmd/ateapi/internal/controlapi/service.go +++ b/cmd/ateapi/internal/controlapi/service.go @@ -46,6 +46,7 @@ type RPCService struct { csiDriverConfigLister listersv1alpha1.CSIDriverConfigLister actorWorkflow *ActorWorkflow workerWorkflow *WorkerWorkflow + sharedVolumeWorkflow *SharedVolumeWorkflow instruments *Instruments mu sync.RWMutex volumePlugins map[string]volume.VolumePluginControlPlane @@ -103,7 +104,8 @@ func NewRPCService( actorIDJWTPool: actorIDJWTPool, actorIDCAPool: actorIDCAPool, } - s.actorWorkflow = NewActorWorkflow(impl, workerCache, dialer, sandboxConfigLister, storageClassLister, instruments, egressGatewayAddress, s, objectStorePlugin) + s.sharedVolumeWorkflow = NewSharedVolumeWorkflow(impl, storageClassLister, s) + s.actorWorkflow = NewActorWorkflow(impl, workerCache, dialer, sandboxConfigLister, storageClassLister, instruments, egressGatewayAddress, s, objectStorePlugin, s.sharedVolumeWorkflow) s.workerWorkflow = NewWorkerWorkflow(impl) return s } @@ -123,6 +125,11 @@ type serviceStore interface { ListTags(ctx context.Context, atespace string, opts store.ListOptions) (store.ListResponse[*ateapipb.Tag], error) UpdateTag(ctx context.Context, tagRef resources.TagRef, precondition store.Precondition, mutate func(toUpdate *ateapipb.Tag) error) (*ateapipb.Tag, error) DeleteTag(ctx context.Context, tagRef resources.TagRef, precondition store.DeletePreconditions) (*ateapipb.Tag, error) + CreateSharedVolume(ctx context.Context, volume *ateapipb.SharedVolume) (*ateapipb.SharedVolume, error) + GetSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef) (*ateapipb.SharedVolume, error) + ListSharedVolumes(ctx context.Context, atespace string, opts store.ListOptions) (store.ListResponse[*ateapipb.SharedVolume], error) + UpdateSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef, precondition store.Precondition, mutate func(toUpdate *ateapipb.SharedVolume) error) (*ateapipb.SharedVolume, error) + DeleteSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef, precondition store.DeletePreconditions) (*ateapipb.SharedVolume, error) CreateAtespace(ctx context.Context, atespace *ateapipb.Atespace) (*ateapipb.Atespace, error) GetAtespace(ctx context.Context, name string) (*ateapipb.Atespace, error) ListAtespaces(ctx context.Context, opts store.ListOptions) (store.ListResponse[*ateapipb.Atespace], error) diff --git a/cmd/ateapi/internal/controlapi/shared_volume.go b/cmd/ateapi/internal/controlapi/shared_volume.go new file mode 100644 index 0000000000..a78d40bbc1 --- /dev/null +++ b/cmd/ateapi/internal/controlapi/shared_volume.go @@ -0,0 +1,151 @@ +// 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" + "errors" + "fmt" + + "github.com/agent-substrate/substrate/cmd/ateapi/internal/apivalidation" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/defaults" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" + "github.com/agent-substrate/substrate/internal/apierror" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "google.golang.org/protobuf/proto" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func (s *RPCService) CreateSharedVolume(ctx context.Context, req *ateapipb.CreateSharedVolumeRequest) (*ateapipb.SharedVolume, error) { + inVolume := req.SharedVolume + if inVolume != nil { + scrubResourceMetadataForCreate(inVolume.Metadata) + inVolume.Status = nil + defaults.Apply(inVolume) + } + if errs := apivalidation.ValidateCreateSharedVolumeRequest(ctx, req); len(errs) > 0 { + return nil, resources.ToAPIError(errs) + } + return s.sharedVolumeWorkflow.CreateSharedVolume(ctx, inVolume) +} + +func (s *ServiceImpl) CreateSharedVolume(ctx context.Context, volume *ateapipb.SharedVolume) (*ateapipb.SharedVolume, error) { + return s.store.CreateSharedVolume(ctx, volume) +} + +func (s *RPCService) GetSharedVolume(ctx context.Context, req *ateapipb.GetSharedVolumeRequest) (*ateapipb.SharedVolume, error) { + if errs := apivalidation.ValidateGetSharedVolumeRequest(ctx, req); len(errs) > 0 { + return nil, resources.ToAPIError(errs) + } + volumeRef := resources.SharedVolumeRefFromObjectRef(req.GetSharedVolume()) + volume, err := s.impl.GetSharedVolume(ctx, volumeRef) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + return nil, apierror.NotFound("SharedVolume %s not found", volumeRef) + } + return nil, fmt.Errorf("while getting shared volume: %w", err) + } + return volume, nil +} + +func (s *ServiceImpl) GetSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef) (*ateapipb.SharedVolume, error) { + return s.store.GetSharedVolume(ctx, volumeRef) +} + +func (s *RPCService) ListSharedVolumes(ctx context.Context, req *ateapipb.ListSharedVolumesRequest) (*ateapipb.ListSharedVolumesResponse, error) { + if errs := apivalidation.ValidateListSharedVolumesRequest(ctx, req); len(errs) > 0 { + return nil, resources.ToAPIError(errs) + } + page, err := s.impl.ListSharedVolumes(ctx, req.GetAtespace(), store.ListOptions{PageSize: effectivePageSize(req.GetPageSize()), PageToken: req.GetPageToken()}) + if err != nil { + return nil, mapListError(fmt.Errorf("while listing shared volumes: %w", err)) + } + return &ateapipb.ListSharedVolumesResponse{SharedVolumes: page.Items, NextPageToken: page.NextPageToken}, nil +} + +func (s *ServiceImpl) ListSharedVolumes(ctx context.Context, atespace string, opts store.ListOptions) (store.ListResponse[*ateapipb.SharedVolume], error) { + return s.store.ListSharedVolumes(ctx, atespace, opts) +} + +var errSharedVolumeNotReady = errors.New("shared volume is not ready for updates") + +func (s *RPCService) UpdateSharedVolume(ctx context.Context, req *ateapipb.UpdateSharedVolumeRequest) (*ateapipb.SharedVolume, error) { + inVolume := req.SharedVolume + if inVolume != nil { + scrubResourceMetadataForUpdate(inVolume.Metadata) + inVolume.Status = nil + } + if errs := apivalidation.ValidateUpdateSharedVolumeRequest(ctx, req); len(errs) > 0 { + return nil, resources.ToAPIError(errs) + } + in := req.GetSharedVolume() + volumeRef := resources.SharedVolumeRefFromSharedVolume(in) + + stored, err := s.impl.UpdateSharedVolume(ctx, volumeRef, store.PreconditionFrom(in), func(toUpdate *ateapipb.SharedVolume) error { + if toUpdate.GetStatus().GetState() != ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY { + return errSharedVolumeNotReady + } + metadata, volumeStatus := toUpdate.GetMetadata(), toUpdate.GetStatus() + proto.Reset(toUpdate) + proto.Merge(toUpdate, in) + toUpdate.Metadata, toUpdate.Status = metadata, volumeStatus + defaults.Apply(toUpdate) + return nil + }) + if err != nil { + if errors.Is(err, errSharedVolumeNotReady) { + return nil, apierror.FailedPrecondition("SharedVolume %s is not ready for updates", volumeRef) + } + if errors.Is(err, store.ErrImmutableField) || errors.Is(err, store.ErrPreconditionRequired) { + return nil, apierror.InvalidArgument("while updating shared volume %s: %v", volumeRef, err) + } + if errors.Is(err, store.ErrVersionConflict) { + return nil, apierror.Aborted("concurrent update conflict, please retry") + } + if errors.Is(err, store.ErrUIDConflict) { + return nil, apierror.Aborted("SharedVolume %s not found with uid %s", volumeRef, in.GetMetadata().GetUid()) + } + if errors.Is(err, store.ErrNotFound) { + return nil, apierror.NotFound("SharedVolume %s not found", volumeRef) + } + return nil, fmt.Errorf("while updating shared volume: %w", err) + } + return stored, nil +} + +func (s *ServiceImpl) UpdateSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef, precondition store.Precondition, mutate func(toUpdate *ateapipb.SharedVolume) error) (*ateapipb.SharedVolume, error) { + return s.store.UpdateSharedVolume(ctx, volumeRef, precondition, func(toUpdate *ateapipb.SharedVolume) error { + oldVal := proto.CloneOf(toUpdate) + if err := mutate(toUpdate); err != nil { + return err + } + if errs := apivalidation.ValidateSharedVolumeUpdate(ctx, field.NewPath("shared_volume"), toUpdate, oldVal); len(errs) > 0 { + return resources.ToAPIError(errs) + } + return nil + }) +} + +func (s *RPCService) DeleteSharedVolume(ctx context.Context, req *ateapipb.DeleteSharedVolumeRequest) (*ateapipb.SharedVolume, error) { + if errs := apivalidation.ValidateDeleteSharedVolumeRequest(ctx, req); len(errs) > 0 { + return nil, resources.ToAPIError(errs) + } + return s.sharedVolumeWorkflow.DeleteSharedVolume(ctx, resources.SharedVolumeRefFromObjectRef(req.GetSharedVolume()), toDeletePreconditions(req.GetOptions())) +} + +func (s *ServiceImpl) DeleteSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef, precondition store.DeletePreconditions) (*ateapipb.SharedVolume, error) { + return s.store.DeleteSharedVolume(ctx, volumeRef, precondition) +} diff --git a/cmd/ateapi/internal/controlapi/shared_volume_golden_test.go b/cmd/ateapi/internal/controlapi/shared_volume_golden_test.go new file mode 100644 index 0000000000..d292dea526 --- /dev/null +++ b/cmd/ateapi/internal/controlapi/shared_volume_golden_test.go @@ -0,0 +1,45 @@ +// 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 ( + "testing" + + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" +) + +func TestTemplateHasSharedVolumeRef(t *testing.T) { + tests := []struct { + name string + volumes []*ateapipb.Volume + want bool + }{ + {name: "no volumes"}, + {name: "durable dir only", volumes: []*ateapipb.Volume{{Name: "home", DurableDir: &ateapipb.DurableDirVolumeSource{}}}}, + {name: "shared volume template", volumes: []*ateapipb.Volume{{Name: "owned", External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{StorageClassName: "standard"}}}}}, + {name: "named ref", volumes: []*ateapipb.Volume{{Name: "shared", External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{Name: "ctx"}}}}, want: true}, + {name: "unnamed ref after other volumes", volumes: []*ateapipb.Volume{ + {Name: "home", DurableDir: &ateapipb.DurableDirVolumeSource{}}, + {Name: "shared", External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{}}}, + }, want: true}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := templateHasSharedVolumeRef(&ateapipb.ActorTemplate{Volumes: tc.volumes}); got != tc.want { + t.Errorf("templateHasSharedVolumeRef() = %v, want %v", got, tc.want) + } + }) + } +} diff --git a/cmd/ateapi/internal/controlapi/shared_volume_refs.go b/cmd/ateapi/internal/controlapi/shared_volume_refs.go new file mode 100644 index 0000000000..65a66bf1e0 --- /dev/null +++ b/cmd/ateapi/internal/controlapi/shared_volume_refs.go @@ -0,0 +1,266 @@ +// 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" + "errors" + "fmt" + "google.golang.org/protobuf/proto" + "slices" + "time" + + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" + "github.com/agent-substrate/substrate/internal/apierror" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type sharedVolumeRefStore interface { + GetSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef) (*ateapipb.SharedVolume, error) + UpdateSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef, precondition store.Precondition, mutate func(toUpdate *ateapipb.SharedVolume) error) (*ateapipb.SharedVolume, error) +} + +type sharedVolumeDeleter interface { + DeleteSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef, precondition store.DeletePreconditions) (*ateapipb.SharedVolume, error) +} + +func templateHasSharedVolumeRef(template *ateapipb.ActorTemplate) bool { + for _, vol := range template.GetVolumes() { + if vol.GetExternal().GetSharedVolume() != nil { + return true + } + } + return false +} + +// resolveSharedVolumeRefs checks the actor's bindings against the template +// and returns a mount for every SharedVolume the actor references. +func resolveSharedVolumeRefs(ctx context.Context, st sharedVolumeRefStore, actor *ateapipb.Actor, template *ateapipb.ActorTemplate) ([]*ateapipb.ExternalVolumeRef, error) { + bindings := actor.GetSharedVolumeBindings() + unnamed := make(map[string]bool) + for _, vol := range template.GetVolumes() { + if vol.GetExternal().GetSharedVolume() != nil && vol.GetExternal().GetSharedVolume().GetName() == "" { + unnamed[vol.GetName()] = true + } + } + for slot := range bindings { + if !unnamed[slot] { + return nil, apierror.InvalidArgument("shared_volume_bindings[%q] does not name an unnamed external.shared_volume in the template", slot) + } + } + + var mounts []*ateapipb.ExternalVolumeRef + for _, vol := range template.GetVolumes() { + ref := vol.GetExternal().GetSharedVolume() + if ref == nil { + continue + } + name := ref.GetName() + if name == "" { + if name = bindings[vol.GetName()]; name == "" { + return nil, apierror.InvalidArgument("shared_volume_bindings must bind volume %q", vol.GetName()) + } + } + volume, err := getReadySharedVolume(ctx, st, resources.SharedVolumeRef{Atespace: actor.GetMetadata().GetAtespace(), Name: name}) + if err != nil { + return nil, err + } + mounts = append(mounts, sharedVolumeMount(vol.GetName(), volume)) + } + return mounts, nil +} + +func getReadySharedVolume(ctx context.Context, st sharedVolumeRefStore, volumeRef resources.SharedVolumeRef) (*ateapipb.SharedVolume, error) { + volume, err := st.GetSharedVolume(ctx, volumeRef) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + return nil, apierror.FailedPrecondition("SharedVolume %s not found", volumeRef) + } + return nil, fmt.Errorf("while getting shared volume %s: %w", volumeRef, err) + } + if state := volume.GetStatus().GetState(); state != ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY { + return nil, apierror.FailedPrecondition("SharedVolume %s is not ready (state %s)", volumeRef, state) + } + return volume, nil +} + +func sharedVolumeMount(templateVolumeName string, volume *ateapipb.SharedVolume) *ateapipb.ExternalVolumeRef { + return &ateapipb.ExternalVolumeRef{ + Name: templateVolumeName, + SharedVolumeName: volume.GetMetadata().GetName(), + StorageVolumeId: volume.GetVolumeId(), + VolumeType: volume.GetVolumeType(), + VolumeContext: volume.GetVolumeContext(), + } +} + +func borrowedVolumeRefs(actor *ateapipb.Actor) []resources.SharedVolumeRef { + var refs []resources.SharedVolumeRef + for _, vol := range actor.GetStatus().GetExternalVolumes() { + if vol.GetSharedVolumeName() != "" { + refs = append(refs, resources.SharedVolumeRef{Atespace: actor.GetMetadata().GetAtespace(), Name: vol.GetSharedVolumeName()}) + } + } + return refs +} + +func actorRefOf(actor *ateapipb.Actor, active bool) *ateapipb.ActorRef { + return &ateapipb.ActorRef{ + ActorUid: actor.GetMetadata().GetUid(), + ActorName: actor.GetMetadata().GetName(), + Active: active, + } +} + +func findActorRef(volume *ateapipb.SharedVolume, actorUID string) *ateapipb.ActorRef { + for _, ref := range volume.GetStatus().GetRefs() { + if ref.GetActorUid() == actorUID { + return ref + } + } + return nil +} + +func setActorRef(volume *ateapipb.SharedVolume, ref *ateapipb.ActorRef) { + if existing := findActorRef(volume, ref.GetActorUid()); existing != nil { + existing.ActorName, existing.Active = ref.GetActorName(), ref.GetActive() + return + } + if volume.Status == nil { + volume.Status = &ateapipb.SharedVolumeStatus{} + } + volume.Status.Refs = append(volume.Status.Refs, ref) +} + +// updateSharedVolumeRefs applies mutate under the volume's version guard. +// TODO: move references to their own table so claims do not contend on one row. +func updateSharedVolumeRefs(ctx context.Context, st sharedVolumeRefStore, volumeRef resources.SharedVolumeRef, mutate func(*ateapipb.SharedVolume) error) (*ateapipb.SharedVolume, error) { + volume, err := st.GetSharedVolume(ctx, volumeRef) + if err != nil { + return nil, err + } + return st.UpdateSharedVolume(ctx, volumeRef, store.PreconditionFrom(volume), mutate) +} + +var errSharedVolumeHeld = errors.New("shared volume is held by another actor") + +// claimSharedVolumes marks the actor's references active and refreshes the +// recorded mounts. A READ_WRITE_ONCE volume another actor holds is refused. +// TODO: let READ_WRITE_MANY volumes carry several active references. +func claimSharedVolumes(ctx context.Context, st sharedVolumeRefStore, actor *ateapipb.Actor) (_ []*ateapipb.ExternalVolumeRef, retErr error) { + volumes := slices.Clone(actor.GetStatus().GetExternalVolumes()) + actorUID := actor.GetMetadata().GetUid() + var acquired []*ateapipb.ExternalVolumeRef + defer func() { + if retErr == nil || len(acquired) == 0 { + return + } + cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) + defer cancel() + rollback := proto.CloneOf(actor) + rollback.Status.ExternalVolumes = acquired + retErr = errors.Join(retErr, deactivateSharedVolumes(cleanupCtx, st, rollback)) + }() + for i, mount := range volumes { + if mount.GetSharedVolumeName() == "" { + continue + } + volumeRef := resources.SharedVolumeRef{Atespace: actor.GetMetadata().GetAtespace(), Name: mount.GetSharedVolumeName()} + var holder string + newlyActive := false + claimed, err := updateSharedVolumeRefs(ctx, st, volumeRef, func(toUpdate *ateapipb.SharedVolume) error { + if toUpdate.GetStatus().GetState() != ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY { + return fmt.Errorf("shared volume is not ready (state %s)", toUpdate.GetStatus().GetState()) + } + if findActorRef(toUpdate, actorUID) == nil || toUpdate.GetVolumeId() != mount.GetStorageVolumeId() { + return apierror.FailedPrecondition("actor no longer references this volume") + } + for _, ref := range toUpdate.GetStatus().GetRefs() { + if ref.GetActive() && ref.GetActorUid() != actorUID { + holder = ref.GetActorName() + return errSharedVolumeHeld + } + } + newlyActive = !findActorRef(toUpdate, actorUID).GetActive() + setActorRef(toUpdate, actorRefOf(actor, true)) + return nil + }) + switch { + case err == nil: + case errors.Is(err, errSharedVolumeHeld): + return nil, apierror.FailedPrecondition("SharedVolume %s is already held by actor %q; pause or suspend it before resuming another actor", volumeRef, holder) + case errors.Is(err, store.ErrNotFound): + return nil, apierror.FailedPrecondition("SharedVolume %s not found", volumeRef) + case errors.Is(err, store.ErrVersionConflict), errors.Is(err, store.ErrUIDConflict): + return nil, apierror.Aborted("concurrent update conflict, please retry") + default: + return nil, apierror.FailedPrecondition("SharedVolume %s: %v", volumeRef, err) + } + if newlyActive { + acquired = append(acquired, mount) + } + volumes[i] = sharedVolumeMount(mount.GetName(), claimed) + } + return volumes, nil +} + +// deactivateSharedVolumes marks the actor's references inactive. +func deactivateSharedVolumes(ctx context.Context, st sharedVolumeRefStore, actor *ateapipb.Actor) error { + var errs []error + for _, volumeRef := range borrowedVolumeRefs(actor) { + _, err := updateSharedVolumeRefs(ctx, st, volumeRef, func(toUpdate *ateapipb.SharedVolume) error { + if ref := findActorRef(toUpdate, actor.GetMetadata().GetUid()); ref != nil { + ref.Active = false + } + return nil + }) + if err != nil && !errors.Is(err, store.ErrNotFound) { + errs = append(errs, fmt.Errorf("while deactivating shared volume %s: %w", volumeRef, err)) + } + } + return errors.Join(errs...) +} + +// releaseSharedVolumes drops the actor's references and deletes each volume +// left without references whose delete trigger is LAST_ACTOR. +func releaseSharedVolumes(ctx context.Context, st sharedVolumeRefStore, deleter sharedVolumeDeleter, actor *ateapipb.Actor) error { + var errs []error + for _, volumeRef := range borrowedVolumeRefs(actor) { + released, err := updateSharedVolumeRefs(ctx, st, volumeRef, func(toUpdate *ateapipb.SharedVolume) error { + toUpdate.Status.Refs = slices.DeleteFunc(toUpdate.GetStatus().GetRefs(), func(ref *ateapipb.ActorRef) bool { + return ref.GetActorUid() == actor.GetMetadata().GetUid() + }) + return nil + }) + if errors.Is(err, store.ErrNotFound) { + continue + } + if err != nil { + errs = append(errs, fmt.Errorf("while releasing shared volume %s: %w", volumeRef, err)) + continue + } + if released.GetDeleteTrigger() != ateapipb.DeleteTrigger_DELETE_TRIGGER_LAST_ACTOR || len(released.GetStatus().GetRefs()) > 0 { + continue + } + _, err = deleter.DeleteSharedVolume(ctx, volumeRef, store.DeletePreconditions{UID: released.GetMetadata().GetUid(), Version: released.GetMetadata().GetVersion()}) + if code := status.Code(err); err != nil && code != codes.NotFound && code != codes.FailedPrecondition { + errs = append(errs, fmt.Errorf("while deleting shared volume %s: %w", volumeRef, err)) + } + } + return errors.Join(errs...) +} diff --git a/cmd/ateapi/internal/controlapi/shared_volume_refs_test.go b/cmd/ateapi/internal/controlapi/shared_volume_refs_test.go new file mode 100644 index 0000000000..a80eda0807 --- /dev/null +++ b/cmd/ateapi/internal/controlapi/shared_volume_refs_test.go @@ -0,0 +1,308 @@ +// 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" + "strings" + "testing" + + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" + "github.com/agent-substrate/substrate/internal/apierror" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "github.com/google/go-cmp/cmp" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/testing/protocmp" +) + +const ( + refTestAtespace = "team-refs" + refTestActorUID = "11111111-1111-1111-1111-111111111111" + refTestOtherUID = "22222222-2222-2222-2222-222222222222" +) + +func refTestTemplate(volumes ...*ateapipb.Volume) *ateapipb.ActorTemplate { + return &ateapipb.ActorTemplate{ + Metadata: &ateapipb.ResourceMetadata{Atespace: refTestAtespace, Name: "tmpl"}, + Volumes: volumes, + } +} + +func refTestActor(volumes ...*ateapipb.ExternalVolumeRef) *ateapipb.Actor { + return &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: refTestAtespace, Name: "consumer", Uid: refTestActorUID}, + Status: &ateapipb.ActorStatus{ExternalVolumes: volumes}, + } +} + +func borrowedMount(volumeName string) *ateapipb.ExternalVolumeRef { + return &ateapipb.ExternalVolumeRef{ + Name: "shared-data", + SharedVolumeName: volumeName, + StorageVolumeId: "disk/" + volumeName, + VolumeType: "mock", + VolumeContext: map[string]string{"type": "pd-ssd"}, + } +} + +func mustStoreReadyVolume(t *testing.T, ctx context.Context, st store.Interface, name string, trigger ateapipb.DeleteTrigger, refs ...*ateapipb.ActorRef) *ateapipb.SharedVolume { + t.Helper() + return storetest.MustCreateSharedVolume(t, ctx, st, &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: refTestAtespace, Name: name}, + DeleteTrigger: trigger, + AccessMode: ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_ONCE, + VolumeId: "disk/" + name, + VolumeType: "mock", + VolumeContext: map[string]string{"type": "pd-ssd"}, + Status: &ateapipb.SharedVolumeStatus{ + State: ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY, + Refs: refs, + }, + }) +} + +func mustGetVolume(t *testing.T, ctx context.Context, st store.Interface, name string) *ateapipb.SharedVolume { + t.Helper() + got, err := st.GetSharedVolume(ctx, resources.SharedVolumeRef{Atespace: refTestAtespace, Name: name}) + if err != nil { + t.Fatalf("GetSharedVolume(%q): %v", name, err) + } + return got +} + +// refStates maps each referencing actor's UID to whether its reference is active. +func refStates(volume *ateapipb.SharedVolume) map[string]bool { + states := make(map[string]bool, len(volume.GetStatus().GetRefs())) + for _, ref := range volume.GetStatus().GetRefs() { + states[ref.GetActorUid()] = ref.GetActive() + } + return states +} + +func TestResolveSharedVolumeRefs(t *testing.T) { + ctx := context.Background() + st := newTestPersistence(t) + mustStoreReadyVolume(t, ctx, st, "shared", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL) + mustStoreReadyVolume(t, ctx, st, "bound", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL) + storetest.MustCreateSharedVolume(t, ctx, st, &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: refTestAtespace, Name: "pending"}, + StorageClassName: "standard", + Status: &ateapipb.SharedVolumeStatus{State: ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_PENDING}, + }) + named := &ateapipb.Volume{Name: "shared-data", External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{Name: "shared"}}} + unnamed := &ateapipb.Volume{Name: "slot", External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{}}} + + tests := []struct { + name string + template *ateapipb.ActorTemplate + bindings map[string]string + want []string + wantCode codes.Code + }{ + {name: "named ref", template: refTestTemplate(named), want: []string{"shared"}}, + {name: "bound slot", template: refTestTemplate(named, unnamed), bindings: map[string]string{"slot": "bound"}, want: []string{"shared", "bound"}}, + {name: "unbound slot", template: refTestTemplate(unnamed), wantCode: codes.InvalidArgument}, + {name: "binding for a named ref", template: refTestTemplate(named), bindings: map[string]string{"shared-data": "bound"}, wantCode: codes.InvalidArgument}, + {name: "binding for an unknown volume", template: refTestTemplate(unnamed), bindings: map[string]string{"slot": "bound", "other": "bound"}, wantCode: codes.InvalidArgument}, + {name: "missing volume", template: refTestTemplate(unnamed), bindings: map[string]string{"slot": "absent"}, wantCode: codes.FailedPrecondition}, + {name: "pending volume", template: refTestTemplate(unnamed), bindings: map[string]string{"slot": "pending"}, wantCode: codes.FailedPrecondition}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actor := refTestActor() + actor.SharedVolumeBindings = tc.bindings + mounts, err := resolveSharedVolumeRefs(ctx, st, actor, tc.template) + if apierror.Code(err) != tc.wantCode { + t.Fatalf("resolveSharedVolumeRefs() error = %v, want code %v", err, tc.wantCode) + } + var got []string + for _, mount := range mounts { + if mount.GetStorageVolumeId() != "disk/"+mount.GetSharedVolumeName() { + t.Errorf("mount %v does not carry the volume's handle", mount) + } + got = append(got, mount.GetSharedVolumeName()) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("resolved volumes mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestBindClaimDeactivateRelease(t *testing.T) { + ctx := context.Background() + st := newTestPersistence(t) + mustStoreReadyVolume(t, ctx, st, "shared", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL) + actor := storetest.MustCreateActor(t, ctx, st, refTestActor(borrowedMount("shared"))) + uid := actor.GetMetadata().GetUid() + volumeRef := resources.SharedVolumeRef{Atespace: refTestAtespace, Name: "shared"} + + if diff := cmp.Diff(map[string]bool{uid: false}, refStates(mustGetVolume(t, ctx, st, "shared"))); diff != "" { + t.Errorf("refs after bind mismatch (-want +got):\n%s", diff) + } + + claimed, err := claimSharedVolumes(ctx, st, actor) + if err != nil { + t.Fatalf("claimSharedVolumes: %v", err) + } + if diff := cmp.Diff(actor.GetStatus().GetExternalVolumes(), claimed, protocmp.Transform()); diff != "" { + t.Errorf("claimed mounts mismatch (-want +got):\n%s", diff) + } + if diff := cmp.Diff(map[string]bool{uid: true}, refStates(mustGetVolume(t, ctx, st, "shared"))); diff != "" { + t.Errorf("refs after claim mismatch (-want +got):\n%s", diff) + } + if _, err := claimSharedVolumes(ctx, st, actor); err != nil { + t.Fatalf("second claimSharedVolumes: %v", err) + } + + if err := deactivateSharedVolumes(ctx, st, actor); err != nil { + t.Fatalf("deactivateSharedVolumes: %v", err) + } + if diff := cmp.Diff(map[string]bool{uid: false}, refStates(mustGetVolume(t, ctx, st, "shared"))); diff != "" { + t.Errorf("refs after deactivate mismatch (-want +got):\n%s", diff) + } + + deleter := &recordingSharedVolumeDeleter{} + if err := releaseSharedVolumes(ctx, st, deleter, actor); err != nil { + t.Fatalf("releaseSharedVolumes: %v", err) + } + if got := refStates(mustGetVolume(t, ctx, st, "shared")); len(got) != 0 { + t.Errorf("refs after release = %v, want none", got) + } + if len(deleter.deleted) != 0 { + t.Errorf("deleted %v, want a MANUAL volume left in place", deleter.deleted) + } + if err := releaseSharedVolumes(ctx, st, deleter, actor); err != nil { + t.Fatalf("second releaseSharedVolumes: %v", err) + } + if _, err := st.GetSharedVolume(ctx, volumeRef); err != nil { + t.Errorf("GetSharedVolume after release: %v", err) + } +} + +func TestClaimSharedVolumes_Exclusion(t *testing.T) { + ctx := context.Background() + other := &ateapipb.ActorRef{ActorUid: refTestOtherUID, ActorName: "producer"} + + t.Run("active holder blocks", func(t *testing.T) { + st := newTestPersistence(t) + other := &ateapipb.ActorRef{ActorUid: other.ActorUid, ActorName: other.ActorName, Active: true} + mustStoreReadyVolume(t, ctx, st, "shared", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL, other, &ateapipb.ActorRef{ActorUid: refTestActorUID, ActorName: "consumer"}) + + _, err := claimSharedVolumes(ctx, st, refTestActor(borrowedMount("shared"))) + if apierror.Code(err) != codes.FailedPrecondition || !strings.Contains(err.Error(), "producer") { + t.Fatalf("claimSharedVolumes() = %v, want FailedPrecondition naming the holder", err) + } + if diff := cmp.Diff(map[string]bool{refTestOtherUID: true, refTestActorUID: false}, refStates(mustGetVolume(t, ctx, st, "shared"))); diff != "" { + t.Errorf("refs after refused claim mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("inactive holder does not block", func(t *testing.T) { + st := newTestPersistence(t) + mustStoreReadyVolume(t, ctx, st, "shared", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL, other, &ateapipb.ActorRef{ActorUid: refTestActorUID, ActorName: "consumer"}) + + if _, err := claimSharedVolumes(ctx, st, refTestActor(borrowedMount("shared"))); err != nil { + t.Fatalf("claimSharedVolumes: %v", err) + } + if diff := cmp.Diff(map[string]bool{refTestOtherUID: false, refTestActorUID: true}, refStates(mustGetVolume(t, ctx, st, "shared"))); diff != "" { + t.Errorf("refs mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("missing volume", func(t *testing.T) { + st := newTestPersistence(t) + storetest.MustCreateAtespace(t, ctx, st, refTestAtespace) + + _, err := claimSharedVolumes(ctx, st, refTestActor(borrowedMount("absent"))) + if apierror.Code(err) != codes.FailedPrecondition { + t.Fatalf("claimSharedVolumes() = %v, want FailedPrecondition", err) + } + }) +} + +func TestReleaseSharedVolumes_LastActorDeletes(t *testing.T) { + ctx := context.Background() + st := newTestPersistence(t) + other := &ateapipb.ActorRef{ActorUid: refTestOtherUID, ActorName: "other"} + mustStoreReadyVolume(t, ctx, st, "shared", ateapipb.DeleteTrigger_DELETE_TRIGGER_LAST_ACTOR, + &ateapipb.ActorRef{ActorUid: refTestActorUID, ActorName: "consumer"}, other) + actor := refTestActor(borrowedMount("shared")) + deleter := &recordingSharedVolumeDeleter{} + + if err := releaseSharedVolumes(ctx, st, deleter, actor); err != nil { + t.Fatalf("releaseSharedVolumes: %v", err) + } + if len(deleter.deleted) != 0 { + t.Fatalf("deleted %v while another actor still references the volume", deleter.deleted) + } + + otherActor := refTestActor(borrowedMount("shared")) + otherActor.Metadata.Uid = refTestOtherUID + if err := releaseSharedVolumes(ctx, st, deleter, otherActor); err != nil { + t.Fatalf("releaseSharedVolumes(other): %v", err) + } + if diff := cmp.Diff([]string{refTestAtespace + "/shared"}, deleter.deleted); diff != "" { + t.Errorf("deleted volumes mismatch (-want +got):\n%s", diff) + } +} + +func TestCrashActorKeepsSharedVolumeRefs(t *testing.T) { + ctx := context.Background() + st := newTestPersistence(t) + + created := storetest.MustCreateActor(t, ctx, st, refTestActor()) + mustStoreReadyVolume(t, ctx, st, "shared", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL, + &ateapipb.ActorRef{ActorUid: created.GetMetadata().GetUid(), ActorName: "consumer", Active: true}) + mustUpdateActorStatus(t, ctx, st, created, func(s *ateapipb.ActorStatus) { + s.State = ateapipb.ActorState_ACTOR_STATE_RUNNING + s.ExternalVolumes = []*ateapipb.ExternalVolumeRef{borrowedMount("shared")} + }) + + if err := crashActor(ctx, st, resources.ActorRef{Atespace: refTestAtespace, Name: "consumer"}, "resume", "worker lost"); err != nil { + t.Fatalf("crashActor: %v", err) + } + if diff := cmp.Diff(map[string]bool{created.GetMetadata().GetUid(): true}, refStates(mustGetVolume(t, ctx, st, "shared"))); diff != "" { + t.Errorf("refs after crash mismatch (-want +got):\n%s", diff) + } +} + +type recordingSharedVolumeDeleter struct { + deleted []string +} + +func (d *recordingSharedVolumeDeleter) DeleteSharedVolume(_ context.Context, volumeRef resources.SharedVolumeRef, _ store.DeletePreconditions) (*ateapipb.SharedVolume, error) { + d.deleted = append(d.deleted, volumeRef.String()) + return nil, nil +} + +func TestClaimSharedVolumesRollsBackOnlyNewClaims(t *testing.T) { + ctx := context.Background() + st := newTestPersistence(t) + own := &ateapipb.ActorRef{ActorUid: refTestActorUID, ActorName: "consumer"} + mustStoreReadyVolume(t, ctx, st, "first", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL, own) + mustStoreReadyVolume(t, ctx, st, "second", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL, own, &ateapipb.ActorRef{ActorUid: refTestOtherUID, ActorName: "other", Active: true}) + if _, err := claimSharedVolumes(ctx, st, refTestActor(borrowedMount("first"), borrowedMount("second"))); err == nil { + t.Fatal("expected exclusion") + } + if refStates(mustGetVolume(t, ctx, st, "first"))[refTestActorUID] { + t.Fatal("partial claim leaked") + } + if !refStates(mustGetVolume(t, ctx, st, "second"))[refTestOtherUID] { + t.Fatal("other actor's claim released") + } +} diff --git a/cmd/ateapi/internal/controlapi/shared_volume_resume.go b/cmd/ateapi/internal/controlapi/shared_volume_resume.go new file mode 100644 index 0000000000..4220745860 --- /dev/null +++ b/cmd/ateapi/internal/controlapi/shared_volume_resume.go @@ -0,0 +1,37 @@ +// 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" + "github.com/agent-substrate/substrate/internal/ateattr" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "time" +) + +// releaseFailedResumeClaims retains ownership unless both possible access +// paths (the workload and the node's attachment) have been shut down. +func (w *ActorWorkflow) releaseFailedResumeClaims(ctx context.Context, ref resources.ActorRef, actor *ateapipb.Actor, template *ateapipb.ActorTemplate) error { + ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + defer cancel() + if err := w.ensureAteletTerminated(ctx, ref, actor, template, ateattr.OperationResume); err != nil { + return err + } + if err := detachActorVolumes(ctx, w.pluginRegistry, actor, template, ateattr.OperationResume); err != nil { + return err + } + return deactivateSharedVolumes(ctx, w.store, actor) +} diff --git a/cmd/ateapi/internal/controlapi/shared_volume_test.go b/cmd/ateapi/internal/controlapi/shared_volume_test.go new file mode 100644 index 0000000000..71272a026d --- /dev/null +++ b/cmd/ateapi/internal/controlapi/shared_volume_test.go @@ -0,0 +1,370 @@ +// 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" + "errors" + "testing" + + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" + "github.com/agent-substrate/substrate/internal/apierror" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/internal/volume" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "github.com/google/go-cmp/cmp" + "google.golang.org/grpc/codes" + storagev1 "k8s.io/api/storage/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// recordingVolumePlugin answers CreateVolume with a handle derived from the +// name it was asked for, and records both calls so a test can assert on what +// the workflow asked the storage system to do. +type recordingVolumePlugin struct { + volume.VolumePluginControlPlane + + createErr error + deleteErr error + beforeDelete func() + createdIDs []string + deletedIDs []string +} + +func (p *recordingVolumePlugin) CreateVolume(_ context.Context, req volume.CreateVolumeRequest) (volume.CreateVolumeResponse, error) { + if p.createErr != nil { + return volume.CreateVolumeResponse{}, p.createErr + } + p.createdIDs = append(p.createdIDs, req.Name) + return volume.CreateVolumeResponse{VolumeID: "disk/" + req.Name, VolumeContext: map[string]string{"capacity": req.Capacity}}, nil +} + +func (p *recordingVolumePlugin) DeleteVolume(_ context.Context, volumeID string) error { + if p.beforeDelete != nil { + p.beforeDelete() + } + if p.deleteErr != nil { + return p.deleteErr + } + p.deletedIDs = append(p.deletedIDs, volumeID) + return nil +} + +// newTestSharedVolumeWorkflow builds a SharedVolumeWorkflow over a real +// store, with one StorageClass named "standard" whose reclaim policy the caller +// chooses, and a plugin registered under that class's provisioner. +func newTestSharedVolumeWorkflow(t *testing.T) (*SharedVolumeWorkflow, store.Interface, *recordingVolumePlugin) { + t.Helper() + persistence := newTestPersistence(t) + plugin := &recordingVolumePlugin{} + scLister := &fakeStorageClassLister{ + storageClasses: map[string]*storagev1.StorageClass{ + "standard": { + ObjectMeta: metav1.ObjectMeta{Name: "standard"}, + Provisioner: "mock", + Parameters: map[string]string{"type": "pd-ssd"}, + }, + }, + } + registry := &mockPluginRegistry{plugins: map[string]volume.VolumePluginControlPlane{"mock": plugin}} + return NewSharedVolumeWorkflow(persistence, scLister, registry), persistence, plugin +} + +// TestCreateSharedVolume_Provisioned checks the whole create: the record is +// reserved, the disk is asked for under an ID derived from the record's UID, +// and the handle the driver returned is stamped back. +func TestCreateSharedVolume_Provisioned(t *testing.T) { + ctx := context.Background() + w, _, plugin := newTestSharedVolumeWorkflow(t) + + got, err := w.CreateSharedVolume(ctx, &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "shared"}, + StorageClassName: "standard", + Capacity: "10Gi", + }) + if err != nil { + t.Fatalf("CreateSharedVolume: %v", err) + } + + wantID := sharedVolumeID(got.GetMetadata().GetUid()) + if diff := cmp.Diff([]string{wantID}, plugin.createdIDs); diff != "" { + t.Errorf("provisioned volume names mismatch (-want +got):\n%s", diff) + } + if want := "disk/" + wantID; got.GetVolumeId() != want { + t.Errorf("volume_id = %q, want the handle the driver returned %q", got.GetVolumeId(), want) + } + if got.GetVolumeType() != "mock" { + t.Errorf("volume_type = %q, want the class's provisioner %q", got.GetVolumeType(), "mock") + } + if want := ateapipb.DeleteTrigger_DELETE_TRIGGER_LAST_ACTOR; got.GetDeleteTrigger() != want { + t.Errorf("delete_trigger = %v, want %v", got.GetDeleteTrigger(), want) + } + if want := ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY; got.GetStatus().GetState() != want { + t.Errorf("state = %v, want %v", got.GetStatus().GetState(), want) + } + if got.GetVolumeContext()["capacity"] != "10Gi" { + t.Errorf("volume_context = %v, want the context the driver returned for a 10Gi disk", got.GetVolumeContext()) + } +} + +func TestCreateSharedVolume_Registered(t *testing.T) { + ctx := context.Background() + w, _, plugin := newTestSharedVolumeWorkflow(t) + + got, err := w.CreateSharedVolume(ctx, &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "adopted"}, + VolumeId: "projects/p/zones/z/disks/preexisting", + VolumeType: "mock", + VolumeContext: map[string]string{"fsType": "ext4"}, + }) + if err != nil { + t.Fatalf("CreateSharedVolume: %v", err) + } + + if len(plugin.createdIDs) != 0 { + t.Errorf("provisioned %v, want nothing provisioned for a volume that already exists", plugin.createdIDs) + } + if want := "projects/p/zones/z/disks/preexisting"; got.GetVolumeId() != want { + t.Errorf("volume_id = %q, want the supplied %q", got.GetVolumeId(), want) + } + if want := ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY; got.GetStatus().GetState() != want { + t.Errorf("state = %v, want %v", got.GetStatus().GetState(), want) + } + if want := ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL; got.GetDeleteTrigger() != want { + t.Errorf("delete_trigger = %v, want %v", got.GetDeleteTrigger(), want) + } +} + +func TestCreateSharedVolume_Errors(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + volume *ateapipb.SharedVolume + wantCode codes.Code + }{ + { + name: "unknown storage class", + volume: &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "vol"}, + StorageClassName: "nonexistent", + Capacity: "1Gi", + }, + wantCode: codes.FailedPrecondition, + }, + { + name: "unknown atespace", + volume: &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: "no-such-space", Name: "vol"}, + StorageClassName: "standard", + Capacity: "1Gi", + }, + wantCode: codes.FailedPrecondition, + }, + { + name: "no source at all", + volume: &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "vol"}, + }, + wantCode: codes.InvalidArgument, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + w, _, _ := newTestSharedVolumeWorkflow(t) + _, err := w.CreateSharedVolume(ctx, tc.volume) + if got := apierror.Code(err); got != tc.wantCode { + t.Fatalf("CreateSharedVolume error = %v (code %v), want code %v", err, got, tc.wantCode) + } + }) + } +} + +func TestCreateSharedVolume_ReusedName(t *testing.T) { + ctx := context.Background() + w, _, plugin := newTestSharedVolumeWorkflow(t) + + toCreate := func() *ateapipb.SharedVolume { + return &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "shared"}, + StorageClassName: "standard", + Capacity: "10Gi", + } + } + if _, err := w.CreateSharedVolume(ctx, toCreate()); err != nil { + t.Fatalf("CreateSharedVolume: %v", err) + } + _, err := w.CreateSharedVolume(ctx, toCreate()) + if got := apierror.Code(err); got != codes.AlreadyExists { + t.Fatalf("second CreateSharedVolume error = %v (code %v), want code %v", err, got, codes.AlreadyExists) + } + if len(plugin.createdIDs) != 1 { + t.Errorf("provisioned %v, want only the first create to reach the storage system", plugin.createdIDs) + } +} + +func TestDeleteSharedVolume(t *testing.T) { + ctx := context.Background() + w, persistence, plugin := newTestSharedVolumeWorkflow(t) + created, err := w.CreateSharedVolume(ctx, &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "shared"}, + StorageClassName: "standard", + Capacity: "10Gi", + }) + if err != nil { + t.Fatalf("CreateSharedVolume: %v", err) + } + volumeRef := resources.SharedVolumeRefFromSharedVolume(created) + + if _, err := w.DeleteSharedVolume(ctx, volumeRef, store.DeletePreconditions{Version: created.GetMetadata().GetVersion() + 1}); apierror.Code(err) != codes.Aborted { + t.Fatalf("DeleteSharedVolume with a stale version error = %v, want code %v", err, codes.Aborted) + } + if len(plugin.deletedIDs) != 0 { + t.Fatalf("collected %v before the precondition check passed", plugin.deletedIDs) + } + + if _, err := w.DeleteSharedVolume(ctx, volumeRef, store.DeletePreconditions{UID: created.GetMetadata().GetUid()}); err != nil { + t.Fatalf("DeleteSharedVolume: %v", err) + } + if diff := cmp.Diff([]string{created.GetVolumeId()}, plugin.deletedIDs); diff != "" { + t.Errorf("collected disks mismatch (-want +got):\n%s", diff) + } + if _, err := persistence.GetSharedVolume(ctx, volumeRef); err == nil { + t.Errorf("GetSharedVolume after delete succeeded, want the row gone") + } +} + +func TestDeleteSharedVolume_StillReferenced(t *testing.T) { + ctx := context.Background() + w, persistence, plugin := newTestSharedVolumeWorkflow(t) + + created, err := w.CreateSharedVolume(ctx, &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "shared"}, + StorageClassName: "standard", + Capacity: "10Gi", + }) + if err != nil { + t.Fatalf("CreateSharedVolume: %v", err) + } + volumeRef := resources.SharedVolumeRefFromSharedVolume(created) + if _, err := persistence.UpdateSharedVolume(ctx, volumeRef, store.PreconditionFrom(created), func(toUpdate *ateapipb.SharedVolume) error { + toUpdate.Status.Refs = []*ateapipb.ActorRef{{ActorUid: "uid-1", ActorName: "producer"}} + return nil + }); err != nil { + t.Fatalf("UpdateSharedVolume: %v", err) + } + + _, err = w.DeleteSharedVolume(ctx, volumeRef, store.DeletePreconditions{}) + if got := apierror.Code(err); got != codes.FailedPrecondition { + t.Fatalf("DeleteSharedVolume error = %v (code %v), want code %v", err, got, codes.FailedPrecondition) + } + if len(plugin.deletedIDs) != 0 { + t.Errorf("collected %v, want the disk left alone while an actor holds it", plugin.deletedIDs) + } +} + +func TestDeleteSharedVolume_Pending(t *testing.T) { + ctx := context.Background() + w, persistence, plugin := newTestSharedVolumeWorkflow(t) + + pending, err := persistence.CreateSharedVolume(ctx, &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "stranded"}, + StorageClassName: "standard", + VolumeType: "mock", + Status: &ateapipb.SharedVolumeStatus{State: ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_PENDING}, + }) + if err != nil { + t.Fatalf("CreateSharedVolume: %v", err) + } + + if _, err := w.DeleteSharedVolume(ctx, resources.SharedVolumeRefFromSharedVolume(pending), store.DeletePreconditions{}); err != nil { + t.Fatalf("DeleteSharedVolume: %v", err) + } + want := []string{sharedVolumeID(pending.GetMetadata().GetUid())} + if diff := cmp.Diff(want, plugin.deletedIDs); diff != "" { + t.Errorf("collected disks mismatch (-want +got):\n%s", diff) + } +} + +func TestDeleteSharedVolume_NotFound(t *testing.T) { + ctx := context.Background() + w, _, _ := newTestSharedVolumeWorkflow(t) + + _, err := w.DeleteSharedVolume(ctx, resources.SharedVolumeRef{Atespace: "team-a", Name: "nope"}, store.DeletePreconditions{}) + if got := apierror.Code(err); got != codes.NotFound { + t.Fatalf("DeleteSharedVolume error = %v (code %v), want code %v", err, got, codes.NotFound) + } +} + +func TestDeleteSharedVolumeFencesBindingsBeforeStorageDelete(t *testing.T) { + ctx := context.Background() + w, st, plugin := newTestSharedVolumeWorkflow(t) + v := mustStoreReadyVolume(t, ctx, st, "fenced", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL) + ref := resources.SharedVolumeRefFromSharedVolume(v) + plugin.beforeDelete = func() { + current, err := st.GetSharedVolume(ctx, ref) + if err != nil { + t.Fatal(err) + } + if current.GetStatus().GetState() != ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_DELETING { + t.Fatal("CSI called before deletion fence") + } + if _, err := st.CreateActor(ctx, refTestActor(borrowedMount("fenced"))); err == nil { + t.Fatal("bind crossed deletion fence") + } + } + plugin.deleteErr = errors.New("transient CSI failure") + if _, err := w.DeleteSharedVolume(ctx, ref, store.DeletePreconditions{}); err == nil { + t.Fatal("expected CSI error") + } + plugin.deleteErr = nil + if _, err := w.DeleteSharedVolume(ctx, ref, store.DeletePreconditions{}); err != nil { + t.Fatalf("retry: %v", err) + } +} + +type bindDuringDeleteStore struct { + store.Interface + beforeUpdate func() +} + +func (s *bindDuringDeleteStore) UpdateSharedVolume(ctx context.Context, ref resources.SharedVolumeRef, precondition store.Precondition, mutate func(*ateapipb.SharedVolume) error) (*ateapipb.SharedVolume, error) { + if s.beforeUpdate != nil { + f := s.beforeUpdate + s.beforeUpdate = nil + f() + } + return s.Interface.UpdateSharedVolume(ctx, ref, precondition, mutate) +} + +func TestDeleteSharedVolumeConcurrentBindPreventsStorageDeletion(t *testing.T) { + ctx := context.Background() + w, st, plugin := newTestSharedVolumeWorkflow(t) + v := mustStoreReadyVolume(t, ctx, st, "race", ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL) + w.store = &bindDuringDeleteStore{Interface: st, beforeUpdate: func() { + if _, err := st.CreateActor(ctx, refTestActor(borrowedMount("race"))); err != nil { + t.Fatal(err) + } + }} + if _, err := w.DeleteSharedVolume(ctx, resources.SharedVolumeRefFromSharedVolume(v), store.DeletePreconditions{}); apierror.Code(err) != codes.Aborted { + t.Fatalf("got %v, want version conflict", err) + } + if len(plugin.deletedIDs) != 0 { + t.Fatalf("storage deleted after concurrent bind: %v", plugin.deletedIDs) + } + if refs := mustGetVolume(t, ctx, st, "race").GetStatus().GetRefs(); len(refs) != 1 { + t.Fatalf("lost binding: %v", refs) + } +} diff --git a/cmd/ateapi/internal/controlapi/template_reconciler.go b/cmd/ateapi/internal/controlapi/template_reconciler.go index 2a00e3439b..899546f2d7 100644 --- a/cmd/ateapi/internal/controlapi/template_reconciler.go +++ b/cmd/ateapi/internal/controlapi/template_reconciler.go @@ -260,7 +260,7 @@ func (r *ActorTemplateReconciler) reconcileOne(ctx context.Context, ref resource // after ResumeActor). The elapsed warmup is unknowable, so // restart the clock. slog.WarnContext(ctx, "Golden actor running without a snapshot deadline; restarting warmup", slog.String("ActorTemplate", ref.String())) - deadline := time.Now().Add(goldenSnapshotWarmupFor(tmpl.GetContainers())) + deadline := time.Now().Add(goldenSnapshotWarmupFor(tmpl)) if tmpl, err = r.checkpoint(ctx, tmpl, func(snapshotStatus *ateapipb.GoldenSnapshotStatus) { snapshotStatus.SnapshotTime = timestamppb.New(deadline) }); err != nil { @@ -301,7 +301,7 @@ func (r *ActorTemplateReconciler) reconcileOne(ctx context.Context, ref resource // A crash during resume is observed as CRASHED on the retry. return 0, fmt.Errorf("while resuming golden actor: %w", err) } - deadline := time.Now().Add(goldenSnapshotWarmupFor(tmpl.GetContainers())) + deadline := time.Now().Add(goldenSnapshotWarmupFor(tmpl)) if tmpl, err = r.checkpoint(ctx, tmpl, func(snapshotStatus *ateapipb.GoldenSnapshotStatus) { snapshotStatus.SnapshotTime = timestamppb.New(deadline) }); err != nil { @@ -410,7 +410,11 @@ func goldenSnapshotDone(snapshotStatus *ateapipb.GoldenSnapshotStatus) bool { // goldenSnapshotWarmupFor returns 0 when every container has a wakeup probe // (ResumeActor already blocked until the workload reported 200), and the // default warmup otherwise. -func goldenSnapshotWarmupFor(containers []*ateapipb.Container) time.Duration { +func goldenSnapshotWarmupFor(tmpl *ateapipb.ActorTemplate) time.Duration { + if tmpl.GetSnapshotConfig().GetGoldenSnapshotConfig().GetMode() == ateapipb.GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_RUNTIME { + return 0 + } + containers := tmpl.GetContainers() if len(containers) == 0 { return goldenSnapshotWarmup } diff --git a/cmd/ateapi/internal/controlapi/template_reconciler_test.go b/cmd/ateapi/internal/controlapi/template_reconciler_test.go index f4dfd27dd9..343c8c54d9 100644 --- a/cmd/ateapi/internal/controlapi/template_reconciler_test.go +++ b/cmd/ateapi/internal/controlapi/template_reconciler_test.go @@ -390,7 +390,7 @@ func TestGoldenSnapshotWarmupFor(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := goldenSnapshotWarmupFor(tt.containers); got != tt.want { + if got := goldenSnapshotWarmupFor(&ateapipb.ActorTemplate{Containers: tt.containers}); got != tt.want { t.Errorf("goldenSnapshotWarmupFor() = %v, want %v", got, tt.want) } }) @@ -1011,3 +1011,10 @@ func TestReconcileOne_GoldenTagConflict(t *testing.T) { }) } } + +func TestRuntimeGoldenNeedsNoApplicationWarmup(t *testing.T) { + tmpl := &ateapipb.ActorTemplate{SnapshotConfig: &ateapipb.SnapshotConfig{GoldenSnapshotConfig: &ateapipb.GoldenSnapshotConfig{Mode: ateapipb.GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_RUNTIME}}} + if got := goldenSnapshotWarmupFor(tmpl); got != 0 { + t.Fatalf("runtime warmup = %s, want zero", got) + } +} diff --git a/cmd/ateapi/internal/controlapi/volumes.go b/cmd/ateapi/internal/controlapi/volumes.go index 021a79ccf9..0f675fdb28 100644 --- a/cmd/ateapi/internal/controlapi/volumes.go +++ b/cmd/ateapi/internal/controlapi/volumes.go @@ -29,16 +29,16 @@ import ( storagev1listers "k8s.io/client-go/listers/storage/v1" ) -// initialExternalVolumes constructs the actor's volume objects before volume +// initialActorVolumes constructs the actor's volume objects before volume // creation. A volume has no storage volume ID until it is provisioned. -func initialExternalVolumes(ctx context.Context, scLister storagev1listers.StorageClassLister, template *ateapipb.ActorTemplate) ([]*ateapipb.ExternalVolume, error) { +func initialActorVolumes(ctx context.Context, scLister storagev1listers.StorageClassLister, template *ateapipb.ActorTemplate) ([]*ateapipb.ExternalVolumeRef, error) { if template == nil { return nil, apierror.InvalidArgument("template is required") } - var volumes []*ateapipb.ExternalVolume + var volumes []*ateapipb.ExternalVolumeRef for _, vol := range template.GetVolumes() { - if vol.GetExternalVolumeTemplate() != nil { - scName := vol.GetExternalVolumeTemplate().GetStorageClassName() + if vol.GetExternal().GetTemplate() != nil { + scName := vol.GetExternal().GetTemplate().GetStorageClassName() sc, err := scLister.Get(scName) if err != nil { if k8serrors.IsNotFound(err) { @@ -47,7 +47,7 @@ func initialExternalVolumes(ctx context.Context, scLister storagev1listers.Stora return nil, apierror.Internal("failed to get StorageClass %q: %v", scName, err) } - volumes = append(volumes, &ateapipb.ExternalVolume{ + volumes = append(volumes, &ateapipb.ExternalVolumeRef{ Name: vol.GetName(), VolumeType: sc.Provisioner, }) @@ -59,8 +59,8 @@ func initialExternalVolumes(ctx context.Context, scLister storagev1listers.Stora // createActorVolumes provisions the external volumes in volumesToCreate that have no storage volume ID, using the provided volume plugin. // It returns the list of external volumes (with storage IDs), or an error if any creation fails. // Any volumes processed before or during a failure are returned alongside the error so they can be persisted on the actor. -func createActorVolumes(ctx context.Context, registry VolumePluginRegistry, scLister storagev1listers.StorageClassLister, actorUID string, template *ateapipb.ActorTemplate, volumesToCreate []*ateapipb.ExternalVolume) (resultVolumes []*ateapipb.ExternalVolume, err error) { - resultVolumes = make([]*ateapipb.ExternalVolume, 0, len(volumesToCreate)) +func createActorVolumes(ctx context.Context, registry VolumePluginRegistry, scLister storagev1listers.StorageClassLister, actorUID string, template *ateapipb.ActorTemplate, volumesToCreate []*ateapipb.ExternalVolumeRef) (resultVolumes []*ateapipb.ExternalVolumeRef, err error) { + resultVolumes = make([]*ateapipb.ExternalVolumeRef, 0, len(volumesToCreate)) var currentIdx int defer func() { @@ -74,6 +74,11 @@ func createActorVolumes(ctx context.Context, registry VolumePluginRegistry, scLi for idx, vol := range volumesToCreate { currentIdx = idx + if vol.GetSharedVolumeName() != "" { + resultVolumes = append(resultVolumes, vol) + continue + } + var specVol *ateapipb.Volume volName := vol.GetName() for _, tVol := range template.GetVolumes() { @@ -82,7 +87,7 @@ func createActorVolumes(ctx context.Context, registry VolumePluginRegistry, scLi break } } - if specVol == nil || specVol.GetExternalVolumeTemplate() == nil { + if specVol == nil || specVol.GetExternal().GetTemplate() == nil { return resultVolumes, apierror.NotFound("volume %q not found in template", volName) } @@ -93,7 +98,7 @@ func createActorVolumes(ctx context.Context, registry VolumePluginRegistry, scLi actVolID := actorVolumeID(actorUID, volName) - scName := specVol.GetExternalVolumeTemplate().GetStorageClassName() + scName := specVol.GetExternal().GetTemplate().GetStorageClassName() sc, err := scLister.Get(scName) if err != nil { if k8serrors.IsNotFound(err) { @@ -113,7 +118,7 @@ func createActorVolumes(ctx context.Context, registry VolumePluginRegistry, scLi resp, volErr := plugin.CreateVolume(ctx, volume.CreateVolumeRequest{ Name: actVolID, - Capacity: specVol.GetExternalVolumeTemplate().GetCapacity(), + Capacity: specVol.GetExternal().GetTemplate().GetCapacity(), Parameters: sc.Parameters, }) if volErr != nil { @@ -125,7 +130,7 @@ func createActorVolumes(ctx context.Context, registry VolumePluginRegistry, scLi return resultVolumes, apierror.Internal("volume plugin returned no ID for volume %q", specVol.GetName()) } - resultVolumes = append(resultVolumes, &ateapipb.ExternalVolume{ + resultVolumes = append(resultVolumes, &ateapipb.ExternalVolumeRef{ Name: volName, StorageVolumeId: resp.VolumeID, VolumeType: sc.Provisioner, @@ -135,13 +140,16 @@ func createActorVolumes(ctx context.Context, registry VolumePluginRegistry, scLi return resultVolumes, nil } -// deleteActorVolumes deletes all external volumes in the list. -func deleteActorVolumes(ctx context.Context, registry VolumePluginRegistry, actorUID string, volumes []*ateapipb.ExternalVolume) error { +// deleteActorVolumes deletes the external volumes the actor owns. +func deleteActorVolumes(ctx context.Context, registry VolumePluginRegistry, actorUID string, volumes []*ateapipb.ExternalVolumeRef) error { if actorUID == "" { return errors.New("actorUID is required") } var errs []error for _, vol := range volumes { + if vol.GetSharedVolumeName() != "" { + continue + } volID := vol.GetStorageVolumeId() if volID == "" { // If the volume hasn't been successfully created yet, it's possible @@ -168,8 +176,8 @@ func deleteActorVolumes(ctx context.Context, registry VolumePluginRegistry, acto } // 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 *ateapipb.ActorTemplate) []*ateapipb.ExternalVolume { - var mounted []*ateapipb.ExternalVolume +func getMountedActorVolumes(ctx context.Context, ref *ateapipb.ObjectRef, volumes []*ateapipb.ExternalVolumeRef, template *ateapipb.ActorTemplate) []*ateapipb.ExternalVolumeRef { + var mounted []*ateapipb.ExternalVolumeRef for _, vol := range volumes { // Find the corresponding volume in the ActorTemplate to check if it's mounted var matchedTemplateVol *ateapipb.Volume diff --git a/cmd/ateapi/internal/controlapi/volumes_test.go b/cmd/ateapi/internal/controlapi/volumes_test.go index 545384d569..21dc57ff6c 100644 --- a/cmd/ateapi/internal/controlapi/volumes_test.go +++ b/cmd/ateapi/internal/controlapi/volumes_test.go @@ -63,12 +63,12 @@ func TestActorVolumesStorageClassErrors(t *testing.T) { tmpl := &ateapipb.ActorTemplate{ Volumes: []*ateapipb.Volume{{ Name: "data-vol", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", - }, + }}, }}, } - volumes := []*ateapipb.ExternalVolume{{ + volumes := []*ateapipb.ExternalVolumeRef{{ Name: "data-vol", VolumeType: "mock-standard", }} @@ -91,12 +91,12 @@ func TestActorVolumesStorageClassErrors(t *testing.T) { t.Run(tt.name, func(t *testing.T) { lister := &fakeStorageClassLister{getErr: tt.getErr} t.Run("initial", func(t *testing.T) { - _, err := initialExternalVolumes(ctx, lister, tmpl) + _, err := initialActorVolumes(ctx, lister, tmpl) if got := apierror.Code(err); got != tt.wantCode { - t.Fatalf("initialExternalVolumes() code = %v, want %v; error = %v", got, tt.wantCode, err) + t.Fatalf("initialActorVolumes() code = %v, want %v; error = %v", got, tt.wantCode, err) } if !strings.Contains(err.Error(), `StorageClass "standard"`) { - t.Errorf("initialExternalVolumes() error does not name the StorageClass: %v", err) + t.Errorf("initialActorVolumes() error does not name the StorageClass: %v", err) } }) t.Run("create", func(t *testing.T) { @@ -120,9 +120,9 @@ func TestInitialActorVolumes(t *testing.T) { Volumes: []*ateapipb.Volume{ { Name: "data-vol-1", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", - }, + }}, }, { Name: "scratch-vol", @@ -133,14 +133,14 @@ func TestInitialActorVolumes(t *testing.T) { }, { Name: "data-vol-2", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "fast", - }, + }}, }, }, } - want := []*ateapipb.ExternalVolume{ + want := []*ateapipb.ExternalVolumeRef{ { Name: "data-vol-1", VolumeType: "mock-standard", @@ -163,12 +163,12 @@ func TestInitialActorVolumes(t *testing.T) { }, }, } - initVols, err := initialExternalVolumes(context.Background(), scLister, tmpl) + initVols, err := initialActorVolumes(context.Background(), scLister, tmpl) if err != nil { - t.Fatalf("initialExternalVolumes failed: %v", err) + t.Fatalf("initialActorVolumes failed: %v", err) } if diff := cmp.Diff(want, initVols, protocmp.Transform()); diff != "" { - t.Errorf("initialExternalVolumes mismatch (-want +got):\n%s", diff) + t.Errorf("initialActorVolumes mismatch (-want +got):\n%s", diff) } } @@ -179,9 +179,9 @@ func TestCreateActorVolumes(t *testing.T) { Volumes: []*ateapipb.Volume{ { Name: "data-vol", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", - }, + }}, }, }, } @@ -190,21 +190,21 @@ func TestCreateActorVolumes(t *testing.T) { Volumes: []*ateapipb.Volume{ { Name: "vol1", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", - }, + }}, }, { Name: "vol2", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", - }, + }}, }, { Name: "vol3", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "standard", - }, + }}, }, }, } @@ -212,22 +212,22 @@ func TestCreateActorVolumes(t *testing.T) { tests := []struct { name string tmpl *ateapipb.ActorTemplate - inputVolumes []*ateapipb.ExternalVolume + inputVolumes []*ateapipb.ExternalVolumeRef storageClasses map[string]*storagev1.StorageClass wantErr bool - wantRes []*ateapipb.ExternalVolume + wantRes []*ateapipb.ExternalVolumeRef }{ { name: "partial failure returns error and preserves succeeded, failed, and remaining volumes", tmpl: multiVolTmpl, - inputVolumes: []*ateapipb.ExternalVolume{ + inputVolumes: []*ateapipb.ExternalVolumeRef{ {Name: "vol1", VolumeType: "mock-standard"}, // Its type differs from its StorageClass's provisioner, so creating it fails. {Name: "vol2", VolumeType: "mock-fast"}, {Name: "vol3", VolumeType: "mock-standard"}, }, wantErr: true, - wantRes: []*ateapipb.ExternalVolume{ + wantRes: []*ateapipb.ExternalVolumeRef{ { Name: "vol1", StorageVolumeId: "mock-vol-substrate-actor-uid-123-vol1", @@ -240,14 +240,14 @@ func TestCreateActorVolumes(t *testing.T) { { name: "volume with a storage volume id is not created again", tmpl: standardTmpl, - inputVolumes: []*ateapipb.ExternalVolume{ + inputVolumes: []*ateapipb.ExternalVolumeRef{ { Name: "data-vol", StorageVolumeId: "existing-vol-id", }, }, wantErr: false, - wantRes: []*ateapipb.ExternalVolume{ + wantRes: []*ateapipb.ExternalVolumeRef{ { Name: "data-vol", StorageVolumeId: "existing-vol-id", @@ -257,18 +257,18 @@ func TestCreateActorVolumes(t *testing.T) { { name: "volume not found in template returns error", tmpl: &ateapipb.ActorTemplate{}, - inputVolumes: []*ateapipb.ExternalVolume{ + inputVolumes: []*ateapipb.ExternalVolumeRef{ {Name: "missing-vol"}, }, wantErr: true, - wantRes: []*ateapipb.ExternalVolume{ + wantRes: []*ateapipb.ExternalVolumeRef{ {Name: "missing-vol"}, }, }, { name: "storage class parameters are propagated to volume context", tmpl: standardTmpl, - inputVolumes: []*ateapipb.ExternalVolume{ + inputVolumes: []*ateapipb.ExternalVolumeRef{ { Name: "data-vol", VolumeType: "mock-standard", @@ -285,7 +285,7 @@ func TestCreateActorVolumes(t *testing.T) { }, }, wantErr: false, - wantRes: []*ateapipb.ExternalVolume{ + wantRes: []*ateapipb.ExternalVolumeRef{ { Name: "data-vol", StorageVolumeId: "mock-vol-substrate-actor-uid-123-data-vol", @@ -345,8 +345,8 @@ func (emptyIDVolumePlugin) CreateVolume(context.Context, volume.CreateVolumeRequ func TestCreateActorVolumesRejectsEmptyPluginID(t *testing.T) { tmpl := &ateapipb.ActorTemplate{ Volumes: []*ateapipb.Volume{{ - Name: "data-vol", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{StorageClassName: "standard"}, + Name: "data-vol", + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{StorageClassName: "standard"}}, }}, } registry := &mockPluginRegistry{ @@ -355,7 +355,7 @@ func TestCreateActorVolumesRejectsEmptyPluginID(t *testing.T) { scLister := &fakeStorageClassLister{storageClasses: map[string]*storagev1.StorageClass{ "standard": {ObjectMeta: metav1.ObjectMeta{Name: "standard"}, Provisioner: "mock-standard"}, }} - input := []*ateapipb.ExternalVolume{{Name: "data-vol", VolumeType: "mock-standard"}} + input := []*ateapipb.ExternalVolumeRef{{Name: "data-vol", VolumeType: "mock-standard"}} res, err := createActorVolumes(context.Background(), registry, scLister, "actor-uid-123", tmpl, input) if got := apierror.Code(err); got != codes.Internal { @@ -383,14 +383,14 @@ func TestDeleteActorVolumes(t *testing.T) { tests := []struct { name string actorUID string - volumes []*ateapipb.ExternalVolume + volumes []*ateapipb.ExternalVolumeRef wantDeleted []string wantErr bool }{ { name: "uses storage volume ID when present", actorUID: "uid-abc", - volumes: []*ateapipb.ExternalVolume{ + volumes: []*ateapipb.ExternalVolumeRef{ {Name: "vol1", StorageVolumeId: "storage-vol-123", VolumeType: "mock"}, }, wantDeleted: []string{"storage-vol-123"}, @@ -399,7 +399,7 @@ func TestDeleteActorVolumes(t *testing.T) { { name: "falls back to actorVolumeID when storage volume ID is empty", actorUID: "uid-abc", - volumes: []*ateapipb.ExternalVolume{ + volumes: []*ateapipb.ExternalVolumeRef{ {Name: "vol1", StorageVolumeId: "", VolumeType: "mock"}, }, wantDeleted: []string{"substrate-uid-abc-vol1"}, @@ -479,7 +479,7 @@ func TestDetachActorVolumes(t *testing.T) { Metadata: &ateapipb.ResourceMetadata{Name: "actor-1", Atespace: "default"}, Status: &ateapipb.ActorStatus{ AssignedNode: "node-1", - ExternalVolumes: []*ateapipb.ExternalVolume{ + ExternalVolumes: []*ateapipb.ExternalVolumeRef{ {Name: "vol1", StorageVolumeId: "storage-vol-1", VolumeType: "mock"}, {Name: "vol2", StorageVolumeId: "storage-vol-2", VolumeType: "mock"}, }, @@ -510,7 +510,7 @@ func TestDetachActorVolumes(t *testing.T) { Metadata: &ateapipb.ResourceMetadata{Name: "actor-1", Atespace: "default"}, Status: &ateapipb.ActorStatus{ AssignedNode: "node-1", - ExternalVolumes: []*ateapipb.ExternalVolume{ + ExternalVolumes: []*ateapipb.ExternalVolumeRef{ {Name: "mounted-vol", StorageVolumeId: "storage-vol-mounted", VolumeType: "mock"}, {Name: "unmounted-vol", StorageVolumeId: "storage-vol-unmounted", VolumeType: "mock"}, }, @@ -539,7 +539,7 @@ func TestDetachActorVolumes(t *testing.T) { Metadata: &ateapipb.ResourceMetadata{Name: "actor-1", Atespace: "default"}, Status: &ateapipb.ActorStatus{ AssignedNode: "node-1", - ExternalVolumes: []*ateapipb.ExternalVolume{ + ExternalVolumes: []*ateapipb.ExternalVolumeRef{ {Name: "vol1", StorageVolumeId: "", VolumeType: "mock"}, {Name: "vol2", StorageVolumeId: "storage-vol-2", VolumeType: "mock"}, }, @@ -569,7 +569,7 @@ func TestDetachActorVolumes(t *testing.T) { Metadata: &ateapipb.ResourceMetadata{Name: "actor-1", Atespace: "default"}, Status: &ateapipb.ActorStatus{ AssignedNode: "node-1", - ExternalVolumes: []*ateapipb.ExternalVolume{ + ExternalVolumes: []*ateapipb.ExternalVolumeRef{ {Name: "vol1", StorageVolumeId: "storage-vol-1", VolumeType: "mock"}, {Name: "vol2", StorageVolumeId: "storage-vol-2", VolumeType: "mock"}, }, @@ -587,7 +587,7 @@ func TestDetachActorVolumes(t *testing.T) { Metadata: &ateapipb.ResourceMetadata{Name: "actor-1", Atespace: "default"}, Status: &ateapipb.ActorStatus{ AssignedNode: "node-1", - ExternalVolumes: []*ateapipb.ExternalVolume{ + ExternalVolumes: []*ateapipb.ExternalVolumeRef{ {Name: "vol1", StorageVolumeId: "storage-vol-1", VolumeType: "mock"}, }, }, @@ -608,7 +608,7 @@ func TestDetachActorVolumes(t *testing.T) { Metadata: &ateapipb.ResourceMetadata{Name: "actor-1", Atespace: "default"}, Status: &ateapipb.ActorStatus{ AssignedNode: "node-1", - ExternalVolumes: []*ateapipb.ExternalVolume{ + ExternalVolumes: []*ateapipb.ExternalVolumeRef{ {Name: "vol1", StorageVolumeId: "storage-vol-1", VolumeType: "mock"}, {Name: "vol2", StorageVolumeId: "storage-vol-2", VolumeType: "mock"}, }, @@ -632,7 +632,7 @@ func TestDetachActorVolumes(t *testing.T) { Metadata: &ateapipb.ResourceMetadata{Name: "actor-1", Atespace: "default"}, Status: &ateapipb.ActorStatus{ AssignedNode: "node-1", - ExternalVolumes: []*ateapipb.ExternalVolume{ + ExternalVolumes: []*ateapipb.ExternalVolumeRef{ {Name: "vol1", StorageVolumeId: "storage-vol-1", VolumeType: "unknown-plugin"}, }, }, @@ -646,7 +646,7 @@ func TestDetachActorVolumes(t *testing.T) { Metadata: &ateapipb.ResourceMetadata{Name: "actor-1", Atespace: "default"}, Status: &ateapipb.ActorStatus{ AssignedNode: "", - ExternalVolumes: []*ateapipb.ExternalVolume{ + ExternalVolumes: []*ateapipb.ExternalVolumeRef{ {Name: "vol1", StorageVolumeId: "storage-vol-1", VolumeType: "mock"}, }, }, @@ -686,3 +686,42 @@ func TestDetachActorVolumes(t *testing.T) { }) } } + +func TestBorrowedVolumesAreNotProvisionedOrDeleted(t *testing.T) { + ctx := context.Background() + template := &ateapipb.ActorTemplate{ + Volumes: []*ateapipb.Volume{{Name: "shared-data", External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{Name: "shared"}}}}, + } + scLister := &fakeStorageClassLister{storageClasses: map[string]*storagev1.StorageClass{}} + + initial, err := initialActorVolumes(ctx, scLister, template) + if err != nil { + t.Fatalf("initialActorVolumes: %v", err) + } + if len(initial) != 0 { + t.Errorf("initialActorVolumes() = %v, want no entry for a borrowed volume", initial) + } + + borrowed := []*ateapipb.ExternalVolumeRef{{ + Name: "shared-data", + SharedVolumeName: "shared", + StorageVolumeId: "disk/shared", + VolumeType: "mock", + }} + plugin := &trackingVolumePlugin{} + registry := &mockPluginRegistry{plugins: map[string]volume.VolumePluginControlPlane{"mock": plugin}} + + got, err := createActorVolumes(ctx, registry, scLister, "actor-uid-123", template, borrowed) + if err != nil { + t.Fatalf("createActorVolumes: %v", err) + } + if diff := cmp.Diff(borrowed, got, protocmp.Transform()); diff != "" { + t.Errorf("createActorVolumes() mismatch (-want +got):\n%s", diff) + } + if err := deleteActorVolumes(ctx, registry, "actor-uid-123", borrowed); err != nil { + t.Fatalf("deleteActorVolumes: %v", err) + } + if len(plugin.deletedIDs) != 0 { + t.Errorf("deleted %v, want the borrowed volume left alone", plugin.deletedIDs) + } +} diff --git a/cmd/ateapi/internal/controlapi/workflow.go b/cmd/ateapi/internal/controlapi/workflow.go index eb824400a3..d27179be2c 100644 --- a/cmd/ateapi/internal/controlapi/workflow.go +++ b/cmd/ateapi/internal/controlapi/workflow.go @@ -106,6 +106,7 @@ func logActorState(ctx context.Context, actor *ateapipb.Actor, opName, state str // already validated, and what the workflow writes is the system's own state. type ActorWorkflow struct { store actorWorkflowStore + sharedVolumes sharedVolumeDeleter workerCache *workercache.Cache scheduler scheduling.Scheduler dialer *AteletDialer @@ -132,9 +133,11 @@ func NewActorWorkflow( egressGatewayAddress string, pluginRegistry VolumePluginRegistry, objectStorePlugin objectstorev1.ControlProviderClient, + sharedVolumes sharedVolumeDeleter, ) *ActorWorkflow { return &ActorWorkflow{ store: store, + sharedVolumes: sharedVolumes, workerCache: workerCache, scheduler: scheduling.New(workerCache), dialer: dialer, @@ -183,6 +186,7 @@ type actorWorkflowStore interface { GetActorTemplate(ctx context.Context, templateRef resources.ActorTemplateRef) (*ateapipb.ActorTemplate, error) DeleteActorTemplate(ctx context.Context, templateRef resources.ActorTemplateRef, precondition store.DeletePreconditions) (*ateapipb.ActorTemplate, error) AcquireLease(ctx context.Context, key string) (*store.Lease, error) + sharedVolumeRefStore } // WorkerWorkflow handles the multi-step operations on a Worker. Like @@ -246,3 +250,7 @@ func (w *ActorWorkflow) acquireActorLease(ctx context.Context, actorRef resource func acquireTagLease(ctx context.Context, holder leaseHolder, tagRef resources.TagRef) (context.Context, *store.Lease, error) { return acquireLease(ctx, holder, "lease:tag:"+tagRef.Atespace+":"+tagRef.Name, "Tag") } + +func acquireSharedVolumeLease(ctx context.Context, holder leaseHolder, volumeRef resources.SharedVolumeRef) (context.Context, *store.Lease, error) { + return acquireLease(ctx, holder, "lease:sharedvolume:"+volumeRef.Atespace+":"+volumeRef.Name, "SharedVolume") +} diff --git a/cmd/ateapi/internal/controlapi/workflow_delete.go b/cmd/ateapi/internal/controlapi/workflow_delete.go index d0402c9e91..2ad6a403bc 100644 --- a/cmd/ateapi/internal/controlapi/workflow_delete.go +++ b/cmd/ateapi/internal/controlapi/workflow_delete.go @@ -93,8 +93,15 @@ func (w *ActorWorkflow) DeleteActor(ctx context.Context, actorRef resources.Acto slog.Any("volumesDetachedErr", volumesDetachedErr)) } - if err := w.ensureVolumesDeleted(ctx, actor); err != nil { - errs = append(errs, fmt.Errorf("while deleting volumes: %w", err)) + // A failed termination or detach may leave a live writer on the node. + // Retain references (and LAST_ACTOR storage) until both have succeeded. + if atletTerminatedErr == nil && volumesDetachedErr == nil { + if err := w.ensureVolumesDeleted(ctx, actor); err != nil { + errs = append(errs, fmt.Errorf("while deleting volumes: %w", err)) + } + if err := w.ensureSharedVolumeRefsReleased(ctx, actor); err != nil { + errs = append(errs, fmt.Errorf("while releasing external volume references: %w", err)) + } } if err := w.ensureExternalSnapshotsReleased(ctx, actor); err != nil { @@ -187,7 +194,7 @@ func (w *ActorWorkflow) ensureAteletTerminated(ctx context.Context, actorRef res if vol.GetStorageVolumeId() != "" { workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ Name: vol.GetName(), - External: &ateletpb.ExternalVolumeSource{ + External: &ateletpb.ExternalVolume{ StorageVolumeId: vol.GetStorageVolumeId(), VolumeType: vol.GetVolumeType(), VolumeContext: vol.GetVolumeContext(), @@ -379,6 +386,15 @@ func (w *ActorWorkflow) ensureVolumesDeleted(ctx context.Context, actor *ateapip return nil } +// ensureSharedVolumeRefsReleased drops the actor's SharedVolume references +// and deletes any volume that reached its LAST_ACTOR trigger. +func (w *ActorWorkflow) ensureSharedVolumeRefsReleased(ctx context.Context, actor *ateapipb.Actor) (err error) { + ctx, done := stepSpan(ctx, "ReleaseSharedVolumeRefs") + defer func() { err = done(err) }() + + return releaseSharedVolumes(ctx, w.store, w.sharedVolumes, actor) +} + // ensureExternalSnapshotsReleased collects everything the actor wrote to // object storage, by deleting its own prefix. It runs before the actor is // removed, so a failed attempt is still rediscoverable on the retry. diff --git a/cmd/ateapi/internal/controlapi/workflow_delete_test.go b/cmd/ateapi/internal/controlapi/workflow_delete_test.go index 7a90002b3f..36b3efd477 100644 --- a/cmd/ateapi/internal/controlapi/workflow_delete_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_delete_test.go @@ -333,7 +333,7 @@ func TestDeleteActor_CollectsInFlightSnapshotWithoutTemplate(t *testing.T) { ctx := context.Background() persistence := newTestPersistence(t) objects := objectstoretest.New() - w := NewActorWorkflow(persistence, nil, nil, nil, nil, nil, "", nil, objectstoreplugintest.ControlClient(objects)) + w := NewActorWorkflow(persistence, nil, nil, nil, nil, nil, "", nil, objectstoreplugintest.ControlClient(objects), nil) actorRef := resources.ActorRef{Atespace: "team-a", Name: "actor-1"} actor := storetest.MustCreateActor(t, ctx, persistence, &ateapipb.Actor{ diff --git a/cmd/ateapi/internal/controlapi/workflow_resume.go b/cmd/ateapi/internal/controlapi/workflow_resume.go index cb82726ec9..dbe7ceda35 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume.go @@ -126,6 +126,26 @@ func (w *ActorWorkflow) ResumeActor(ctx context.Context, actorRef resources.Acto return nil, false, err } actor = assigned + if len(borrowedVolumeRefs(actor)) > 0 { + mounts, claimErr := claimSharedVolumes(leaseCtx, w.store, actor) + if claimErr != nil { + return nil, false, claimErr + } + + // After claiming, never release ownership until node access is gone. + defer func() { + if err == nil { + return + } + err = errors.Join(err, w.releaseFailedResumeClaims(leaseCtx, actorRef, actor, actorTemplate)) + }() + // Attachment uses the current driver context, not metadata frozen at create. + updated, updateErr := w.store.UpdateActor(leaseCtx, actorRef, store.PreconditionFrom(actor), func(a *ateapipb.Actor) error { a.Status.ExternalVolumes = mounts; return nil }) + if updateErr != nil { + return nil, false, updateErr + } + actor = updated + } volumePublishContexts, err := w.ensureVolumesAttached(leaseCtx, actor, worker, actorTemplate) if err != nil { return nil, false, err @@ -205,7 +225,7 @@ func (w *ActorWorkflow) ensureVolumesCreated(ctx context.Context, actorRef resou ctx, done := stepSpan(ctx, "CreateVolumes") defer func() { err = done(err) }() - pending := slices.ContainsFunc(actor.GetStatus().GetExternalVolumes(), func(vol *ateapipb.ExternalVolume) bool { + pending := slices.ContainsFunc(actor.GetStatus().GetExternalVolumes(), func(vol *ateapipb.ExternalVolumeRef) bool { return vol.GetStorageVolumeId() == "" }) if !pending { @@ -216,7 +236,8 @@ func (w *ActorWorkflow) ensureVolumesCreated(ctx context.Context, actorRef resou return nil, apierror.FailedPrecondition("cannot create volumes for Actor %s in state %s", actorRef, st) } - volumes, createErr := createActorVolumes(ctx, w.pluginRegistry, w.storageClassLister, actor.GetMetadata().GetUid(), actorTemplate, actor.GetStatus().GetExternalVolumes()) + volumes := actor.GetStatus().GetExternalVolumes() + volumes, createErr := createActorVolumes(ctx, w.pluginRegistry, w.storageClassLister, actor.GetMetadata().GetUid(), actorTemplate, volumes) // createActorVolumes reports the state it got to even when it fails, so both // paths persist the same field. updatePrecondition := store.PreconditionFrom(actor) diff --git a/cmd/ateapi/internal/controlapi/workflow_resume_test.go b/cmd/ateapi/internal/controlapi/workflow_resume_test.go index c089f36a12..52ff5d1cf2 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume_test.go @@ -1728,7 +1728,7 @@ func TestEnsureVolumesAttached_ReturnsPublishContext(t *testing.T) { Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "id1"}, Status: &ateapipb.ActorStatus{ State: ateapipb.ActorState_ACTOR_STATE_RESUMING, - ExternalVolumes: []*ateapipb.ExternalVolume{ + ExternalVolumes: []*ateapipb.ExternalVolumeRef{ {Name: "mounted", StorageVolumeId: "storage-mounted", VolumeType: "mock"}, {Name: "unmounted", StorageVolumeId: "storage-unmounted", VolumeType: "mock"}, }, @@ -1748,8 +1748,8 @@ func TestEnsureVolumesAttached_ReturnsPublishContext(t *testing.T) { worker := &ateapipb.Worker{NodeName: "node-1"} tmpl := &ateapipb.ActorTemplate{ Volumes: []*ateapipb.Volume{ - {Name: "mounted", ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{StorageClassName: "sc"}}, - {Name: "unmounted", ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{StorageClassName: "sc"}}, + {Name: "mounted", External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{StorageClassName: "sc"}}}, + {Name: "unmounted", External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{StorageClassName: "sc"}}}, }, Containers: []*ateapipb.Container{ {Name: "main", Image: "img", VolumeMounts: []*ateapipb.VolumeMount{{Name: "mounted", MountPath: "/data"}}}, @@ -1789,8 +1789,8 @@ func (p *createCountingVolumePlugin) CreateVolume(ctx context.Context, req volum func TestEnsureVolumesCreated(t *testing.T) { tmpl := &ateapipb.ActorTemplate{ Volumes: []*ateapipb.Volume{{ - Name: "data", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{StorageClassName: "sc"}, + Name: "data", + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{StorageClassName: "sc"}}, }}, } tests := []struct { @@ -1811,7 +1811,7 @@ func TestEnsureVolumesCreated(t *testing.T) { Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "id1"}, Status: &ateapipb.ActorStatus{ State: tt.state, - ExternalVolumes: []*ateapipb.ExternalVolume{{Name: "data", VolumeType: "mock"}}, + ExternalVolumes: []*ateapipb.ExternalVolumeRef{{Name: "data", VolumeType: "mock"}}, }, }) actor, err := persistence.GetActor(ctx, actorRef) diff --git a/cmd/ateapi/internal/controlapi/workflow_shared_volume.go b/cmd/ateapi/internal/controlapi/workflow_shared_volume.go new file mode 100644 index 0000000000..2de93b2404 --- /dev/null +++ b/cmd/ateapi/internal/controlapi/workflow_shared_volume.go @@ -0,0 +1,304 @@ +// 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" + "errors" + "fmt" + "log/slog" + + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" + "github.com/agent-substrate/substrate/internal/apierror" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/internal/volume" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "google.golang.org/grpc/codes" + storagev1 "k8s.io/api/storage/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + storagev1listers "k8s.io/client-go/listers/storage/v1" +) + +// SharedVolumeWorkflow handles the multi-step create and delete of an +// SharedVolume, each of which spans a store write and a CSI call. +type SharedVolumeWorkflow struct { + store sharedVolumeWorkflowStore + storageClassLister storagev1listers.StorageClassLister + pluginRegistry VolumePluginRegistry +} + +// NewSharedVolumeWorkflow creates a new SharedVolumeWorkflow. +func NewSharedVolumeWorkflow( + store sharedVolumeWorkflowStore, + storageClassLister storagev1listers.StorageClassLister, + pluginRegistry VolumePluginRegistry, +) *SharedVolumeWorkflow { + return &SharedVolumeWorkflow{ + store: store, + storageClassLister: storageClassLister, + pluginRegistry: pluginRegistry, + } +} + +type sharedVolumeWorkflowStore interface { + CreateSharedVolume(ctx context.Context, volume *ateapipb.SharedVolume) (*ateapipb.SharedVolume, error) + GetSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef) (*ateapipb.SharedVolume, error) + UpdateSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef, precondition store.Precondition, mutate func(toUpdate *ateapipb.SharedVolume) error) (*ateapipb.SharedVolume, error) + DeleteSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef, precondition store.DeletePreconditions) (*ateapipb.SharedVolume, error) + AcquireLease(ctx context.Context, key string) (*store.Lease, error) +} + +// CreateSharedVolume reserves the name, provisions the volume under an ID +// derived from the record's UID, and stamps the handle the driver returned. +// A create that dies leaves a PENDING record; delete it to retry. +func (w *SharedVolumeWorkflow) CreateSharedVolume(ctx context.Context, volume *ateapipb.SharedVolume) (*ateapipb.SharedVolume, error) { + volumeRef := resources.SharedVolumeRefFromSharedVolume(volume) + + ctx, lease, err := acquireSharedVolumeLease(ctx, w.store, volumeRef) + if err != nil { + return nil, err + } + defer lease.Close() + + toCreate, provisioning, err := w.resolveExternalVolumeSource(ctx, volume) + if err != nil { + return nil, err + } + reserved, err := w.ensureSharedVolumeReserved(ctx, volumeRef, toCreate) + if err != nil { + return nil, err + } + if provisioning == nil { + return reserved, nil + } + volumeID, volumeContext, err := w.ensureSharedVolumeProvisioned(ctx, reserved, provisioning) + if err != nil { + return nil, err + } + return w.ensureSharedVolumeFinalized(ctx, reserved, volumeID, volumeContext) +} + +type sharedVolumeProvisioning struct { + provisioner string + parameters map[string]string +} + +// resolveExternalVolumeSource returns the record to reserve and, unless the +// request registers an existing volume, what provisioning it needs. +func (w *SharedVolumeWorkflow) resolveExternalVolumeSource(ctx context.Context, volume *ateapipb.SharedVolume) (_ *ateapipb.SharedVolume, _ *sharedVolumeProvisioning, err error) { + _, done := stepSpan(ctx, "ResolveExternalVolumeSource") + defer func() { err = done(err) }() + + toCreate := &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{ + Atespace: volume.GetMetadata().GetAtespace(), + Name: volume.GetMetadata().GetName(), + }, + DeleteTrigger: volume.GetDeleteTrigger(), + AccessMode: volume.GetAccessMode(), + StorageClassName: volume.GetStorageClassName(), + VolumeId: volume.GetVolumeId(), + VolumeType: volume.GetVolumeType(), + VolumeContext: volume.GetVolumeContext(), + Capacity: volume.GetCapacity(), + Status: &ateapipb.SharedVolumeStatus{State: ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_PENDING}, + } + + var sc *storagev1.StorageClass + if scName := volume.GetStorageClassName(); scName != "" { + var err error + if sc, err = w.storageClassLister.Get(scName); err != nil { + if k8serrors.IsNotFound(err) { + return nil, nil, apierror.FailedPrecondition("StorageClass %q not found", scName) + } + return nil, nil, apierror.Internal("failed to get StorageClass %q: %v", scName, err) + } + } + + if toCreate.VolumeId != "" { + if toCreate.DeleteTrigger == ateapipb.DeleteTrigger_DELETE_TRIGGER_UNSPECIFIED { + toCreate.DeleteTrigger = ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL + } + if toCreate.VolumeType == "" && sc != nil { + toCreate.VolumeType = sc.Provisioner + } + toCreate.Status.State = ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY + return toCreate, nil, nil + } + if sc == nil { + return nil, nil, apierror.InvalidArgument("one of storage_class_name or volume_id is required") + } + if toCreate.DeleteTrigger == ateapipb.DeleteTrigger_DELETE_TRIGGER_UNSPECIFIED { + toCreate.DeleteTrigger = ateapipb.DeleteTrigger_DELETE_TRIGGER_LAST_ACTOR + } + toCreate.VolumeType = sc.Provisioner + return toCreate, &sharedVolumeProvisioning{provisioner: sc.Provisioner, parameters: sc.Parameters}, nil +} + +func (w *SharedVolumeWorkflow) ensureSharedVolumeReserved(ctx context.Context, volumeRef resources.SharedVolumeRef, toCreate *ateapipb.SharedVolume) (_ *ateapipb.SharedVolume, err error) { + ctx, done := stepSpan(ctx, "ReserveSharedVolume") + defer func() { err = done(err) }() + + stored, err := w.store.CreateSharedVolume(ctx, toCreate) + switch { + case err == nil: + return stored, nil + case errors.Is(err, store.ErrFailedPrecondition): + return nil, apierror.FailedPrecondition("Atespace %s not found", volumeRef.Atespace) + case errors.Is(err, store.ErrAlreadyExists): + return nil, apierror.AlreadyExists("SharedVolume %s already exists; delete it and create it again to retry", volumeRef) + } + return nil, fmt.Errorf("while reserving shared volume %s: %w", volumeRef, err) +} + +// TODO: pass access_mode to CreateVolume as the volume capability once +// READ_WRITE_MANY is accepted. +func (w *SharedVolumeWorkflow) ensureSharedVolumeProvisioned(ctx context.Context, volume *ateapipb.SharedVolume, provisioning *sharedVolumeProvisioning) (_ string, _ map[string]string, err error) { + ctx, done := stepSpan(ctx, "ProvisionSharedVolume") + defer func() { err = done(err) }() + + volumeRef := resources.SharedVolumeRefFromSharedVolume(volume) + plugin, err := w.pluginRegistry.GetPlugin(ctx, provisioning.provisioner) + if err != nil { + return "", nil, apierror.FailedPrecondition("failed to get volume plugin for driver %q (StorageClass %q): %v", provisioning.provisioner, volume.GetStorageClassName(), err) + } + resp, err := plugin.CreateVolume(ctx, createVolumeRequest(sharedVolumeID(volume.GetMetadata().GetUid()), volume.GetCapacity(), provisioning.parameters)) + if err != nil { + return "", nil, apierror.Internal("failed to create shared volume %s: %v", volumeRef, err) + } + return resp.VolumeID, resp.VolumeContext, nil +} + +// createVolumeRequest keeps the request construction out of callers whose +// parameters shadow the volume package. +func createVolumeRequest(name, capacity string, parameters map[string]string) volume.CreateVolumeRequest { + return volume.CreateVolumeRequest{Name: name, Capacity: capacity, Parameters: parameters} +} + +func (w *SharedVolumeWorkflow) ensureSharedVolumeFinalized(ctx context.Context, volume *ateapipb.SharedVolume, volumeID string, volumeContext map[string]string) (_ *ateapipb.SharedVolume, err error) { + ctx, done := stepSpan(ctx, "FinalizeSharedVolume") + defer func() { err = done(err) }() + + volumeRef := resources.SharedVolumeRefFromSharedVolume(volume) + stored, err := w.store.UpdateSharedVolume(ctx, volumeRef, store.PreconditionFrom(volume), func(toUpdate *ateapipb.SharedVolume) error { + toUpdate.VolumeId = volumeID + toUpdate.VolumeContext = volumeContext + toUpdate.Status.State = ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY + return nil + }) + if err != nil { + if errors.Is(err, store.ErrVersionConflict) { + return nil, apierror.Aborted("concurrent update conflict, please retry") + } + if errors.Is(err, store.ErrNotFound) || errors.Is(err, store.ErrUIDConflict) { + return nil, apierror.Aborted("SharedVolume %s was deleted while it was being created, please retry", volumeRef) + } + return nil, fmt.Errorf("while finalizing shared volume %s: %w", volumeRef, err) + } + return stored, nil +} + +// DeleteSharedVolume deletes the storage and then removes the record, so a +// failure part way is retried by deleting again. Rejected while any actor +// references the volume. +func (w *SharedVolumeWorkflow) DeleteSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef, precondition store.DeletePreconditions) (*ateapipb.SharedVolume, error) { + ctx, lease, err := acquireSharedVolumeLease(ctx, w.store, volumeRef) + if err != nil { + return nil, err + } + defer lease.Close() + + stored, err := w.store.GetSharedVolume(ctx, volumeRef) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + return nil, apierror.NotFound("SharedVolume %s not found", volumeRef) + } + return nil, fmt.Errorf("while getting shared volume: %w", err) + } + if err := precondition.Check(stored.GetMetadata()); err != nil { + return nil, sharedVolumeDeleteConflict(err, volumeRef, precondition) + } + if refs := stored.GetStatus().GetRefs(); len(refs) > 0 { + return nil, apierror.FailedPrecondition("SharedVolume %s is still referenced by %d actor(s), starting with %q", volumeRef, len(refs), refs[0].GetActorName()) + } + // Fence every new reference before calling CSI. A version conflict means + // a reference or policy changed since the check; no storage was touched. + if stored.GetStatus().GetState() != ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_DELETING { + stored, err = w.store.UpdateSharedVolume(ctx, volumeRef, store.PreconditionFrom(stored), func(v *ateapipb.SharedVolume) error { + v.Status.State = ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_DELETING + return nil + }) + if err != nil { + if errors.Is(err, store.ErrVersionConflict) || errors.Is(err, store.ErrUIDConflict) { + return nil, apierror.Aborted("SharedVolume %s changed before deletion; retry", volumeRef) + } + return nil, fmt.Errorf("while marking shared volume deleting: %w", err) + } + } + if err := w.reclaimSharedVolume(ctx, stored); err != nil { + return nil, err + } + + volume, err := w.store.DeleteSharedVolume(ctx, volumeRef, store.DeletePreconditions{UID: stored.GetMetadata().GetUid(), Version: stored.GetMetadata().GetVersion()}) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + return nil, apierror.NotFound("SharedVolume %s not found", volumeRef) + } + if errors.Is(err, store.ErrUIDConflict) || errors.Is(err, store.ErrVersionConflict) { + return nil, sharedVolumeDeleteConflict(err, volumeRef, precondition) + } + return nil, fmt.Errorf("while deleting shared volume: %w", err) + } + return volume, nil +} + +func sharedVolumeDeleteConflict(err error, volumeRef resources.SharedVolumeRef, precondition store.DeletePreconditions) error { + if errors.Is(err, store.ErrUIDConflict) { + return apierror.Aborted("SharedVolume %s does not have uid %s", volumeRef, precondition.UID) + } + return apierror.Aborted("concurrent update conflict, please retry") +} + +// TODO: add a reclaim policy that leaves the storage in place. +func (w *SharedVolumeWorkflow) reclaimSharedVolume(ctx context.Context, volume *ateapipb.SharedVolume) (err error) { + ctx, done := stepSpan(ctx, "ReclaimSharedVolume") + defer func() { err = done(err) }() + + volumeRef := resources.SharedVolumeRefFromSharedVolume(volume) + if volume.GetVolumeType() == "" { + markSkipped(ctx, "volume names no driver") + return nil + } + volumeID := volume.GetVolumeId() + if volumeID == "" { + volumeID = sharedVolumeID(volume.GetMetadata().GetUid()) + } + plugin, err := w.pluginRegistry.GetPlugin(ctx, volume.GetVolumeType()) + if err != nil { + return fmt.Errorf("while getting the volume plugin for %q of shared volume %s: %w", volume.GetVolumeType(), volumeRef, err) + } + if err := plugin.DeleteVolume(ctx, volumeID); err != nil { + if apierror.Code(err) == codes.NotFound { + slog.WarnContext(ctx, "Volume not found during delete, assuming already deleted", slog.String("volume_id", volumeID)) + return nil + } + return fmt.Errorf("while reclaiming the storage %q of shared volume %s: %w", volumeID, volumeRef, err) + } + return nil +} + +func sharedVolumeID(volumeUID string) string { + return "substrate-" + volumeUID +} diff --git a/cmd/ateapi/internal/controlapi/workflow_suspend.go b/cmd/ateapi/internal/controlapi/workflow_suspend.go index b2655daa16..7d3a605f19 100644 --- a/cmd/ateapi/internal/controlapi/workflow_suspend.go +++ b/cmd/ateapi/internal/controlapi/workflow_suspend.go @@ -361,21 +361,18 @@ func newInProgressSnapshotURI(actorTemplate *ateapipb.ActorTemplate, actor *atea return uri, nil } -// ensureVolumesDetached detaches the actor's mounted external volumes from -// its worker node. Detachment is idempotent, so a re-entered workflow safely -// runs it again. spanName distinguishes the suspend and pause steps in -// traces; op labels the volume metrics. +// ensureVolumesDetached detaches the actor's mounted external volumes from its +// worker node and marks its SharedVolume references inactive. Both steps are +// idempotent; spanName distinguishes suspend from pause and op labels metrics. // TODO replace re-execution with a proper check on the volumes' attach state. func (w *ActorWorkflow) ensureVolumesDetached(ctx context.Context, actor *ateapipb.Actor, actorTemplate *ateapipb.ActorTemplate, spanName, op string) (err error) { ctx, done := stepSpan(ctx, spanName) defer func() { err = done(err) }() - // Given that external volumes has been in the codebase for a while and - // its preview-ness is solely around the API, and that the impact of - // disabling the gate while in use would be an orphaned volume - // attachment, we are NOT checking the ExternalVolumes gate in the - // delete/suspend path. - return detachActorVolumes(ctx, w.pluginRegistry, actor, actorTemplate, op) + if err := detachActorVolumes(ctx, w.pluginRegistry, actor, actorTemplate, op); err != nil { + return err + } + return deactivateSharedVolumes(ctx, w.store, actor) } // ensureSuspendedFinalized releases the actor's worker (only when it is still diff --git a/cmd/ateapi/internal/controlapi/workflow_testutil_test.go b/cmd/ateapi/internal/controlapi/workflow_testutil_test.go index 3468e5066e..faf51b66fe 100644 --- a/cmd/ateapi/internal/controlapi/workflow_testutil_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_testutil_test.go @@ -60,7 +60,7 @@ func newTestActorWorkflow(t *testing.T, st store.Interface, tmplAtespace, tmplNa }); err != nil && !errors.Is(err, store.ErrAlreadyExists) { t.Fatalf("create test ActorTemplate: %v", err) } - return NewActorWorkflow(st, nil, nil, nil, nil, nil, "", nil, objectstoreplugintest.ControlClient(objectstoretest.New())) + return NewActorWorkflow(st, nil, nil, nil, nil, nil, "", nil, objectstoreplugintest.ControlClient(objectstoretest.New()), nil) } // newFinalizeWorkflow builds an ActorWorkflow over persistence with an diff --git a/cmd/ateapi/internal/controlapi/workload_spec.go b/cmd/ateapi/internal/controlapi/workload_spec.go index bed3c3f602..49dd7d1662 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec.go +++ b/cmd/ateapi/internal/controlapi/workload_spec.go @@ -49,12 +49,20 @@ func toAteletResources(r *ateapipb.Resources) (*ateletpb.ResourceLimits, error) // workloadSpecFromActorTemplate builds a WorkloadSpec from the template; // container env is copied verbatim. func workloadSpecFromActorTemplate(actorTemplate *ateapipb.ActorTemplate, actor *ateapipb.Actor, volumePublishContexts map[string]map[string]string) (*ateletpb.WorkloadSpec, error) { - workloadSpec := &ateletpb.WorkloadSpec{} + workloadSpec := &ateletpb.WorkloadSpec{ + RuntimeGolden: isGoldenActor(actor) && actorTemplate.GetSnapshotConfig().GetGoldenSnapshotConfig().GetMode() == ateapipb.GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_RUNTIME, + } // Convert volumes to atelet's representation. ActorTemplate validation has // already ensured that only one source is set. for _, vol := range actorTemplate.GetVolumes() { switch { + case vol.GetExternal().GetSharedVolume() != nil && isGoldenActor(actor): + workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ + Name: vol.GetName(), + EmptyDir: &ateletpb.EmptyDirVolume{}, + }) + case vol.GetDurableDir() != nil: workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ Name: vol.GetName(), @@ -102,8 +110,11 @@ func workloadSpecFromActorTemplate(actorTemplate *ateapipb.ActorTemplate, actor }, }) + case vol.GetExternal().GetTemplate() != nil, vol.GetExternal().GetSharedVolume() != nil: + continue // appended by appendExternalVolumes from the actor's status + default: - continue // Drop unrecognized volumes. + return nil, fmt.Errorf("volume %q has no supported source", vol.GetName()) } } @@ -147,21 +158,24 @@ func workloadSpecFromActorTemplate(actorTemplate *ateapipb.ActorTemplate, actor return workloadSpec, nil } -// appendExternalVolumes maps template external volumes to resolved actor volumes -// and appends them to workloadSpec if they are referenced in container -// volumeMounts. volumePublishContexts carries the attachment metadata of the current -// resume by volume name; it is nil on paths that do not mount. +// appendExternalVolumes maps mounted external volumes, owned or referenced, to +// the handles recorded on the actor and appends them to workloadSpec. +// volumePublishContexts carries the attachment metadata of the current resume +// by volume name; it is nil on paths that do not mount. func appendExternalVolumes(workloadSpec *ateletpb.WorkloadSpec, template *ateapipb.ActorTemplate, actor *ateapipb.Actor, volumePublishContexts map[string]map[string]string) error { if template == nil { return nil } for _, vol := range template.GetVolumes() { - if vol.GetExternalVolumeTemplate() != nil { + if vol.GetExternal().GetTemplate() != nil || vol.GetExternal().GetSharedVolume() != nil { if !isVolumeMounted(vol.GetName(), template) { continue } + if vol.GetExternal().GetSharedVolume() != nil && isGoldenActor(actor) { + continue + } if actor == nil { - return fmt.Errorf("actor is required when externalVolumeTemplate is present") + return fmt.Errorf("actor is required when an external volume is present") } var storageVolID string @@ -180,7 +194,7 @@ func appendExternalVolumes(workloadSpec *ateletpb.WorkloadSpec, template *ateapi } workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ Name: vol.GetName(), - External: &ateletpb.ExternalVolumeSource{ + External: &ateletpb.ExternalVolume{ StorageVolumeId: storageVolID, VolumeType: volType, VolumeContext: volCtx, diff --git a/cmd/ateapi/internal/controlapi/workload_spec_test.go b/cmd/ateapi/internal/controlapi/workload_spec_test.go index a1b640ef0f..6fff241137 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec_test.go +++ b/cmd/ateapi/internal/controlapi/workload_spec_test.go @@ -15,9 +15,11 @@ package controlapi import ( + "strings" "testing" "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" "google.golang.org/protobuf/testing/protocmp" @@ -215,11 +217,12 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { }, }, { - name: "skips non-DurableDir volumes", + name: "leaves unmounted external volumes out", template: &ateapipb.ActorTemplate{ Metadata: &ateapipb.ResourceMetadata{Atespace: "agent-ns", Name: "tmpl1"}, Volumes: []*ateapipb.Volume{ - {Name: "unsupported"}, + {Name: "borrowed", External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{Name: "shared"}}}, + {Name: "owned", External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{StorageClassName: "standard"}}}, {Name: "home", DurableDir: &ateapipb.DurableDirVolumeSource{}}, }, Containers: []*ateapipb.Container{ @@ -390,9 +393,9 @@ func TestAppendExternalVolumes(t *testing.T) { Volumes: []*ateapipb.Volume{ { Name: "vol-1", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "pd-standard", - }, + }}, }, { Name: "vol-2", @@ -400,9 +403,9 @@ func TestAppendExternalVolumes(t *testing.T) { }, { Name: "unmounted-vol", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ StorageClassName: "pd-standard", - }, + }}, }, }, } @@ -413,7 +416,7 @@ func TestAppendExternalVolumes(t *testing.T) { Name: "actor-123", }, Status: &ateapipb.ActorStatus{ - ExternalVolumes: []*ateapipb.ExternalVolume{ + ExternalVolumes: []*ateapipb.ExternalVolumeRef{ { Name: "vol-1", StorageVolumeId: "vol-gce-pd-123", @@ -448,7 +451,7 @@ func TestAppendExternalVolumes(t *testing.T) { Volumes: []*ateletpb.Volume{ { Name: "vol-1", - External: &ateletpb.ExternalVolumeSource{ + External: &ateletpb.ExternalVolume{ StorageVolumeId: "vol-gce-pd-123", VolumeType: "pd-standard", VolumeContext: map[string]string{"foo": "bar"}, @@ -470,7 +473,7 @@ func TestAppendExternalVolumes(t *testing.T) { Atespace: "space-abc", Name: "actor-123", }, - Status: &ateapipb.ActorStatus{ExternalVolumes: []*ateapipb.ExternalVolume{}}, + Status: &ateapipb.ActorStatus{ExternalVolumes: []*ateapipb.ExternalVolumeRef{}}, } if err := appendExternalVolumes(&ateletpb.WorkloadSpec{}, template, missingActor, nil); err == nil { t.Errorf("appendExternalVolumes expected error for missing volume, got nil") @@ -642,3 +645,109 @@ func TestWorkloadSpecFromActorTemplatePropagatesResources(t *testing.T) { t.Errorf("unlimited container Resources = %v, want nil", r) } } + +func TestWorkloadSpecFromActorTemplateRejectsUnknownVolumeSource(t *testing.T) { + _, err := workloadSpecFromActorTemplate(&ateapipb.ActorTemplate{ + Metadata: &ateapipb.ResourceMetadata{Atespace: "agent-ns", Name: "tmpl-unknown"}, + Volumes: []*ateapipb.Volume{{Name: "unsupported"}}, + }, nil, nil) + if err == nil || !strings.Contains(err.Error(), "unsupported") { + t.Fatalf("workloadSpecFromActorTemplate() error = %v, want one naming the volume", err) + } +} + +func TestWorkloadSpecBorrowedVolume(t *testing.T) { + template := &ateapipb.ActorTemplate{ + Metadata: &ateapipb.ResourceMetadata{Atespace: "space-abc", Name: "tmpl1"}, + Volumes: []*ateapipb.Volume{ + {Name: "borrowed", External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{}}}, + {Name: "owned", External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{StorageClassName: "standard"}}}, + }, + Containers: []*ateapipb.Container{{ + Name: "main", + VolumeMounts: []*ateapipb.VolumeMount{ + {Name: "borrowed", MountPath: "/mnt/shared"}, + {Name: "owned", MountPath: "/mnt/owned"}, + }, + }}, + } + owned := &ateletpb.Volume{ + Name: "owned", + External: &ateletpb.ExternalVolume{ + StorageVolumeId: "disk-1", + VolumeType: "pd.csi.storage.gke.io", + }, + } + + tests := []struct { + name string + actor *ateapipb.Actor + want []*ateletpb.Volume + }{ + { + name: "golden actor gets an empty dir in place of the shared volume", + actor: &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: resources.GoldenActorAtespace, Name: "tmpl1-uid"}, + Status: &ateapipb.ActorStatus{ExternalVolumes: []*ateapipb.ExternalVolumeRef{ + {Name: "owned", StorageVolumeId: "disk-1", VolumeType: "pd.csi.storage.gke.io"}, + }}, + }, + want: []*ateletpb.Volume{ + {Name: "borrowed", EmptyDir: &ateletpb.EmptyDirVolume{}}, + owned, + }, + }, + { + name: "user actor mounts the shared volume's handle", + actor: &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: "space-abc", Name: "consumer"}, + SharedVolumeBindings: map[string]string{"borrowed": "shared"}, + Status: &ateapipb.ActorStatus{ExternalVolumes: []*ateapipb.ExternalVolumeRef{ + {Name: "borrowed", SharedVolumeName: "shared", StorageVolumeId: "disk-shared", VolumeType: "nfs.csi.k8s.io", VolumeContext: map[string]string{"server": "10.0.0.2"}}, + {Name: "owned", StorageVolumeId: "disk-1", VolumeType: "pd.csi.storage.gke.io"}, + }}, + }, + want: []*ateletpb.Volume{ + { + Name: "borrowed", + External: &ateletpb.ExternalVolume{ + StorageVolumeId: "disk-shared", + VolumeType: "nfs.csi.k8s.io", + VolumeContext: map[string]string{"server": "10.0.0.2"}, + }, + }, + owned, + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := workloadSpecFromActorTemplate(template, tc.actor, nil) + if err != nil { + t.Fatalf("workloadSpecFromActorTemplate: %v", err) + } + if diff := cmp.Diff(tc.want, got.GetVolumes(), protocmp.Transform()); diff != "" { + t.Errorf("volumes mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestRuntimeGoldenWorkloadBoundary(t *testing.T) { + for _, golden := range []bool{false, true} { + for _, mode := range []ateapipb.GoldenSnapshotMode{ateapipb.GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_ENABLED, ateapipb.GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_RUNTIME} { + a := &ateapipb.Actor{Metadata: &ateapipb.ResourceMetadata{Atespace: "users"}} + if golden { + a.Metadata.Atespace = resources.GoldenActorAtespace + } + tmpl := &ateapipb.ActorTemplate{SnapshotConfig: &ateapipb.SnapshotConfig{GoldenSnapshotConfig: &ateapipb.GoldenSnapshotConfig{Mode: mode}}} + spec, err := workloadSpecFromActorTemplate(tmpl, a, nil) + if err != nil { + t.Fatal(err) + } + if spec.GetRuntimeGolden() != (golden && mode == ateapipb.GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_RUNTIME) { + t.Fatalf("incorrect boundary: golden=%v mode=%v spec=%v", golden, mode, spec) + } + } + } +} diff --git a/cmd/ateapi/internal/defaults/defaults.go b/cmd/ateapi/internal/defaults/defaults.go index ff31f792d4..ca36e794be 100644 --- a/cmd/ateapi/internal/defaults/defaults.go +++ b/cmd/ateapi/internal/defaults/defaults.go @@ -38,6 +38,8 @@ func Apply(m proto.Message) { applyAtespaceDefaults(v) case *ateapipb.EgressPolicy: applyEgressPolicyDefaults(v) + case *ateapipb.SharedVolume: + applySharedVolumeDefaults(v) case *ateapipb.Tag: applyTagDefaults(v) case *ateapipb.Worker: @@ -111,6 +113,15 @@ func applyEgressPolicyDefaults(p *ateapipb.EgressPolicy) { } } +func applySharedVolumeDefaults(v *ateapipb.SharedVolume) { + if v == nil { + return + } + if v.AccessMode == ateapipb.AccessMode_ACCESS_MODE_UNSPECIFIED { + v.AccessMode = ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_ONCE + } +} + func applyTagDefaults(*ateapipb.Tag) {} func applyWorkerDefaults(*ateapipb.Worker) {} diff --git a/cmd/ateapi/internal/defaults/defaults_test.go b/cmd/ateapi/internal/defaults/defaults_test.go index 5ccb5ba159..1d5e8f77b0 100644 --- a/cmd/ateapi/internal/defaults/defaults_test.go +++ b/cmd/ateapi/internal/defaults/defaults_test.go @@ -57,6 +57,14 @@ func TestApply(t *testing.T) { PreferredFidelity: scopeData, GoldenSnapshotConfig: &ateapipb.GoldenSnapshotConfig{Mode: ateapipb.GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_ENABLED}, }}, + }, { + name: "external volume without an access mode gets read write once", + in: &ateapipb.SharedVolume{}, + want: &ateapipb.SharedVolume{AccessMode: ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_ONCE}, + }, { + name: "external volume keeps its access mode", + in: &ateapipb.SharedVolume{AccessMode: ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_MANY}, + want: &ateapipb.SharedVolume{AccessMode: ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_MANY}, }, { name: "container without wakeup probe stays without one", in: &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{{Name: "main"}}}, diff --git a/cmd/ateapi/internal/store/atepg/actor.go b/cmd/ateapi/internal/store/atepg/actor.go index 538d263511..d51b0da7fa 100644 --- a/cmd/ateapi/internal/store/atepg/actor.go +++ b/cmd/ateapi/internal/store/atepg/actor.go @@ -42,7 +42,15 @@ func (p *Persistence) CreateActor(ctx context.Context, actor *ateapipb.Actor) (* return nil, fmt.Errorf("marshaling actor: %w", err) } - _, err = p.pool.Exec(ctx, ` + tx, err := p.pool.Begin(ctx) + if err != nil { + return nil, err + } + defer tx.Rollback(ctx) //nolint:errcheck // Commit or rollback closes the transaction. + if err := bindActorSharedVolumes(ctx, tx, dbActor); err != nil { + return nil, err + } + _, err = tx.Exec(ctx, ` INSERT INTO actors (atespace, name, uid, version, proto) VALUES ($1, $2, $3, $4, $5)`, atespace, name, dbActor.GetMetadata().GetUid(), dbActor.GetMetadata().GetVersion(), protoBytes) @@ -57,6 +65,9 @@ func (p *Persistence) CreateActor(ctx context.Context, actor *ateapipb.Actor) (* } return nil, fmt.Errorf("inserting actor %s/%s: %w", atespace, name, err) } + if err := tx.Commit(ctx); err != nil { + return nil, err + } return dbActor, nil } diff --git a/cmd/ateapi/internal/store/atepg/actor_shared_volumes.go b/cmd/ateapi/internal/store/atepg/actor_shared_volumes.go new file mode 100644 index 0000000000..0e51e44581 --- /dev/null +++ b/cmd/ateapi/internal/store/atepg/actor_shared_volumes.go @@ -0,0 +1,73 @@ +// 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 atepg + +import ( + "context" + "errors" + "fmt" + "slices" + + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "github.com/jackc/pgx/v5" + "google.golang.org/protobuf/proto" +) + +// bindActorSharedVolumes commits references with the actor itself. Row locks +// serialize against deletion fencing and other creates; sorted names prevent +// two multi-volume creates from taking locks in opposite orders. +func bindActorSharedVolumes(ctx context.Context, tx pgx.Tx, actor *ateapipb.Actor) error { + mounts := make(map[string]*ateapipb.ExternalVolumeRef) + for _, mount := range actor.GetStatus().GetExternalVolumes() { + if mount.GetSharedVolumeName() != "" { + mounts[mount.GetSharedVolumeName()] = mount + } + } + names := make([]string, 0, len(mounts)) + for name := range mounts { + names = append(names, name) + } + slices.Sort(names) + for _, name := range names { + var data []byte + err := tx.QueryRow(ctx, `SELECT proto FROM shared_volumes WHERE atespace = $1 AND name = $2 FOR UPDATE`, actor.GetMetadata().GetAtespace(), name).Scan(&data) + if errors.Is(err, pgx.ErrNoRows) { + return store.ErrFailedPrecondition + } + if err != nil { + return err + } + volume := &ateapipb.SharedVolume{} + if err := unmarshalStored(data, volume); err != nil { + return err + } + mount := mounts[name] + if volume.GetStatus().GetState() != ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY || volume.GetVolumeId() != mount.GetStorageVolumeId() || volume.GetVolumeType() != mount.GetVolumeType() || len(volume.GetStatus().GetRefs()) >= 256 { + return fmt.Errorf("shared volume %s is unavailable or changed: %w", name, store.ErrFailedPrecondition) + } + oldMeta := proto.CloneOf(volume.Metadata) + volume.Status.Refs = append(volume.Status.Refs, &ateapipb.ActorRef{ActorUid: actor.GetMetadata().GetUid(), ActorName: actor.GetMetadata().GetName()}) + setUpdateMetadata(volume.Metadata, oldMeta) + data, err = proto.Marshal(volume) + if err != nil { + return err + } + if _, err := tx.Exec(ctx, `UPDATE shared_volumes SET proto = $1, version = $2 WHERE atespace = $3 AND name = $4`, data, volume.GetMetadata().GetVersion(), actor.GetMetadata().GetAtespace(), name); err != nil { + return err + } + } + return nil +} diff --git a/cmd/ateapi/internal/store/atepg/actor_shared_volumes_test.go b/cmd/ateapi/internal/store/atepg/actor_shared_volumes_test.go new file mode 100644 index 0000000000..2a5dfb8820 --- /dev/null +++ b/cmd/ateapi/internal/store/atepg/actor_shared_volumes_test.go @@ -0,0 +1,97 @@ +// 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 atepg + +import ( + "context" + "errors" + "fmt" + "sync" + "testing" + + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" +) + +func TestCreateActorSharedVolumeTransaction(t *testing.T) { + p := setupPostgresPersistence(t) + ctx := context.Background() + createTestAtespace(t, p, "team") + _, err := p.CreateSharedVolume(ctx, &ateapipb.SharedVolume{Metadata: &ateapipb.ResourceMetadata{Atespace: "team", Name: "data"}, VolumeId: "disk", VolumeType: "mock", Status: &ateapipb.SharedVolumeStatus{State: ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY}}) + if err != nil { + t.Fatal(err) + } + actor := func(name string) *ateapipb.Actor { + return &ateapipb.Actor{Metadata: &ateapipb.ResourceMetadata{Atespace: "team", Name: name}, Status: &ateapipb.ActorStatus{ExternalVolumes: []*ateapipb.ExternalVolumeRef{{Name: "data", SharedVolumeName: "data", StorageVolumeId: "disk", VolumeType: "mock"}}}} + } + const count = 12 + errs := make(chan error, count) + var wg sync.WaitGroup + for i := range count { + wg.Go(func() { _, err := p.CreateActor(ctx, actor(fmt.Sprintf("actor-%d", i))); errs <- err }) + } + wg.Wait() + close(errs) + for err := range errs { + if err != nil { + t.Fatal(err) + } + } + ref := resources.SharedVolumeRef{Atespace: "team", Name: "data"} + v, err := p.GetSharedVolume(ctx, ref) + if err != nil { + t.Fatal(err) + } + if len(v.GetStatus().GetRefs()) != count { + t.Fatalf("got %d references, want %d", len(v.GetStatus().GetRefs()), count) + } + if _, err = p.CreateActor(ctx, actor("actor-0")); !errors.Is(err, store.ErrAlreadyExists) { + t.Fatalf("duplicate: %v", err) + } + v, err = p.GetSharedVolume(ctx, ref) + if err != nil { + t.Fatal(err) + } + if len(v.GetStatus().GetRefs()) != count { + t.Fatal("failed create leaked a reference") + } + // A later volume failure must roll back earlier references as well as the actor. + bad := actor("partial") + bad.Status.ExternalVolumes = append(bad.Status.ExternalVolumes, &ateapipb.ExternalVolumeRef{Name: "missing", SharedVolumeName: "missing", StorageVolumeId: "none"}) + if _, err = p.CreateActor(ctx, bad); !errors.Is(err, store.ErrFailedPrecondition) { + t.Fatalf("partial create: %v", err) + } + if _, err = p.GetActor(ctx, resources.ActorRef{Atespace: "team", Name: "partial"}); !errors.Is(err, store.ErrNotFound) { + t.Fatalf("partial actor remains: %v", err) + } + v, err = p.GetSharedVolume(ctx, ref) + if err != nil { + t.Fatal(err) + } + if len(v.GetStatus().GetRefs()) != count { + t.Fatal("partial create leaked a reference") + } + _, err = p.UpdateSharedVolume(ctx, ref, store.PreconditionFrom(v), func(v *ateapipb.SharedVolume) error { + v.Status.State = ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_DELETING + return nil + }) + if err != nil { + t.Fatal(err) + } + if _, err = p.CreateActor(ctx, actor("late")); !errors.Is(err, store.ErrFailedPrecondition) { + t.Fatalf("deleting volume accepted a new actor: %v", err) + } +} diff --git a/cmd/ateapi/internal/store/atepg/common_test.go b/cmd/ateapi/internal/store/atepg/common_test.go index f85e8741a6..a82c0762ec 100644 --- a/cmd/ateapi/internal/store/atepg/common_test.go +++ b/cmd/ateapi/internal/store/atepg/common_test.go @@ -28,7 +28,7 @@ import ( // state, so the statement lives here rather than on Persistence. func clearAll(t *testing.T, p *Persistence) { t.Helper() - if _, err := p.pool.Exec(context.Background(), `TRUNCATE atespaces, global_access_policy, atespace_access_policies, actors, actor_egress_policies, actor_templates, tags, workers, worker_assignments, leases, worker_outbox, worker_outbox_trim, tuple, changelog`); err != nil { + if _, err := p.pool.Exec(context.Background(), `TRUNCATE atespaces, global_access_policy, atespace_access_policies, actors, actor_egress_policies, actor_templates, tags, shared_volumes, workers, worker_assignments, leases, worker_outbox, worker_outbox_trim, tuple, changelog`); err != nil { t.Fatalf("truncating tables: %v", err) } } diff --git a/cmd/ateapi/internal/store/atepg/migrations/000004_shared_volumes.sql b/cmd/ateapi/internal/store/atepg/migrations/000004_shared_volumes.sql new file mode 100644 index 0000000000..c63c55dd8d --- /dev/null +++ b/cmd/ateapi/internal/store/atepg/migrations/000004_shared_volumes.sql @@ -0,0 +1,27 @@ +-- 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. + +-- +goose Up + +-- RESTRICT so that deleting an atespace cannot strand provisioned storage. +CREATE TABLE shared_volumes ( + atespace text NOT NULL, + name text NOT NULL, + uid text NOT NULL, + version bigint NOT NULL, + proto bytea NOT NULL, + PRIMARY KEY (atespace, name), + CONSTRAINT shared_volumes_atespace_fk + FOREIGN KEY (atespace) REFERENCES atespaces(name) ON DELETE RESTRICT +); diff --git a/cmd/ateapi/internal/store/atepg/pagetoken.go b/cmd/ateapi/internal/store/atepg/pagetoken.go index ebf56b5ae2..a282938339 100644 --- a/cmd/ateapi/internal/store/atepg/pagetoken.go +++ b/cmd/ateapi/internal/store/atepg/pagetoken.go @@ -35,6 +35,7 @@ const ( kindActor resourceKind = "actor" kindActorTemplate resourceKind = "actor-template" kindTag resourceKind = "tag" + kindExternalVol resourceKind = "external-volume" kindWorker resourceKind = "worker" kindWorkerAssign resourceKind = "worker-assignment" ) diff --git a/cmd/ateapi/internal/store/atepg/partition_test.go b/cmd/ateapi/internal/store/atepg/partition_test.go index 20ba51fd29..8c28668414 100644 --- a/cmd/ateapi/internal/store/atepg/partition_test.go +++ b/cmd/ateapi/internal/store/atepg/partition_test.go @@ -45,7 +45,7 @@ func TestActorsTablePartitionable(t *testing.T) { // by definition, under each partition key. spansPartitions := map[string][]string{ // A global list walks every atespace. - "atespace": {globalList("actors"), globalList("actor_templates"), globalList("tags")}, + "atespace": {globalList("actors"), globalList("actor_templates"), globalList("tags"), globalList("shared_volumes")}, // A global list walks every name, and a list within one atespace // spans every name hash. "name": {globalList("actors"), scopedActorList}, diff --git a/cmd/ateapi/internal/store/atepg/shared_volume.go b/cmd/ateapi/internal/store/atepg/shared_volume.go new file mode 100644 index 0000000000..84e77d6640 --- /dev/null +++ b/cmd/ateapi/internal/store/atepg/shared_volume.go @@ -0,0 +1,297 @@ +// 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 atepg + +import ( + "context" + "errors" + "fmt" + "maps" + + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "github.com/jackc/pgx/v5" + "google.golang.org/protobuf/proto" +) + +func (p *Persistence) GetSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef) (*ateapipb.SharedVolume, error) { + atespace, name := volumeRef.Atespace, volumeRef.Name + var protoBytes []byte + if err := p.pool.QueryRow(ctx, ` + SELECT proto FROM shared_volumes + WHERE atespace = $1 AND name = $2`, atespace, name).Scan(&protoBytes); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, fmt.Errorf("getting shared volume %s/%s: %w", atespace, name, err) + } + volume := &ateapipb.SharedVolume{} + if err := unmarshalStored(protoBytes, volume); err != nil { + return nil, fmt.Errorf("unmarshaling shared volume: %w", err) + } + return volume, nil +} + +func (p *Persistence) ListSharedVolumes(ctx context.Context, atespace string, opts store.ListOptions) (store.ListResponse[*ateapipb.SharedVolume], error) { + opts, err := store.NormalizeListOptions(opts) + if err != nil { + return store.ListResponse[*ateapipb.SharedVolume]{}, err + } + var items []*ateapipb.SharedVolume + var nextToken string + if atespace != "" { + items, nextToken, err = p.listSharedVolumesScoped(ctx, atespace, opts.PageSize, opts.PageToken) + } else { + items, nextToken, err = p.listSharedVolumesGlobal(ctx, opts.PageSize, opts.PageToken) + } + if err != nil { + return store.ListResponse[*ateapipb.SharedVolume]{}, err + } + return store.ListResponse[*ateapipb.SharedVolume]{Items: items, NextPageToken: nextToken}, nil +} + +func (p *Persistence) listSharedVolumesScoped(ctx context.Context, atespace string, pageSize int32, pageTokenStr string) ([]*ateapipb.SharedVolume, string, error) { + token, err := decodePageToken(pageTokenStr, kindExternalVol, atespace, 1) + if err != nil { + return nil, "", err + } + var last *string + if len(token.Last) > 0 { + last = &token.Last[0] + } + rows, err := p.pool.Query(ctx, ` + SELECT name, proto FROM shared_volumes + WHERE atespace = $1 AND ($2::text IS NULL OR name > $2) + ORDER BY name + LIMIT $3`, atespace, last, int64(pageSize)+1) + if err != nil { + return nil, "", fmt.Errorf("listing shared volumes in %q: %w", atespace, err) + } + defer rows.Close() + + var names []string + var result []*ateapipb.SharedVolume + for rows.Next() { + var name string + var protoBytes []byte + if err := rows.Scan(&name, &protoBytes); err != nil { + return nil, "", fmt.Errorf("scanning shared volume row: %w", err) + } + volume := &ateapipb.SharedVolume{} + if err := unmarshalStored(protoBytes, volume); err != nil { + return nil, "", fmt.Errorf("unmarshaling shared volume: %w", err) + } + result = append(result, volume) + names = append(names, name) + } + if err := rows.Err(); err != nil { + return nil, "", fmt.Errorf("listing shared volumes in %q: %w", atespace, err) + } + var nextToken string + if len(result) > int(pageSize) { + result = result[:pageSize] + nextToken = encodePageToken(kindExternalVol, atespace, []string{names[pageSize-1]}) + } + return result, nextToken, nil +} + +func (p *Persistence) listSharedVolumesGlobal(ctx context.Context, pageSize int32, pageTokenStr string) ([]*ateapipb.SharedVolume, string, error) { + token, err := decodePageToken(pageTokenStr, kindExternalVol, "", 2) + if err != nil { + return nil, "", err + } + var lastAtespace, lastName *string + if len(token.Last) == 2 { + lastAtespace, lastName = &token.Last[0], &token.Last[1] + } + rows, err := p.pool.Query(ctx, ` + SELECT atespace, name, proto FROM shared_volumes + WHERE $1::text IS NULL OR (atespace, name) > ($1, $2) + ORDER BY atespace, name + LIMIT $3`, lastAtespace, lastName, int64(pageSize)+1) + if err != nil { + return nil, "", fmt.Errorf("listing shared volumes: %w", err) + } + defer rows.Close() + + type key struct{ atespace, name string } + var keys []key + var result []*ateapipb.SharedVolume + for rows.Next() { + var k key + var protoBytes []byte + if err := rows.Scan(&k.atespace, &k.name, &protoBytes); err != nil { + return nil, "", fmt.Errorf("scanning shared volume row: %w", err) + } + volume := &ateapipb.SharedVolume{} + if err := unmarshalStored(protoBytes, volume); err != nil { + return nil, "", fmt.Errorf("unmarshaling shared volume: %w", err) + } + result = append(result, volume) + keys = append(keys, k) + } + if err := rows.Err(); err != nil { + return nil, "", fmt.Errorf("listing shared volumes: %w", err) + } + var nextToken string + if len(result) > int(pageSize) { + result = result[:pageSize] + last := keys[pageSize-1] + nextToken = encodePageToken(kindExternalVol, "", []string{last.atespace, last.name}) + } + return result, nextToken, nil +} + +func (p *Persistence) CreateSharedVolume(ctx context.Context, volume *ateapipb.SharedVolume) (*ateapipb.SharedVolume, error) { + atespace := volume.GetMetadata().GetAtespace() + name := volume.GetMetadata().GetName() + dbVolume := proto.CloneOf(volume) + setCreateMetadata(dbVolume.Metadata) + protoBytes, err := proto.Marshal(dbVolume) + if err != nil { + return nil, fmt.Errorf("marshaling shared volume: %w", err) + } + + _, err = p.pool.Exec(ctx, ` + INSERT INTO shared_volumes (atespace, name, uid, version, proto) + VALUES ($1, $2, $3, $4, $5)`, atespace, name, + dbVolume.GetMetadata().GetUid(), dbVolume.GetMetadata().GetVersion(), protoBytes) + if err != nil { + if isUniqueViolation(err) { + return nil, store.ErrAlreadyExists + } + if isForeignKeyViolation(err) { + switch pgErrConstraint(err) { + case "shared_volumes_atespace_fk": + return nil, store.ErrFailedPrecondition + default: + return nil, fmt.Errorf("inserting shared volume %s/%s violated unknown foreign key %q: %w", atespace, name, pgErrConstraint(err), err) + } + } + return nil, fmt.Errorf("inserting shared volume %s/%s: %w", atespace, name, err) + } + return dbVolume, nil +} + +func validateUpdateSharedVolumeMutation(storedVolume, mutatedVolume *ateapipb.SharedVolume) error { + if stored, mutated := storedVolume.GetMetadata().GetAtespace(), mutatedVolume.GetMetadata().GetAtespace(); stored != mutated { + return fmt.Errorf("metadata.atespace is immutable: mutation changed it from %q to %q", stored, mutated) + } + if stored, mutated := storedVolume.GetMetadata().GetName(), mutatedVolume.GetMetadata().GetName(); stored != mutated { + return fmt.Errorf("metadata.name is immutable: mutation changed it from %q to %q", stored, mutated) + } + if stored, mutated := storedVolume.GetAccessMode(), mutatedVolume.GetAccessMode(); stored != ateapipb.AccessMode_ACCESS_MODE_UNSPECIFIED && stored != mutated { + return fmt.Errorf("access_mode is immutable once set: mutation changed it from %v to %v", stored, mutated) + } + if stored, mutated := storedVolume.GetStorageClassName(), mutatedVolume.GetStorageClassName(); stored != "" && stored != mutated { + return fmt.Errorf("storage_class_name is immutable once set: mutation changed it from %q to %q", stored, mutated) + } + if stored, mutated := storedVolume.GetCapacity(), mutatedVolume.GetCapacity(); stored != "" && stored != mutated { + return fmt.Errorf("capacity is immutable once set: mutation changed it from %q to %q", stored, mutated) + } + if stored, mutated := storedVolume.GetVolumeId(), mutatedVolume.GetVolumeId(); stored != "" && stored != mutated { + return fmt.Errorf("volume_id is immutable once set: mutation changed it from %q to %q", stored, mutated) + } + if stored, mutated := storedVolume.GetVolumeType(), mutatedVolume.GetVolumeType(); stored != "" && stored != mutated { + return fmt.Errorf("volume_type is immutable once set: mutation changed it from %q to %q", stored, mutated) + } + if stored, mutated := storedVolume.GetVolumeContext(), mutatedVolume.GetVolumeContext(); len(stored) > 0 && !maps.Equal(stored, mutated) { + return fmt.Errorf("volume_context is immutable once set: mutation changed it from %v to %v", stored, mutated) + } + return nil +} + +func (p *Persistence) UpdateSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef, precondition store.Precondition, mutate func(*ateapipb.SharedVolume) error) (*ateapipb.SharedVolume, error) { + if err := precondition.Validate(); err != nil { + return nil, err + } + atespace, name := volumeRef.Atespace, volumeRef.Name + var currentUID string + var currentVersion int64 + var currentBytes []byte + if err := p.pool.QueryRow(ctx, ` + SELECT uid, version, proto FROM shared_volumes + WHERE atespace = $1 AND name = $2`, atespace, name).Scan(¤tUID, ¤tVersion, ¤tBytes); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, store.ErrNotFound + } + return nil, fmt.Errorf("getting shared volume %s/%s for update: %w", atespace, name, err) + } + + dbVolume := &ateapipb.SharedVolume{} + if err := unmarshalStored(currentBytes, dbVolume); err != nil { + return nil, fmt.Errorf("unmarshaling shared volume: %w", err) + } + if err := validateProtoMetadataMatchesColumns(fmt.Sprintf("shared volume %s/%s", atespace, name), dbVolume.GetMetadata(), currentUID, currentVersion); err != nil { + return nil, err + } + if err := precondition.Check(dbVolume.GetMetadata()); err != nil { + return nil, err + } + volumeBeforeMutation := proto.CloneOf(dbVolume) + if err := mutate(dbVolume); err != nil { + return nil, err + } + if err := validateUpdateSharedVolumeMutation(volumeBeforeMutation, dbVolume); err != nil { + return nil, fmt.Errorf("%w: %w", store.ErrImmutableField, err) + } + setUpdateMetadata(dbVolume.Metadata, volumeBeforeMutation.GetMetadata()) + + updatedBytes, err := proto.Marshal(dbVolume) + if err != nil { + return nil, fmt.Errorf("marshaling shared volume: %w", err) + } + commandTag, err := p.pool.Exec(ctx, ` + UPDATE shared_volumes + SET version = $1, proto = $2 + WHERE atespace = $3 AND name = $4 AND uid = $5 AND version = $6`, + dbVolume.GetMetadata().GetVersion(), updatedBytes, atespace, name, currentUID, currentVersion) + if err != nil { + return nil, fmt.Errorf("updating shared volume %s/%s: %w", atespace, name, err) + } + if commandTag.RowsAffected() == 0 { + return nil, store.ErrVersionConflict + } + if commandTag.RowsAffected() != 1 { + return nil, fmt.Errorf("updating shared volume %s/%s affected %d rows, want 1", atespace, name, commandTag.RowsAffected()) + } + return dbVolume, nil +} + +func (p *Persistence) DeleteSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef, precondition store.DeletePreconditions) (*ateapipb.SharedVolume, error) { + atespace, name := volumeRef.Atespace, volumeRef.Name + var protoBytes []byte + err := p.pool.QueryRow(ctx, ` + DELETE FROM shared_volumes + WHERE atespace = $1 AND name = $2 + AND ($3::text = '' OR uid = $3::text) + AND ($4::bigint = 0 OR version = $4::bigint) + RETURNING proto`, atespace, name, precondition.UID, precondition.Version).Scan(&protoBytes) + if errors.Is(err, pgx.ErrNoRows) { + var uid string + var version int64 + err := p.pool.QueryRow(ctx, `SELECT uid, version FROM shared_volumes WHERE atespace = $1 AND name = $2`, atespace, name).Scan(&uid, &version) + return nil, mapDeleteError(err, uid, version, precondition) + } + if err != nil { + return nil, fmt.Errorf("deleting shared volume %s/%s: %w", atespace, name, err) + } + volume := &ateapipb.SharedVolume{} + if err := unmarshalStored(protoBytes, volume); err != nil { + return nil, fmt.Errorf("unmarshaling deleted shared volume: %w", err) + } + return volume, nil +} diff --git a/cmd/ateapi/internal/store/store.go b/cmd/ateapi/internal/store/store.go index cc494082c1..162fee3fc1 100644 --- a/cmd/ateapi/internal/store/store.go +++ b/cmd/ateapi/internal/store/store.go @@ -159,6 +159,26 @@ type Interface interface { // tag the caller observed. DeleteTag(ctx context.Context, tagRef resources.TagRef, precondition DeletePreconditions) (*ateapipb.Tag, error) + // Stores a new external volume and returns it with server-assigned metadata. + // Returns ErrAlreadyExists if the name is taken, or ErrFailedPrecondition if + // the atespace does not exist. + CreateSharedVolume(ctx context.Context, volume *ateapipb.SharedVolume) (*ateapipb.SharedVolume, error) + + // Fetches an external volume by reference. Returns ErrNotFound if missing. + GetSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef) (*ateapipb.SharedVolume, error) + + // Lists external volumes in one atespace, or all atespaces when empty. + ListSharedVolumes(ctx context.Context, atespace string, opts ListOptions) (ListResponse[*ateapipb.SharedVolume], error) + + // UpdateSharedVolume performs a guarded read-modify-write like UpdateTag. + // Everything naming the storage, and access_mode, is set-once and returns + // ErrImmutableField when changed. + UpdateSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef, precondition Precondition, mutate func(toUpdate *ateapipb.SharedVolume) error) (*ateapipb.SharedVolume, error) + + // Deletes and returns an external volume record; the storage is the caller's + // job. + DeleteSharedVolume(ctx context.Context, volumeRef resources.SharedVolumeRef, precondition DeletePreconditions) (*ateapipb.SharedVolume, error) + // Stores a new atespace and returns the stored resource with server-assigned // metadata (uid, version, timestamps). The input is not mutated. Returns // ErrAlreadyExists if the name is taken. diff --git a/cmd/ateapi/internal/store/storecontract/contract.go b/cmd/ateapi/internal/store/storecontract/contract.go index 2093146326..c11829748d 100644 --- a/cmd/ateapi/internal/store/storecontract/contract.go +++ b/cmd/ateapi/internal/store/storecontract/contract.go @@ -209,6 +209,7 @@ func RunContractTests(t *testing.T, setup func(t *testing.T) store.Interface) { runAtespaceContractTests(t, setup) runActorTemplateContractTests(t, setup) runTagContractTests(t, setup) + runSharedVolumeContractTests(t, setup) runLeaseContractTests(t, setup) runListOptionsContractTests(t, setup) runUnknownFieldContractTests(t, setup) @@ -394,6 +395,7 @@ func runListOptionsContractTests(t *testing.T, setup func(t *testing.T) store.In {"actors", func(opts store.ListOptions) error { _, err := s.ListActors(ctx, "", opts); return err }}, {"actor templates", func(opts store.ListOptions) error { _, err := s.ListActorTemplates(ctx, "", opts); return err }}, {"tags", func(opts store.ListOptions) error { _, err := s.ListTags(ctx, "", opts); return err }}, + {"external volumes", func(opts store.ListOptions) error { _, err := s.ListSharedVolumes(ctx, "", opts); return err }}, {"workers", func(opts store.ListOptions) error { _, err := s.ListWorkers(ctx, opts); return err }}, } for _, call := range calls { @@ -419,6 +421,7 @@ func runListOptionsContractTests(t *testing.T, setup func(t *testing.T) store.In {"actors", func(opts store.ListOptions) error { _, err := s.ListActors(ctx, "", opts); return err }}, {"actor templates", func(opts store.ListOptions) error { _, err := s.ListActorTemplates(ctx, "", opts); return err }}, {"tags", func(opts store.ListOptions) error { _, err := s.ListTags(ctx, "", opts); return err }}, + {"external volumes", func(opts store.ListOptions) error { _, err := s.ListSharedVolumes(ctx, "", opts); return err }}, {"workers", func(opts store.ListOptions) error { _, err := s.ListWorkers(ctx, opts); return err }}, } for _, call := range calls { @@ -1574,6 +1577,302 @@ func storeTag(t *testing.T, s store.Interface, tag *ateapipb.Tag) *ateapipb.Tag return stored } +func newTestPendingSharedVolume(atespace, name string) *ateapipb.SharedVolume { + return &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: atespace, Name: name}, + DeleteTrigger: ateapipb.DeleteTrigger_DELETE_TRIGGER_LAST_ACTOR, + AccessMode: ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_ONCE, + StorageClassName: "standard-rwx", + Status: &ateapipb.SharedVolumeStatus{State: ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_PENDING}, + } +} + +func finalizeSharedVolume(toUpdate *ateapipb.SharedVolume) error { + toUpdate.VolumeId = "substrate-" + toUpdate.GetMetadata().GetUid() + toUpdate.VolumeType = "filestore.csi.storage.gke.io" + toUpdate.VolumeContext = map[string]string{"ip": "10.0.0.2"} + toUpdate.Status.State = ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY + return nil +} + +func storeSharedVolume(t *testing.T, s store.Interface, volume *ateapipb.SharedVolume) *ateapipb.SharedVolume { + t.Helper() + reserved, err := s.CreateSharedVolume(context.Background(), volume) + if err != nil { + t.Fatalf("CreateSharedVolume(%q) failed: %v", volume.GetMetadata().GetName(), err) + } + stored, err := s.UpdateSharedVolume(context.Background(), resources.SharedVolumeRefFromSharedVolume(reserved), store.PreconditionFrom(reserved), finalizeSharedVolume) + if err != nil { + t.Fatalf("finalizing external volume %q failed: %v", volume.GetMetadata().GetName(), err) + } + return stored +} + +func runSharedVolumeContractTests(t *testing.T, setup func(t *testing.T) store.Interface) { + t.Helper() + + t.Run("SharedVolume_Lifecycle", func(t *testing.T) { + s := setup(t) + ctx := context.Background() + mustCreateAtespace(t, s, "team-a") + + input := newTestPendingSharedVolume("team-a", "scratch") + pending, err := s.CreateSharedVolume(ctx, input) + if err != nil { + t.Fatalf("CreateSharedVolume failed: %v", err) + } + if pending.GetMetadata().GetVersion() != 1 || pending.GetMetadata().GetUid() == "" { + t.Errorf("created volume metadata = %v, want server-owned uid and version 1", pending.GetMetadata()) + } + if input.GetMetadata().GetUid() != "" || input.GetMetadata().GetVersion() != 0 { + t.Errorf("CreateSharedVolume mutated its input metadata: %v", input.GetMetadata()) + } + + volumeRef := resources.SharedVolumeRef{Atespace: "team-a", Name: "scratch"} + volume, err := s.GetSharedVolume(ctx, volumeRef) + if err != nil { + t.Fatalf("GetSharedVolume failed: %v", err) + } + if volume.GetVolumeId() != "" { + t.Errorf("reserved volume id = %q, want unset", volume.GetVolumeId()) + } + if diff := cmp.Diff(input, volume, protocmp.Transform(), ignoreUID, ignoreVersion, ignoreTimestamps); diff != "" { + t.Errorf("stored external volume mismatch (-want +got):\n%s", diff) + } + if _, err := s.GetSharedVolume(ctx, resources.SharedVolumeRef{Atespace: "team-a", Name: "missing"}); !errors.Is(err, store.ErrNotFound) { + t.Errorf("missing GetSharedVolume = %v, want ErrNotFound", err) + } + + ready, err := s.UpdateSharedVolume(ctx, volumeRef, store.PreconditionFrom(volume), finalizeSharedVolume) + if err != nil { + t.Fatalf("finalizing external volume failed: %v", err) + } + if got, want := ready.GetVolumeId(), "substrate-"+volume.GetMetadata().GetUid(); got != want { + t.Errorf("finalized volume id = %q, want %q", got, want) + } + if ready.GetStatus().GetState() != ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY { + t.Errorf("finalized volume state = %v, want READY", ready.GetStatus().GetState()) + } + + claimed, err := s.UpdateSharedVolume(ctx, volumeRef, store.PreconditionFrom(ready), func(toUpdate *ateapipb.SharedVolume) error { + toUpdate.DeleteTrigger = ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL + toUpdate.Status.Refs = append(toUpdate.Status.Refs, &ateapipb.ActorRef{ActorUid: "actor-uid-1", ActorName: "producer", Active: true}) + return nil + }) + if err != nil { + t.Fatalf("UpdateSharedVolume failed: %v", err) + } + if claimed.GetDeleteTrigger() != ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL || len(claimed.GetStatus().GetRefs()) != 1 { + t.Errorf("updated volume = %v, want MANUAL and one ref", claimed) + } + if claimed.GetMetadata().GetVersion() != ready.GetMetadata().GetVersion()+1 { + t.Errorf("updated volume version = %d, want %d", claimed.GetMetadata().GetVersion(), ready.GetMetadata().GetVersion()+1) + } + if _, err := s.UpdateSharedVolume(ctx, volumeRef, store.PreconditionFrom(ready), func(toUpdate *ateapipb.SharedVolume) error { + return nil + }); !errors.Is(err, store.ErrVersionConflict) { + t.Errorf("stale UpdateSharedVolume = %v, want ErrVersionConflict", err) + } + if _, err := s.DeleteAtespace(ctx, "team-a", store.DeletePreconditions{}); !errors.Is(err, store.ErrFailedPrecondition) { + t.Errorf("DeleteAtespace with an external volume = %v, want ErrFailedPrecondition", err) + } + + if _, err := s.DeleteSharedVolume(ctx, volumeRef, store.DeletePreconditions{Version: ready.GetMetadata().GetVersion()}); !errors.Is(err, store.ErrVersionConflict) { + t.Errorf("stale DeleteSharedVolume = %v, want ErrVersionConflict", err) + } + deleted, err := s.DeleteSharedVolume(ctx, volumeRef, store.DeletePreconditions{UID: claimed.GetMetadata().GetUid()}) + if err != nil || !proto.Equal(deleted, claimed) { + t.Errorf("DeleteSharedVolume = (%v, %v), want the claimed volume", deleted, err) + } + if _, err := s.GetSharedVolume(ctx, volumeRef); !errors.Is(err, store.ErrNotFound) { + t.Errorf("deleted GetSharedVolume = %v, want ErrNotFound", err) + } + }) + + t.Run("SharedVolume_ImmutableFields", func(t *testing.T) { + s := setup(t) + ctx := context.Background() + mustCreateAtespace(t, s, "team-a") + volume := storeSharedVolume(t, s, newTestPendingSharedVolume("team-a", "scratch")) + + tests := []struct { + name string + mutate func(*ateapipb.SharedVolume) + }{ + { + name: "access mode", + mutate: func(toUpdate *ateapipb.SharedVolume) { + toUpdate.AccessMode = ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_MANY + }, + }, + { + name: "storage class name", + mutate: func(toUpdate *ateapipb.SharedVolume) { toUpdate.StorageClassName = "premium-rwx" }, + }, + { + name: "clearing the storage class name", + mutate: func(toUpdate *ateapipb.SharedVolume) { toUpdate.StorageClassName = "" }, + }, + { + name: "volume id", + mutate: func(toUpdate *ateapipb.SharedVolume) { toUpdate.VolumeId = "substrate-somewhere-else" }, + }, + { + name: "clearing the volume id", + mutate: func(toUpdate *ateapipb.SharedVolume) { toUpdate.VolumeId = "" }, + }, + { + name: "volume type", + mutate: func(toUpdate *ateapipb.SharedVolume) { toUpdate.VolumeType = "pd.csi.storage.gke.io" }, + }, + { + name: "volume context", + mutate: func(toUpdate *ateapipb.SharedVolume) { toUpdate.VolumeContext = map[string]string{"ip": "10.0.0.3"} }, + }, + { + name: "clearing the volume context", + mutate: func(toUpdate *ateapipb.SharedVolume) { toUpdate.VolumeContext = nil }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := s.UpdateSharedVolume(ctx, resources.SharedVolumeRef{Atespace: "team-a", Name: "scratch"}, store.PreconditionFrom(volume), func(toUpdate *ateapipb.SharedVolume) error { + tt.mutate(toUpdate) + return nil + }) + if !errors.Is(err, store.ErrImmutableField) { + t.Errorf("UpdateSharedVolume error = %v, want one matching store.ErrImmutableField", err) + } + }) + } + }) + + t.Run("CreateSharedVolume_ReusedName", func(t *testing.T) { + s := setup(t) + ctx := context.Background() + mustCreateAtespace(t, s, "team-a") + input := newTestPendingSharedVolume("team-a", "scratch") + if _, err := s.CreateSharedVolume(ctx, input); err != nil { + t.Fatalf("CreateSharedVolume failed: %v", err) + } + if _, err := s.CreateSharedVolume(ctx, input); !errors.Is(err, store.ErrAlreadyExists) { + t.Errorf("duplicate CreateSharedVolume = %v, want ErrAlreadyExists", err) + } + + stored, err := s.GetSharedVolume(ctx, resources.SharedVolumeRef{Atespace: "team-a", Name: "scratch"}) + if err != nil { + t.Fatalf("GetSharedVolume failed: %v", err) + } + if stored.GetMetadata().GetVersion() != 1 { + t.Errorf("volume version after rejected create = %d, want 1", stored.GetMetadata().GetVersion()) + } + }) + + t.Run("CreateSharedVolume_UnknownAtespace", func(t *testing.T) { + s := setup(t) + ctx := context.Background() + + if _, err := s.CreateSharedVolume(ctx, newTestPendingSharedVolume("team-missing", "scratch")); !errors.Is(err, store.ErrFailedPrecondition) { + t.Errorf("CreateSharedVolume in an unknown atespace = %v, want ErrFailedPrecondition", err) + } + }) + + t.Run("UpdateSharedVolume_MissingPrecondition", func(t *testing.T) { + s := setup(t) + ctx := context.Background() + mustCreateAtespace(t, s, "team-a") + created := storeSharedVolume(t, s, newTestPendingSharedVolume("team-a", "scratch")) + + tests := []struct { + name string + precondition store.Precondition + }{ + { + name: "no precondition", + precondition: store.Precondition{}, + }, + { + name: "guarding on only a uid", + precondition: store.Precondition{UID: created.GetMetadata().GetUid()}, + }, + { + name: "guarding on only a version", + precondition: store.Precondition{Version: created.GetMetadata().GetVersion()}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := s.UpdateSharedVolume(ctx, resources.SharedVolumeRef{Atespace: "team-a", Name: "scratch"}, tt.precondition, func(toUpdate *ateapipb.SharedVolume) error { + t.Fatal("mutate ran for a blind write") + return nil + }) + if !errors.Is(err, store.ErrPreconditionRequired) { + t.Errorf("UpdateSharedVolume error = %v, want one matching store.ErrPreconditionRequired", err) + } + }) + } + }) + + t.Run("UpdateSharedVolume_NotFound", func(t *testing.T) { + s := setup(t) + ctx := context.Background() + mustCreateAtespace(t, s, "team-a") + + _, err := s.UpdateSharedVolume(ctx, resources.SharedVolumeRef{Atespace: "team-a", Name: "missing"}, store.Precondition{UID: "u", Version: 1}, func(*ateapipb.SharedVolume) error { return nil }) + if !errors.Is(err, store.ErrNotFound) { + t.Errorf("UpdateSharedVolume on a missing volume = %v, want ErrNotFound", err) + } + if _, err := s.DeleteSharedVolume(ctx, resources.SharedVolumeRef{Atespace: "team-a", Name: "missing"}, store.DeletePreconditions{}); !errors.Is(err, store.ErrNotFound) { + t.Errorf("DeleteSharedVolume on a missing volume = %v, want ErrNotFound", err) + } + }) + + t.Run("ListSharedVolumes_PaginationAndScope", func(t *testing.T) { + s := setup(t) + ctx := context.Background() + for _, atespace := range []string{"team-a", "team-b"} { + mustCreateAtespace(t, s, atespace) + for i := 0; i < 3; i++ { + storeSharedVolume(t, s, newTestPendingSharedVolume(atespace, fmt.Sprintf("volume-%d", i))) + } + } + + var scoped []*ateapipb.SharedVolume + for token := ""; ; { + page, err := s.ListSharedVolumes(ctx, "team-a", store.ListOptions{PageSize: 2, PageToken: token}) + if err != nil { + t.Fatalf("scoped ListSharedVolumes failed: %v", err) + } + scoped = append(scoped, page.Items...) + if page.NextPageToken == "" { + break + } + token = page.NextPageToken + } + if len(scoped) != 3 { + t.Errorf("scoped ListSharedVolumes returned %d volumes, want 3", len(scoped)) + } + + var global []*ateapipb.SharedVolume + for token := ""; ; { + page, err := s.ListSharedVolumes(ctx, "", store.ListOptions{PageSize: 2, PageToken: token}) + if err != nil { + t.Fatalf("global ListSharedVolumes failed: %v", err) + } + global = append(global, page.Items...) + if page.NextPageToken == "" { + break + } + token = page.NextPageToken + } + if len(global) != 6 { + t.Errorf("global ListSharedVolumes returned %d volumes, want 6", len(global)) + } + }) +} + func runWorkerContractTests(t *testing.T, setup func(t *testing.T) store.Interface) { t.Helper() diff --git a/cmd/ateapi/internal/store/storetest/storetest.go b/cmd/ateapi/internal/store/storetest/storetest.go index 96d7e17428..94f0afd9b9 100644 --- a/cmd/ateapi/internal/store/storetest/storetest.go +++ b/cmd/ateapi/internal/store/storetest/storetest.go @@ -147,6 +147,19 @@ func MustCreateTag(t *testing.T, ctx context.Context, s store.Interface, tag *at return created } +// MustCreateSharedVolume ensures volume's parent atespace exists, then stores volume as given. +func MustCreateSharedVolume(t *testing.T, ctx context.Context, s store.Interface, volume *ateapipb.SharedVolume) *ateapipb.SharedVolume { + t.Helper() + atespace := volume.GetMetadata().GetAtespace() + name := volume.GetMetadata().GetName() + MustCreateAtespace(t, ctx, s, atespace) + created, err := s.CreateSharedVolume(ctx, volume) + if err != nil { + t.Fatalf("creating test external volume %q/%q: %v", atespace, name, err) + } + return created +} + // RunTests runs m and terminates the shared PostgreSQL container afterwards. // Packages with no other TestMain work should use it as their whole TestMain; // the rest must call [Shutdown] themselves. diff --git a/cmd/atelet/internal/apivalidation/validation_test.go b/cmd/atelet/internal/apivalidation/validation_test.go index d84212e8cb..1a7f1d7dc5 100644 --- a/cmd/atelet/internal/apivalidation/validation_test.go +++ b/cmd/atelet/internal/apivalidation/validation_test.go @@ -1476,8 +1476,8 @@ func TestValidateVolume(t *testing.T) { } return v } - external := func(mutate ...func(*ateletpb.ExternalVolumeSource)) *ateletpb.Volume { - e := &ateletpb.ExternalVolumeSource{ + external := func(mutate ...func(*ateletpb.ExternalVolume)) *ateletpb.Volume { + e := &ateletpb.ExternalVolume{ StorageVolumeId: "projects/p/zones/z/disks/vol-1", VolumeType: "substrate.io/mock", VolumeContext: map[string]string{"fsType": "ext4"}, @@ -1533,48 +1533,48 @@ func TestValidateVolume(t *testing.T) { }, { name: "two sources set", obj: durable(func(v *ateletpb.Volume) { - v.External = &ateletpb.ExternalVolumeSource{StorageVolumeId: "vol-1"} + v.External = &ateletpb.ExternalVolume{StorageVolumeId: "vol-1"} }), want: field.ErrorList{field.Invalid(nil, nil, "").WithOrigin("union")}, }, - // ExternalVolumeSource. + // ExternalVolume. { name: "valid external", obj: external(), }, { name: "external: missing storage_volume_id", - obj: external(func(e *ateletpb.ExternalVolumeSource) { e.StorageVolumeId = "" }), + obj: external(func(e *ateletpb.ExternalVolume) { e.StorageVolumeId = "" }), want: field.ErrorList{field.Required(extPath.Child("storage_volume_id"), "")}, }, { name: "external: storage_volume_id with a control character", - obj: external(func(e *ateletpb.ExternalVolumeSource) { e.StorageVolumeId = "vol\x01" }), + obj: external(func(e *ateletpb.ExternalVolume) { e.StorageVolumeId = "vol\x01" }), want: field.ErrorList{field.Invalid(extPath.Child("storage_volume_id"), nil, "")}, }, { name: "external: storage_volume_id too long", - obj: external(func(e *ateletpb.ExternalVolumeSource) { e.StorageVolumeId = strings.Repeat("x", 257) }), + obj: external(func(e *ateletpb.ExternalVolume) { e.StorageVolumeId = strings.Repeat("x", 257) }), want: field.ErrorList{field.TooLong(extPath.Child("storage_volume_id"), nil, 256).WithOrigin("maxLength")}, }, { name: "external: unset volume_type is allowed", - obj: external(func(e *ateletpb.ExternalVolumeSource) { e.VolumeType = "" }), + obj: external(func(e *ateletpb.ExternalVolume) { e.VolumeType = "" }), }, { name: "external: invalid volume_type: uppercase", - obj: external(func(e *ateletpb.ExternalVolumeSource) { e.VolumeType = "substrate.io/Mock" }), + obj: external(func(e *ateletpb.ExternalVolume) { e.VolumeType = "substrate.io/Mock" }), want: field.ErrorList{field.Invalid(extPath.Child("volume_type"), nil, "")}, }, { name: "external: volume_context with one long value, as a CSI driver may return", - obj: external(func(e *ateletpb.ExternalVolumeSource) { + obj: external(func(e *ateletpb.ExternalVolume) { e.VolumeContext = map[string]string{"k": strings.Repeat("v", 4000)} }), }, { name: "external: volume_context over the size bound", - obj: external(func(e *ateletpb.ExternalVolumeSource) { + obj: external(func(e *ateletpb.ExternalVolume) { e.VolumeContext = map[string]string{"k": strings.Repeat("v", 40*1024)} }), want: field.ErrorList{field.TooLong(extPath.Child("volume_context"), nil, 40*1024)}, }, { name: "external: publish_context with many entries, as a CSI driver may return", - obj: external(func(e *ateletpb.ExternalVolumeSource) { + obj: external(func(e *ateletpb.ExternalVolume) { e.PublishContext = make(map[string]string) for i := range 100 { e.PublishContext[fmt.Sprintf("key-%03d", i)] = "v" @@ -1582,7 +1582,7 @@ func TestValidateVolume(t *testing.T) { }), }, { name: "external: publish_context over the size bound", - obj: external(func(e *ateletpb.ExternalVolumeSource) { + obj: external(func(e *ateletpb.ExternalVolume) { e.PublishContext = map[string]string{"a": strings.Repeat("v", 20*1024), "b": strings.Repeat("v", 20*1024)} }), want: field.ErrorList{field.TooLong(extPath.Child("publish_context"), nil, 40*1024)}, diff --git a/cmd/atelet/internal/apivalidation/workloadspec.go b/cmd/atelet/internal/apivalidation/workloadspec.go index c0e3f94693..8e8a4b0561 100644 --- a/cmd/atelet/internal/apivalidation/workloadspec.go +++ b/cmd/atelet/internal/apivalidation/workloadspec.go @@ -89,19 +89,19 @@ func ValidateCustom_HttpGetAction_Path(_ context.Context, _ operation.Operation, return resources.ValidateHTTPGetPath(fldPath, *value) } -func ValidateCustom_ExternalVolumeSource_StorageVolumeId(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { +func ValidateCustom_ExternalVolume_StorageVolumeId(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { return resources.ValidateStorageVolumeID(fldPath, *value) } -func ValidateCustom_ExternalVolumeSource_VolumeType(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { +func ValidateCustom_ExternalVolume_VolumeType(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { return resources.ValidateVolumeType(fldPath, *value) } -func ValidateCustom_ExternalVolumeSource_VolumeContext(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[string]string) field.ErrorList { +func ValidateCustom_ExternalVolume_VolumeContext(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[string]string) field.ErrorList { return resources.ValidateCSIMap(fldPath, value) } -func ValidateCustom_ExternalVolumeSource_PublishContext(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[string]string) field.ErrorList { +func ValidateCustom_ExternalVolume_PublishContext(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[string]string) field.ErrorList { return resources.ValidateCSIMap(fldPath, value) } diff --git a/cmd/atelet/internal/apivalidation/zz_generated.validation.go b/cmd/atelet/internal/apivalidation/zz_generated.validation.go index 8cbdbae51c..7615a25b2a 100644 --- a/cmd/atelet/internal/apivalidation/zz_generated.validation.go +++ b/cmd/atelet/internal/apivalidation/zz_generated.validation.go @@ -1223,13 +1223,13 @@ func Validate_EnvEntry( return errs } -// Validate_ExternalVolumeSource validates an instance of ExternalVolumeSource according +// Validate_ExternalVolume validates an instance of ExternalVolume according // to declarative validation rules in the API schema. -func Validate_ExternalVolumeSource( +func Validate_ExternalVolume( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateletpb.ExternalVolumeSource) (errs field.ErrorList) { + obj, oldObj *ateletpb.ExternalVolume) (errs field.ErrorList) { - { // field ateletpb.ExternalVolumeSource.StorageVolumeId + { // field ateletpb.ExternalVolume.StorageVolumeId fn := func( fldPath *field.Path, obj, oldObj *string, @@ -1250,7 +1250,7 @@ func Validate_ExternalVolumeSource( return // do not proceed } // custom validation - if e := ValidateCustom_ExternalVolumeSource_StorageVolumeId(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + if e := ValidateCustom_ExternalVolume_StorageVolumeId(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256); len(e) != 0 { @@ -1259,13 +1259,13 @@ func Validate_ExternalVolumeSource( return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.ExternalVolumeSource) *string { + func(oldObj *ateletpb.ExternalVolume) *string { return &oldObj.StorageVolumeId }) errs = append(errs, fn(fldPath.Child("storage_volume_id"), &obj.StorageVolumeId, oldVal, oldObj != nil)...) } - { // field ateletpb.ExternalVolumeSource.VolumeType + { // field ateletpb.ExternalVolume.VolumeType fn := func( fldPath *field.Path, obj, oldObj *string, @@ -1285,7 +1285,7 @@ func Validate_ExternalVolumeSource( return // do not proceed } // custom validation - if e := ValidateCustom_ExternalVolumeSource_VolumeType(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + if e := ValidateCustom_ExternalVolume_VolumeType(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 253); len(e) != 0 { @@ -1294,13 +1294,13 @@ func Validate_ExternalVolumeSource( return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.ExternalVolumeSource) *string { + func(oldObj *ateletpb.ExternalVolume) *string { return &oldObj.VolumeType }) errs = append(errs, fn(fldPath.Child("volume_type"), &obj.VolumeType, oldVal, oldObj != nil)...) } - { // field ateletpb.ExternalVolumeSource.VolumeContext + { // field ateletpb.ExternalVolume.VolumeContext fn := func( fldPath *field.Path, obj, oldObj map[string]string, @@ -1320,19 +1320,19 @@ func Validate_ExternalVolumeSource( return // do not proceed } // custom validation - if e := ValidateCustom_ExternalVolumeSource_VolumeContext(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + if e := ValidateCustom_ExternalVolume_VolumeContext(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.ExternalVolumeSource) map[string]string { + func(oldObj *ateletpb.ExternalVolume) map[string]string { return oldObj.VolumeContext }) errs = append(errs, fn(fldPath.Child("volume_context"), obj.VolumeContext, oldVal, oldObj != nil)...) } - { // field ateletpb.ExternalVolumeSource.PublishContext + { // field ateletpb.ExternalVolume.PublishContext fn := func( fldPath *field.Path, obj, oldObj map[string]string, @@ -1352,13 +1352,13 @@ func Validate_ExternalVolumeSource( return // do not proceed } // custom validation - if e := ValidateCustom_ExternalVolumeSource_PublishContext(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + if e := ValidateCustom_ExternalVolume_PublishContext(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.ExternalVolumeSource) map[string]string { + func(oldObj *ateletpb.ExternalVolume) map[string]string { return oldObj.PublishContext }) errs = append(errs, fn(fldPath.Child("publish_context"), obj.PublishContext, oldVal, oldObj != nil)...) @@ -4110,7 +4110,7 @@ func Validate_VersionedSandboxCompat( return errs } -var unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_Volume_ = validate.NewUnionMembership(validate.NewUnionMember("durable_dir"), validate.NewUnionMember("external"), validate.NewUnionMember("system_info"), validate.NewUnionMember("image")) +var unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_Volume_ = validate.NewUnionMembership(validate.NewUnionMember("durable_dir"), validate.NewUnionMember("external"), validate.NewUnionMember("system_info"), validate.NewUnionMember("image"), validate.NewUnionMember("empty_dir")) // Validate_Volume validates an instance of Volume according // to declarative validation rules in the API schema. @@ -4142,6 +4142,12 @@ func Validate_Volume( return false } return obj.Image != nil + }, + func(obj *ateletpb.Volume) bool { + if obj == nil { + return false + } + return obj.EmptyDir != nil }); len(e) != 0 { errs = append(errs, e...) } @@ -4209,7 +4215,7 @@ func Validate_Volume( { // field ateletpb.Volume.External fn := func( fldPath *field.Path, - obj, oldObj *ateletpb.ExternalVolumeSource, + obj, oldObj *ateletpb.ExternalVolume, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -4226,11 +4232,11 @@ func Validate_Volume( return // do not proceed } // call the type's validation function - errs = append(errs, Validate_ExternalVolumeSource(ctx, op, fldPath, obj, oldObj)...) + errs = append(errs, Validate_ExternalVolume(ctx, op, fldPath, obj, oldObj)...) return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.Volume) *ateletpb.ExternalVolumeSource { + func(oldObj *ateletpb.Volume) *ateletpb.ExternalVolume { return oldObj.External }) errs = append(errs, fn(fldPath.Child("external"), obj.External, oldVal, oldObj != nil)...) @@ -4296,6 +4302,34 @@ func Validate_Volume( errs = append(errs, fn(fldPath.Child("image"), obj.Image, oldVal, oldObj != nil)...) } + { // field ateletpb.Volume.EmptyDir + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.EmptyDirVolume, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Volume) *ateletpb.EmptyDirVolume { + return oldObj.EmptyDir + }) + errs = append(errs, fn(fldPath.Child("empty_dir"), obj.EmptyDir, oldVal, oldObj != nil)...) + } + return errs } @@ -4624,5 +4658,33 @@ func Validate_WorkloadSpec( errs = append(errs, fn(fldPath.Child("volumes"), obj.Volumes, oldVal, oldObj != nil)...) } + { // field ateletpb.WorkloadSpec.RuntimeGolden + fn := func( + fldPath *field.Path, + obj, oldObj *bool, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.WorkloadSpec) *bool { + return &oldObj.RuntimeGolden + }) + errs = append(errs, fn(fldPath.Child("runtime_golden"), &obj.RuntimeGolden, oldVal, oldObj != nil)...) + } + return errs } diff --git a/cmd/atelet/main.go b/cmd/atelet/main.go index 230a06b99d..0b2b2c3e44 100644 --- a/cmd/atelet/main.go +++ b/cmd/atelet/main.go @@ -1685,7 +1685,7 @@ func buildWorkerWorkloadSpec(spec *ateletpb.WorkloadSpec, containerSpecs []*atew volumes[name] = vol } - out := &ateworkerpb.WorkloadSpec{} + out := &ateworkerpb.WorkloadSpec{RuntimeGolden: spec.GetRuntimeGolden()} for i, ctr := range spec.GetContainers() { var ddMounts []*ateworkerpb.DurableDirVolumeMount var csiMounts []*ateworkerpb.VolumeMount @@ -1704,7 +1704,7 @@ func buildWorkerWorkloadSpec(spec *ateletpb.WorkloadSpec, containerSpecs []*atew VolumeName: volName, MountPath: vm.GetMountPath(), }) - case vol.GetExternal() != nil: + case vol.GetExternal() != nil, vol.GetEmptyDir() != nil: csiMounts = append(csiMounts, &ateworkerpb.VolumeMount{ VolumeName: volName, MountPath: vm.GetMountPath(), diff --git a/cmd/atelet/main_test.go b/cmd/atelet/main_test.go index 4939569c84..157cd7c29d 100644 --- a/cmd/atelet/main_test.go +++ b/cmd/atelet/main_test.go @@ -766,7 +766,8 @@ func TestBuildWorkerWorkloadSpecForwardsDurableDirMounts(t *testing.T) { Volumes: []*ateletpb.Volume{ {Name: "data", DurableDir: &ateletpb.DurableDirVolume{}}, {Name: "cache", DurableDir: &ateletpb.DurableDirVolume{}}, - {Name: "scratch", External: &ateletpb.ExternalVolumeSource{}}, + {Name: "scratch", External: &ateletpb.ExternalVolume{}}, + {Name: "empty", EmptyDir: &ateletpb.EmptyDirVolume{}}, {Name: "system-info", SystemInfo: &ateletpb.SystemInfoVolume{}}, }, Containers: []*ateletpb.Container{ @@ -776,6 +777,7 @@ func TestBuildWorkerWorkloadSpecForwardsDurableDirMounts(t *testing.T) { {Name: "data", MountPath: "/home/counter"}, {Name: "cache", MountPath: "/var/cache"}, {Name: "scratch", MountPath: "/scratch"}, + {Name: "empty", MountPath: "/empty"}, {Name: "system-info", MountPath: "/run/ate"}, }, }, @@ -800,6 +802,7 @@ func TestBuildWorkerWorkloadSpecForwardsDurableDirMounts(t *testing.T) { }, CsiVolumeMounts: []*ateworkerpb.VolumeMount{ {VolumeName: "scratch", MountPath: "/scratch"}, + {VolumeName: "empty", MountPath: "/empty"}, }, SystemInfoVolumeMounts: []*ateworkerpb.SystemInfoVolumeMount{ {VolumeName: "system-info", MountPath: "/run/ate"}, @@ -868,7 +871,7 @@ func TestBuildWorkerWorkloadSpecValidation(t *testing.T) { in: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ {Name: "data", DurableDir: &ateletpb.DurableDirVolume{}}, - {Name: "data", External: &ateletpb.ExternalVolumeSource{}}, + {Name: "data", External: &ateletpb.ExternalVolume{}}, }, Containers: []*ateletpb.Container{ { @@ -1153,7 +1156,7 @@ func TestBuildWorkerWorkloadSpec_ImageVolumeMounts(t *testing.T) { Volumes: []*ateletpb.Volume{ {Name: "agent", Image: &ateletpb.ImageVolumeSource{}}, {Name: "data", DurableDir: &ateletpb.DurableDirVolume{}}, - {Name: "ext", External: &ateletpb.ExternalVolumeSource{}}, + {Name: "ext", External: &ateletpb.ExternalVolume{}}, }, Containers: []*ateletpb.Container{{ Name: "app", @@ -1513,7 +1516,7 @@ func TestShouldHaveSnapshots(t *testing.T) { Snapshot: &ateletpb.Snapshot{Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES}, Spec: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "csi", External: &ateletpb.ExternalVolumeSource{}}, + {Name: "csi", External: &ateletpb.ExternalVolume{}}, }, }, }, @@ -1526,7 +1529,7 @@ func TestShouldHaveSnapshots(t *testing.T) { Spec: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ {Name: "durable", DurableDir: &ateletpb.DurableDirVolume{}}, - {Name: "csi", External: &ateletpb.ExternalVolumeSource{}}, + {Name: "csi", External: &ateletpb.ExternalVolume{}}, }, }, }, diff --git a/cmd/atelet/volumes.go b/cmd/atelet/volumes.go index 149d89ac97..90d8c6eb4a 100644 --- a/cmd/atelet/volumes.go +++ b/cmd/atelet/volumes.go @@ -31,16 +31,17 @@ import ( func (s *Atelet) mountExternalVolumes(ctx context.Context, actorUID string, volumes []*ateletpb.Volume) error { for _, vol := range volumes { - // Given that external volumes has been in the codebase for a while and - // its preview-ness is solely around the API, and that the impact of - // disabling the gate while in use would be an orphaned volume - // attachment, we are NOT checking the ExternalVolumes gate in the - // mount/unmount logic. + hostPath := ateletpath.VolumeHostPath(actorUID, vol.GetName()) + if vol.GetEmptyDir() != nil { + if err := resetEmptyDir(hostPath); err != nil { + return err + } + continue + } ext := vol.GetExternal() if ext == nil { continue } - hostPath := ateletpath.VolumeHostPath(actorUID, vol.GetName()) if err := os.MkdirAll(hostPath, 0o750); err != nil { return fmt.Errorf("failed to create mount point %q: %w", hostPath, err) } @@ -61,6 +62,16 @@ func (s *Atelet) mountExternalVolumes(ctx context.Context, actorUID string, volu return nil } +func resetEmptyDir(hostPath string) error { + if err := os.RemoveAll(hostPath); err != nil { + return fmt.Errorf("failed to clear empty dir %q: %w", hostPath, err) + } + if err := os.MkdirAll(hostPath, 0o750); err != nil { + return fmt.Errorf("failed to create empty dir %q: %w", hostPath, err) + } + return nil +} + func (s *Atelet) unmountExternalVolumes(ctx context.Context, actorUID string, volumes []*ateletpb.Volume) error { var errs []error for _, vol := range volumes { diff --git a/cmd/atelet/volumes_test.go b/cmd/atelet/volumes_test.go index 7e57e36a2b..8217e7566e 100644 --- a/cmd/atelet/volumes_test.go +++ b/cmd/atelet/volumes_test.go @@ -91,14 +91,14 @@ func TestUnmountExternalVolumes(t *testing.T) { extVol1 := &ateletpb.Volume{ Name: "vol-1", - External: &ateletpb.ExternalVolumeSource{ + External: &ateletpb.ExternalVolume{ StorageVolumeId: "mock-vol-1", VolumeType: "mock-driver", }, } extVol2 := &ateletpb.Volume{ Name: "vol-2", - External: &ateletpb.ExternalVolumeSource{ + External: &ateletpb.ExternalVolume{ StorageVolumeId: "mock-vol-2", VolumeType: "mock-driver", }, @@ -171,7 +171,7 @@ func TestMountExternalVolumes(t *testing.T) { extVol1 := &ateletpb.Volume{ Name: "vol-1", - External: &ateletpb.ExternalVolumeSource{ + External: &ateletpb.ExternalVolume{ StorageVolumeId: "mock-vol-1", VolumeType: "mock-driver", VolumeContext: map[string]string{"key": "val1"}, @@ -179,7 +179,7 @@ func TestMountExternalVolumes(t *testing.T) { } extVol2 := &ateletpb.Volume{ Name: "vol-2", - External: &ateletpb.ExternalVolumeSource{ + External: &ateletpb.ExternalVolume{ StorageVolumeId: "mock-vol-2", VolumeType: "mock-driver", VolumeContext: map[string]string{"key": "val2"}, @@ -263,7 +263,7 @@ func TestMountExternalVolumes(t *testing.T) { unknownVol := &ateletpb.Volume{ Name: "vol-unknown", - External: &ateletpb.ExternalVolumeSource{ + External: &ateletpb.ExternalVolume{ StorageVolumeId: "mock-vol-unknown", VolumeType: "unknown-driver", }, @@ -380,14 +380,14 @@ func TestVolumeHostDirectoryCleanup(t *testing.T) { extVol1 := &ateletpb.Volume{ Name: "vol-1", - External: &ateletpb.ExternalVolumeSource{ + External: &ateletpb.ExternalVolume{ StorageVolumeId: "mock-vol-1", VolumeType: "mock-driver", }, } extVol2 := &ateletpb.Volume{ Name: "vol-2", - External: &ateletpb.ExternalVolumeSource{ + External: &ateletpb.ExternalVolume{ StorageVolumeId: "mock-vol-2", VolumeType: "mock-driver", }, @@ -552,3 +552,23 @@ func TestDirectCSIDriverConfigGetter(t *testing.T) { t.Fatalf("expected error for missing driver, got nil") } } + +func TestResetEmptyDirClearsPreviousContents(t *testing.T) { + dir := filepath.Join(t.TempDir(), "volumes", "shared") + if err := os.MkdirAll(dir, 0o750); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "stale"), []byte("x"), 0o600); err != nil { + t.Fatal(err) + } + if err := resetEmptyDir(dir); err != nil { + t.Fatalf("resetEmptyDir: %v", err) + } + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("ReadDir: %v", err) + } + if len(entries) != 0 { + t.Fatalf("dir still has %d entries after reset", len(entries)) + } +} diff --git a/cmd/ateworker-gvisor/main.go b/cmd/ateworker-gvisor/main.go index be216acb61..3036a1df0d 100644 --- a/cmd/ateworker-gvisor/main.go +++ b/cmd/ateworker-gvisor/main.go @@ -52,9 +52,7 @@ import ( "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/internal/serverboot" - "github.com/agent-substrate/substrate/internal/sizing" "github.com/agent-substrate/substrate/internal/version" - "github.com/agent-substrate/substrate/internal/wakeupprobe" "github.com/agent-substrate/substrate/internal/workerstats" atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" "github.com/spf13/pflag" @@ -580,137 +578,6 @@ func validateFidelity(fidelity ateworkerpb.SnapshotFidelity) error { return nil } -func (s *AteWorkerService) RunWorkload(ctx context.Context, req *ateworkerpb.RunWorkloadRequest) (resp *ateworkerpb.RunWorkloadResponse, retErr error) { - if err := validateActorDirs(req.GetActorDirs()); err != nil { - return nil, err - } - if err := validateRunscPath(req.GetRunscPath()); err != nil { - return nil, err - } - if !s.locks.Lock(ctx, req.GetActorUid()) { - return nil, fmt.Errorf("gave up waiting for the actor's lock: %w", ctx.Err()) - } - defer s.locks.Unlock(req.GetActorUid()) - ctx, cancel := context.WithCancel(ctx) - defer cancel() - release, err := s.beginRPC(req.GetActorUid(), rpcRunWorkload, cancel) - if err != nil { - return nil, err - } - defer release() - if err := resetRunscStateAndPidFileDirs(req.GetActorDirs()); err != nil { - return nil, err - } - - if err := s.tunnel.Deactivate(ctx, workerstats.ActorAttributionFromRequest(req)); err != nil { - return nil, err - } - - attribution := workerstats.ActorAttributionFromRequest(req) - s.actorLogger.EmitLifecycleLog(ctx, "Actor starting", attribution) - - // Contract with atelet: - // - // * Correct runsc version is downloaded and placed on disk. - // * All OCI bundles are set up, including for the pause container. - - egress, err := s.tunnel.PrepareEgress(ctx, workerstats.ActorAttributionFromRequest(req), req.GetEgressGateway()) - if err != nil { - return nil, err - } - // Publish attribution before boot so stats can include startup usage. - hosted, err := s.hostActor(ctx, attribution, req.GetActorDirs()) - if err != nil { - return nil, err - } - rcmd := &runsc{ - path: req.GetRunscPath(), - actorUID: req.GetActorUid(), - actorDirs: req.GetActorDirs(), - size: sizing.FromLimits(req.GetCpuMilli(), req.GetMemoryBytes()), - durableVolumes: durableVolumeNames(req.GetSpec()), - containers: req.GetSpec().GetContainers(), - flags: s.flags, - } - var containersToDelete []string - defer func() { - if retErr != nil { - cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) - defer cancel() - if err := s.tunnel.Deactivate(cleanupCtx, workerstats.ActorAttributionFromRequest(req)); err != nil { - slog.WarnContext(cleanupCtx, "Failed to deactivate actor networking after Run failure", slog.Any("err", err)) - } - deleteContainers(cleanupCtx, rcmd, containersToDelete, "Run") - // Detach any bundle rootfs overlays a partially-completed setup - // mounted, mirroring the post-checkpoint cleanup — otherwise they - // linger in this namespace until atelet wipes the bundle dirs. - // Run before the network cleanup. - if err := imagecache.UnmountAllUnder(req.GetActorDirs().GetOciBundleDir()); err != nil { - slog.WarnContext(ctx, "Failed to unmount bundle rootfs overlays after Run failure", - "actorUID", req.GetActorUid(), "err", err) - } - if err := s.unhostActor(cleanupCtx, req.GetActorUid()); err != nil { - slog.WarnContext(cleanupCtx, "Failed to clean up actor network after Run failure", slog.Any("err", err)) - } - } - }() - // Create and start pause container. The bundle rootfs is composed here — - // an overlay of the node's cached image layers plus the bundle's private - // upper — because mounting is ateworker's job (atelet runs with no - // capabilities); runsc's gofer resolves the mount in this pod's mount - // namespace. - if err := imagecache.SetupBundleRootfs(ociBundlePath(req.GetActorDirs(), ocispec.PauseContainer)); err != nil { - return nil, fmt.Errorf("while composing pause rootfs: %w", err) - } - containersToDelete = append(containersToDelete, ocispec.PauseContainer) - // Attribute runsc CLI output and the pause container's stdio to the actor. - pw, err := s.actorLogger.StartJSONLogPipe(attribution, ocispec.PauseContainer) - if err != nil { - return nil, fmt.Errorf("while starting pause log pipe: %w", err) - } - defer pw.Close() - if err := rcmd.cmdCreate(ctx, pw, ocispec.PauseContainer, nil); err != nil { - return nil, fmt.Errorf("while creating pause container: %w", err) - } - if err := rcmd.cmdStart(ctx, pw, ocispec.PauseContainer); err != nil { - return nil, fmt.Errorf("while starting pause container: %w", err) - } - - // Create and start each application container, each with its own log pipe so - // every line is tagged with the originating container (ate.actor.container.name). - for _, ac := range req.GetSpec().GetContainers() { - pw, err := s.actorLogger.StartJSONLogPipe(attribution, ac.GetName()) - if err != nil { - return nil, fmt.Errorf("while starting json log pipe for %q: %w", ac.GetName(), err) - } - defer pw.Close() - if err := imagecache.SetupBundleRootfs(ociBundlePath(req.GetActorDirs(), ac.GetName())); err != nil { - return nil, fmt.Errorf("while composing %q rootfs: %w", ac.GetName(), err) - } - containersToDelete = append(containersToDelete, ac.GetName()) - if err := rcmd.cmdCreate(ctx, pw, ac.GetName(), nil); err != nil { - return nil, fmt.Errorf("while creating %q application container: %w", ac.GetName(), err) - } - if err := rcmd.cmdStart(ctx, pw, ac.GetName()); err != nil { - return nil, fmt.Errorf("while starting %q application container: %w", ac.GetName(), err) - } - } - - // Block until every wakeup-probe-enabled container reports 200. - if err := wakeupprobe.WaitAll(ctx, req.GetSpec().GetContainers(), actornet.ActorVethIP, wakeupprobe.DialFunc(s.sandboxDialer(req.GetActorUid()))); err != nil { - return nil, fmt.Errorf("while waiting for container wakeup probe: %w", err) - } - if err := s.tunnel.Activate(workerstats.ActorAttributionFromRequest(req), s.sandboxDialer(req.GetActorUid()), egress); err != nil { - return nil, err - } - - s.actorLogger.EmitLifecycleLog(ctx, "Actor started", attribution) - s.setSession(req.GetActorUid(), &workloadSession{rcmd: rcmd, containers: containerNames(req.GetSpec().GetContainers())}) - s.recordInitial(ctx, hosted) - - return &ateworkerpb.RunWorkloadResponse{}, nil -} - // Allow checkpointing even if the pod is shutting down. This will allow actors // (or the harness) to suspend on shutdown. func (s *AteWorkerService) CheckpointWorkload(ctx context.Context, req *ateworkerpb.CheckpointWorkloadRequest) (_ *ateworkerpb.CheckpointWorkloadResponse, err error) { @@ -730,6 +597,11 @@ func (s *AteWorkerService) CheckpointWorkload(ctx context.Context, req *ateworke } defer s.locks.Unlock(req.GetActorUid()) + runtimeGolden := req.GetSpec().GetRuntimeGolden() + if runtimeGolden && req.GetFidelity() != ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY { + return nil, fmt.Errorf("runtime golden capture requires MEMORY fidelity") + } + ctx, cancel := context.WithCancel(ctx) defer cancel() // Not cancelable: a checkpoint is saving the actor's state. @@ -781,6 +653,16 @@ func (s *AteWorkerService) CheckpointWorkload(ctx context.Context, req *ateworke containers := req.GetSpec().GetContainers() durablePaths := durableFSCheckpointPaths(containers) + if runtimeGolden { + durablePaths = nil + } + // Persist the format before checkpointing: runsc stops the sandbox on + // success, so a later marker failure would make that snapshot unusable. + if runtimeGolden { + if err := os.WriteFile(filepath.Join(checkpointPath, runtimeGoldenFile), []byte(runtimeGoldenFormat), 0o600); err != nil { + return nil, fmt.Errorf("writing runtime golden marker: %w", err) + } + } switch fidelity { case ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES: if len(durablePaths) == 0 { @@ -925,208 +807,6 @@ func isContainerAlreadyGone(ctx context.Context, rcmd containerRuntime, name str return !slices.Contains(ids, name), nil } -func (s *AteWorkerService) RestoreWorkload(ctx context.Context, req *ateworkerpb.RestoreWorkloadRequest) (resp *ateworkerpb.RestoreWorkloadResponse, retErr error) { - // Taken before the lock, so prep covers the wait for it. - tStart := time.Now() - if err := validateActorDirs(req.GetActorDirs()); err != nil { - return nil, err - } - if err := validateFidelity(req.GetFidelity()); err != nil { - return nil, err - } - if err := validateRunscPath(req.GetRunscPath()); err != nil { - return nil, err - } - if !s.locks.Lock(ctx, req.GetActorUid()) { - return nil, fmt.Errorf("gave up waiting for the actor's lock: %w", ctx.Err()) - } - defer s.locks.Unlock(req.GetActorUid()) - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - // Per-phase timing, recorded on the way out so a failed restore still - // reports the phases it completed, and the failing step its elapsed time. - // Registered ahead of the failure cleanup below, so on failure the total - // includes that cleanup; the record is marked by error.type either way. - attribution := workerstats.ActorAttributionFromRequest(req) - fidelity := req.GetFidelity() - containers := req.GetSpec().GetContainers() - var timing restoreTiming - tLast := tStart - defer func() { - timing.total = time.Since(tStart) - attrs := phaselog.SnapshotPhaseAttrs(attribution, fidelity, phaselog.RestoreDurationKey, retErr, timing.phases()) - attrs = append(attrs, slog.Int(containerCountKey, len(containers))) - slog.LogAttrs(ctx, slog.LevelInfo, "Restore timing breakdown", attrs...) - }() - - release, err := s.beginRPC(req.GetActorUid(), rpcRestoreWorkload, cancel) - if err != nil { - return nil, err - } - defer release() - if err := resetRunscStateAndPidFileDirs(req.GetActorDirs()); err != nil { - return nil, err - } - - err = s.tunnel.Deactivate(ctx, attribution) - timing.prep = lap(&tLast) - if err != nil { - return nil, err - } - - s.actorLogger.EmitLifecycleLog(ctx, "Actor restoring", attribution) - - // Contract with atelet: - // - // * Correct runsc version is downloaded and placed on disk. - // * All OCI bundles are set up, including for the pause container. - // * Checkpoint downloaded and placed on disk - - egress, err := s.tunnel.PrepareEgress(ctx, attribution, req.GetEgressGateway()) - timing.egressPrepare = lap(&tLast) - if err != nil { - return nil, err - } - hosted, err := s.hostActor(ctx, attribution, req.GetActorDirs()) - timing.netSetup = lap(&tLast) - if err != nil { - return nil, err - } - rcmd := &runsc{ - path: req.GetRunscPath(), - actorUID: req.GetActorUid(), - actorDirs: req.GetActorDirs(), - size: sizing.FromLimits(req.GetCpuMilli(), req.GetMemoryBytes()), - durableVolumes: durableVolumeNames(req.GetSpec()), - containers: req.GetSpec().GetContainers(), - flags: s.flags, - } - var containersToDelete []string - defer func() { - if retErr != nil { - cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) - defer cancel() - if err := s.tunnel.Deactivate(cleanupCtx, attribution); err != nil { - slog.WarnContext(cleanupCtx, "Failed to deactivate actor networking after Restore failure", slog.Any("err", err)) - } - deleteContainers(cleanupCtx, rcmd, containersToDelete, "Restore") - // Same overlay detach as the Run-failure path above. - if err := imagecache.UnmountAllUnder(req.GetActorDirs().GetOciBundleDir()); err != nil { - slog.WarnContext(ctx, "Failed to unmount bundle rootfs overlays after Restore failure", - "actorUID", req.GetActorUid(), "err", err) - } - if err := s.unhostActor(cleanupCtx, req.GetActorUid()); err != nil { - slog.WarnContext(cleanupCtx, "Failed to clean up actor network after Restore failure", slog.Any("err", err)) - } - } - }() - checkpointDir := req.GetActorDirs().GetRestoreDir() - restoreArgs, err := fsRestoreArgs(checkpointDir) - if err != nil { - return nil, err - } - - // Compose the pause rootfs before create (see RunWorkload). runsc restore - // only needs the rootfs to hold the correct content; whether it came from - // an untar or an overlay of cached layers is transparent to it. - err = imagecache.SetupBundleRootfs(ociBundlePath(req.GetActorDirs(), ocispec.PauseContainer)) - timing.pauseRootfs = lap(&tLast) - if err != nil { - return nil, fmt.Errorf("while composing pause rootfs: %w", err) - } - - // Attribute runsc CLI output and the pause container's stdio to the actor. - pw, err := s.actorLogger.StartJSONLogPipe(attribution, ocispec.PauseContainer) - if err != nil { - return nil, fmt.Errorf("while starting pause log pipe: %w", err) - } - defer pw.Close() - switch fidelity { - case ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES: - // Create and start pause container (cold boot with durable-dir volumes restored from fs/) - containersToDelete = append(containersToDelete, ocispec.PauseContainer) - err := rcmd.cmdCreate(ctx, pw, ocispec.PauseContainer, restoreArgs) - timing.pauseCreate = lap(&tLast) - if err != nil { - return nil, fmt.Errorf("while creating pause container: %w", err) - } - err = rcmd.cmdStart(ctx, pw, ocispec.PauseContainer) - timing.pauseRestore = lap(&tLast) - if err != nil { - return nil, fmt.Errorf("while starting pause container: %w", err) - } - case ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY: - // Create and restore pause container (restores both process/rootfs and fs/ durable-dir volumes) - containersToDelete = append(containersToDelete, ocispec.PauseContainer) - err := rcmd.cmdCreate(ctx, pw, ocispec.PauseContainer, restoreArgs) - timing.pauseCreate = lap(&tLast) - if err != nil { - return nil, fmt.Errorf("while creating pause container: %w", err) - } - err = rcmd.cmdRestore(ctx, pw, ocispec.PauseContainer, checkpointDir) - timing.pauseRestore = lap(&tLast) - if err != nil { - return nil, fmt.Errorf("while restoring pause container: %w", err) - } - } - - // Create and restore each application container, each with its own log pipe so - // every line is tagged with the originating container (ate.actor.container.name). - // app_create includes starting that pipe. - for _, ac := range containers { - pw, err := s.actorLogger.StartJSONLogPipe(attribution, ac.GetName()) - timing.appCreate += lap(&tLast) - if err != nil { - return nil, fmt.Errorf("while starting json log pipe for %q: %w", ac.GetName(), err) - } - defer pw.Close() - err = imagecache.SetupBundleRootfs(ociBundlePath(req.GetActorDirs(), ac.GetName())) - timing.appRootfs += lap(&tLast) - if err != nil { - return nil, fmt.Errorf("while composing %q rootfs: %w", ac.GetName(), err) - } - containersToDelete = append(containersToDelete, ac.GetName()) - err = rcmd.cmdCreate(ctx, pw, ac.GetName(), nil) - timing.appCreate += lap(&tLast) - if err != nil { - return nil, fmt.Errorf("while creating %q application container: %w", ac.GetName(), err) - } - switch fidelity { - case ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES: - err = rcmd.cmdStart(ctx, pw, ac.GetName()) - timing.appRestore += lap(&tLast) - if err != nil { - return nil, fmt.Errorf("while starting %q application container: %w", ac.GetName(), err) - } - case ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY: - err = rcmd.cmdRestore(ctx, pw, ac.GetName(), checkpointDir) - timing.appRestore += lap(&tLast) - if err != nil { - return nil, fmt.Errorf("while restoring %q application container: %w", ac.GetName(), err) - } - } - } - - // Block until every wakeup-probe-enabled container reports 200. - err = wakeupprobe.WaitAll(ctx, containers, actornet.ActorVethIP, wakeupprobe.DialFunc(s.sandboxDialer(req.GetActorUid()))) - timing.wakeupProbe = lap(&tLast) - if err != nil { - return nil, fmt.Errorf("while waiting for container wakeup probe: %w", err) - } - err = s.tunnel.Activate(attribution, s.sandboxDialer(req.GetActorUid()), egress) - timing.activate = lap(&tLast) - if err != nil { - return nil, err - } - - s.actorLogger.EmitLifecycleLog(ctx, "Actor restored", attribution) - s.setSession(req.GetActorUid(), &workloadSession{rcmd: rcmd, containers: containerNames(containers)}) - s.recordInitial(ctx, hosted) - - return &ateworkerpb.RestoreWorkloadResponse{}, nil -} - func (s *AteWorkerService) TerminateWorkload(ctx context.Context, req *ateworkerpb.TerminateWorkloadRequest) (*ateworkerpb.TerminateWorkloadResponse, error) { if err := validateActorDirs(req.GetActorDirs()); err != nil { return nil, err diff --git a/cmd/ateworker-gvisor/restore.go b/cmd/ateworker-gvisor/restore.go new file mode 100644 index 0000000000..1664678a81 --- /dev/null +++ b/cmd/ateworker-gvisor/restore.go @@ -0,0 +1,240 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "log/slog" + "time" + + "github.com/agent-substrate/substrate/internal/ateworker/actornet" + "github.com/agent-substrate/substrate/internal/ateworker/phaselog" + "github.com/agent-substrate/substrate/internal/imagecache" + "github.com/agent-substrate/substrate/internal/ocispec" + "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "github.com/agent-substrate/substrate/internal/sizing" + "github.com/agent-substrate/substrate/internal/wakeupprobe" + "github.com/agent-substrate/substrate/internal/workerstats" +) + +func (s *AteWorkerService) RestoreWorkload(ctx context.Context, req *ateworkerpb.RestoreWorkloadRequest) (resp *ateworkerpb.RestoreWorkloadResponse, retErr error) { + // Taken before the lock, so prep covers the wait for it. + tStart := time.Now() + if err := validateActorDirs(req.GetActorDirs()); err != nil { + return nil, err + } + if err := validateFidelity(req.GetFidelity()); err != nil { + return nil, err + } + if err := validateRunscPath(req.GetRunscPath()); err != nil { + return nil, err + } + if !s.locks.Lock(ctx, req.GetActorUid()) { + return nil, fmt.Errorf("gave up waiting for the actor's lock: %w", ctx.Err()) + } + defer s.locks.Unlock(req.GetActorUid()) + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + // Per-phase timing, recorded on the way out so a failed restore still + // reports the phases it completed, and the failing step its elapsed time. + // Registered ahead of the failure cleanup below, so on failure the total + // includes that cleanup; the record is marked by error.type either way. + attribution := workerstats.ActorAttributionFromRequest(req) + fidelity := req.GetFidelity() + containers := req.GetSpec().GetContainers() + var timing restoreTiming + tLast := tStart + defer func() { + timing.total = time.Since(tStart) + attrs := phaselog.SnapshotPhaseAttrs(attribution, fidelity, phaselog.RestoreDurationKey, retErr, timing.phases()) + attrs = append(attrs, slog.Int(containerCountKey, len(containers))) + slog.LogAttrs(ctx, slog.LevelInfo, "Restore timing breakdown", attrs...) + }() + + runtimeGolden, err := readRuntimeGolden(req.GetActorDirs().GetRestoreDir()) + if err != nil { + return nil, err + } + restoreAppMemory, err := restoreApplicationMemory(fidelity, runtimeGolden) + if err != nil { + return nil, err + } + + release, err := s.beginRPC(req.GetActorUid(), rpcRestoreWorkload, cancel) + if err != nil { + return nil, err + } + defer release() + if err := resetRunscStateAndPidFileDirs(req.GetActorDirs()); err != nil { + return nil, err + } + + err = s.tunnel.Deactivate(ctx, attribution) + timing.prep = lap(&tLast) + if err != nil { + return nil, err + } + + s.actorLogger.EmitLifecycleLog(ctx, "Actor restoring", attribution) + + // Contract with atelet: + // + // * Correct runsc version is downloaded and placed on disk. + // * All OCI bundles are set up, including for the pause container. + // * Checkpoint downloaded and placed on disk + + egress, err := s.tunnel.PrepareEgress(ctx, attribution, req.GetEgressGateway()) + timing.egressPrepare = lap(&tLast) + if err != nil { + return nil, err + } + hosted, err := s.hostActor(ctx, attribution, req.GetActorDirs()) + timing.netSetup = lap(&tLast) + if err != nil { + return nil, err + } + rcmd := &runsc{ + path: req.GetRunscPath(), + actorUID: req.GetActorUid(), + actorDirs: req.GetActorDirs(), + size: sizing.FromLimits(req.GetCpuMilli(), req.GetMemoryBytes()), + durableVolumes: durableVolumeNames(req.GetSpec()), + containers: req.GetSpec().GetContainers(), + } + var containersToDelete []string + defer func() { + if retErr != nil { + cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + defer cancel() + if err := s.tunnel.Deactivate(cleanupCtx, attribution); err != nil { + slog.WarnContext(cleanupCtx, "Failed to deactivate actor networking after Restore failure", slog.Any("err", err)) + } + deleteContainers(cleanupCtx, rcmd, containersToDelete, "Restore") + // Same overlay detach as the Run-failure path above. + if err := imagecache.UnmountAllUnder(req.GetActorDirs().GetOciBundleDir()); err != nil { + slog.WarnContext(ctx, "Failed to unmount bundle rootfs overlays after Restore failure", + "actorUID", req.GetActorUid(), "err", err) + } + if err := s.unhostActor(cleanupCtx, req.GetActorUid()); err != nil { + slog.WarnContext(cleanupCtx, "Failed to clean up actor network after Restore failure", slog.Any("err", err)) + } + } + }() + checkpointDir := req.GetActorDirs().GetRestoreDir() + restoreArgs, err := fsRestoreArgs(checkpointDir) + if err != nil { + return nil, err + } + + // Compose the pause rootfs before create (see RunWorkload). runsc restore + // only needs the rootfs to hold the correct content; whether it came from + // an untar or an overlay of cached layers is transparent to it. + err = imagecache.SetupBundleRootfs(ociBundlePath(req.GetActorDirs(), ocispec.PauseContainer)) + timing.pauseRootfs = lap(&tLast) + if err != nil { + return nil, fmt.Errorf("while composing pause rootfs: %w", err) + } + + // Preserve per-actor attribution for pause-container runtime output. + pw, err := s.actorLogger.StartJSONLogPipe(attribution, ocispec.PauseContainer) + if err != nil { + return nil, fmt.Errorf("while starting pause log pipe: %w", err) + } + defer pw.Close() + switch fidelity { + case ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES: + // Create and start pause container (cold boot with durable-dir volumes restored from fs/) + containersToDelete = append(containersToDelete, ocispec.PauseContainer) + err := rcmd.cmdCreate(ctx, pw, ocispec.PauseContainer, restoreArgs) + timing.pauseCreate = lap(&tLast) + if err != nil { + return nil, fmt.Errorf("while creating pause container: %w", err) + } + err = rcmd.cmdStart(ctx, pw, ocispec.PauseContainer) + timing.pauseRestore = lap(&tLast) + if err != nil { + return nil, fmt.Errorf("while starting pause container: %w", err) + } + case ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY: + // Create and restore pause container (restores both process/rootfs and fs/ durable-dir volumes) + containersToDelete = append(containersToDelete, ocispec.PauseContainer) + err := rcmd.cmdCreate(ctx, pw, ocispec.PauseContainer, restoreArgs) + timing.pauseCreate = lap(&tLast) + if err != nil { + return nil, fmt.Errorf("while creating pause container: %w", err) + } + err = rcmd.cmdRestore(ctx, pw, ocispec.PauseContainer, checkpointDir) + timing.pauseRestore = lap(&tLast) + if err != nil { + return nil, fmt.Errorf("while restoring pause container: %w", err) + } + default: + return nil, fmt.Errorf("unexpected snapshot fidelity: %v", fidelity) + } + + // Create and restore each application container, each with its own log pipe so + // every line is tagged with the originating container (ate.actor.container.name). + // app_create includes starting that pipe. + for _, ac := range containers { + pw, err := s.actorLogger.StartJSONLogPipe(attribution, ac.GetName()) + timing.appCreate += lap(&tLast) + if err != nil { + return nil, fmt.Errorf("while starting json log pipe for %q: %w", ac.GetName(), err) + } + defer pw.Close() + err = imagecache.SetupBundleRootfs(ociBundlePath(req.GetActorDirs(), ac.GetName())) + timing.appRootfs += lap(&tLast) + if err != nil { + return nil, fmt.Errorf("while composing %q rootfs: %w", ac.GetName(), err) + } + containersToDelete = append(containersToDelete, ac.GetName()) + err = rcmd.cmdCreate(ctx, pw, ac.GetName(), nil) + timing.appCreate += lap(&tLast) + if err != nil { + return nil, fmt.Errorf("while creating %q application container: %w", ac.GetName(), err) + } + if restoreAppMemory { + err = rcmd.cmdRestore(ctx, pw, ac.GetName(), checkpointDir) + } else { + err = rcmd.cmdStart(ctx, pw, ac.GetName()) + } + timing.appRestore += lap(&tLast) + if err != nil { + return nil, fmt.Errorf("while activating %q application container: %w", ac.GetName(), err) + } + } + + // Block until every wakeup-probe-enabled container reports 200. + err = wakeupprobe.WaitAll(ctx, containers, actornet.ActorVethIP, wakeupprobe.DialFunc(s.sandboxDialer(req.GetActorUid()))) + timing.wakeupProbe = lap(&tLast) + if err != nil { + return nil, fmt.Errorf("while waiting for container wakeup probe: %w", err) + } + err = s.tunnel.Activate(attribution, s.sandboxDialer(req.GetActorUid()), egress) + timing.activate = lap(&tLast) + if err != nil { + return nil, err + } + + s.actorLogger.EmitLifecycleLog(ctx, "Actor restored", attribution) + s.setSession(req.GetActorUid(), &workloadSession{rcmd: rcmd, containers: containerNames(containers)}) + s.recordInitial(ctx, hosted) + + return &ateworkerpb.RestoreWorkloadResponse{}, nil +} diff --git a/cmd/ateworker-gvisor/run.go b/cmd/ateworker-gvisor/run.go new file mode 100644 index 0000000000..ae37de18a6 --- /dev/null +++ b/cmd/ateworker-gvisor/run.go @@ -0,0 +1,171 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "log/slog" + "time" + + "github.com/agent-substrate/substrate/internal/ateworker/actornet" + "github.com/agent-substrate/substrate/internal/imagecache" + "github.com/agent-substrate/substrate/internal/ocispec" + "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/internal/sizing" + "github.com/agent-substrate/substrate/internal/wakeupprobe" + "github.com/agent-substrate/substrate/internal/workerstats" +) + +func (s *AteWorkerService) RunWorkload(ctx context.Context, req *ateworkerpb.RunWorkloadRequest) (resp *ateworkerpb.RunWorkloadResponse, retErr error) { + if req.GetSpec().GetRuntimeGolden() && req.GetAtespace() != resources.GoldenActorAtespace { + return nil, fmt.Errorf("runtime golden capture requires a golden actor") + } + if err := validateActorDirs(req.GetActorDirs()); err != nil { + return nil, err + } + if err := validateRunscPath(req.GetRunscPath()); err != nil { + return nil, err + } + if !s.locks.Lock(ctx, req.GetActorUid()) { + return nil, fmt.Errorf("gave up waiting for the actor's lock: %w", ctx.Err()) + } + defer s.locks.Unlock(req.GetActorUid()) + ctx, cancel := context.WithCancel(ctx) + defer cancel() + release, err := s.beginRPC(req.GetActorUid(), rpcRunWorkload, cancel) + if err != nil { + return nil, err + } + defer release() + if err := resetRunscStateAndPidFileDirs(req.GetActorDirs()); err != nil { + return nil, err + } + + if err := s.tunnel.Deactivate(ctx, workerstats.ActorAttributionFromRequest(req)); err != nil { + return nil, err + } + + attribution := workerstats.ActorAttributionFromRequest(req) + s.actorLogger.EmitLifecycleLog(ctx, "Actor starting", attribution) + + // Contract with atelet: + // + // * Correct runsc version is downloaded and placed on disk. + // * All OCI bundles are set up, including for the pause container. + + egress, err := s.tunnel.PrepareEgress(ctx, workerstats.ActorAttributionFromRequest(req), req.GetEgressGateway()) + if err != nil { + return nil, err + } + // Publish attribution before boot so stats can include startup usage. + hosted, err := s.hostActor(ctx, attribution, req.GetActorDirs()) + if err != nil { + return nil, err + } + rcmd := &runsc{ + path: req.GetRunscPath(), + actorUID: req.GetActorUid(), + actorDirs: req.GetActorDirs(), + size: sizing.FromLimits(req.GetCpuMilli(), req.GetMemoryBytes()), + durableVolumes: durableVolumeNames(req.GetSpec()), + containers: req.GetSpec().GetContainers(), + } + var containersToDelete []string + defer func() { + if retErr != nil { + cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + defer cancel() + if err := s.tunnel.Deactivate(cleanupCtx, workerstats.ActorAttributionFromRequest(req)); err != nil { + slog.WarnContext(cleanupCtx, "Failed to deactivate actor networking after Run failure", slog.Any("err", err)) + } + deleteContainers(cleanupCtx, rcmd, containersToDelete, "Run") + // Detach any bundle rootfs overlays a partially-completed setup + // mounted, mirroring the post-checkpoint cleanup — otherwise they + // linger in this namespace until atelet wipes the bundle dirs. + // Run before the network cleanup. + if err := imagecache.UnmountAllUnder(req.GetActorDirs().GetOciBundleDir()); err != nil { + slog.WarnContext(ctx, "Failed to unmount bundle rootfs overlays after Run failure", + "actorUID", req.GetActorUid(), "err", err) + } + if err := s.unhostActor(cleanupCtx, req.GetActorUid()); err != nil { + slog.WarnContext(cleanupCtx, "Failed to clean up actor network after Run failure", slog.Any("err", err)) + } + } + }() + // Create and start pause container. The bundle rootfs is composed here — + // an overlay of the node's cached image layers plus the bundle's private + // upper — because mounting is ateworker's job (atelet runs with no + // capabilities); runsc's gofer resolves the mount in this pod's mount + // namespace. + if err := imagecache.SetupBundleRootfs(ociBundlePath(req.GetActorDirs(), ocispec.PauseContainer)); err != nil { + return nil, fmt.Errorf("while composing pause rootfs: %w", err) + } + // Preserve per-actor attribution for pause-container runtime output. + pw, err := s.actorLogger.StartJSONLogPipe(attribution, ocispec.PauseContainer) + if err != nil { + return nil, fmt.Errorf("while starting pause log pipe: %w", err) + } + defer pw.Close() + containersToDelete = append(containersToDelete, ocispec.PauseContainer) + if err := rcmd.cmdCreate(ctx, pw, ocispec.PauseContainer, nil); err != nil { + return nil, fmt.Errorf("while creating pause container: %w", err) + } + if err := rcmd.cmdStart(ctx, pw, ocispec.PauseContainer); err != nil { + return nil, fmt.Errorf("while starting pause container: %w", err) + } + + if req.GetSpec().GetRuntimeGolden() { + s.setSession(req.GetActorUid(), &workloadSession{rcmd: rcmd}) + return &ateworkerpb.RunWorkloadResponse{}, nil + } + + // Create and start each application container, each with its own log pipe so + // every line is tagged with the originating container (ate.actor.container.name). + for _, ac := range req.GetSpec().GetContainers() { + pw, err := s.actorLogger.StartJSONLogPipe(attribution, ac.GetName()) + if err != nil { + return nil, fmt.Errorf("while starting json log pipe for %q: %w", ac.GetName(), err) + } + defer pw.Close() + if err := imagecache.SetupBundleRootfs(ociBundlePath(req.GetActorDirs(), ac.GetName())); err != nil { + return nil, fmt.Errorf("while composing %q rootfs: %w", ac.GetName(), err) + } + containersToDelete = append(containersToDelete, ac.GetName()) + if err := rcmd.cmdCreate(ctx, pw, ac.GetName(), nil); err != nil { + return nil, fmt.Errorf("while creating %q application container: %w", ac.GetName(), err) + } + if err := rcmd.cmdStart(ctx, pw, ac.GetName()); err != nil { + return nil, fmt.Errorf("while starting %q application container: %w", ac.GetName(), err) + } + } + + // Block until every wakeup-probe-enabled container reports 200. + if err := wakeupprobe.WaitAll(ctx, req.GetSpec().GetContainers(), actornet.ActorVethIP, wakeupprobe.DialFunc(s.sandboxDialer(req.GetActorUid()))); err != nil { + return nil, fmt.Errorf("while waiting for container wakeup probe: %w", err) + } + if err := s.tunnel.Activate(workerstats.ActorAttributionFromRequest(req), s.sandboxDialer(req.GetActorUid()), egress); err != nil { + return nil, err + } + + s.actorLogger.EmitLifecycleLog(ctx, "Actor started", attribution) + s.setSession(req.GetActorUid(), &workloadSession{rcmd: rcmd, containers: containerNames(req.GetSpec().GetContainers())}) + s.recordInitial(ctx, hosted) + + return &ateworkerpb.RunWorkloadResponse{}, nil +} diff --git a/cmd/ateworker-gvisor/runtime_golden.go b/cmd/ateworker-gvisor/runtime_golden.go new file mode 100644 index 0000000000..e914239555 --- /dev/null +++ b/cmd/ateworker-gvisor/runtime_golden.go @@ -0,0 +1,50 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "os" + "path/filepath" +) + +const runtimeGoldenFile = "runtime-golden" +const runtimeGoldenFormat = "gvisor-pause-only-v1\n" + +func readRuntimeGolden(dir string) (bool, error) { + b, err := os.ReadFile(filepath.Join(dir, runtimeGoldenFile)) + if os.IsNotExist(err) { + return false, nil + } + if err != nil { + return false, err + } + if string(b) != runtimeGoldenFormat { + return false, fmt.Errorf("invalid runtime golden marker") + } + return true, nil +} + +// A pause-only checkpoint has no application state. Its applications start +// fresh after the sandbox restore and use the destination's mounted volumes. +func restoreApplicationMemory(fidelity ateworkerpb.SnapshotFidelity, runtimeGolden bool) (bool, error) { + if runtimeGolden && fidelity != ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY { + return false, fmt.Errorf("runtime golden restore requires MEMORY fidelity") + } + return fidelity == ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY && !runtimeGolden, nil +} diff --git a/cmd/ateworker-gvisor/runtime_golden_test.go b/cmd/ateworker-gvisor/runtime_golden_test.go new file mode 100644 index 0000000000..676f5a35eb --- /dev/null +++ b/cmd/ateworker-gvisor/runtime_golden_test.go @@ -0,0 +1,110 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "github.com/agent-substrate/substrate/internal/actorlock" + "github.com/agent-substrate/substrate/internal/nodepath" + "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestRuntimeGoldenMarker(t *testing.T) { + for _, tc := range []struct { + name, content string + present, want, bad bool + }{ + {name: "ordinary snapshot"}, + {name: "pause only", content: runtimeGoldenFormat, present: true, want: true}, + {name: "wrong runtime", content: "microvm-before-create-sandbox-v1\n", present: true, bad: true}, + {name: "empty", present: true, bad: true}, + } { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + if tc.present { + if err := os.WriteFile(filepath.Join(dir, runtimeGoldenFile), []byte(tc.content), 0600); err != nil { + t.Fatal(err) + } + } + got, err := readRuntimeGolden(dir) + if got != tc.want || (err != nil) != tc.bad { + t.Fatalf("got %v, %v", got, err) + } + }) + } +} + +func TestRuntimeGoldenApplicationRestore(t *testing.T) { + memory := ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY + volumes := ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES + for _, tc := range []struct { + name string + capture bool + fidelity ateworkerpb.SnapshotFidelity + want bool + bad bool + }{ + {"own memory restores apps", false, memory, true, false}, + {"own volumes starts apps", false, volumes, false, false}, + {"runtime memory starts apps", true, memory, false, false}, + {"runtime volumes rejected", true, volumes, false, true}, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := restoreApplicationMemory(tc.fidelity, tc.capture) + if got != tc.want || (err != nil) != tc.bad { + t.Fatalf("got %v, %v", got, err) + } + }) + } +} + +func TestRuntimeGoldenRejectsNormalActorBeforeMutation(t *testing.T) { + s := &AteWorkerService{} + _, err := s.RunWorkload(context.Background(), &ateworkerpb.RunWorkloadRequest{Spec: &ateworkerpb.WorkloadSpec{RuntimeGolden: true}}) + if err == nil { + t.Fatal("normal actor allowed to capture runtime-only snapshot") + } +} + +func TestRuntimeGoldenCheckpointRejectsVolumesBeforeMutation(t *testing.T) { + originalStaticDir := nodepath.StaticFilesDir + nodepath.StaticFilesDir = t.TempDir() + t.Cleanup(func() { nodepath.StaticFilesDir = originalStaticDir }) + if err := os.WriteFile(filepath.Join(nodepath.StaticFilesDir, "runsc"), nil, 0700); err != nil { + t.Fatal(err) + } + s := &AteWorkerService{locks: actorlock.New(), actors: map[string]*hostedActor{}} + dirs := &ateworkerpb.ActorDirs{ + RootDir: "/node/actors/a", OciBundleDir: "/node/actors/a/bundle", + CheckpointDir: "/node/actors/a/checkpoint-state", RestoreDir: "/node/actors/a/restore-state", + DurableDirVolumeMountsDir: "/node/actors/a/durable-dirs", SystemInfoVolumeRootsDir: "/node/actors/a/system-info", + VolumesDir: "/node/actors/a/volumes", + } + // All mutation dependencies are nil; touching networking or runtime would panic. + _, err := s.CheckpointWorkload(context.Background(), &ateworkerpb.CheckpointWorkloadRequest{ + ActorUid: "a", ActorDirs: dirs, RunscPath: filepath.Join(nodepath.StaticFilesDir, "runsc"), + Fidelity: ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES, + Spec: &ateworkerpb.WorkloadSpec{RuntimeGolden: true}, + }) + if err == nil || !strings.Contains(err.Error(), "requires MEMORY") { + t.Fatalf("got %v", err) + } +} diff --git a/cmd/ateworker-microvm/application.go b/cmd/ateworker-microvm/application.go new file mode 100644 index 0000000000..c784f3d9c2 --- /dev/null +++ b/cmd/ateworker-microvm/application.go @@ -0,0 +1,175 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "log/slog" + "time" + + "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" + "golang.org/x/sys/unix" + + "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/kata" + "github.com/agent-substrate/substrate/internal/ateworker/actornet" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/internal/wakeupprobe" +) + +// guestPhase distinguishes a booted kernel from a guest with live applications. +// Existing ordinary activations use the zero value. +type guestPhase uint8 + +const ( + applicationRunning guestPhase = iota + guestBooted +) + +func (s *AteWorkerService) waitActorReady(ctx context.Context, p actorBootParams) error { + if err := wakeupprobe.WaitAll(ctx, p.containers, actornet.ActorVethIP, wakeupprobe.DialFunc(s.sandboxDialer(p.actorUID))); err != nil { + return fmt.Errorf("while waiting for container wakeup probe: %w", err) + } + return nil +} + +// publishActorApplication runs only after readiness and the last fallible step. +// Until then the caller owns cleanup of the guest connection and VM processes. +func (s *AteWorkerService) publishActorApplication(ctx context.Context, p actorBootParams, ra *runningActor, ctrs []actorContainer) { + ra.phase = applicationRunning + ra.workloadIDs = workloadIDs(ctrs) + s.setRunningVM(p.actorUID, ra) + attribution := p.actorAttribution() + for _, id := range ra.workloadIDs { + s.startActorLogForwarding(ra.guestAgent, attribution, id, id) + } + s.setGuestStats(p.actorUID, &guestStatsTarget{actorUID: p.actorUID, agent: ra.guestAgent, workloadIDs: ra.workloadIDs}) + if hosted := s.lookupActor(p.actorUID); hosted != nil { + go s.recordInitial(context.WithoutCancel(ctx), hosted) + } +} + +func (s *AteWorkerService) startActorContainers(ctx context.Context, ac *kata.AgentClient, id, vsockPath string, ctrs []actorContainer) error { + // Establish the agent sandbox + the kataShared virtio-fs mount (every + // container's merged rootfs, durable volumes, CSI volumes, and system-info + // volumes). All containers share it, so use the first container's hostname. + tStart := time.Now() + sbCtx, sbCancel := context.WithTimeout(ctx, 20*time.Second) + err := ac.CreateSandboxForActor(sbCtx, kata.CreateSandboxOpts{ + SandboxID: id, + Hostname: ctrs[0].spec.Hostname, + }) + sbCancel() + if err != nil { + return fmt.Errorf("while creating agent sandbox: %w", err) + } + tSandbox := time.Now() + + // Configure guest networking (the shim's job): eth0 IP/MAC/MTU, routes, ARP. + mtu := uint64(actorTapMTUOf(ctx, s.sandboxNetNS(id), "tap0_kata")) + netCtx, netCancel := context.WithTimeout(ctx, 20*time.Second) + err = s.configureGuestNetwork(netCtx, ac, mtu) + netCancel() + if err != nil { + if s.guestDebug { + dump := kata.DebugConsoleDump(ctx, vsockPath, "ip addr 2>&1; echo '== route =='; ip route 2>&1; echo '== neigh =='; ip neigh 2>&1") + slog.ErrorContext(ctx, "guest network config failed; dump", slog.String("dump", dump)) + } + return fmt.Errorf("while configuring guest network: %w", err) + } + + tNetwork := time.Now() + + for _, c := range ctrs { + if err := s.startRootfsContainer(ctx, ac, vsockPath, c); err != nil { + return err + } + } + slog.InfoContext(ctx, "Agent setup phases", slog.String("id", id), + slog.Duration("sandbox", tSandbox.Sub(tStart)), + slog.Duration("network", tNetwork.Sub(tSandbox)), + slog.Duration("containers", time.Since(tNetwork)), + slog.Int("container_count", len(ctrs))) + return nil +} + +// startRootfsContainer brings up one container on its host-merged rootfs (the +// stock kata flow: create + start against shared//rootfs). On failure it +// dumps the guest's view of the shared tree. +// +// Its spec binds every declared volume at its mount path. +func (s *AteWorkerService) startRootfsContainer(ctx context.Context, ac *kata.AgentClient, vsockPath string, c actorContainer) error { + cCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + err := ac.StartRootfsContainer(cCtx, c.name, c.spec) + cancel() + if err != nil { + if s.guestDebug { + dump := kata.DebugConsoleDump(ctx, vsockPath, + "echo '== shared/containers =='; ls -la /run/kata-containers/shared/containers/ 2>&1 | head -40; "+ + "echo '== rootfs =='; ls /run/kata-containers/shared/containers/"+c.name+"/rootfs/ 2>&1 | head; "+ + "echo '== mounts =='; grep -E 'kata|virtiofs' /proc/mounts 2>&1") + slog.ErrorContext(ctx, "rootfs container failed; dump", slog.String("container", c.name), slog.String("dump", dump)) + } + return fmt.Errorf("while starting rootfs container %q: %w", c.name, err) + } + return nil +} + +// startActorLogForwarding spawns two goroutines that pump the actor container's +// stdout and stderr (read over the kata-agent ttrpc client ac via repeated +// ReadStdout/ReadStderr) through the shared actorlog forwarder, which annotates +// each line with the actor's ate.dev/* labels and writes it to the pod's stdout. +// +// The streams are keyed by streamID == the kata containerID==execID (the overlay +// workload id); lines are tagged with actorName + containerName +// (ate.actor.container.name) so a multi-container actor demultiplexes. +// The reader contexts are context.Background() — the goroutines are NOT bound to the +// RPC that started them; they terminate when ac is closed (by teardownActor), which +// makes the in-flight ReadStdout/ReadStderr fail and the StreamReader return io.EOF, +// ending WrapContainerLogs. This keeps the agent connection (which ttrpc allows +// concurrent Calls on) alive for forwarding while guaranteeing no goroutine outlives +// the connection. +func (s *AteWorkerService) startActorLogForwarding(ac *kata.AgentClient, a resources.ActorAttribution, streamID, containerName string) { + go s.actorLogger.WrapContainerLogs(kata.NewStdioReader(context.Background(), ac, streamID, streamID, false), a, containerName) + go s.actorLogger.WrapContainerLogs(kata.NewStdioReader(context.Background(), ac, streamID, streamID, true), a, containerName) +} + +func (s *AteWorkerService) configureGuestNetwork(ctx context.Context, ac *kata.AgentClient, mtu uint64) error { + if err := ac.UpdateInterface(ctx, &agentpb.Interface{ + Device: actornet.ActorVethName, + Name: actornet.ActorVethName, + HwAddr: actorGuestMAC, + Mtu: mtu, + IPAddresses: []*agentpb.IPAddress{ + {Family: agentpb.IPFamily_v4, Address: actornet.ActorVethIP, Mask: "30"}, + }, + }); err != nil { + return err + } + if err := ac.UpdateRoutes(ctx, []*agentpb.Route{ + {Dest: actornet.ActorVethSubnet, Device: actornet.ActorVethName, Scope: uint32(unix.RT_SCOPE_LINK), Family: agentpb.IPFamily_v4}, + {Dest: "", Gateway: actornet.ActorVethGateway, Device: actornet.ActorVethName, Family: agentpb.IPFamily_v4}, + }); err != nil { + return err + } + return ac.AddARPNeighbors(ctx, []*agentpb.ARPNeighbor{{ + ToIPAddress: &agentpb.IPAddress{Family: agentpb.IPFamily_v4, Address: actornet.ActorVethGateway}, + Device: actornet.ActorVethName, + Lladdr: gatewayMAC, + State: 0x80, // NUD_PERMANENT + }}) +} diff --git a/cmd/ateworker-microvm/boot_marker.go b/cmd/ateworker-microvm/boot_marker.go new file mode 100644 index 0000000000..7b9ac78fb7 --- /dev/null +++ b/cmd/ateworker-microvm/boot_marker.go @@ -0,0 +1,20 @@ +//go:build linux && !e2e_runtime_identity + +// 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 + +// Production kernels receive no test instrumentation. +func runtimeBootMarker() string { return "" } diff --git a/cmd/ateworker-microvm/boot_marker_e2e.go b/cmd/ateworker-microvm/boot_marker_e2e.go new file mode 100644 index 0000000000..1678f1ce47 --- /dev/null +++ b/cmd/ateworker-microvm/boot_marker_e2e.go @@ -0,0 +1,23 @@ +//go:build linux && e2e_runtime_identity + +// 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 "crypto/rand" + +// Linux initializes boot_id lazily; a pre-application checkpoint therefore +// needs an eager nonce to prove guest continuity in the E2E probe. +func runtimeBootMarker() string { return " ate.boot_id=" + rand.Text() } diff --git a/cmd/ateworker-microvm/boot_marker_test.go b/cmd/ateworker-microvm/boot_marker_test.go new file mode 100644 index 0000000000..1220bc8ecc --- /dev/null +++ b/cmd/ateworker-microvm/boot_marker_test.go @@ -0,0 +1,25 @@ +//go:build linux && !e2e_runtime_identity + +// 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 "testing" + +func TestProductionBootHasNoTestMarker(t *testing.T) { + if got := runtimeBootMarker(); got != "" { + t.Fatalf("production boot instrumentation: %q", got) + } +} diff --git a/cmd/ateworker-microvm/checkpoint.go b/cmd/ateworker-microvm/checkpoint.go index 394a9dee5f..8dfe86f0ee 100644 --- a/cmd/ateworker-microvm/checkpoint.go +++ b/cmd/ateworker-microvm/checkpoint.go @@ -103,6 +103,13 @@ func (s *AteWorkerService) CheckpointWorkload(ctx context.Context, req *ateworke }) }() + // Reject impossible captures before disconnecting traffic, pausing the guest, + // or replacing any existing checkpoint files. + ra := s.runningVM(req.GetActorUid()) + if ra != nil && ra.phase == guestBooted && scope != ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY { + return nil, apierror.FailedPrecondition("runtime golden requires MEMORY fidelity") + } + if err := s.tunnel.Deactivate(ctx, attribution); err != nil { return nil, err } @@ -140,7 +147,6 @@ func (s *AteWorkerService) CheckpointWorkload(ctx context.Context, req *ateworke // The actor's CH was booted by RunWorkload or relaunched by RestoreWorkload; // either way ateworker owns it and tracks its api-socket. - ra := s.runningVM(actorUID) chSocket := kata.CLHSocketPath(actorUID) if ra != nil && ra.apiSocket != "" { chSocket = ra.apiSocket @@ -171,6 +177,12 @@ func (s *AteWorkerService) CheckpointWorkload(ctx context.Context, req *ateworke return nil, fmt.Errorf("while creating checkpoint dir %q: %w", checkpointDir, err) } + if ra != nil && ra.phase == guestBooted { + if err := os.WriteFile(filepath.Join(checkpointDir, runtimeGoldenFile), []byte(runtimeGoldenFormat), 0o600); err != nil { + return nil, err + } + } + // Capture the snapshot's pieces CONCURRENTLY: the CH snapshot, the // durable-dir tars, and the rootfs upper tars read independent data from a // quiesced guest and write distinct files into checkpointDir, so the paused diff --git a/cmd/ateworker-microvm/restore.go b/cmd/ateworker-microvm/restore.go index 02bf2971bf..6b5f1508a3 100644 --- a/cmd/ateworker-microvm/restore.go +++ b/cmd/ateworker-microvm/restore.go @@ -31,15 +31,12 @@ import ( "github.com/agent-substrate/substrate/internal/ateworker/phaselog" "github.com/agent-substrate/substrate/internal/workerstats" - "github.com/agent-substrate/substrate/internal/ateworker/actornet" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/ch" "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/kata" "github.com/agent-substrate/substrate/internal/imagecache" "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/internal/sizing" - "github.com/agent-substrate/substrate/internal/wakeupprobe" ) // restoreMemMode picks how cloud-hypervisor should load guest RAM, from what the VMM @@ -233,6 +230,10 @@ func (s *AteWorkerService) RestoreWorkload(ctx context.Context, req *ateworkerpb // comes back from the memory snapshot; the durable-dir volumes were restored by the // caller from their tar. func (s *AteWorkerService) restoreMemoryFidelity(ctx context.Context, p actorBootParams, scope ateworkerpb.SnapshotFidelity, restoreDir string, preserveRestoreDir bool, tStart time.Time) (retErr error) { + runtimeGolden, err := readRuntimeGolden(restoreDir) + if err != nil { + return err + } actorUID := p.actorUID rr := s.resolveRuntime(p.assetPaths) @@ -450,9 +451,17 @@ func (s *AteWorkerService) restoreMemoryFidelity(ctx context.Context, p actorBoo return fmt.Errorf("while reseeding guest CRNG: %w", err) } + if runtimeGolden { + // The restored kernel has never mounted the share. The host has now + // staged this actor's real volumes; create its sandbox and processes. + if err := s.startActorContainers(ctx, guestAC, actorUID, kata.VsockSocketPath(actorUID), ctrs); err != nil { + return err + } + } + // Block until every wakeup-probe-enabled container reports 200. - if err := wakeupprobe.WaitAll(ctx, containers, actornet.ActorVethIP, wakeupprobe.DialFunc(s.sandboxDialer(actorUID))); err != nil { - return fmt.Errorf("while waiting for container wakeup probe: %w", err) + if err := s.waitActorReady(ctx, p); err != nil { + return err } // Where a resume goes. Like the boot phases, this used to be a single total, @@ -501,28 +510,10 @@ func (s *AteWorkerService) restoreMemoryFidelity(ctx context.Context, p actorBoo guestAgent: guestAC, } - // Re-attach stdout/stderr forwarding for each container over the agent - // connection dialed after resume: the restored guest's containers are alive, so - // ReadStdout/ReadStderr pick up where they left off. - attribution := p.actorAttribution() - for _, c := range containers { - s.startActorLogForwarding(guestAC, attribution, c.GetName(), c.GetName()) - } - if err := s.tunnel.Activate(p.attribution(), s.sandboxDialer(p.actorUID), egress); err != nil { return err } - s.setRunningVM(actorUID, ra) - - // Publish the guest to GetWorkloadStats, past the last error return above - // for the same reason as in coldBootActor. Same client the forwarding above - // reads over. - s.setGuestStats(actorUID, &guestStatsTarget{actorUID: actorUID, agent: guestAC, workloadIDs: ra.workloadIDs}) - // Looked up while the caller holds the actor's lock, so it is this - // activation. - if hosted := s.lookupActor(actorUID); hosted != nil { - go s.recordInitial(context.WithoutCancel(ctx), hosted) - } + s.publishActorApplication(ctx, p, ra, ctrs) slog.InfoContext(ctx, "Actor restored (overlay rootfs)", slog.String("id", actorUID), slog.Duration("total", time.Since(tStart))) diff --git a/cmd/ateworker-microvm/run.go b/cmd/ateworker-microvm/run.go index ea076e6888..94b272b76b 100644 --- a/cmd/ateworker-microvm/run.go +++ b/cmd/ateworker-microvm/run.go @@ -32,25 +32,22 @@ import ( "github.com/agent-substrate/substrate/internal/apierror" "github.com/agent-substrate/substrate/internal/workerstats" - "github.com/agent-substrate/substrate/internal/ateworker/actornet" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/ch" "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/kata" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" "github.com/agent-substrate/substrate/internal/imagecache" "github.com/agent-substrate/substrate/internal/ocispec" "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/internal/sizing" - "github.com/agent-substrate/substrate/internal/wakeupprobe" specs "github.com/opencontainers/runtime-spec/specs-go" - "golang.org/x/sys/unix" ) // runningActor holds the live state for one actor's micro-VM. ateworker owns the // cloud-hypervisor process directly (booted by RunWorkload or relaunched by // RestoreWorkload), so it tracks that process and its api-socket for teardown. type runningActor struct { + phase guestPhase + // baseID is the FROZEN base sandbox id propagated across this actor's restore // lineage. For a cold-run actor this is the actor's own id; for a restored // actor it is the id read from the snapshot's base-id file (the golden id, @@ -232,6 +229,9 @@ func (s *AteWorkerService) resolveRuntime(paths map[string]string) resolvedRunti // - Each container's bundle holds the overlay spec its rootfs/ is composed // from. ateworker builds the OCI spec itself. func (s *AteWorkerService) RunWorkload(ctx context.Context, req *ateworkerpb.RunWorkloadRequest) (resp *ateworkerpb.RunWorkloadResponse, retErr error) { + if req.GetSpec().GetRuntimeGolden() && req.GetAtespace() != resources.GoldenActorAtespace { + return nil, apierror.InvalidArgument("runtime golden capture requires a golden actor") + } if err := validateActorDirs(req.GetActorDirs()); err != nil { return nil, err } @@ -256,6 +256,7 @@ func (s *AteWorkerService) RunWorkload(ctx context.Context, req *ateworkerpb.Run } p := actorBootParams{ + runtimeGolden: req.GetSpec().GetRuntimeGolden(), actorRef: resources.ActorRef{Atespace: req.GetAtespace(), Name: req.GetActorName()}, actorUID: req.GetActorUid(), actorDirs: req.GetActorDirs(), @@ -298,6 +299,8 @@ func (s *AteWorkerService) RunWorkload(ctx context.Context, req *ateworkerpb.Run // request, or from a Restore request whose snapshot fidelity covers only the // durable-dir volumes (the workload itself cold-starts). type actorBootParams struct { + runtimeGolden bool + actorRef resources.ActorRef actorUID string actorDirs *ateworkerpb.ActorDirs @@ -532,6 +535,8 @@ func (s *AteWorkerService) coldBootActor(ctx context.Context, p actorBootParams) consoleLog := kata.ConsoleLogPath(actorUID) vmCfg := buildVMConfig(actorUID, kernel, image, kparams, consoleLog, memMiB, vcpus, agentInit(ctx, client.Info()), s.guestDebug) + // Only instrumented E2E builds add a boot-continuity marker. + vmCfg.Payload.Cmdline += runtimeBootMarker() if err := client.CreateVM(ctx, vmCfg); err != nil { return fmt.Errorf("while creating VM: %w", err) } @@ -587,55 +592,29 @@ func (s *AteWorkerService) coldBootActor(ctx context.Context, p actorBootParams) } }() - // Post-boot kata-agent setup: sandbox, guest networking, start each container. + ra := &runningActor{phase: guestBooted, chCmd: chCmd, vfsdCmd: vfsdCmd, apiSocket: apiSocket, baseID: actorUID, guestAgent: ac} + // Capture at the one boundary before any guest sandbox or application exists. + if p.runtimeGolden { + s.setRunningVM(actorUID, ra) + return nil + } if err := s.startActorContainers(ctx, ac, actorUID, vsockPath, ctrs); err != nil { return err } tContainers := time.Now() - - // Block until every wakeup-probe-enabled container reports 200. - if err := wakeupprobe.WaitAll(ctx, containers, actornet.ActorVethIP, wakeupprobe.DialFunc(s.sandboxDialer(actorUID))); err != nil { - return fmt.Errorf("while waiting for container wakeup probe: %w", err) + if err := s.waitActorReady(ctx, p); err != nil { + return err } - - // Everything from BootVM onward, split. ateworker used to log only the total, which - // hid where a cold boot actually goes: it is not the guest booting. slog.InfoContext(ctx, "Actor boot phases", slog.String("id", actorUID), slog.Duration("vsock_wait", tVsock.Sub(tBooted)), slog.Duration("agent_dial", tDialed.Sub(tVsock)), slog.Duration("containers", tContainers.Sub(tDialed)), slog.Duration("wakeup_probe", time.Since(tContainers)), slog.Duration("since_boot", time.Since(tBooted))) - - ra := &runningActor{chCmd: chCmd, vfsdCmd: vfsdCmd, apiSocket: apiSocket, baseID: actorUID, guestAgent: ac, workloadIDs: workloadIDs(ctrs)} - if err := s.tunnel.Activate(p.attribution(), s.sandboxDialer(p.actorUID), egress); err != nil { + if err := s.tunnel.Activate(p.attribution(), s.sandboxDialer(actorUID), egress); err != nil { return err } - s.setRunningVM(actorUID, ra) - - // Forward each container's stdout/stderr into the pod logs, keyed by the - // container id (== the name; see StartRootfsContainer). The goroutines read - // over ac for the actor's lifetime and exit (io.EOF) when teardownActor - // closes ac. - workloadIDs := make([]string, 0, len(ctrs)) - attribution := p.actorAttribution() - for _, c := range ctrs { - s.startActorLogForwarding(ac, attribution, c.name, c.name) - workloadIDs = append(workloadIDs, c.name) - } - - // Publish the guest to GetWorkloadStats, past every error return above: a - // failing attempt closes ac on its way out (and coldBootActorRetrying may - // then try the whole boot again), so a target published earlier would leave - // the handler polling a connection nobody owns. Same client the forwarding - // above reads over — ttrpc multiplexes, and teardownActor ends both. - s.setGuestStats(actorUID, &guestStatsTarget{actorUID: actorUID, agent: ac, workloadIDs: workloadIDs}) - // Looked up while the caller holds the actor's lock, so it is this - // activation even if the read below outlives the RPC. - if hosted := s.lookupActor(actorUID); hosted != nil { - go s.recordInitial(context.WithoutCancel(ctx), hosted) - } - + s.publishActorApplication(ctx, p, ra, ctrs) return nil } @@ -848,95 +827,6 @@ func buildFsConfigs(id string) []ch.FsConfig { }} } -// startActorContainers performs the post-boot kata-agent setup the shim normally -// does at boot: establish the sandbox once (mounting the kataShared virtio-fs base), -// configure guest networking (eth0 IP/MAC/MTU + routes) once, then start each -// container on its own overlay rootfs. On failure it dumps guest diagnostics. -func (s *AteWorkerService) startActorContainers(ctx context.Context, ac *kata.AgentClient, id, vsockPath string, ctrs []actorContainer) error { - // Establish the agent sandbox + the kataShared virtio-fs mount (every - // container's merged rootfs, durable volumes, CSI volumes, and system-info - // volumes). All containers share it, so use the first container's hostname. - tStart := time.Now() - sbCtx, sbCancel := context.WithTimeout(ctx, 20*time.Second) - err := ac.CreateSandboxForActor(sbCtx, kata.CreateSandboxOpts{ - SandboxID: id, - Hostname: ctrs[0].spec.Hostname, - }) - sbCancel() - if err != nil { - return fmt.Errorf("while creating agent sandbox: %w", err) - } - tSandbox := time.Now() - - // Configure guest networking (the shim's job): eth0 IP/MAC/MTU, routes, ARP. - mtu := uint64(actorTapMTUOf(ctx, s.sandboxNetNS(id), "tap0_kata")) - netCtx, netCancel := context.WithTimeout(ctx, 20*time.Second) - err = s.configureGuestNetwork(netCtx, ac, mtu) - netCancel() - if err != nil { - if s.guestDebug { - dump := kata.DebugConsoleDump(ctx, vsockPath, "ip addr 2>&1; echo '== route =='; ip route 2>&1; echo '== neigh =='; ip neigh 2>&1") - slog.ErrorContext(ctx, "guest network config failed; dump", slog.String("dump", dump)) - } - return fmt.Errorf("while configuring guest network: %w", err) - } - - tNetwork := time.Now() - - for _, c := range ctrs { - if err := s.startRootfsContainer(ctx, ac, vsockPath, c); err != nil { - return err - } - } - slog.InfoContext(ctx, "Agent setup phases", slog.String("id", id), - slog.Duration("sandbox", tSandbox.Sub(tStart)), - slog.Duration("network", tNetwork.Sub(tSandbox)), - slog.Duration("containers", time.Since(tNetwork)), - slog.Int("container_count", len(ctrs))) - return nil -} - -// startRootfsContainer brings up one container on its host-merged rootfs (the -// stock kata flow: create + start against shared//rootfs). On failure it -// dumps the guest's view of the shared tree. -// -// Its spec binds every declared volume at its mount path. -func (s *AteWorkerService) startRootfsContainer(ctx context.Context, ac *kata.AgentClient, vsockPath string, c actorContainer) error { - cCtx, cancel := context.WithTimeout(ctx, 30*time.Second) - err := ac.StartRootfsContainer(cCtx, c.name, c.spec) - cancel() - if err != nil { - if s.guestDebug { - dump := kata.DebugConsoleDump(ctx, vsockPath, - "echo '== shared/containers =='; ls -la /run/kata-containers/shared/containers/ 2>&1 | head -40; "+ - "echo '== rootfs =='; ls /run/kata-containers/shared/containers/"+c.name+"/rootfs/ 2>&1 | head; "+ - "echo '== mounts =='; grep -E 'kata|virtiofs' /proc/mounts 2>&1") - slog.ErrorContext(ctx, "rootfs container failed; dump", slog.String("container", c.name), slog.String("dump", dump)) - } - return fmt.Errorf("while starting rootfs container %q: %w", c.name, err) - } - return nil -} - -// startActorLogForwarding spawns two goroutines that pump the actor container's -// stdout and stderr (read over the kata-agent ttrpc client ac via repeated -// ReadStdout/ReadStderr) through the shared actorlog forwarder, which annotates -// each line with the actor's ate.dev/* labels and writes it to the pod's stdout. -// -// The streams are keyed by streamID == the kata containerID==execID (the overlay -// workload id); lines are tagged with actorName + containerName -// (ate.actor.container.name) so a multi-container actor demultiplexes. -// The reader contexts are context.Background() — the goroutines are NOT bound to the -// RPC that started them; they terminate when ac is closed (by teardownActor), which -// makes the in-flight ReadStdout/ReadStderr fail and the StreamReader return io.EOF, -// ending WrapContainerLogs. This keeps the agent connection (which ttrpc allows -// concurrent Calls on) alive for forwarding while guaranteeing no goroutine outlives -// the connection. -func (s *AteWorkerService) startActorLogForwarding(ac *kata.AgentClient, a resources.ActorAttribution, streamID, containerName string) { - go s.actorLogger.WrapContainerLogs(kata.NewStdioReader(context.Background(), ac, streamID, streamID, false), a, containerName) - go s.actorLogger.WrapContainerLogs(kata.NewStdioReader(context.Background(), ac, streamID, streamID, true), a, containerName) -} - // errGuestStopped reports that the micro-VM stopped before the kata-agent // answered. Callers that can start over (a cold boot has no observable side // effects until the agent runs the containers) retry on it. @@ -1033,36 +923,6 @@ func tailString(s string, n int) string { return s[len(s)-n:] } -// configureGuestNetwork replicates the kata shim's guest network setup over the -// agent: configure eth0 (IP/MAC/MTU), install the connected + default routes, and -// pin the gateway's ARP entry to its fixed MAC (so a restored guest's frozen -// neighbor entry stays valid). -func (s *AteWorkerService) configureGuestNetwork(ctx context.Context, ac *kata.AgentClient, mtu uint64) error { - if err := ac.UpdateInterface(ctx, &agentpb.Interface{ - Device: actornet.ActorVethName, - Name: actornet.ActorVethName, - HwAddr: actorGuestMAC, - Mtu: mtu, - IPAddresses: []*agentpb.IPAddress{ - {Family: agentpb.IPFamily_v4, Address: actornet.ActorVethIP, Mask: "30"}, - }, - }); err != nil { - return err - } - if err := ac.UpdateRoutes(ctx, []*agentpb.Route{ - {Dest: actornet.ActorVethSubnet, Device: actornet.ActorVethName, Scope: uint32(unix.RT_SCOPE_LINK), Family: agentpb.IPFamily_v4}, - {Dest: "", Gateway: actornet.ActorVethGateway, Device: actornet.ActorVethName, Family: agentpb.IPFamily_v4}, - }); err != nil { - return err - } - return ac.AddARPNeighbors(ctx, []*agentpb.ARPNeighbor{{ - ToIPAddress: &agentpb.IPAddress{Family: agentpb.IPFamily_v4, Address: actornet.ActorVethGateway}, - Device: actornet.ActorVethName, - Lladdr: gatewayMAC, - State: 0x80, // NUD_PERMANENT - }}) -} - // waitForFile polls for path to exist, up to d. Used to wait for the kata-agent // hybrid-vsock socket the shim creates during VM boot before dialing it. func waitForFile(path string, d time.Duration) bool { diff --git a/cmd/ateworker-microvm/runtime_golden.go b/cmd/ateworker-microvm/runtime_golden.go new file mode 100644 index 0000000000..a384f03e41 --- /dev/null +++ b/cmd/ateworker-microvm/runtime_golden.go @@ -0,0 +1,40 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "os" + "path/filepath" +) + +const runtimeGoldenFile = "runtime-golden" +const runtimeGoldenFormat = "microvm-before-create-sandbox-v1\n" + +func readRuntimeGolden(dir string) (bool, error) { + b, err := os.ReadFile(filepath.Join(dir, runtimeGoldenFile)) + if os.IsNotExist(err) { + return false, nil + } + if err != nil { + return false, err + } + if string(b) != runtimeGoldenFormat { + return false, fmt.Errorf("invalid runtime golden marker") + } + return true, nil +} diff --git a/cmd/ateworker-microvm/runtime_golden_test.go b/cmd/ateworker-microvm/runtime_golden_test.go new file mode 100644 index 0000000000..561a72b2f1 --- /dev/null +++ b/cmd/ateworker-microvm/runtime_golden_test.go @@ -0,0 +1,90 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "github.com/agent-substrate/substrate/internal/actorlock" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" +) + +func TestRuntimeGoldenMarker(t *testing.T) { + for _, tc := range []struct { + name, content string + exists, want, bad bool + }{ + {name: "ordinary"}, {name: "runtime", content: runtimeGoldenFormat, exists: true, want: true}, {name: "empty", exists: true, bad: true}, {name: "unknown", content: "different", exists: true, bad: true}, + } { + t.Run(tc.name, func(t *testing.T) { + d := t.TempDir() + if tc.exists { + if err := os.WriteFile(filepath.Join(d, runtimeGoldenFile), []byte(tc.content), 0600); err != nil { + t.Fatal(err) + } + } + got, err := readRuntimeGolden(d) + if got != tc.want || (err != nil) != tc.bad { + t.Fatalf("got %v,%v", got, err) + } + }) + } +} + +func TestRuntimeGoldenRejectsOrdinaryActorBeforeActivation(t *testing.T) { + s := &AteWorkerService{} + _, err := s.RunWorkload(context.Background(), &ateworkerpb.RunWorkloadRequest{Atespace: "user", Spec: &ateworkerpb.WorkloadSpec{RuntimeGolden: true}}) + if err == nil || !strings.Contains(err.Error(), "requires a golden actor") { + t.Fatalf("got %v", err) + } +} + +func TestRuntimeGoldenCheckpointRejectsBeforeMutation(t *testing.T) { + for _, fidelity := range []ateworkerpb.SnapshotFidelity{ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES, ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS} { + t.Run(fidelity.String(), func(t *testing.T) { + root := t.TempDir() + checkpoint := filepath.Join(root, "checkpoint") + if err := os.Mkdir(checkpoint, 0700); err != nil { + t.Fatal(err) + } + sentinel := filepath.Join(checkpoint, "existing") + if err := os.WriteFile(sentinel, []byte("keep"), 0600); err != nil { + t.Fatal(err) + } + ra := &runningActor{phase: guestBooted} + // No tunnel or CH client: crossing the mutation boundary would panic or + // fail to reach CH instead of returning the expected precondition error. + s := &AteWorkerService{locks: actorlock.New(), inFlight: actorlock.NewInFlight(), actors: map[string]*hostedActor{"a": {vm: ra}}} + dirs := &ateworkerpb.ActorDirs{RootDir: root, OciBundleDir: root, CheckpointDir: checkpoint, RestoreDir: root, DurableDirVolumeMountsDir: root, SystemInfoVolumeRootsDir: root, VolumesDir: root} + _, err := s.CheckpointWorkload(context.Background(), &ateworkerpb.CheckpointWorkloadRequest{ActorUid: "a", ActorDirs: dirs, Fidelity: fidelity}) + if err == nil || !strings.Contains(err.Error(), "requires MEMORY") { + t.Fatalf("got %v", err) + } + data, err := os.ReadFile(sentinel) + if err != nil || string(data) != "keep" { + t.Fatalf("checkpoint changed: %s %v", data, err) + } + if s.runningVM("a") != ra || ra.phase != guestBooted { + t.Fatal("guest state changed") + } + }) + } +} diff --git a/cmd/kubectl-ate/internal/cmd/actor.go b/cmd/kubectl-ate/internal/cmd/actor.go index 20b59a3ad1..8437bc6f6d 100644 --- a/cmd/kubectl-ate/internal/cmd/actor.go +++ b/cmd/kubectl-ate/internal/cmd/actor.go @@ -47,6 +47,7 @@ var ( createActorAtespaceFlag string createActorTemplateFlag string createActorTagFlag string + createActorVolumeFlag map[string]string deleteActorAtespaceFlag string deleteActorAnyStateFlag bool pauseActorAtespaceFlag string @@ -118,7 +119,7 @@ var createActorCmd = &cobra.Command{ Short: "Create an actor", Args: exactlyOneArg("", "atespace", "template"), RunE: func(cmd *cobra.Command, args []string) error { - request, err := buildCreateActorRequest(args[0], createActorAtespaceFlag, createActorTemplateFlag, createActorTagFlag) + request, err := buildCreateActorRequest(args[0], createActorAtespaceFlag, createActorTemplateFlag, createActorTagFlag, createActorVolumeFlag) if err != nil { return wrapUsageError(err) } @@ -244,7 +245,7 @@ var logsActorsCmd = &cobra.Command{ RunE: runLogsActor, } -func buildCreateActorRequest(actorName, atespace, template, tag string) (*ateapipb.CreateActorRequest, error) { +func buildCreateActorRequest(actorName, atespace, template, tag string, volumes map[string]string) (*ateapipb.CreateActorRequest, error) { templateRef, err := parseAtespacedName(template, atespace) if err != nil { return nil, err @@ -256,6 +257,9 @@ func buildCreateActorRequest(actorName, atespace, template, tag string) (*ateapi }, ActorTemplate: templateRef, } + if len(volumes) > 0 { + actor.SharedVolumeBindings = volumes + } if tag != "" { ref, err := parseAtespacedName(tag, atespace) @@ -740,6 +744,7 @@ func init() { createActorCmd.Flags().StringVar(&createActorTemplateFlag, "template", "", "actor-template to derive the actor from, as /, or just to use the actor's own atespace (see --atespace)") createActorCmd.Flags().StringVarP(&createActorAtespaceFlag, "atespace", "a", "", "atespace to create the actor in") createActorCmd.Flags().StringVar(&createActorTagFlag, "tag", "", "tag to initialize the actor from, as /, or just to use the actor's own atespace (see --atespace)") + createActorCmd.Flags().StringToStringVar(&createActorVolumeFlag, "volume", nil, "Bind an unnamed external.sharedVolume in the template to a SharedVolume, as =; repeatable") createCmd.AddCommand(createActorCmd) deleteActorCmd.Flags().StringVarP(&deleteActorAtespaceFlag, "atespace", "a", "", "the actor's atespace") diff --git a/cmd/kubectl-ate/internal/cmd/actor_test.go b/cmd/kubectl-ate/internal/cmd/actor_test.go index ce4c31bc2b..6fe52d2b99 100644 --- a/cmd/kubectl-ate/internal/cmd/actor_test.go +++ b/cmd/kubectl-ate/internal/cmd/actor_test.go @@ -48,6 +48,7 @@ func TestBuildCreateActorRequest(t *testing.T) { name string templateRef string tag string + volumes map[string]string want *ateapipb.Actor wantErr bool }{ @@ -87,13 +88,23 @@ func TestBuildCreateActorRequest(t *testing.T) { ActorTemplate: &ateapipb.ObjectRef{Atespace: "shared-templates", Name: "counter"}, }, }, + { + name: "volume bindings", + templateRef: "counter", + volumes: map[string]string{"shared": "ctx-42"}, + want: &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: "demo", Name: "my-counter"}, + ActorTemplate: &ateapipb.ObjectRef{Atespace: "demo", Name: "counter"}, + SharedVolumeBindings: map[string]string{"shared": "ctx-42"}, + }, + }, {name: "malformed template ref", templateRef: "a/b/c", wantErr: true}, {name: "malformed tag", templateRef: "counter", tag: "a/b/c", wantErr: true}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got, err := buildCreateActorRequest("my-counter", "demo", test.templateRef, test.tag) + got, err := buildCreateActorRequest("my-counter", "demo", test.templateRef, test.tag, test.volumes) if (err != nil) != test.wantErr { t.Fatalf("buildCreateActorRequest error = %v, wantErr %t", err, test.wantErr) } diff --git a/cmd/kubectl-ate/internal/cmd/shared_volume.go b/cmd/kubectl-ate/internal/cmd/shared_volume.go new file mode 100644 index 0000000000..742a12db8a --- /dev/null +++ b/cmd/kubectl-ate/internal/cmd/shared_volume.go @@ -0,0 +1,268 @@ +// 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 cmd + +import ( + "context" + "fmt" + "strings" + + "github.com/agent-substrate/substrate/cmd/kubectl-ate/internal/printer" + "github.com/agent-substrate/substrate/internal/ateclient" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "github.com/spf13/cobra" + "google.golang.org/grpc" +) + +var ( + sharedVolumeAtespaceFlag string + sharedVolumeAllAtespacesFlag bool + createSharedVolumeAtespaceFlag string + createSharedVolumeClassFlag string + createSharedVolumeCapacityFlag string + createSharedVolumeVolumeIDFlag string + createSharedVolumeTypeFlag string + createSharedVolumeContextFlag map[string]string + createSharedVolumeDeleteTriggerFlag string + createSharedVolumeAccessModeFlag string + updateSharedVolumeAtespaceFlag string + updateSharedVolumeDeleteTriggerFlag string + deleteSharedVolumeAtespaceFlag string +) + +var getSharedVolumesCmd = &cobra.Command{ + Use: "sharedvolumes [volume-name ...]", + Aliases: []string{"sharedvolume", "sv"}, + Short: "List or get shared volumes", + RunE: func(cmd *cobra.Command, args []string) error { + if sharedVolumeAllAtespacesFlag && sharedVolumeAtespaceFlag != "" { + return fmt.Errorf("--atespace and -A/--all-atespaces are mutually exclusive") + } + if len(args) > 0 && sharedVolumeAtespaceFlag == "" { + return fmt.Errorf("--atespace is required when getting shared volumes") + } + if len(args) == 0 && !sharedVolumeAllAtespacesFlag && sharedVolumeAtespaceFlag == "" { + return fmt.Errorf("specify --atespace , or -A/--all-atespaces") + } + + ctx := cmd.Context() + client, err := ateclient.NewClient(ctx, kubeconfig, k8sContext, endpoint, tokenFile, traceEnabled) + if err != nil { + return fmt.Errorf("failed to connect to ate-api-server: %w", err) + } + defer client.Close() + + if len(args) > 0 { + volumes := make([]*ateapipb.SharedVolume, 0, len(args)) + for _, name := range args { + volume, err := client.GetSharedVolume(ctx, &ateapipb.GetSharedVolumeRequest{ + SharedVolume: &ateapipb.ObjectRef{Atespace: sharedVolumeAtespaceFlag, Name: name}, + }) + if err != nil { + return fmt.Errorf("failed to get shared volume %q: %w", name, err) + } + volumes = append(volumes, volume) + } + if len(volumes) == 1 { + return printer.PrintSharedVolumeTo(cmd.OutOrStdout(), volumes[0], outputFmt) + } + return printer.PrintSharedVolumesTo(cmd.OutOrStdout(), volumes, outputFmt) + } + + var volumes []*ateapipb.SharedVolume + pageToken := "" + for { + resp, err := client.ListSharedVolumes(ctx, &ateapipb.ListSharedVolumesRequest{ + Atespace: sharedVolumeAtespaceFlag, PageSize: 1000, PageToken: pageToken, + }) + if err != nil { + return fmt.Errorf("failed to list shared volumes: %w", err) + } + volumes = append(volumes, resp.GetSharedVolumes()...) + pageToken = resp.GetNextPageToken() + if pageToken == "" { + break + } + } + return printer.PrintSharedVolumesTo(cmd.OutOrStdout(), volumes, outputFmt) + }, +} + +var createSharedVolumeCmd = &cobra.Command{ + Use: "sharedvolume ", + Aliases: []string{"sharedvolumes", "sv"}, + Short: "Create an shared volume actors can share", + Long: "Create an shared volume actors can share.\n\n" + + "Name a storage class with --storage-class to provision a new volume, or\n" + + "supply --volume-id to register a volume that already exists, but not both.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + trigger, err := parseDeleteTrigger(createSharedVolumeDeleteTriggerFlag) + if err != nil { + return err + } + mode, err := parseAccessMode(createSharedVolumeAccessModeFlag) + if err != nil { + return err + } + ctx := cmd.Context() + client, err := ateclient.NewClient(ctx, kubeconfig, k8sContext, endpoint, tokenFile, traceEnabled) + if err != nil { + return fmt.Errorf("failed to connect to ate-api-server: %w", err) + } + defer client.Close() + + volume, err := client.CreateSharedVolume(ctx, &ateapipb.CreateSharedVolumeRequest{ + SharedVolume: &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: createSharedVolumeAtespaceFlag, Name: args[0]}, + DeleteTrigger: trigger, + AccessMode: mode, + StorageClassName: createSharedVolumeClassFlag, + Capacity: createSharedVolumeCapacityFlag, + VolumeId: createSharedVolumeVolumeIDFlag, + VolumeType: createSharedVolumeTypeFlag, + VolumeContext: createSharedVolumeContextFlag, + }, + }) + if err != nil { + return fmt.Errorf("failed to create shared volume: %w", err) + } + return printer.PrintSharedVolumeTo(cmd.OutOrStdout(), volume, outputFmt) + }, +} + +var updateSharedVolumeCmd = &cobra.Command{ + Use: "sharedvolume ", + Aliases: []string{"sharedvolumes", "sv"}, + Short: "Change an shared volume's delete trigger", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + trigger, err := parseDeleteTrigger(updateSharedVolumeDeleteTriggerFlag) + if err != nil { + return err + } + ctx := cmd.Context() + client, err := ateclient.NewClient(ctx, kubeconfig, k8sContext, endpoint, tokenFile, traceEnabled) + if err != nil { + return fmt.Errorf("failed to connect to ate-api-server: %w", err) + } + defer client.Close() + + ref := &ateapipb.ObjectRef{Atespace: updateSharedVolumeAtespaceFlag, Name: args[0]} + resp, err := updateSharedVolumeDeleteTrigger(ctx, client, ref, trigger) + if err != nil { + return err + } + return printer.PrintSharedVolumeTo(cmd.OutOrStdout(), resp, outputFmt) + }, +} + +var deleteSharedVolumeCmd = &cobra.Command{ + Use: "sharedvolume ", + Aliases: []string{"sharedvolumes", "sv"}, + Short: "Delete an shared volume and its storage", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + client, err := ateclient.NewClient(ctx, kubeconfig, k8sContext, endpoint, tokenFile, traceEnabled) + if err != nil { + return fmt.Errorf("failed to connect to ate-api-server: %w", err) + } + defer client.Close() + + _, err = client.DeleteSharedVolume(ctx, &ateapipb.DeleteSharedVolumeRequest{ + SharedVolume: &ateapipb.ObjectRef{Atespace: deleteSharedVolumeAtespaceFlag, Name: args[0]}, + }) + if err != nil { + return fmt.Errorf("failed to delete shared volume: %w", err) + } + fmt.Fprintf(cmd.OutOrStdout(), "shared volume %q deleted\n", args[0]) + return nil + }, +} + +type sharedVolumeClient interface { + GetSharedVolume(ctx context.Context, in *ateapipb.GetSharedVolumeRequest, opts ...grpc.CallOption) (*ateapipb.SharedVolume, error) + UpdateSharedVolume(ctx context.Context, in *ateapipb.UpdateSharedVolumeRequest, opts ...grpc.CallOption) (*ateapipb.SharedVolume, error) +} + +// updateSharedVolumeDeleteTrigger reads the volume first so the update carries +// the uid and version the server requires as preconditions. +func updateSharedVolumeDeleteTrigger(ctx context.Context, client sharedVolumeClient, ref *ateapipb.ObjectRef, trigger ateapipb.DeleteTrigger) (*ateapipb.SharedVolume, error) { + volume, err := client.GetSharedVolume(ctx, &ateapipb.GetSharedVolumeRequest{SharedVolume: ref}) + if err != nil { + return nil, fmt.Errorf("failed to get shared volume %q: %w", ref.GetName(), err) + } + volume.DeleteTrigger = trigger + + resp, err := client.UpdateSharedVolume(ctx, &ateapipb.UpdateSharedVolumeRequest{SharedVolume: volume}) + if err != nil { + return nil, fmt.Errorf("failed to update shared volume: %w", err) + } + return resp, nil +} + +func parseDeleteTrigger(value string) (ateapipb.DeleteTrigger, error) { + switch strings.ToLower(value) { + case "": + return ateapipb.DeleteTrigger_DELETE_TRIGGER_UNSPECIFIED, nil + case "manual": + return ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL, nil + case "last-actor", "lastactor": + return ateapipb.DeleteTrigger_DELETE_TRIGGER_LAST_ACTOR, nil + default: + return ateapipb.DeleteTrigger_DELETE_TRIGGER_UNSPECIFIED, fmt.Errorf("invalid delete trigger %q; must be manual or last-actor", value) + } +} + +func parseAccessMode(value string) (ateapipb.AccessMode, error) { + switch strings.ToLower(value) { + case "": + return ateapipb.AccessMode_ACCESS_MODE_UNSPECIFIED, nil + case "rwo", "readwriteonce": + return ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_ONCE, nil + case "rwx", "readwritemany": + return ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_MANY, nil + default: + return ateapipb.AccessMode_ACCESS_MODE_UNSPECIFIED, fmt.Errorf("invalid access mode %q; must be rwo or rwx", value) + } +} + +func init() { + getSharedVolumesCmd.Flags().StringVarP(&sharedVolumeAtespaceFlag, "atespace", "a", "", "Atespace to list/get shared volumes in") + getSharedVolumesCmd.Flags().BoolVarP(&sharedVolumeAllAtespacesFlag, "all-atespaces", "A", false, "List shared volumes across all atespaces") + getCmd.AddCommand(getSharedVolumesCmd) + + createSharedVolumeCmd.Flags().StringVarP(&createSharedVolumeAtespaceFlag, "atespace", "a", "", "Atespace to create the shared volume in (required)") + _ = createSharedVolumeCmd.MarkFlagRequired("atespace") + createSharedVolumeCmd.Flags().StringVar(&createSharedVolumeClassFlag, "storage-class", "", "StorageClass to provision the volume from") + createSharedVolumeCmd.Flags().StringVar(&createSharedVolumeCapacityFlag, "capacity", "", "Size to provision, as a Kubernetes quantity (e.g. 10Gi); required with --storage-class") + createSharedVolumeCmd.Flags().StringVar(&createSharedVolumeVolumeIDFlag, "volume-id", "", "Volume ID of a volume that already exists, to register instead of provisioning") + createSharedVolumeCmd.Flags().StringVar(&createSharedVolumeTypeFlag, "volume-type", "", "CSI driver name; required with --volume-id unless --storage-class is set") + createSharedVolumeCmd.Flags().StringToStringVar(&createSharedVolumeContextFlag, "volume-context", nil, "Driver metadata for mounting, as key=value pairs; only valid with --volume-id") + createSharedVolumeCmd.Flags().StringVar(&createSharedVolumeDeleteTriggerFlag, "delete-trigger", "", "What removes the volume: manual or last-actor (default: last-actor when provisioned, manual when registered)") + createSharedVolumeCmd.Flags().StringVar(&createSharedVolumeAccessModeFlag, "access-mode", "", "How many resumed actors may hold the volume: rwo (default) or rwx") + createCmd.AddCommand(createSharedVolumeCmd) + + updateSharedVolumeCmd.Flags().StringVarP(&updateSharedVolumeAtespaceFlag, "atespace", "a", "", "Atespace owning the shared volume (required)") + updateSharedVolumeCmd.Flags().StringVar(&updateSharedVolumeDeleteTriggerFlag, "delete-trigger", "", "What removes the volume: manual or last-actor (required)") + _ = updateSharedVolumeCmd.MarkFlagRequired("atespace") + _ = updateSharedVolumeCmd.MarkFlagRequired("delete-trigger") + updateCmd.AddCommand(updateSharedVolumeCmd) + + deleteSharedVolumeCmd.Flags().StringVarP(&deleteSharedVolumeAtespaceFlag, "atespace", "a", "", "Atespace owning the shared volume (required)") + _ = deleteSharedVolumeCmd.MarkFlagRequired("atespace") + deleteCmd.AddCommand(deleteSharedVolumeCmd) +} diff --git a/cmd/kubectl-ate/internal/cmd/shared_volume_test.go b/cmd/kubectl-ate/internal/cmd/shared_volume_test.go new file mode 100644 index 0000000000..b4f4da47ff --- /dev/null +++ b/cmd/kubectl-ate/internal/cmd/shared_volume_test.go @@ -0,0 +1,138 @@ +// 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 cmd + +import ( + "context" + "testing" + + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "github.com/google/go-cmp/cmp" + "google.golang.org/grpc" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" +) + +type fakeSharedVolumeClient struct { + volume *ateapipb.SharedVolume + + getCalls int + updateReq *ateapipb.UpdateSharedVolumeRequest +} + +func (f *fakeSharedVolumeClient) GetSharedVolume(_ context.Context, _ *ateapipb.GetSharedVolumeRequest, _ ...grpc.CallOption) (*ateapipb.SharedVolume, error) { + f.getCalls++ + return proto.Clone(f.volume).(*ateapipb.SharedVolume), nil +} + +func (f *fakeSharedVolumeClient) UpdateSharedVolume(_ context.Context, in *ateapipb.UpdateSharedVolumeRequest, _ ...grpc.CallOption) (*ateapipb.SharedVolume, error) { + f.updateReq = in + updated := proto.Clone(in.GetSharedVolume()).(*ateapipb.SharedVolume) + updated.Metadata.Version++ + return updated, nil +} + +func testSharedVolume() *ateapipb.SharedVolume { + return &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{ + Atespace: "space-1", + Name: "vol-1", + Uid: "8ba9b6ee-2e1a-4c0f-9f4e-2f0a0f1c3d55", + Version: 1, + }, + DeleteTrigger: ateapipb.DeleteTrigger_DELETE_TRIGGER_LAST_ACTOR, + AccessMode: ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_ONCE, + VolumeId: "projects/p/zones/z/disks/substrate-8ba9b6ee", + VolumeType: "pd.csi.storage.gke.io", + Status: &ateapipb.SharedVolumeStatus{ + State: ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY, + }, + } +} + +func TestUpdateSharedVolumeDeleteTrigger(t *testing.T) { + client := &fakeSharedVolumeClient{volume: testSharedVolume()} + ref := &ateapipb.ObjectRef{Atespace: "space-1", Name: "vol-1"} + want := ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL + + resp, err := updateSharedVolumeDeleteTrigger(context.Background(), client, ref, want) + if err != nil { + t.Fatalf("updateSharedVolumeDeleteTrigger() error = %v", err) + } + + if client.getCalls != 1 { + t.Errorf("GetSharedVolume called %d times, want 1", client.getCalls) + } + + sent := client.updateReq.GetSharedVolume().GetMetadata() + if sent.GetUid() != "8ba9b6ee-2e1a-4c0f-9f4e-2f0a0f1c3d55" || sent.GetVersion() != 1 { + t.Errorf("update sent uid %q version %d, want the values just read", sent.GetUid(), sent.GetVersion()) + } + + wantResp := testSharedVolume() + wantResp.DeleteTrigger = want + wantResp.Metadata.Version = 2 + if diff := cmp.Diff(wantResp, resp, protocmp.Transform()); diff != "" { + t.Errorf("returned shared volume mismatch (-want +got):\n%s", diff) + } +} + +func TestParseDeleteTrigger(t *testing.T) { + tests := []struct { + value string + want ateapipb.DeleteTrigger + wantErr bool + }{ + {value: "", want: ateapipb.DeleteTrigger_DELETE_TRIGGER_UNSPECIFIED}, + {value: "manual", want: ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL}, + {value: "Last-Actor", want: ateapipb.DeleteTrigger_DELETE_TRIGGER_LAST_ACTOR}, + {value: "atespace", wantErr: true}, + } + for _, tc := range tests { + t.Run(tc.value, func(t *testing.T) { + got, err := parseDeleteTrigger(tc.value) + if (err != nil) != tc.wantErr { + t.Fatalf("parseDeleteTrigger(%q) error = %v, wantErr %v", tc.value, err, tc.wantErr) + } + if got != tc.want { + t.Errorf("parseDeleteTrigger(%q) = %v, want %v", tc.value, got, tc.want) + } + }) + } +} + +func TestParseAccessMode(t *testing.T) { + tests := []struct { + value string + want ateapipb.AccessMode + wantErr bool + }{ + {value: "", want: ateapipb.AccessMode_ACCESS_MODE_UNSPECIFIED}, + {value: "rwo", want: ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_ONCE}, + {value: "ReadWriteMany", want: ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_MANY}, + {value: "rox", wantErr: true}, + } + for _, tc := range tests { + t.Run(tc.value, func(t *testing.T) { + got, err := parseAccessMode(tc.value) + if (err != nil) != tc.wantErr { + t.Fatalf("parseAccessMode(%q) error = %v, wantErr %v", tc.value, err, tc.wantErr) + } + if got != tc.want { + t.Errorf("parseAccessMode(%q) = %v, want %v", tc.value, got, tc.want) + } + }) + } +} diff --git a/cmd/kubectl-ate/internal/printer/printer.go b/cmd/kubectl-ate/internal/printer/printer.go index f77cbcecf6..7db6332891 100644 --- a/cmd/kubectl-ate/internal/printer/printer.go +++ b/cmd/kubectl-ate/internal/printer/printer.go @@ -388,6 +388,85 @@ func PrintTagTo(out io.Writer, tag *ateapipb.Tag, format string) error { return PrintTagsTo(out, []*ateapipb.Tag{tag}, format) } +// PrintSharedVolumesTo prints external volumes to the provided writer. +func PrintSharedVolumesTo(out io.Writer, volumes []*ateapipb.SharedVolume, format string) error { + slices.SortFunc(volumes, func(a, b *ateapipb.SharedVolume) int { + if c := cmp.Compare(a.GetMetadata().GetAtespace(), b.GetMetadata().GetAtespace()); c != 0 { + return c + } + return cmp.Compare(a.GetMetadata().GetName(), b.GetMetadata().GetName()) + }) + switch format { + case "json", "yaml": + return printProto(out, &ateapipb.ListSharedVolumesResponse{SharedVolumes: volumes}, format) + case "table": + w := tabwriter.NewWriter(out, 0, 0, 3, ' ', 0) + fmt.Fprintln(w, "ATESPACE\tNAME\tSTATE\tVOLUME TYPE\tVOLUME ID\tACCESS MODE\tDELETE TRIGGER\tREFS\tAGE") + for _, volume := range volumes { + volumeType, volumeID := "", "" + if volume.GetVolumeId() != "" { + volumeType, volumeID = volume.GetVolumeType(), volume.GetVolumeId() + } + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + volume.GetMetadata().GetAtespace(), volume.GetMetadata().GetName(), + sharedVolumeState(volume), volumeType, volumeID, + accessModeLabel(volume.GetAccessMode()), deleteTriggerLabel(volume.GetDeleteTrigger()), + sharedVolumeRefs(volume), formatAge(volume.GetMetadata().GetCreateTime())) + } + return w.Flush() + default: + return fmt.Errorf("unsupported format %q", format) + } +} + +func sharedVolumeState(volume *ateapipb.SharedVolume) string { + if volume.GetStatus().GetState() != ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY { + return "Pending" + } + return "Ready" +} + +func accessModeLabel(mode ateapipb.AccessMode) string { + switch mode { + case ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_ONCE: + return "RWO" + case ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_MANY: + return "RWX" + default: + return "" + } +} + +func deleteTriggerLabel(trigger ateapipb.DeleteTrigger) string { + switch trigger { + case ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL: + return "Manual" + case ateapipb.DeleteTrigger_DELETE_TRIGGER_LAST_ACTOR: + return "LastActor" + default: + return "" + } +} + +// sharedVolumeRefs renders the reference count as active/total. +func sharedVolumeRefs(volume *ateapipb.SharedVolume) string { + active := 0 + for _, ref := range volume.GetStatus().GetRefs() { + if ref.GetActive() { + active++ + } + } + return fmt.Sprintf("%d/%d", active, len(volume.GetStatus().GetRefs())) +} + +// PrintSharedVolumeTo prints a single external volume to the provided writer. +func PrintSharedVolumeTo(out io.Writer, volume *ateapipb.SharedVolume, format string) error { + if format == "json" || format == "yaml" { + return printProto(out, volume, format) + } + return PrintSharedVolumesTo(out, []*ateapipb.SharedVolume{volume}, format) +} + func sortAtespaces(atespaces []*ateapipb.Atespace) { slices.SortFunc(atespaces, func(a, b *ateapipb.Atespace) int { return cmp.Compare(a.GetMetadata().GetName(), b.GetMetadata().GetName()) diff --git a/cmd/kubectl-ate/internal/printer/printer_test.go b/cmd/kubectl-ate/internal/printer/printer_test.go index 08c9b76366..86faa291c9 100644 --- a/cmd/kubectl-ate/internal/printer/printer_test.go +++ b/cmd/kubectl-ate/internal/printer/printer_test.go @@ -717,6 +717,116 @@ func TestPrintTagTo_YAML(t *testing.T) { } } +func TestPrintSharedVolumesTo_Table(t *testing.T) { + now := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) + pinNow(t, now) + + var buf bytes.Buffer + volumes := []*ateapipb.SharedVolume{ + { + Metadata: &ateapipb.ResourceMetadata{ + Atespace: "team-a", + Name: "shared", + CreateTime: timestamppb.New(now.Add(-5 * time.Minute)), + }, + DeleteTrigger: ateapipb.DeleteTrigger_DELETE_TRIGGER_LAST_ACTOR, + AccessMode: ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_ONCE, + VolumeId: "projects/p/zones/z/disks/substrate-1", + VolumeType: "pd.csi.storage.gke.io", + Status: &ateapipb.SharedVolumeStatus{ + State: ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY, + Refs: []*ateapipb.ActorRef{ + {ActorUid: "uid-1", ActorName: "producer", Active: true}, + {ActorUid: "uid-2", ActorName: "consumer"}, + }, + }, + }, + { + Metadata: &ateapipb.ResourceMetadata{ + Atespace: "team-a", + Name: "adopted", + CreateTime: timestamppb.New(now.Add(-5 * time.Hour)), + }, + DeleteTrigger: ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL, + AccessMode: ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_ONCE, + VolumeId: "projects/p/zones/z/disks/preexisting", + VolumeType: "pd.csi.storage.gke.io", + Status: &ateapipb.SharedVolumeStatus{ + State: ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_READY, + }, + }, + { + Metadata: &ateapipb.ResourceMetadata{ + Atespace: "team-a", + Name: "stranded", + CreateTime: timestamppb.New(now.Add(-30 * time.Second)), + }, + DeleteTrigger: ateapipb.DeleteTrigger_DELETE_TRIGGER_LAST_ACTOR, + AccessMode: ateapipb.AccessMode_ACCESS_MODE_READ_WRITE_ONCE, + Status: &ateapipb.SharedVolumeStatus{ + State: ateapipb.SharedVolumeState_SHARED_VOLUME_STATE_PENDING, + }, + }, + } + + if err := PrintSharedVolumesTo(&buf, volumes, "table"); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expected := `ATESPACE NAME STATE VOLUME TYPE VOLUME ID ACCESS MODE DELETE TRIGGER REFS AGE +team-a adopted Ready pd.csi.storage.gke.io projects/p/zones/z/disks/preexisting RWO Manual 0/0 5h +team-a shared Ready pd.csi.storage.gke.io projects/p/zones/z/disks/substrate-1 RWO LastActor 1/2 5m +team-a stranded Pending RWO LastActor 0/0 30s +` + if diff := cmp.Diff(expected, buf.String()); diff != "" { + t.Errorf("output mismatch (-want +got):\n%s", diff) + } +} + +func TestPrintSharedVolumesTo_Invalid(t *testing.T) { + var buf bytes.Buffer + if err := PrintSharedVolumesTo(&buf, nil, "xml"); err == nil { + t.Errorf("expected error for invalid format, got nil") + } +} + +func TestPrintSharedVolumeTo_JSON(t *testing.T) { + var buf bytes.Buffer + volume := &ateapipb.SharedVolume{Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "shared"}} + + if err := PrintSharedVolumeTo(&buf, volume, "json"); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expected := `{ + "metadata": { + "atespace": "team-a", + "name": "shared" + } +} +` + if diff := cmp.Diff(expected, buf.String()); diff != "" { + t.Errorf("output mismatch (-want +got):\n%s", diff) + } +} + +func TestPrintSharedVolumeTo_YAML(t *testing.T) { + var buf bytes.Buffer + volume := &ateapipb.SharedVolume{Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "shared"}} + + if err := PrintSharedVolumeTo(&buf, volume, "yaml"); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expected := `metadata: + atespace: team-a + name: shared +` + if diff := cmp.Diff(expected, buf.String()); diff != "" { + t.Errorf("output mismatch (-want +got):\n%s", diff) + } +} + func TestPrintAtespacesTo_Table(t *testing.T) { now := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) pinNow(t, now) diff --git a/demos/counter/counter-microvm-csi-test-template.yaml b/demos/counter/counter-microvm-csi-test-template.yaml index fd20354aae..deafed5381 100644 --- a/demos/counter/counter-microvm-csi-test-template.yaml +++ b/demos/counter/counter-microvm-csi-test-template.yaml @@ -48,6 +48,7 @@ sandboxConfig: configName: microvm volumes: - name: data - externalVolumeTemplate: - capacity: 1Gi - storageClassName: csi-hostpath-sc + external: + template: + capacity: 1Gi + storageClassName: csi-hostpath-sc diff --git a/demos/sandbox/manual-test-multi-template.yaml b/demos/sandbox/manual-test-multi-template.yaml index 3adaa45c92..c554c55f95 100644 --- a/demos/sandbox/manual-test-multi-template.yaml +++ b/demos/sandbox/manual-test-multi-template.yaml @@ -50,10 +50,12 @@ sandboxConfig: configName: gvisor-default volumes: - name: vol-hostpath - externalVolumeTemplate: - capacity: 1Gi - storageClassName: csi-hostpath-sc + external: + template: + capacity: 1Gi + storageClassName: csi-hostpath-sc - name: vol-nfs - externalVolumeTemplate: - capacity: 1Gi - storageClassName: csi-nfs-sc + external: + template: + capacity: 1Gi + storageClassName: csi-nfs-sc diff --git a/docs/api-guide.md b/docs/api-guide.md index 8958b99338..b4a7be73a3 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -102,7 +102,7 @@ The `ActorTemplate` defines the code, environment, and state-management policies | `sandboxConfig` | `SandboxConfig` | **Required.** The sandbox runtime selection: `sandboxClass` (**required**, `gvisor` or `microvm`) picks the runtime family this template's actors require — only `WorkerPool`s whose `sandboxClasses` entry matches are eligible — and `configName` (**required**) names the cluster-scoped [`SandboxConfig`](#3-sandboxconfig-the-sandbox-itself) object supplying the sandbox binaries. It must reference an existing config of the matching class; `CreateActorTemplate` rejects the template otherwise. | | `workerSelector` | `*Selector` | Optional. Gates which `WorkerPool`s actors from this template may use, by matching against each pool's labels (`matchLabels`). If unset, all pools are eligible (subject to the actor's own `worker_selector`). | | `snapshotConfig` | `SnapshotConfig` | **Required.** The base object-storage location snapshots are written under, the snapshot fidelity (`preferredFidelity`) every Pause and Suspend captures (`SNAPSHOT_FIDELITY_ROOTFS` requires `sandboxClass: microvm`), and automated golden snapshot creation (`goldenSnapshotConfig.mode`). See [Snapshot Storage Layout](#snapshot-storage-layout). | -| `volumes` | `[]Volume` | Optional. Volumes the containers may mount, each a `durableDir`, an `externalVolumeTemplate` (see [CSI Volumes Guide](csi-volumes.md)), or a `systemInfo` volume (see [SystemInfo Volumes](#systeminfo-volumes)). Every declared volume must be mounted by at least one container. A `microvm` template may declare several `durableDir` volumes; a `gvisor` template is limited to one. | +| `volumes` | `[]Volume` | Optional. Volumes the containers may mount, each a `durableDir`, an `external` CSI source (see [CSI Volumes Guide](csi-volumes.md)), or a `systemInfo` volume (see [SystemInfo Volumes](#systeminfo-volumes)). Every declared volume must be mounted by at least one container. A `microvm` template may declare several `durableDir` volumes; a `gvisor` template is limited to one. | | `resources` | `*ResourceRequirements` | Required. Declares each actor's compute size via `limits`, which must set both `cpu` and `memory` — see [Sandbox Right-Sizing](#sandbox-right-sizing-resources). Immutable, like the rest of the template. | The sandbox itself — the binaries (e.g. the gVisor `runsc` binary) and the `pauseImage` holding the sandbox's namespaces — comes from the cluster-scoped [`SandboxConfig`](#3-sandboxconfig-the-sandbox-itself) object the template names via `sandboxConfig.configName`. An actor always resolves the config from its current template — repointing the actor at another template requires the same config. diff --git a/docs/api-style-guide.md b/docs/api-style-guide.md index 641c110a11..5624b3afe6 100644 --- a/docs/api-style-guide.md +++ b/docs/api-style-guide.md @@ -622,7 +622,7 @@ message Volume { // +k8s:format=k8s-short-name string name = 1; - // Exactly one of durable_dir / external_volume_template / image must be set. + // Exactly one of durable_dir / external / image must be set. // // +k8s:optional // +k8s:unionMember @@ -630,7 +630,7 @@ message Volume { // +k8s:optional // +k8s:unionMember - ExternalVolumeTemplate external_volume_template = 3; + ExternalVolumeSource external = 7; // +k8s:optional // +k8s:unionMember diff --git a/docs/csi-volumes.md b/docs/csi-volumes.md index 59b77af85f..a85ece8283 100644 --- a/docs/csi-volumes.md +++ b/docs/csi-volumes.md @@ -16,7 +16,7 @@ do not need it. In Kubernetes, volumes are reconciled asynchronously via standard Kubernetes objects (e.g. `PersistentVolumeClaim`, `PersistentVolume`). Agent Substrate takes a different approach tailored for actor lifecycle operations: -* **No PV or PVC Objects:** External volumes are declaratively defined in the [`ActorTemplate`](api-guide.md#2-actortemplate-the-workload-blueprint) via `externalVolumeTemplate` and provisioned dynamically for each actor instance. Volume operations are coupled directly with the actor lifecycle. +* **No PV or PVC Objects:** External volumes are declaratively defined in the [`ActorTemplate`](api-guide.md#2-actortemplate-the-workload-blueprint) via `external.template` and provisioned dynamically for each actor instance. Volume operations are coupled directly with the actor lifecycle. * **Direct Network-Based CSI Controller:** The Substrate control plane (`ateapi`) communicates directly with the CSI Controller gRPC service over the network (via TCP or DNS endpoints, optionally secured with TLS/mTLS). --- @@ -74,7 +74,7 @@ External volumes are declared on the `ActorTemplate` resource. For complete deta To attach a CSI volume to an actor: -1. Define the volume under `volumes` with an `externalVolumeTemplate`. +1. Define the volume under `volumes` with an `external.template`. 2. Mount the volume inside one or more containers under `containers[].volumeMounts`. #### `volumes[]` @@ -82,14 +82,15 @@ To attach a CSI volume to an actor: ```yaml volumes: - name: my-data-volume - externalVolumeTemplate: - capacity: 10Gi - storageClassName: standard-rwx + external: + template: + capacity: 10Gi + storageClassName: standard-rwx ``` * `name`: Unique DNS-label-compliant volume name. -* `externalVolumeTemplate.capacity`: Quantity string representing the requested volume size (e.g. `1Gi`, `50Gi`). -* `externalVolumeTemplate.storageClassName`: Name of a Kubernetes `StorageClass` present in the cluster whose `provisioner` matches a registered `CSIDriverConfig`. +* `external.template.capacity`: Quantity string representing the requested volume size (e.g. `1Gi`, `50Gi`). +* `external.template.storageClassName`: Name of a Kubernetes `StorageClass` present in the cluster whose `provisioner` matches a registered `CSIDriverConfig`. #### `containers[].volumeMounts[]` @@ -200,7 +201,147 @@ snapshotConfig: storageLocation: gs://my-snapshots-bucket/stateful-agent volumes: - name: shared-storage - externalVolumeTemplate: - capacity: 5Gi - storageClassName: csi-nfs-sc + external: + template: + capacity: 5Gi + storageClassName: csi-nfs-sc ``` + +## 5. Shared Volumes + +A `SharedVolume` is an atespace-scoped CSI volume whose lifetime is +independent of any actor, so several actors can mount the same storage in turn. +Create one from a StorageClass, or register a volume that already exists: + +```bash +kubectl ate create sharedvolume ctx-42 -a demo --storage-class csi-nfs-sc --capacity 10Gi +kubectl ate create sharedvolume imported -a demo --volume-id --volume-type nfs.csi.k8s.io +``` + +Each volume records: + +- `accessMode`: fixed at create. Only `READ_WRITE_ONCE` is accepted today, so + at most one resumed actor holds the volume; a resume that would take a held + volume fails and names the holder. +- `deleteTrigger`: `MANUAL` removes the volume only on an explicit delete; + `LAST_ACTOR` removes it when the last actor referencing it is deleted. + Provisioned volumes default to `LAST_ACTOR`, registered ones to `MANUAL`. + Deleting the volume also deletes the storage. +- `status.refs`: every actor referencing the volume, with `active` set while + the actor is resumed. A referenced volume cannot be deleted. + +A template mounts a `SharedVolume` with `external.sharedVolume`. Name the volume +in the template, or leave the name empty and bind it per actor at creation: + +```yaml +volumes: +- name: shared + external: {sharedVolume: {}} +``` + +```bash +kubectl ate create actor producer-42 -a demo --template producer --volume shared=ctx-42 +kubectl ate create actor consumer-42 -a demo --template consumer --volume shared=ctx-42 +``` + +Bindings are validated and recorded when the actor is created and cannot change +afterwards. + +Templates referencing a `SharedVolume` cold-boot by default and restore their own +snapshots on later resumes. Explicit `sourceTag` cloning remains refused. + +MicroVM templates can opt into `snapshotConfig.goldenSnapshotConfig.mode: +GOLDEN_SNAPSHOT_MODE_RUNTIME`. This snapshots a booted kernel and agent **before +CreateSandbox mounts virtio-fs and before any application process exists**. +On restore, the host stages the actor-selected real volumes, resumes the kernel, +reseeds its random generator, and only then creates the sandbox and application +containers. Readiness is published after the application probes succeed. This +warms VM startup, not application memory. Ordinary own snapshots continue to +capture the running application; the runtime-only marker is not propagated to +those snapshots. + +Application-warm goldens cannot transparently substitute arbitrary filesystem +state: applications may retain bytes from the source volume in their heap. +Ordinary own-snapshot handoffs also retain application memory; changed backing +data and deleted-open handles need application-specific semantics. Runtime +goldens do not change those semantics or claim to repair those cases. + +gVisor templates also support `GOLDEN_SNAPSHOT_MODE_RUNTIME`. They capture only +the running pause container. Application containers and their filesystem handles +do not exist at capture time. Restore resumes that sandbox, then creates and +starts application containers against the actor-selected volumes. This avoids +carrying placeholder-filesystem options into an application restore. Later own +snapshots restore application memory normally. + +### Runtime golden regression coverage + +The preview-enabled microVM and gVisor CI lanes require the runtime golden +correctness cases and NFS CSI; missing prerequisites fail those lanes. Local cluster runs opt in with +`E2E_RUNTIME_GOLDEN=true`, `E2E_SANDBOX_CLASS=microvm` or `gvisor`, and +`E2E_RUNTIME_LOG_DIR` pointing to an evidence directory. For microVM continuity +checks, build/deploy the worker with `GOFLAGS=-tags=e2e_runtime_identity`, as the +CI E2E jobs do. Only those instrumented builds add the eager kernel boot marker; +production builds do not alter the kernel command line for this test. Run +`TestRuntimeGoldenVolumes` in `internal/e2e/suites/combinedvolumes` through +`hack/run-e2e-kind.sh`. This checks distinct volume bindings, guest/process +identity, and retained state over two own-snapshot generations. The eager guest +boot marker is checked only for microVM: gVisor regenerates its procfs boot ID +on restore, so that ID is diagnostic rather than evidence of continuity. + +The probe contract checks directory entries and negative lookups as well as +cached, open-file, and memory-mapped contents. Only the known microVM +deleted-open-file `EIO` on the second generation is tolerated; other errors fail +the test. gVisor must preserve deleted-open contents in both generations. +MicroVM deleted-open recovery remains limited by virtiofsd's inode migration; +runtime-only capture does not repair a later own snapshot's orphaned inode. + +Latency collection is separate: select `TestRuntimeGoldenLatency`, additionally +set `E2E_RUNTIME_GOLDEN_BENCHMARK=true` and `E2E_LATENCY_OUTPUT` to a JSONL file. +`E2E_LATENCY_SAMPLES` defaults to 100 positive measured samples per mode, with +five warmups. It does not disable or replace the correctness test. + +### Shared-volume failure recovery + +Actor creation and its inactive SharedVolume references commit in one database +transaction. Concurrent creates cannot silently lose references. Deletion marks +the volume `DELETING` before contacting CSI; new bindings and claims are refused +from that point onward. If CSI deletion fails, retry `DeleteSharedVolume` to +finish the same deletion. Fetch the latest version before retrying a request +with a version precondition. + +A failed resume rolls back newly acquired claims before attachment. Once node +access may have started, claims are released only after termination and detach +succeed. Failed cleanup keeps the claim active. A crashed actor can therefore +retain its claim: crash detection alone does not establish that the old node +has stopped writing. + +To recover while retaining data, first update the SharedVolume's `deleteTrigger` +to `DELETE_TRIGGER_MANUAL` using its current version, then delete the failed +actor. Repair node/CSI reachability and retry deletion if termination or detach +fails. Only after cleanup succeeds should a replacement actor mount the volume. +Do not clear active references directly in the database. If the original node +is permanently unreachable, fence its storage access at the infrastructure +level before attempting operator-assisted recovery; this API does not provide +a force-release shortcut. With `LAST_ACTOR`, deleting the final actor deletes +storage after successful node cleanup. + +SharedVolume RPC authorization follows atespace roles: viewers can get/list; +editors can create/update/delete; global viewers can list across atespaces. +This uses the same authorization enforcement configuration as the other +resource RPCs. + +### Upgrade from the old external-volume template field + +This pre-1.0 change replaces `volumes[].externalVolumeTemplate` (protobuf field +3) with `volumes[].external.template`. It is a breaking stored-template change, +not an automatic migration. Do not roll this binary into a deployment with old +stored templates and expect their volume sources to survive decoding. + +Before upgrading, export the affected templates and inventory/back up their +storage using the old binary. Rewrite manifests to the new nested field and +plan recreation of affected templates and actors in a fresh development +installation. Reattach retained storage through registered SharedVolumes after +validating the backups and CSI metadata. Existing actor memory snapshots are +not an upgrade mechanism. Keep the old installation intact until the replacement +is validated. The repository's pre-1.0 policy deliberately provides no legacy +field decoder, deprecated alias, or reserved-field compatibility guarantee. diff --git a/internal/e2e/fixtures/probe/main.go b/internal/e2e/fixtures/probe/main.go index e1c3a113b0..65d2e55ebd 100644 --- a/internal/e2e/fixtures/probe/main.go +++ b/internal/e2e/fixtures/probe/main.go @@ -472,6 +472,9 @@ func main() { mux := http.NewServeMux() mux.HandleFunc("/whoami", whoami) mux.HandleFunc("/fetch", fetch) + mux.HandleFunc("/runtime-state", runtimeState) + mux.HandleFunc("/volume-fixture", seedVolumeFixture) + mux.HandleFunc("/volume-state", (&volumeStateProbe{}).serveHTTP) mux.HandleFunc("/readfile", readfile) mux.HandleFunc("/writefile", writefile) mux.HandleFunc("/resources", resources) diff --git a/internal/e2e/fixtures/probe/runtime_state.go b/internal/e2e/fixtures/probe/runtime_state.go new file mode 100644 index 0000000000..6a1c98e3a8 --- /dev/null +++ b/internal/e2e/fixtures/probe/runtime_state.go @@ -0,0 +1,110 @@ +// 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 ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "os" + "path/filepath" + "strings" +) + +// Generated once per process; unlike boot_id, runtime-only clones must differ. +var processNonce = func() string { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + panic(err) + } + return hex.EncodeToString(b) +}() + +func runtimeState(w http.ResponseWriter, r *http.Request) { + b, err := os.ReadFile("/proc/sys/kernel/random/boot_id") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + kind := r.URL.Query().Get("runtime") + var cmdline []byte + if kind == "" || kind == "microvm" { + cmdline, err = os.ReadFile("/proc/cmdline") + if err != nil { + http.Error(w, err.Error(), 500) + return + } + } + marker, err := runtimeBootIdentity(kind, strings.TrimSpace(string(b)), string(cmdline)) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + writeJSON(w, map[string]string{"boot_id": marker, "kernel_boot_id": strings.TrimSpace(string(b)), "process_nonce": processNonce}) +} + +// gVisor supplies a diagnostic boot ID that changes on restore. MicroVM +// continuity uses our eager marker because Linux initializes boot_id lazily. +func runtimeBootIdentity(kind, kernelBoot, cmdline string) (string, error) { + if kind == "gvisor" { + if kernelBoot == "" { + return "", fmt.Errorf("sandbox boot identity missing") + } + return kernelBoot, nil + } + if kind != "" && kind != "microvm" { + return "", fmt.Errorf("unsupported runtime %q", kind) + } + for _, arg := range strings.Fields(cmdline) { + if marker, ok := strings.CutPrefix(arg, "ate.boot_id="); ok && marker != "" { + return marker, nil + } + } + return "", fmt.Errorf("guest boot marker missing") +} + +// seedVolumeFixture seeds a disposable fixture before the retained-state probe is +// primed. Never call it after restore: that would conceal stale volume state. +func seedVolumeFixture(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + var req struct { + Root string `json:"root"` + Value string `json:"value"` + } + if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8192)).Decode(&req); err != nil { + http.Error(w, err.Error(), 400) + return + } + if !filepath.IsAbs(req.Root) || req.Root == "/" || req.Value == "" { + http.Error(w, "absolute disposable root and value required", 400) + return + } + if err := os.MkdirAll(filepath.Join(req.Root, "directory"), 0755); err != nil { + http.Error(w, err.Error(), 500) + return + } + for _, name := range []string{"cached.txt", "held.txt", "unlinked.txt", "directory/entry"} { + if err := os.WriteFile(filepath.Join(req.Root, name), []byte(req.Value), 0644); err != nil { + http.Error(w, err.Error(), 500) + return + } + } + writeJSON(w, map[string]string{"ok": "true"}) +} diff --git a/internal/e2e/fixtures/probe/runtime_state_test.go b/internal/e2e/fixtures/probe/runtime_state_test.go new file mode 100644 index 0000000000..3745d54b5c --- /dev/null +++ b/internal/e2e/fixtures/probe/runtime_state_test.go @@ -0,0 +1,71 @@ +// 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 ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestSeedVolumeFixture(t *testing.T) { + root := t.TempDir() + body, _ := json.Marshal(map[string]string{"root": root, "value": "actor-a"}) + w := httptest.NewRecorder() + seedVolumeFixture(w, httptest.NewRequest(http.MethodPost, "/volume-fixture", bytes.NewReader(body))) + if w.Code != http.StatusOK { + t.Fatalf("seed: %d %s", w.Code, w.Body.String()) + } + p := &volumeStateProbe{} + volumePrime(t, p, root) + got := volumeObserve(t, p) + if got.Cached != "actor-a" || got.Held.Data != "actor-a" || got.Unlinked.Data != "actor-a" || !got.Negative.NotExist { + t.Fatalf("fixture contents: %+v", got) + } +} + +func TestSeedVolumeFixtureRejectsUnsafeRoot(t *testing.T) { + for _, root := range []string{"", "/", "relative"} { + body, _ := json.Marshal(map[string]string{"root": root, "value": "a"}) + w := httptest.NewRecorder() + seedVolumeFixture(w, httptest.NewRequest(http.MethodPost, "/volume-fixture", bytes.NewReader(body))) + if w.Code != http.StatusBadRequest { + t.Fatalf("root %q: %d", root, w.Code) + } + } +} + +func TestRuntimeBootIdentity(t *testing.T) { + for _, tc := range []struct { + name, kind, kernel, cmdline, want string + bad bool + }{ + {"microvm eager marker", "microvm", "lazy", "quiet ate.boot_id=eager", "eager", false}, + {"default microvm", "", "lazy", "ate.boot_id=eager", "eager", false}, + {"microvm must not fall back", "microvm", "lazy", "", "", true}, + {"gvisor proc identity", "gvisor", "sandbox", "", "sandbox", false}, + {"missing gvisor identity", "gvisor", "", "", "", true}, + {"unsupported runtime", "other", "some", "ate.boot_id=some", "", true}, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := runtimeBootIdentity(tc.kind, tc.kernel, tc.cmdline) + if got != tc.want || (err != nil) != tc.bad { + t.Fatalf("got %q, %v", got, err) + } + }) + } +} diff --git a/internal/e2e/fixtures/probe/volume-state.md b/internal/e2e/fixtures/probe/volume-state.md new file mode 100644 index 0000000000..e4f9641a25 --- /dev/null +++ b/internal/e2e/fixtures/probe/volume-state.md @@ -0,0 +1,56 @@ +# Filesystem state across snapshots + +`/volume-state` is an opt-in, destructive probe for a **disposable writable +fixture directory**. It observes process memory and retained descriptors. It +does not enable external-volume golden restores or assert that a restore is safe. + +Before capture, provision these small fixtures (each file at most 4096 bytes): + +- `cached.txt`: distinguishable source content copied into process memory. +- `held.txt`: a file kept open and read before capture. +- `unlinked.txt`: a file opened, read, and unlinked by the probe before capture. +- `directory/`: a directory kept open and enumerated before capture. +- `negative.txt`: must be absent; priming performs a negative lookup. + +Use different sentinel contents for the source, actor A and actor B. Keep +expected values in the test controller, outside the captured process. + +1. POST `/volume-state?root=&challenge=`. + Expect 204. Priming unlinks `unlinked.txt`; it must be on writable scratch + storage. A second POST returns 409, even after a successful memory restore. +2. GET `/volume-state` and record the response before capturing the snapshot. + The response includes the challenge, an increasing read count, application + memory (`cached`), a fresh read of `cached.txt` (`by_path`), retained file + reads (`held`, `unlinked`), a fresh lookup (`negative`), and enumeration through + the retained directory descriptor (`directory`). Errors are reported separately + and must be asserted by the test; HTTP 200 alone is not success. +3. Restore the captured snapshot, then GET only. Do not POST to the clone. + An unprimed process returns 409, so a cold fallback cannot silently pass this + continuity check. Require the original challenge and a count greater than + the pre-capture count. Independently verify the source snapshot ID and runtime + restore path; a live source process also retains the challenge. +4. After an own-snapshot capture, check the challenge and count again. This + catches loss of probe state in second-generation captures. +5. DELETE `/volume-state` closes descriptors and clears state. It does not + recreate the unlinked file. It returns 204 even when already unprimed. + +For substitution experiments, seed the target backing tree from the host while +workloads are stopped. Create `negative.txt`, use distinct `cached.txt` and +`held.txt` contents, and give `directory/` different entries. Preserve all raw +responses, including read errors. An open deleted file may not be reconstructible +by path. A successful VM resume, EIO, or continued source content is not evidence +of safe substitution. In particular, the `cached` field is a deliberate copy in +application memory: host path rewrites or kernel cache invalidation cannot turn +it into target content. + +An empty production placeholder needs a separate negative-lookup-only test. +These populated fixtures intentionally stress a more demanding contract; they +must not be described as the existing empty-placeholder golden workflow. +This endpoint is not yet wired into a cluster test or the automatic golden +capture lifecycle. It is a building block for that test, not an end-to-end result. + +The local tests replace files and directories on the host. They verify that the +probe distinguishes path reads, retained descriptors and application memory, +rejects unprimed reads/re-priming, cleans up failed priming, and serializes reads. +They do not test NFS, virtio-fs migration, mmap, read-only enforcement, actor +isolation or a restored VM. Those require separate runtime integration tests. diff --git a/internal/e2e/fixtures/probe/volume_state.go b/internal/e2e/fixtures/probe/volume_state.go new file mode 100644 index 0000000000..92594afad3 --- /dev/null +++ b/internal/e2e/fixtures/probe/volume_state.go @@ -0,0 +1,196 @@ +// 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 ( + "encoding/json" + "errors" + "fmt" + "github.com/agent-substrate/substrate/internal/e2e/probeapi" + "io" + "net/http" + "os" + "path/filepath" + "sort" + "sync" + "syscall" +) + +// volumeStateProbe retains guest process state across snapshots. A controller +// primes it once before capture and only GETs after restore. Re-priming a +// restored process would hide cold-boot fallbacks and stale filesystem state. +// Use only a disposable writable fixture directory containing cached.txt, +// held.txt, unlinked.txt and directory/, with negative.txt absent. +type volumeStateProbe struct { + mu sync.Mutex + root, challenge string + cached string + held, unlinked, directory *os.File + mapped []byte + reads uint64 +} + +func volumeResult(data []byte, err error) probeapi.VolumeObservation { + if err != nil { + out := probeapi.VolumeObservation{Error: err.Error(), NotExist: errors.Is(err, os.ErrNotExist)} + if errors.Is(err, syscall.EIO) { + out.ErrorCode = "EIO" + } + return out + } + return probeapi.VolumeObservation{Data: string(data)} +} + +// Fixtures are deliberately small; oversized data is an error, never a +// truncated success that could hide the distinguishing part of a sentinel. +func volumeRead(f *os.File) probeapi.VolumeObservation { + b := make([]byte, 4097) + n, err := f.ReadAt(b, 0) + if err == io.EOF { + err = nil + } + if n > 4096 { + err = fmt.Errorf("fixture exceeds 4096 bytes") + } + return volumeResult(b[:n], err) +} + +func volumeReadPath(path string) probeapi.VolumeObservation { + f, err := os.Open(path) + if err != nil { + return volumeResult(nil, err) + } + defer f.Close() + return volumeRead(f) +} + +func (p *volumeStateProbe) close() { + if p.mapped != nil { + _ = syscall.Munmap(p.mapped) + p.mapped = nil + } + for _, f := range []*os.File{p.held, p.unlinked, p.directory} { + if f != nil { + _ = f.Close() + } + } + p.held, p.unlinked, p.directory = nil, nil, nil + p.root, p.challenge, p.cached, p.reads = "", "", "", 0 +} + +func (p *volumeStateProbe) prime(root, challenge string) (err error) { + if !filepath.IsAbs(root) || challenge == "" || len(challenge) > 256 { + return fmt.Errorf("absolute root and challenge of 1..256 bytes required") + } + defer func() { + if err != nil { + p.close() + } + }() + cached := volumeReadPath(filepath.Join(root, "cached.txt")) + if cached.Error != "" { + return fmt.Errorf("cached.txt: %s", cached.Error) + } + if _, err = os.Stat(filepath.Join(root, "negative.txt")); !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("negative.txt must be absent (stat: %v)", err) + } + if p.held, err = os.Open(filepath.Join(root, "held.txt")); err != nil { + return err + } + info, err := p.held.Stat() + if err != nil { + return err + } + if info.Size() < 1 || info.Size() > 4096 { + return fmt.Errorf("held.txt must contain 1..4096 bytes for mmap") + } + p.mapped, err = syscall.Mmap(int(p.held.Fd()), 0, int(info.Size()), syscall.PROT_READ, syscall.MAP_SHARED) + if err != nil { + return fmt.Errorf("mapping held.txt: %w", err) + } + if p.directory, err = os.Open(filepath.Join(root, "directory")); err != nil { + return err + } + if _, err = p.directory.Readdirnames(-1); err != nil { + return err + } + if p.unlinked, err = os.Open(filepath.Join(root, "unlinked.txt")); err != nil { + return err + } + // Warm the kernel's file data while retaining descriptors. The response + // separately exposes an application-memory copy that remounting cannot reset. + for _, f := range []*os.File{p.held, p.unlinked} { + if v := volumeRead(f); v.Error != "" { + return fmt.Errorf("warming fixture: %s", v.Error) + } + } + if err = os.Remove(filepath.Join(root, "unlinked.txt")); err != nil { + return err + } + p.root, p.challenge, p.cached = root, challenge, cached.Data + return nil +} + +func (p *volumeStateProbe) observe() probeapi.VolumeStateResponse { + p.reads++ + out := probeapi.VolumeStateResponse{ + Challenge: p.challenge, Reads: p.reads, Cached: p.cached, + ByPath: volumeReadPath(filepath.Join(p.root, "cached.txt")), + Held: volumeRead(p.held), Unlinked: volumeRead(p.unlinked), + Mapped: volumeResult(p.mapped, nil), + Negative: volumeReadPath(filepath.Join(p.root, "negative.txt")), + } + if _, err := p.directory.Seek(0, io.SeekStart); err != nil { + out.DirectoryError = err.Error() + } else { + names, err := p.directory.Readdirnames(-1) + out.Directory = names + sort.Strings(out.Directory) + if err != nil { + out.DirectoryError = err.Error() + } + } + return out +} + +func (p *volumeStateProbe) serveHTTP(w http.ResponseWriter, r *http.Request) { + p.mu.Lock() + defer p.mu.Unlock() + switch r.Method { + case http.MethodPost: + if p.challenge != "" { + http.Error(w, "already primed", http.StatusConflict) + return + } + if err := p.prime(r.URL.Query().Get("root"), r.URL.Query().Get("challenge")); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusNoContent) + case http.MethodGet: + if p.challenge == "" { + http.Error(w, "not primed", http.StatusConflict) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(p.observe()) + case http.MethodDelete: + p.close() + w.WriteHeader(http.StatusNoContent) + default: + w.Header().Set("Allow", "POST, GET, DELETE") + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} diff --git a/internal/e2e/fixtures/probe/volume_state_test.go b/internal/e2e/fixtures/probe/volume_state_test.go new file mode 100644 index 0000000000..b5b725a9dd --- /dev/null +++ b/internal/e2e/fixtures/probe/volume_state_test.go @@ -0,0 +1,226 @@ +// 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 ( + "encoding/json" + "github.com/agent-substrate/substrate/internal/e2e/probeapi" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "strings" + "sync" + "syscall" + "testing" +) + +func volumeFixture(t *testing.T) string { + t.Helper() + root := t.TempDir() + if err := os.Mkdir(filepath.Join(root, "directory"), 0700); err != nil { + t.Fatal(err) + } + for _, name := range []string{"cached.txt", "held.txt", "unlinked.txt", "directory/old"} { + volumeWrite(t, filepath.Join(root, name), "source-"+name) + } + return root +} + +func volumeWrite(t *testing.T, path, value string) { + t.Helper() + if err := os.WriteFile(path, []byte(value), 0600); err != nil { + t.Fatal(err) + } +} + +func volumeRequest(p *volumeStateProbe, method, target string) *httptest.ResponseRecorder { + w := httptest.NewRecorder() + p.serveHTTP(w, httptest.NewRequest(method, target, nil)) + return w +} + +func volumePrime(t *testing.T, p *volumeStateProbe, root string) { + t.Helper() + w := volumeRequest(p, http.MethodPost, "/volume-state?root="+url.QueryEscape(root)+"&challenge=capture-unique-token") + if w.Code != http.StatusNoContent { + t.Fatalf("prime: %d %s", w.Code, w.Body.String()) + } + t.Cleanup(p.close) +} + +func volumeObserve(t *testing.T, p *volumeStateProbe) probeapi.VolumeStateResponse { + t.Helper() + w := volumeRequest(p, http.MethodGet, "/volume-state") + if w.Code != http.StatusOK { + t.Fatalf("observe: %d %s", w.Code, w.Body.String()) + } + var out probeapi.VolumeStateResponse + if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil { + t.Fatal(err) + } + return out +} + +// Ordinary host file replacement validates the probe's ability to distinguish +// path reads, open descriptors and application memory. It is not a VM restore. +func TestVolumeStateDistinguishesRetainedState(t *testing.T) { + p := &volumeStateProbe{} + root := volumeFixture(t) + volumePrime(t, p, root) + first := volumeObserve(t, p) + if !first.Negative.NotExist { + t.Fatalf("negative lookup: %+v", first.Negative) + } + if _, err := os.Stat(filepath.Join(root, "unlinked.txt")); !os.IsNotExist(err) { + t.Fatalf("unlink: %v", err) + } + volumeWrite(t, filepath.Join(root, "replacement"), "target-held") + if err := os.Rename(filepath.Join(root, "replacement"), filepath.Join(root, "held.txt")); err != nil { + t.Fatal(err) + } + volumeWrite(t, filepath.Join(root, "cached.txt"), "target-cached") + volumeWrite(t, filepath.Join(root, "negative.txt"), "target-negative") + if err := os.Rename(filepath.Join(root, "directory"), filepath.Join(root, "old-directory")); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(filepath.Join(root, "directory"), 0700); err != nil { + t.Fatal(err) + } + volumeWrite(t, filepath.Join(root, "directory/new"), "target-directory") + got := volumeObserve(t, p) + if got.Challenge != "capture-unique-token" || got.Reads != 2 { + t.Fatalf("state continuity: %+v", got) + } + if got.Cached != "source-cached.txt" || got.ByPath.Data != "target-cached" || got.ByPath.Error != "" { + t.Fatalf("cache vs path: %+v", got) + } + if got.Held.Data != "source-held.txt" || got.Held.Error != "" { + t.Fatalf("held descriptor: %+v", got.Held) + } + if got.Mapped.Data != "source-held.txt" || got.Mapped.Error != "" { + t.Fatalf("retained mapping: %+v", got.Mapped) + } + if got.Unlinked.Data != "source-unlinked.txt" || got.Unlinked.Error != "" { + t.Fatalf("unlinked descriptor: %+v", got.Unlinked) + } + if got.Negative.Data != "target-negative" || got.Negative.Error != "" { + t.Fatalf("negative lookup: %+v", got.Negative) + } + if got.DirectoryError != "" || strings.Join(got.Directory, ",") != "old" { + t.Fatalf("held directory: %+v", got) + } +} + +func TestVolumeStateColdBootAndReprimeFail(t *testing.T) { + p := &volumeStateProbe{} + if w := volumeRequest(p, http.MethodGet, "/volume-state"); w.Code != http.StatusConflict { + t.Fatalf("unprimed returned %d", w.Code) + } + root := volumeFixture(t) + volumePrime(t, p, root) + if w := volumeRequest(p, http.MethodPost, "/volume-state"); w.Code != http.StatusConflict { + t.Fatalf("reprime returned %d", w.Code) + } + if w := volumeRequest(p, http.MethodDelete, "/volume-state"); w.Code != http.StatusNoContent { + t.Fatalf("release returned %d", w.Code) + } + if w := volumeRequest(p, http.MethodGet, "/volume-state"); w.Code != http.StatusConflict { + t.Fatalf("released returned %d", w.Code) + } +} + +func TestVolumeStatePrimeFailureClosesDescriptors(t *testing.T) { + p := &volumeStateProbe{} + root := volumeFixture(t) + if err := os.Remove(filepath.Join(root, "unlinked.txt")); err != nil { + t.Fatal(err) + } + w := volumeRequest(p, http.MethodPost, "/volume-state?root="+url.QueryEscape(root)+"&challenge=test") + if w.Code != http.StatusBadRequest { + t.Fatalf("missing fixture returned %d", w.Code) + } + if p.held != nil || p.directory != nil || p.unlinked != nil || p.challenge != "" || p.mapped != nil { + t.Fatal("failed prime retained state") + } + volumeWrite(t, filepath.Join(root, "unlinked.txt"), "repaired") + volumePrime(t, p, root) +} + +func TestVolumeStateOversizeAndUnexpectedNegativeFail(t *testing.T) { + for _, name := range []string{"cached.txt", "negative.txt"} { + t.Run(name, func(t *testing.T) { + root := volumeFixture(t) + volumeWrite(t, filepath.Join(root, name), strings.Repeat("x", 4097)) + p := &volumeStateProbe{} + defer p.close() + if err := p.prime(root, "test"); err == nil { + t.Fatal("invalid fixture accepted") + } + }) + } +} + +func TestVolumeStateConcurrentReads(t *testing.T) { + p := &volumeStateProbe{} + volumePrime(t, p, volumeFixture(t)) + var wg sync.WaitGroup + for range 16 { + wg.Go(func() { + if w := volumeRequest(p, http.MethodGet, "/volume-state"); w.Code != http.StatusOK { + t.Errorf("read returned %d", w.Code) + } + }) + } + wg.Wait() + if got := volumeObserve(t, p); got.Reads != 17 { + t.Fatalf("reads = %d", got.Reads) + } +} + +func TestVolumeResultClassifiesWrappedIOError(t *testing.T) { + got := volumeResult(nil, &os.PathError{Op: "read", Path: "unlinked.txt", Err: syscall.EIO}) + if got.ErrorCode != "EIO" || got.Error == "" || got.NotExist { + t.Fatalf("unexpected observation: %+v", got) + } + other := volumeResult(nil, os.ErrPermission) + if other.ErrorCode != "" { + t.Fatalf("unrelated error classified as EIO: %+v", other) + } +} + +func TestVolumeStateMappingSeesInPlaceWrites(t *testing.T) { + p := &volumeStateProbe{} + root := volumeFixture(t) + volumePrime(t, p, root) + f, err := os.OpenFile(filepath.Join(root, "held.txt"), os.O_WRONLY, 0) + if err != nil { + t.Fatal(err) + } + defer f.Close() + value := "update-held.txt" + if _, err := f.WriteAt([]byte(value), 0); err != nil { + t.Fatal(err) + } + got := volumeObserve(t, p) + if got.Mapped.Data != value || got.Held.Data != value { + t.Fatalf("mapping did not observe backing inode update: %+v", got) + } + p.close() + if p.mapped != nil { + t.Fatal("mapping retained after close") + } +} diff --git a/internal/e2e/probeapi/volume.go b/internal/e2e/probeapi/volume.go new file mode 100644 index 0000000000..c02dfedfea --- /dev/null +++ b/internal/e2e/probeapi/volume.go @@ -0,0 +1,68 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package probeapi defines the HTTP contracts shared by the probe and its tests. +package probeapi + +import ( + "fmt" + "slices" +) + +type VolumeObservation struct { + Data string `json:"data"` + Error string `json:"error,omitempty"` + ErrorCode string `json:"error_code,omitempty"` + NotExist bool `json:"not_exist,omitempty"` +} + +type VolumeStateResponse struct { + Challenge string `json:"challenge"` + Reads uint64 `json:"reads"` + Cached string `json:"cached"` + ByPath VolumeObservation `json:"by_path"` + Held VolumeObservation `json:"held"` + Mapped VolumeObservation `json:"mapped"` + Unlinked VolumeObservation `json:"unlinked"` + Negative VolumeObservation `json:"negative"` + Directory []string `json:"directory"` + DirectoryError string `json:"directory_error,omitempty"` +} + +// ValidateRetainedState checks the disposable fixture across an own-snapshot +// restore. The microVM-only second-generation EIO exception must be explicit. +func ValidateRetainedState(obs VolumeStateResponse, challenge, value string, generation int, allowDeletedOpenEIO bool) error { + if obs.Challenge != challenge || obs.Cached != value { + return fmt.Errorf("application state mismatch: %+v", obs) + } + for name, got := range map[string]VolumeObservation{"path": obs.ByPath, "held": obs.Held, "mapped": obs.Mapped} { + if got.Error != "" || got.ErrorCode != "" || got.NotExist || got.Data != value { + return fmt.Errorf("%s observation mismatch: %+v", name, got) + } + } + if obs.DirectoryError != "" || !slices.Equal(obs.Directory, []string{"entry"}) { + return fmt.Errorf("directory mismatch: %+v", obs) + } + if !obs.Negative.NotExist || obs.Negative.Error == "" || obs.Negative.Data != "" { + return fmt.Errorf("negative lookup mismatch: %+v", obs.Negative) + } + u := obs.Unlinked + if allowDeletedOpenEIO && generation == 2 && u.ErrorCode == "EIO" && u.Error != "" && u.Data == "" && !u.NotExist { + return nil + } + if u.Error != "" || u.ErrorCode != "" || u.NotExist || u.Data != value { + return fmt.Errorf("deleted-open observation mismatch at generation %d: %+v", generation, u) + } + return nil +} diff --git a/internal/e2e/probeapi/volume_test.go b/internal/e2e/probeapi/volume_test.go new file mode 100644 index 0000000000..5f0bed06a8 --- /dev/null +++ b/internal/e2e/probeapi/volume_test.go @@ -0,0 +1,62 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package probeapi + +import "testing" + +func TestValidateRetainedState(t *testing.T) { + valid := VolumeStateResponse{Challenge: "c", Cached: "v", ByPath: VolumeObservation{Data: "v"}, Held: VolumeObservation{Data: "v"}, Mapped: VolumeObservation{Data: "v"}, Unlinked: VolumeObservation{Data: "v"}, Negative: VolumeObservation{NotExist: true, Error: "not found"}, Directory: []string{"entry"}} + for _, tc := range []struct { + name string + generation int + mutate func(*VolumeStateResponse) + bad bool + }{ + {"first generation", 1, func(*VolumeStateResponse) {}, false}, + {"second generation", 2, func(*VolumeStateResponse) {}, false}, + {"known EIO", 2, func(v *VolumeStateResponse) { + v.Unlinked = VolumeObservation{Error: "read: input/output error", ErrorCode: "EIO"} + }, false}, + {"premature EIO", 1, func(v *VolumeStateResponse) { + v.Unlinked = VolumeObservation{Error: "read: input/output error", ErrorCode: "EIO"} + }, true}, + {"unrelated error", 2, func(v *VolumeStateResponse) { v.Unlinked = VolumeObservation{Error: "permission denied"} }, true}, + {"empty directory", 2, func(v *VolumeStateResponse) { v.Directory = nil }, true}, + {"wrong directory", 2, func(v *VolumeStateResponse) { v.Directory = []string{"other"} }, true}, + {"negative exists", 2, func(v *VolumeStateResponse) { v.Negative = VolumeObservation{Data: "leaked"} }, true}, + {"mapped mismatch", 2, func(v *VolumeStateResponse) { v.Mapped.Data = "other" }, true}, + {"held error", 2, func(v *VolumeStateResponse) { v.Held.Error = "failed" }, true}, + {"cached mismatch", 2, func(v *VolumeStateResponse) { v.Cached = "other" }, true}, + } { + t.Run(tc.name, func(t *testing.T) { + v := valid + tc.mutate(&v) + if err := ValidateRetainedState(v, "c", "v", tc.generation, true); (err != nil) != tc.bad { + t.Fatalf("got %v, want bad=%v", err, tc.bad) + } + }) + } +} + +func TestValidateRetainedStateRejectsUnapprovedEIO(t *testing.T) { + obs := VolumeStateResponse{Challenge: "c", Cached: "v", ByPath: VolumeObservation{Data: "v"}, Held: VolumeObservation{Data: "v"}, Mapped: VolumeObservation{Data: "v"}, Unlinked: VolumeObservation{Data: "v"}, Negative: VolumeObservation{NotExist: true, Error: "not found"}, Directory: []string{"entry"}} + if err := ValidateRetainedState(obs, "c", "v", 2, false); err != nil { + t.Fatal(err) + } + obs.Unlinked = VolumeObservation{Error: "read: input/output error", ErrorCode: "EIO"} + if err := ValidateRetainedState(obs, "c", "v", 2, false); err == nil { + t.Fatal("unapproved deleted-open EIO accepted") + } +} diff --git a/internal/e2e/runtimegoldenenv/env.go b/internal/e2e/runtimegoldenenv/env.go new file mode 100644 index 0000000000..5ad06b19d7 --- /dev/null +++ b/internal/e2e/runtimegoldenenv/env.go @@ -0,0 +1,39 @@ +// 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 runtimegoldenenv defines when runtime golden regression tests are required. +package runtimegoldenenv + +import ( + "fmt" + "os" +) + +// Required is set by the preview-enabled CI lanes for both runtimes. +func Required() bool { + return os.Getenv("CI") == "true" && os.Getenv("E2E_RUNTIME_GOLDEN_REQUIRED") == "true" +} +func Enabled() bool { return Required() || os.Getenv("E2E_RUNTIME_GOLDEN") == "true" } +func Check() error { + if !Enabled() { + return nil + } + if kind := os.Getenv("E2E_SANDBOX_CLASS"); kind != "microvm" && kind != "gvisor" { + return fmt.Errorf("runtime golden tests require microvm or gvisor") + } + if os.Getenv("E2E_RUNTIME_LOG_DIR") == "" { + return fmt.Errorf("runtime golden tests require E2E_RUNTIME_LOG_DIR") + } + return nil +} diff --git a/internal/e2e/runtimegoldenenv/env_test.go b/internal/e2e/runtimegoldenenv/env_test.go new file mode 100644 index 0000000000..6224d7b2cc --- /dev/null +++ b/internal/e2e/runtimegoldenenv/env_test.go @@ -0,0 +1,89 @@ +// 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 runtimegoldenenv + +import ( + "os" + "os/exec" + "strings" + "testing" +) + +func TestRequiredAndEnabled(t *testing.T) { + for _, tc := range []struct { + name, ci, sandbox, opt string + required, enabled bool + }{ + {"local", "", "microvm", "", false, false}, + {"opt in", "", "microvm", "true", false, true}, + {"microvm CI", "true", "microvm", "", true, true}, + {"gvisor CI", "true", "gvisor", "", true, true}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("CI", tc.ci) + t.Setenv("E2E_RUNTIME_GOLDEN_REQUIRED", "true") + t.Setenv("E2E_SANDBOX_CLASS", tc.sandbox) + t.Setenv("E2E_RUNTIME_GOLDEN", tc.opt) + if Required() != tc.required || Enabled() != tc.enabled { + t.Fatal("incorrect requirement") + } + }) + } +} +func TestRequiredMissingEvidenceFails(t *testing.T) { + if os.Getenv("RUNTIME_GOLDEN_GUARD_CHILD") == "true" { + if err := Check(); err != nil { + t.Fatal(err) + } + return + } + t.Setenv("CI", "true") + t.Setenv("E2E_RUNTIME_GOLDEN_REQUIRED", "true") + t.Setenv("E2E_SANDBOX_CLASS", "microvm") + t.Setenv("E2E_RUNTIME_GOLDEN", "") + t.Setenv("E2E_RUNTIME_LOG_DIR", "") + t.Setenv("RUNTIME_GOLDEN_GUARD_CHILD", "true") + cmd := exec.Command(os.Args[0], "-test.run=^TestRequiredMissingEvidenceFails$") + out, err := cmd.CombinedOutput() + if err == nil || !strings.Contains(string(out), "require E2E_RUNTIME_LOG_DIR") { + t.Fatalf("strict child should fail: %v %s", err, out) + } +} +func TestCheckConfigured(t *testing.T) { + t.Setenv("CI", "true") + t.Setenv("E2E_RUNTIME_GOLDEN_REQUIRED", "true") + t.Setenv("E2E_SANDBOX_CLASS", "microvm") + t.Setenv("E2E_RUNTIME_LOG_DIR", t.TempDir()) + if err := Check(); err != nil { + t.Fatal(err) + } +} + +func TestCheckRuntimeSelection(t *testing.T) { + for _, tc := range []struct { + kind string + bad bool + }{{"microvm", false}, {"gvisor", false}, {"other", true}, {"", true}} { + t.Run(tc.kind, func(t *testing.T) { + t.Setenv("CI", "") + t.Setenv("E2E_RUNTIME_GOLDEN", "true") + t.Setenv("E2E_SANDBOX_CLASS", tc.kind) + t.Setenv("E2E_RUNTIME_LOG_DIR", t.TempDir()) + if err := Check(); (err != nil) != tc.bad { + t.Fatalf("got %v", err) + } + }) + } +} diff --git a/internal/e2e/suites/combinedvolumes/combinedvolumes_test.go b/internal/e2e/suites/combinedvolumes/combinedvolumes_test.go index 236ae0f6b7..abaf696d95 100644 --- a/internal/e2e/suites/combinedvolumes/combinedvolumes_test.go +++ b/internal/e2e/suites/combinedvolumes/combinedvolumes_test.go @@ -41,6 +41,8 @@ import ( "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/tarball" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -69,6 +71,10 @@ const ( // probeWrittenContent is the fixed string the probe's /writefile writes. probeWrittenContent = "written by probe" + sharedVolume = "shared" + sharedPath = "/mnt/ate-shared" + sharedFile = "handoff.txt" + payloadName = "payload.txt" payloadContent = "delivered by an image volume" // shadowedName is written by two layers; the upper one must win. @@ -234,10 +240,10 @@ func createTemplate(ctx context.Context, t *testing.T, clients *e2e.Clients, ns &ateapipb.VolumeMount{Name: extVolume, MountPath: extPathB}) tmpl.Volumes = append(tmpl.Volumes, &ateapipb.Volume{ Name: extVolume, - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ Capacity: extCapacity, StorageClassName: storageClass, - }, + }}, }) }, }) @@ -469,3 +475,174 @@ func TestCombinedVolumes(t *testing.T) { }) }) } + +// createHandoffTemplate builds a probe ActorTemplate that references the +// SharedVolume named volumeName. +func createHandoffTemplate(ctx context.Context, t *testing.T, clients *e2e.Clients, ns *e2e.Namespace, volumeName string) *ateapipb.ActorTemplate { + t.Helper() + + env, err := e2e.CheckEnv("BUCKET_NAME") + if err != nil { + t.Fatalf("CheckEnv: %v", err) + } + probeAtespace, _ := e2e.DeployProbe(t, env["BUCKET_NAME"], "combinedvolumes") + src := e2e.SubstrateFixture{ + Atespace: probeAtespace, + Name: probeName, + PoolNamespace: probeAtespace, + PoolName: probeName, + DeployWith: "the combinedvolumes suite's own DeployProbe", + } + return e2e.CreateSubstrateTemplateFrom(ctx, t, clients, ns.Name, src, e2e.SubstrateTemplateOptions{ + Atespace: atespace, + Name: "handoff-" + ns.Name, + PoolName: probeName, + PoolReplicas: 2, + Labels: map[string]string{"combinedvolumes": ns.Name}, + SnapshotConfig: &ateapipb.SnapshotConfig{ + StorageLocation: fmt.Sprintf("gs://%s/%s/", env["BUCKET_NAME"], ns.Name), + }, + Modify: func(tmpl *ateapipb.ActorTemplate) { + tmpl.Containers[0].VolumeMounts = append(tmpl.Containers[0].VolumeMounts, + &ateapipb.VolumeMount{Name: sharedVolume, MountPath: sharedPath}) + tmpl.Volumes = append(tmpl.Volumes, &ateapipb.Volume{ + Name: sharedVolume, + External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{Name: volumeName}}, + }) + }, + }) +} + +// createHandoffActor creates a suspended actor from tmpl and registers its +// teardown, which must run before the shared volume's. +func createHandoffActor(ctx context.Context, t *testing.T, clients *e2e.Clients, tmpl *ateapipb.ActorTemplate, name string) resources.ActorRef { + t.Helper() + + actorRef := resources.ActorRef{Atespace: atespace, Name: name} + if _, err := clients.SubstrateAPI.CreateActor(ctx, &ateapipb.CreateActorRequest{ + Actor: &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: actorRef.Atespace, Name: actorRef.Name}, + ActorTemplate: e2e.TemplateRef(tmpl), + }, + }); err != nil { + t.Fatalf("CreateActor(%s): %v", name, err) + } + t.Cleanup(func() { + cleanupCtx := context.Background() + _, _ = clients.SubstrateAPI.SuspendActor(cleanupCtx, &ateapipb.SuspendActorRequest{Actor: actorRef.ToObjectRef()}) + _, _ = clients.SubstrateAPI.DeleteActor(cleanupCtx, &ateapipb.DeleteActorRequest{Actor: actorRef.ToObjectRef()}) + }) + return actorRef +} + +func TestSharedVolumeHandoff(t *testing.T) { + ctx := context.Background() + clients := e2e.GetClients() + ns := e2e.CreateNamespace(t) + + if storageClassOrEmpty(ctx, t, clients) == "" { + t.Skipf("StorageClass %q is not installed", e2e.StorageClass) + } + + volumeName := "shared-" + ns.Name + // The template is created first because it creates the suite's atespace. + tmpl := createHandoffTemplate(ctx, t, clients, ns, volumeName) + + volumeRef := &ateapipb.ObjectRef{Atespace: atespace, Name: volumeName} + if _, err := clients.SubstrateAPI.CreateSharedVolume(ctx, &ateapipb.CreateSharedVolumeRequest{ + SharedVolume: &ateapipb.SharedVolume{ + Metadata: &ateapipb.ResourceMetadata{Atespace: atespace, Name: volumeName}, + StorageClassName: e2e.StorageClass, + Capacity: extCapacity, + DeleteTrigger: ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL, + }, + }); err != nil { + t.Fatalf("CreateSharedVolume: %v", err) + } + t.Cleanup(func() { + if _, err := clients.SubstrateAPI.DeleteSharedVolume(context.Background(), &ateapipb.DeleteSharedVolumeRequest{SharedVolume: volumeRef}); err != nil { + t.Errorf("DeleteSharedVolume: %v", err) + } + }) + + producer := createHandoffActor(ctx, t, clients, tmpl, "producer-"+ns.Name) + consumer := createHandoffActor(ctx, t, clients, tmpl, "consumer-"+ns.Name) + + router, err := e2e.NewRouterClient(ctx) + if err != nil { + t.Fatalf("NewRouterClient: %v", err) + } + defer router.Close() + + handoffFile := sharedPath + "/" + sharedFile + if _, err := e2e.ResumeActorAwaitCapacity(t, ctx, clients, &ateapipb.ResumeActorRequest{Actor: producer.ToObjectRef()}); err != nil { + t.Fatalf("ResumeActor(producer): %v", err) + } + requireUnreadable(ctx, t, router, producer, handoffFile) + if got := probeJSON(ctx, t, router, producer, "/writefile?path="+handoffFile); got["error"] != "" { + t.Fatalf("writing %s: %s", handoffFile, got["error"]) + } + requireContent(ctx, t, router, producer, handoffFile, probeWrittenContent) + + _, err = e2e.ResumeActorAwaitCapacity(t, ctx, clients, &ateapipb.ResumeActorRequest{Actor: consumer.ToObjectRef()}) + if status.Code(err) != codes.FailedPrecondition || !strings.Contains(err.Error(), producer.Name) { + t.Fatalf("ResumeActor(consumer) while the producer holds the volume = %v, want FailedPrecondition naming %q", err, producer.Name) + } + blocked, err := clients.SubstrateAPI.GetActor(ctx, &ateapipb.GetActorRequest{Actor: consumer.ToObjectRef()}) + if err != nil { + t.Fatalf("GetActor(consumer): %v", err) + } + if got := blocked.GetStatus().GetState(); got != ateapipb.ActorState_ACTOR_STATE_SUSPENDED { + t.Errorf("consumer state after a refused claim = %s, want ACTOR_STATE_SUSPENDED", got) + } + + if _, err := clients.SubstrateAPI.SuspendActor(ctx, &ateapipb.SuspendActorRequest{Actor: producer.ToObjectRef()}); err != nil { + t.Fatalf("SuspendActor(producer): %v", err) + } + if _, err := e2e.ResumeActorAwaitCapacity(t, ctx, clients, &ateapipb.ResumeActorRequest{Actor: consumer.ToObjectRef()}); err != nil { + t.Fatalf("ResumeActor(consumer): %v", err) + } + requireContent(ctx, t, router, consumer, handoffFile, probeWrittenContent) + + if _, err := clients.SubstrateAPI.DeleteSharedVolume(ctx, &ateapipb.DeleteSharedVolumeRequest{SharedVolume: volumeRef}); status.Code(err) != codes.FailedPrecondition { + t.Errorf("DeleteSharedVolume while referenced = %v, want FailedPrecondition", err) + } + stored, err := clients.SubstrateAPI.GetSharedVolume(ctx, &ateapipb.GetSharedVolumeRequest{SharedVolume: volumeRef}) + if err != nil { + t.Fatalf("GetSharedVolume: %v", err) + } + actor, err := clients.SubstrateAPI.GetActor(ctx, &ateapipb.GetActorRequest{Actor: consumer.ToObjectRef()}) + if err != nil { + t.Fatalf("GetActor(consumer): %v", err) + } + var mounted string + for _, vol := range actor.GetStatus().GetExternalVolumes() { + if vol.GetName() == sharedVolume { + mounted = vol.GetStorageVolumeId() + } + } + if mounted != stored.GetVolumeId() { + t.Errorf("the consumer mounted %q, want the shared volume %q", mounted, stored.GetVolumeId()) + } +} + +func TestSharedVolumeMissing(t *testing.T) { + ctx := context.Background() + clients := e2e.GetClients() + ns := e2e.CreateNamespace(t) + + if storageClassOrEmpty(ctx, t, clients) == "" { + t.Skipf("StorageClass %q is not installed", e2e.StorageClass) + } + + tmpl := createHandoffTemplate(ctx, t, clients, ns, "absent-"+ns.Name) + _, err := clients.SubstrateAPI.CreateActor(ctx, &ateapipb.CreateActorRequest{ + Actor: &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: atespace, Name: "orphan-" + ns.Name}, + ActorTemplate: e2e.TemplateRef(tmpl), + }, + }) + if status.Code(err) != codes.FailedPrecondition { + t.Fatalf("CreateActor with an unresolvable volume reference = %v, want FailedPrecondition", err) + } +} diff --git a/internal/e2e/suites/combinedvolumes/runtime_golden_env_test.go b/internal/e2e/suites/combinedvolumes/runtime_golden_env_test.go new file mode 100644 index 0000000000..6c1c6179dd --- /dev/null +++ b/internal/e2e/suites/combinedvolumes/runtime_golden_env_test.go @@ -0,0 +1,31 @@ +// 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 combinedvolumes + +import ( + "testing" + + "github.com/agent-substrate/substrate/internal/e2e/runtimegoldenenv" +) + +func requireRuntimeGolden(t *testing.T) { + t.Helper() + if err := runtimegoldenenv.Check(); err != nil { + t.Fatal(err) + } + if !runtimegoldenenv.Enabled() { + t.Skip("requires microVM CI or E2E_RUNTIME_GOLDEN=true") + } +} diff --git a/internal/e2e/suites/combinedvolumes/runtime_golden_fixture_test.go b/internal/e2e/suites/combinedvolumes/runtime_golden_fixture_test.go new file mode 100644 index 0000000000..7c13cc59ee --- /dev/null +++ b/internal/e2e/suites/combinedvolumes/runtime_golden_fixture_test.go @@ -0,0 +1,174 @@ +// 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 combinedvolumes + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/agent-substrate/substrate/internal/e2e" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "google.golang.org/protobuf/proto" +) + +type runtimeGoldenFixture struct { + golden, cold *ateapipb.ActorTemplate + volumes []string + name string + router *e2e.RouterClient + ctx context.Context + clients *e2e.Clients +} + +func newRuntimeGoldenFixture(t *testing.T) *runtimeGoldenFixture { + t.Helper() + logDir := os.Getenv("E2E_RUNTIME_LOG_DIR") + if logDir == "" { + t.Fatal("E2E_RUNTIME_LOG_DIR required to preserve runtime evidence") + } + if err := os.MkdirAll(logDir, 0755); err != nil { + t.Fatal(err) + } + ctx := context.Background() + clients := e2e.GetClients() + if storageClassOrEmpty(ctx, t, clients) == "" { + t.Fatal("real NFS CSI required") + } + ns := e2e.CreateNamespace(t) + env, err := e2e.CheckEnv("BUCKET_NAME") + if err != nil { + t.Fatal(err) + } + probeAtespace, _ := e2e.DeployProbe(t, env["BUCKET_NAME"], "runtimegolden") + src := e2e.SubstrateFixture{Atespace: probeAtespace, Name: probeName, PoolNamespace: probeAtespace, PoolName: probeName} + golden := e2e.CreateSubstrateTemplateFrom(ctx, t, clients, ns.Name, src, e2e.SubstrateTemplateOptions{ + Atespace: atespace, Name: "runtime-" + ns.Name, PoolName: probeName, PoolReplicas: 2, + Labels: map[string]string{"runtimegolden": ns.Name}, + SnapshotConfig: &ateapipb.SnapshotConfig{StorageLocation: fmt.Sprintf("gs://%s/%s/", env["BUCKET_NAME"], ns.Name), GoldenSnapshotConfig: &ateapipb.GoldenSnapshotConfig{Mode: ateapipb.GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_RUNTIME}}, + Modify: func(x *ateapipb.ActorTemplate) { + x.Containers[0].VolumeMounts = append(x.Containers[0].VolumeMounts, &ateapipb.VolumeMount{Name: sharedVolume, MountPath: sharedPath}) + x.Volumes = append(x.Volumes, &ateapipb.Volume{Name: sharedVolume, External: &ateapipb.ExternalVolumeSource{SharedVolume: &ateapipb.SharedVolumeRef{}}}) + }, + }) + cold := proto.Clone(golden).(*ateapipb.ActorTemplate) + cold.Metadata = &ateapipb.ResourceMetadata{Atespace: atespace, Name: "cold-" + ns.Name} + cold.Status = nil + cold.SnapshotConfig.GoldenSnapshotConfig.Mode = ateapipb.GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_DISABLED + cold, err = clients.SubstrateAPI.CreateActorTemplate(ctx, &ateapipb.CreateActorTemplateRequest{ActorTemplate: cold}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + _, _ = clients.SubstrateAPI.DeleteActorTemplate(context.Background(), &ateapipb.DeleteActorTemplateRequest{ActorTemplate: e2e.TemplateRef(cold)}) + }) + volumes := []string{"a-" + ns.Name, "b-" + ns.Name} + for _, v := range volumes { + _, err := clients.SubstrateAPI.CreateSharedVolume(ctx, &ateapipb.CreateSharedVolumeRequest{SharedVolume: &ateapipb.SharedVolume{Metadata: &ateapipb.ResourceMetadata{Atespace: atespace, Name: v}, StorageClassName: e2e.StorageClass, Capacity: extCapacity, DeleteTrigger: ateapipb.DeleteTrigger_DELETE_TRIGGER_MANUAL}}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + _, err := clients.SubstrateAPI.DeleteSharedVolume(context.Background(), &ateapipb.DeleteSharedVolumeRequest{SharedVolume: &ateapipb.ObjectRef{Atespace: atespace, Name: v}}) + if err != nil { + t.Error(err) + } + }) + } + t.Cleanup(func() { + dir := logDir + kctx := os.Getenv("KUBECTL_CONTEXT") + pods, err := exec.Command("kubectl", "--context="+kctx, "-n", ns.Name, "get", "pods", "-o", "name").Output() + if err != nil { + t.Error(err) + return + } + for _, pod := range strings.Fields(string(pods)) { + logs, err := exec.Command("kubectl", "--context="+kctx, "-n", ns.Name, "logs", pod, "--all-containers=true", "--prefix=true", "--tail=-1").CombinedOutput() + if err != nil { + t.Errorf("collect %s: %v", pod, err) + } + if err := os.WriteFile(filepath.Join(dir, ns.Name+"-"+strings.TrimPrefix(pod, "pod/")+".log"), logs, 0644); err != nil { + t.Error(err) + } + } + }) + router, err := e2e.NewRouterClient(ctx) + if err != nil { + t.Fatal(err) + } + t.Cleanup(router.Close) + return &runtimeGoldenFixture{golden: golden, cold: cold, volumes: volumes, name: ns.Name, router: router, ctx: ctx, clients: clients} +} + +func (f *runtimeGoldenFixture) create(t *testing.T, tmpl *ateapipb.ActorTemplate, name, vol string) *ateapipb.Actor { + t.Helper() + a, err := f.clients.SubstrateAPI.CreateActor(f.ctx, &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{Metadata: &ateapipb.ResourceMetadata{Atespace: atespace, Name: name}, ActorTemplate: e2e.TemplateRef(tmpl), SharedVolumeBindings: map[string]string{sharedVolume: vol}}}) + if err != nil { + t.Fatal(err) + } + return a +} +func (f *runtimeGoldenFixture) remove(t *testing.T, a *ateapipb.Actor) { + ref := resources.ActorRefFromActor(a).ToObjectRef() + _, _ = f.clients.SubstrateAPI.SuspendActor(context.Background(), &ateapipb.SuspendActorRequest{Actor: ref}) + if _, err := f.clients.SubstrateAPI.DeleteActor(context.Background(), &ateapipb.DeleteActorRequest{Actor: ref}); err != nil { + t.Errorf("delete actor: %v", err) + } +} +func (f *runtimeGoldenFixture) resume(t *testing.T, a *ateapipb.Actor) { + t.Helper() + if _, err := e2e.ResumeActorAwaitCapacity(t, f.ctx, f.clients, &ateapipb.ResumeActorRequest{Actor: resources.ActorRefFromActor(a).ToObjectRef()}); err != nil { + t.Fatal(err) + } +} +func (f *runtimeGoldenFixture) suspend(t *testing.T, a *ateapipb.Actor) *ateapipb.Actor { + t.Helper() + r, err := f.clients.SubstrateAPI.SuspendActor(f.ctx, &ateapipb.SuspendActorRequest{Actor: resources.ActorRefFromActor(a).ToObjectRef()}) + if err != nil { + t.Fatal(err) + } + return r.GetActor() +} +func (f *runtimeGoldenFixture) post(t *testing.T, a *ateapipb.Actor, path string, body any) { + t.Helper() + b, err := json.Marshal(body) + if err != nil { + t.Fatal(err) + } + resp, err := f.router.PostJSON(f.ctx, resources.ActorRefFromActor(a), path, b) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { + t.Fatalf("%s status %d", path, resp.StatusCode) + } +} + +func runtimeStatePath() string { + return "/runtime-state?runtime=" + os.Getenv("E2E_SANDBOX_CLASS") +} + +// gVisor regenerates its procfs boot ID on restore. Its runtime-only boundary +// is checked with a retained pause-process nonce in the direct runsc test. +func usesGuestBootMarker() bool { return os.Getenv("E2E_SANDBOX_CLASS") == "microvm" } diff --git a/internal/e2e/suites/combinedvolumes/runtime_golden_latency_test.go b/internal/e2e/suites/combinedvolumes/runtime_golden_latency_test.go new file mode 100644 index 0000000000..370030b0ce --- /dev/null +++ b/internal/e2e/suites/combinedvolumes/runtime_golden_latency_test.go @@ -0,0 +1,129 @@ +// 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 combinedvolumes + +import ( + "context" + "encoding/json" + "fmt" + "math/rand/v2" + "os" + "strconv" + "testing" + "time" + + "github.com/agent-substrate/substrate/internal/resources" +) + +type latencySample struct { + Mode string `json:"mode"` + Index int `json:"index"` + Warmup bool `json:"warmup"` + Milliseconds float64 `json:"milliseconds"` + Success bool `json:"success"` + Actor string `json:"actor"` + BootID string `json:"boot_id,omitempty"` + ProcessNonce string `json:"process_nonce,omitempty"` + Snapshots json.RawMessage `json:"source_snapshots"` +} + +func TestRuntimeGoldenLatency(t *testing.T) { + if os.Getenv("E2E_RUNTIME_GOLDEN_BENCHMARK") != "true" { + t.Skip("optional latency experiment") + } + requireRuntimeGolden(t) + var err error + samples := 100 + if v := os.Getenv("E2E_LATENCY_SAMPLES"); v != "" { + samples, err = strconv.Atoi(v) + if err != nil || samples <= 0 { + t.Fatal("invalid sample count") + } + } + output := os.Getenv("E2E_LATENCY_OUTPUT") + if output == "" { + t.Fatal("E2E_LATENCY_OUTPUT required") + } + outputFile, err := os.Create(output) + if err != nil { + t.Fatal(err) + } + defer outputFile.Close() + enc := json.NewEncoder(outputFile) + f := newRuntimeGoldenFixture(t) + ctx := context.Background() + // Identify the template kernel once, outside measured activations. + seed := f.create(t, f.golden, "identity-"+f.name, f.volumes[0]) + f.resume(t, seed) + goldenBoot := probeJSON(ctx, t, f.router, resources.ActorRefFromActor(seed), runtimeStatePath())["boot_id"] + f.remove(t, seed) + rng := rand.New(rand.NewPCG(42, 91)) + for i := -5; i < samples; i++ { + modes := []string{"cold", "runtime-golden", "own"} + rng.Shuffle(len(modes), func(a, b int) { modes[a], modes[b] = modes[b], modes[a] }) + for _, mode := range modes { + t.Run(fmt.Sprintf("%s/%d", mode, i), func(t *testing.T) { + tmpl := f.golden + if mode == "cold" { + tmpl = f.cold + } + a := f.create(t, tmpl, fmt.Sprintf("sample-%s-%d-%s", mode, i+5, f.name), f.volumes[0]) + defer f.remove(t, a) + var ownNonce string + if mode == "own" { + f.resume(t, a) + ownNonce = probeJSON(ctx, t, f.router, resources.ActorRefFromActor(a), runtimeStatePath())["process_nonce"] + a = f.suspend(t, a) + } + snap, _ := json.Marshal(a.GetStatus().GetSnapshots()) + sample := latencySample{Mode: mode, Index: i, Warmup: i < 0, Actor: a.GetMetadata().GetName(), Snapshots: snap} + start := time.Now() + defer func() { + sample.Milliseconds = float64(time.Since(start).Microseconds()) / 1000 + sample.Success = !t.Failed() + if err := enc.Encode(sample); err != nil { + t.Error(err) + } + if err := outputFile.Sync(); err != nil { + t.Error(err) + } + }() + if mode == "cold" && len(a.GetStatus().GetSnapshots()) != 0 { + t.Fatal("cold sample has snapshot") + } + if mode != "cold" && len(a.GetStatus().GetSnapshots()) == 0 { + t.Fatal("restore sample has no snapshot") + } + f.resume(t, a) + ref := resources.ActorRefFromActor(a) + state := probeJSON(ctx, t, f.router, ref, runtimeStatePath()) + sample.BootID = state["boot_id"] + sample.ProcessNonce = state["process_nonce"] + if usesGuestBootMarker() && mode == "cold" && sample.BootID == goldenBoot { + t.Fatal("cold sample restored golden") + } + if usesGuestBootMarker() && mode != "cold" && sample.BootID != goldenBoot { + t.Fatal("restore sample cold-booted") + } + if mode == "own" && sample.ProcessNonce != ownNonce { + t.Fatal("own restore restarted process") + } + path := sharedPath + "/" + sample.Actor + requireWrite(ctx, t, f.router, ref, path) + requireContent(ctx, t, f.router, ref, path, probeWrittenContent) + }) + } + } +} diff --git a/internal/e2e/suites/combinedvolumes/runtime_golden_test.go b/internal/e2e/suites/combinedvolumes/runtime_golden_test.go new file mode 100644 index 0000000000..bec4991b49 --- /dev/null +++ b/internal/e2e/suites/combinedvolumes/runtime_golden_test.go @@ -0,0 +1,90 @@ +// 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 combinedvolumes + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "os" + "strconv" + "testing" + + "github.com/agent-substrate/substrate/internal/e2e/probeapi" + "github.com/agent-substrate/substrate/internal/resources" +) + +func TestRuntimeGoldenVolumes(t *testing.T) { + requireRuntimeGolden(t) + f := newRuntimeGoldenFixture(t) + ctx := context.Background() + // Distinct actor-selected volumes must receive no placeholder/application + // state. MicroVM clones retain the eager boot marker; on both runtimes, + // distinct process nonces verify fresh application starts. + var goldenBoot, firstNonce string + for i, v := range f.volumes { + a := f.create(t, f.golden, fmt.Sprintf("check-%d-%s", i, f.name), v) + func() { + defer f.remove(t, a) + if len(a.GetStatus().GetSnapshots()) == 0 { + t.Fatal("runtime actor has no source snapshot") + } + f.resume(t, a) + ref := resources.ActorRefFromActor(a) + state := probeJSON(ctx, t, f.router, ref, runtimeStatePath()) + if state["boot_id"] == "" || state["process_nonce"] == "" { + t.Fatal("missing runtime identity") + } + if i == 0 { + goldenBoot = state["boot_id"] + firstNonce = state["process_nonce"] + } else if (usesGuestBootMarker() && state["boot_id"] != goldenBoot) || state["process_nonce"] == firstNonce { + t.Fatalf("unexpected runtime or process identity: %v", state) + } + requireUnreadable(ctx, t, f.router, ref, sharedPath+"/actor-"+strconv.Itoa(1-i)) + requireWrite(ctx, t, f.router, ref, sharedPath+"/actor-"+strconv.Itoa(i)) + root := sharedPath + "/state" + challenge := "challenge-" + v + f.post(t, a, "/volume-fixture", map[string]string{"root": root, "value": v}) + f.post(t, a, "/volume-state?root="+url.QueryEscape(root)+"&challenge="+url.QueryEscape(challenge), nil) + for generation := 1; generation <= 2; generation++ { + a = f.suspend(t, a) + f.resume(t, a) + after := probeJSON(ctx, t, f.router, ref, runtimeStatePath()) + if (usesGuestBootMarker() && after["boot_id"] != goldenBoot) || after["process_nonce"] != state["process_nonce"] { + t.Fatal("own restore restarted application or guest") + } + resp, err := f.router.Get(ctx, ref, "/volume-state") + if err != nil { + t.Fatal(err) + } + var obs probeapi.VolumeStateResponse + err = json.NewDecoder(resp.Body).Decode(&obs) + resp.Body.Close() + if err != nil { + t.Fatal(err) + } + t.Logf("retained state actor=%s generation=%d: %+v", v, generation, obs) + if err := probeapi.ValidateRetainedState(obs, challenge, v, generation, os.Getenv("E2E_SANDBOX_CLASS") == "microvm"); err != nil { + t.Fatal(err) + } + if obs.Unlinked.ErrorCode == "EIO" { + t.Log("known limitation: deleted-open file returned EIO on generation two") + } + } + }() + } +} diff --git a/internal/e2e/suites/demo/demo_test.go b/internal/e2e/suites/demo/demo_test.go index d713ce5bd6..66c365050b 100644 --- a/internal/e2e/suites/demo/demo_test.go +++ b/internal/e2e/suites/demo/demo_test.go @@ -1474,10 +1474,10 @@ func createActorTemplateWithExternalVolume(ctx context.Context, t *testing.T, cl if !hasExtVol { at.Volumes = append(at.Volumes, &ateapipb.Volume{ Name: "external-data", - ExternalVolumeTemplate: &ateapipb.ExternalVolumeTemplate{ + External: &ateapipb.ExternalVolumeSource{Template: &ateapipb.ExternalVolumeTemplate{ Capacity: "1Gi", StorageClassName: scName, - }, + }}, }) } } diff --git a/internal/proto/ateletpb/atelet.pb.go b/internal/proto/ateletpb/atelet.pb.go index b05826b2a6..721f6ddc08 100644 --- a/internal/proto/ateletpb/atelet.pb.go +++ b/internal/proto/ateletpb/atelet.pb.go @@ -1875,7 +1875,10 @@ type WorkloadSpec struct { // +k8s:maxItems=32 # matches the template's volumes bound // +k8s:listType=map // +k8s:listMapKey=name - Volumes []*Volume `protobuf:"bytes,2,rep,name=volumes,proto3" json:"volumes,omitempty"` + Volumes []*Volume `protobuf:"bytes,2,rep,name=volumes,proto3" json:"volumes,omitempty"` + // Capture only the pre-application runtime; golden actors only. + // +k8s:optional + RuntimeGolden bool `protobuf:"varint,3,opt,name=runtime_golden,json=runtimeGolden,proto3" json:"runtime_golden,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1924,6 +1927,13 @@ func (x *WorkloadSpec) GetVolumes() []*Volume { return nil } +func (x *WorkloadSpec) GetRuntimeGolden() bool { + if x != nil { + return x.RuntimeGolden + } + return false +} + type DurableDirVolume struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -1960,17 +1970,54 @@ func (*DurableDirVolume) Descriptor() ([]byte, []int) { return file_atelet_proto_rawDescGZIP(), []int{27} } -type ExternalVolumeSource struct { +// EmptyDirVolume is an empty host directory at the external volume path. +type EmptyDirVolume struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EmptyDirVolume) Reset() { + *x = EmptyDirVolume{} + mi := &file_atelet_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EmptyDirVolume) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmptyDirVolume) ProtoMessage() {} + +func (x *EmptyDirVolume) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[28] + 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 EmptyDirVolume.ProtoReflect.Descriptor instead. +func (*EmptyDirVolume) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{28} +} + +type ExternalVolume struct { state protoimpl.MessageState `protogen:"open.v1"` // Required, unlike the actor status counterpart: the control plane only // sends volumes that finished provisioning. // // +k8s:required - // +k8s:maxLength=256 # matches ExternalVolume.storage_volume_id's bound + // +k8s:maxLength=256 # matches ExternalVolumeRef.storage_volume_id's bound // +k8s:customValidation # no control characters StorageVolumeId string `protobuf:"bytes,1,opt,name=storage_volume_id,json=storageVolumeId,proto3" json:"storage_volume_id,omitempty"` // +k8s:optional - // +k8s:maxLength=253 # matches ExternalVolume.volume_type's bound + // +k8s:maxLength=253 # matches ExternalVolumeRef.volume_type's bound // +k8s:customValidation # optional "substrate.io/" prefix + DNS subdomain VolumeType string `protobuf:"bytes,2,opt,name=volume_type,json=volumeType,proto3" json:"volume_type,omitempty"` // volume_context is the driver's provisioning metadata. Its entries are the @@ -1989,21 +2036,21 @@ type ExternalVolumeSource struct { sizeCache protoimpl.SizeCache } -func (x *ExternalVolumeSource) Reset() { - *x = ExternalVolumeSource{} - mi := &file_atelet_proto_msgTypes[28] +func (x *ExternalVolume) Reset() { + *x = ExternalVolume{} + mi := &file_atelet_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ExternalVolumeSource) String() string { +func (x *ExternalVolume) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ExternalVolumeSource) ProtoMessage() {} +func (*ExternalVolume) ProtoMessage() {} -func (x *ExternalVolumeSource) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[28] +func (x *ExternalVolume) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2014,33 +2061,33 @@ func (x *ExternalVolumeSource) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ExternalVolumeSource.ProtoReflect.Descriptor instead. -func (*ExternalVolumeSource) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{28} +// Deprecated: Use ExternalVolume.ProtoReflect.Descriptor instead. +func (*ExternalVolume) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{29} } -func (x *ExternalVolumeSource) GetStorageVolumeId() string { +func (x *ExternalVolume) GetStorageVolumeId() string { if x != nil { return x.StorageVolumeId } return "" } -func (x *ExternalVolumeSource) GetVolumeType() string { +func (x *ExternalVolume) GetVolumeType() string { if x != nil { return x.VolumeType } return "" } -func (x *ExternalVolumeSource) GetVolumeContext() map[string]string { +func (x *ExternalVolume) GetVolumeContext() map[string]string { if x != nil { return x.VolumeContext } return nil } -func (x *ExternalVolumeSource) GetPublishContext() map[string]string { +func (x *ExternalVolume) GetPublishContext() map[string]string { if x != nil { return x.PublishContext } @@ -2061,7 +2108,7 @@ type ImageVolumeSource struct { func (x *ImageVolumeSource) Reset() { *x = ImageVolumeSource{} - mi := &file_atelet_proto_msgTypes[29] + mi := &file_atelet_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2073,7 +2120,7 @@ func (x *ImageVolumeSource) String() string { func (*ImageVolumeSource) ProtoMessage() {} func (x *ImageVolumeSource) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[29] + mi := &file_atelet_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2086,7 +2133,7 @@ func (x *ImageVolumeSource) ProtoReflect() protoreflect.Message { // Deprecated: Use ImageVolumeSource.ProtoReflect.Descriptor instead. func (*ImageVolumeSource) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{29} + return file_atelet_proto_rawDescGZIP(), []int{30} } func (x *ImageVolumeSource) GetReference() string { @@ -2118,7 +2165,7 @@ type ActorMetadataItem struct { func (x *ActorMetadataItem) Reset() { *x = ActorMetadataItem{} - mi := &file_atelet_proto_msgTypes[30] + mi := &file_atelet_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2130,7 +2177,7 @@ func (x *ActorMetadataItem) String() string { func (*ActorMetadataItem) ProtoMessage() {} func (x *ActorMetadataItem) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[30] + mi := &file_atelet_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2143,7 +2190,7 @@ func (x *ActorMetadataItem) ProtoReflect() protoreflect.Message { // Deprecated: Use ActorMetadataItem.ProtoReflect.Descriptor instead. func (*ActorMetadataItem) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{30} + return file_atelet_proto_rawDescGZIP(), []int{31} } func (x *ActorMetadataItem) GetField() ActorMetadataField { @@ -2179,7 +2226,7 @@ type ActorMetadataDataSource struct { func (x *ActorMetadataDataSource) Reset() { *x = ActorMetadataDataSource{} - mi := &file_atelet_proto_msgTypes[31] + mi := &file_atelet_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2191,7 +2238,7 @@ func (x *ActorMetadataDataSource) String() string { func (*ActorMetadataDataSource) ProtoMessage() {} func (x *ActorMetadataDataSource) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[31] + mi := &file_atelet_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2204,7 +2251,7 @@ func (x *ActorMetadataDataSource) ProtoReflect() protoreflect.Message { // Deprecated: Use ActorMetadataDataSource.ProtoReflect.Descriptor instead. func (*ActorMetadataDataSource) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{31} + return file_atelet_proto_rawDescGZIP(), []int{32} } func (x *ActorMetadataDataSource) GetItems() []*ActorMetadataItem { @@ -2238,7 +2285,7 @@ type TrustBundleDataSource struct { func (x *TrustBundleDataSource) Reset() { *x = TrustBundleDataSource{} - mi := &file_atelet_proto_msgTypes[32] + mi := &file_atelet_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2250,7 +2297,7 @@ func (x *TrustBundleDataSource) String() string { func (*TrustBundleDataSource) ProtoMessage() {} func (x *TrustBundleDataSource) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[32] + mi := &file_atelet_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2263,7 +2310,7 @@ func (x *TrustBundleDataSource) ProtoReflect() protoreflect.Message { // Deprecated: Use TrustBundleDataSource.ProtoReflect.Descriptor instead. func (*TrustBundleDataSource) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{32} + return file_atelet_proto_rawDescGZIP(), []int{33} } func (x *TrustBundleDataSource) GetPath() string { @@ -2298,7 +2345,7 @@ type SystemInfoDataSource struct { func (x *SystemInfoDataSource) Reset() { *x = SystemInfoDataSource{} - mi := &file_atelet_proto_msgTypes[33] + mi := &file_atelet_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2310,7 +2357,7 @@ func (x *SystemInfoDataSource) String() string { func (*SystemInfoDataSource) ProtoMessage() {} func (x *SystemInfoDataSource) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[33] + mi := &file_atelet_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2323,7 +2370,7 @@ func (x *SystemInfoDataSource) ProtoReflect() protoreflect.Message { // Deprecated: Use SystemInfoDataSource.ProtoReflect.Descriptor instead. func (*SystemInfoDataSource) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{33} + return file_atelet_proto_rawDescGZIP(), []int{34} } func (x *SystemInfoDataSource) GetActorMetadata() *ActorMetadataDataSource { @@ -2356,7 +2403,7 @@ type SystemInfoVolume struct { func (x *SystemInfoVolume) Reset() { *x = SystemInfoVolume{} - mi := &file_atelet_proto_msgTypes[34] + mi := &file_atelet_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2368,7 +2415,7 @@ func (x *SystemInfoVolume) String() string { func (*SystemInfoVolume) ProtoMessage() {} func (x *SystemInfoVolume) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[34] + mi := &file_atelet_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2381,7 +2428,7 @@ func (x *SystemInfoVolume) ProtoReflect() protoreflect.Message { // Deprecated: Use SystemInfoVolume.ProtoReflect.Descriptor instead. func (*SystemInfoVolume) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{34} + return file_atelet_proto_rawDescGZIP(), []int{35} } func (x *SystemInfoVolume) GetDataSources() []*SystemInfoDataSource { @@ -2397,27 +2444,30 @@ type Volume struct { // +k8s:required // +k8s:format=k8s-short-name Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Exactly one of durable_dir / external / system_info / image must be set. + // Exactly one of durable_dir / external / system_info / image / empty_dir must be set. // // +k8s:optional // +k8s:unionMember DurableDir *DurableDirVolume `protobuf:"bytes,2,opt,name=durable_dir,json=durableDir,proto3" json:"durable_dir,omitempty"` // +k8s:optional // +k8s:unionMember - External *ExternalVolumeSource `protobuf:"bytes,3,opt,name=external,proto3" json:"external,omitempty"` + External *ExternalVolume `protobuf:"bytes,3,opt,name=external,proto3" json:"external,omitempty"` // +k8s:optional // +k8s:unionMember SystemInfo *SystemInfoVolume `protobuf:"bytes,4,opt,name=system_info,json=systemInfo,proto3" json:"system_info,omitempty"` // +k8s:optional // +k8s:unionMember - Image *ImageVolumeSource `protobuf:"bytes,5,opt,name=image,proto3" json:"image,omitempty"` + Image *ImageVolumeSource `protobuf:"bytes,5,opt,name=image,proto3" json:"image,omitempty"` + // +k8s:optional + // +k8s:unionMember + EmptyDir *EmptyDirVolume `protobuf:"bytes,6,opt,name=empty_dir,json=emptyDir,proto3" json:"empty_dir,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Volume) Reset() { *x = Volume{} - mi := &file_atelet_proto_msgTypes[35] + mi := &file_atelet_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2429,7 +2479,7 @@ func (x *Volume) String() string { func (*Volume) ProtoMessage() {} func (x *Volume) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[35] + mi := &file_atelet_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2442,7 +2492,7 @@ func (x *Volume) ProtoReflect() protoreflect.Message { // Deprecated: Use Volume.ProtoReflect.Descriptor instead. func (*Volume) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{35} + return file_atelet_proto_rawDescGZIP(), []int{36} } func (x *Volume) GetName() string { @@ -2459,7 +2509,7 @@ func (x *Volume) GetDurableDir() *DurableDirVolume { return nil } -func (x *Volume) GetExternal() *ExternalVolumeSource { +func (x *Volume) GetExternal() *ExternalVolume { if x != nil { return x.External } @@ -2480,6 +2530,13 @@ func (x *Volume) GetImage() *ImageVolumeSource { return nil } +func (x *Volume) GetEmptyDir() *EmptyDirVolume { + if x != nil { + return x.EmptyDir + } + return nil +} + type VolumeMount struct { state protoimpl.MessageState `protogen:"open.v1"` // name must match the name of a Volume. @@ -2499,7 +2556,7 @@ type VolumeMount struct { func (x *VolumeMount) Reset() { *x = VolumeMount{} - mi := &file_atelet_proto_msgTypes[36] + mi := &file_atelet_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2511,7 +2568,7 @@ func (x *VolumeMount) String() string { func (*VolumeMount) ProtoMessage() {} func (x *VolumeMount) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[36] + mi := &file_atelet_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2524,7 +2581,7 @@ func (x *VolumeMount) ProtoReflect() protoreflect.Message { // Deprecated: Use VolumeMount.ProtoReflect.Descriptor instead. func (*VolumeMount) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{36} + return file_atelet_proto_rawDescGZIP(), []int{37} } func (x *VolumeMount) GetName() string { @@ -2589,7 +2646,7 @@ type Container struct { func (x *Container) Reset() { *x = Container{} - mi := &file_atelet_proto_msgTypes[37] + mi := &file_atelet_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2601,7 +2658,7 @@ func (x *Container) String() string { func (*Container) ProtoMessage() {} func (x *Container) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[37] + mi := &file_atelet_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2614,7 +2671,7 @@ func (x *Container) ProtoReflect() protoreflect.Message { // Deprecated: Use Container.ProtoReflect.Descriptor instead. func (*Container) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{37} + return file_atelet_proto_rawDescGZIP(), []int{38} } func (x *Container) GetName() string { @@ -2691,7 +2748,7 @@ type SecurityContext struct { func (x *SecurityContext) Reset() { *x = SecurityContext{} - mi := &file_atelet_proto_msgTypes[38] + mi := &file_atelet_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2703,7 +2760,7 @@ func (x *SecurityContext) String() string { func (*SecurityContext) ProtoMessage() {} func (x *SecurityContext) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[38] + mi := &file_atelet_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2716,7 +2773,7 @@ func (x *SecurityContext) ProtoReflect() protoreflect.Message { // Deprecated: Use SecurityContext.ProtoReflect.Descriptor instead. func (*SecurityContext) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{38} + return file_atelet_proto_rawDescGZIP(), []int{39} } func (x *SecurityContext) GetCapabilities() *Capabilities { @@ -2746,7 +2803,7 @@ type Capabilities struct { func (x *Capabilities) Reset() { *x = Capabilities{} - mi := &file_atelet_proto_msgTypes[39] + mi := &file_atelet_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2758,7 +2815,7 @@ func (x *Capabilities) String() string { func (*Capabilities) ProtoMessage() {} func (x *Capabilities) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[39] + mi := &file_atelet_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2771,7 +2828,7 @@ func (x *Capabilities) ProtoReflect() protoreflect.Message { // Deprecated: Use Capabilities.ProtoReflect.Descriptor instead. func (*Capabilities) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{39} + return file_atelet_proto_rawDescGZIP(), []int{40} } func (x *Capabilities) GetAdd() []string { @@ -2809,7 +2866,7 @@ type ResourceLimits struct { func (x *ResourceLimits) Reset() { *x = ResourceLimits{} - mi := &file_atelet_proto_msgTypes[40] + mi := &file_atelet_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2821,7 +2878,7 @@ func (x *ResourceLimits) String() string { func (*ResourceLimits) ProtoMessage() {} func (x *ResourceLimits) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[40] + mi := &file_atelet_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2834,7 +2891,7 @@ func (x *ResourceLimits) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceLimits.ProtoReflect.Descriptor instead. func (*ResourceLimits) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{40} + return file_atelet_proto_rawDescGZIP(), []int{41} } func (x *ResourceLimits) GetMemoryBytes() int64 { @@ -2868,7 +2925,7 @@ type EnvEntry struct { func (x *EnvEntry) Reset() { *x = EnvEntry{} - mi := &file_atelet_proto_msgTypes[41] + mi := &file_atelet_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2880,7 +2937,7 @@ func (x *EnvEntry) String() string { func (*EnvEntry) ProtoMessage() {} func (x *EnvEntry) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[41] + mi := &file_atelet_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2893,7 +2950,7 @@ func (x *EnvEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use EnvEntry.ProtoReflect.Descriptor instead. func (*EnvEntry) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{41} + return file_atelet_proto_rawDescGZIP(), []int{42} } func (x *EnvEntry) GetName() string { @@ -2928,7 +2985,7 @@ type WakeupProbe struct { func (x *WakeupProbe) Reset() { *x = WakeupProbe{} - mi := &file_atelet_proto_msgTypes[42] + mi := &file_atelet_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2940,7 +2997,7 @@ func (x *WakeupProbe) String() string { func (*WakeupProbe) ProtoMessage() {} func (x *WakeupProbe) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[42] + mi := &file_atelet_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2953,7 +3010,7 @@ func (x *WakeupProbe) ProtoReflect() protoreflect.Message { // Deprecated: Use WakeupProbe.ProtoReflect.Descriptor instead. func (*WakeupProbe) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{42} + return file_atelet_proto_rawDescGZIP(), []int{43} } func (x *WakeupProbe) GetHttpGet() *HttpGetAction { @@ -2991,7 +3048,7 @@ type HttpGetAction struct { func (x *HttpGetAction) Reset() { *x = HttpGetAction{} - mi := &file_atelet_proto_msgTypes[43] + mi := &file_atelet_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3003,7 +3060,7 @@ func (x *HttpGetAction) String() string { func (*HttpGetAction) ProtoMessage() {} func (x *HttpGetAction) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[43] + mi := &file_atelet_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3016,7 +3073,7 @@ func (x *HttpGetAction) ProtoReflect() protoreflect.Message { // Deprecated: Use HttpGetAction.ProtoReflect.Descriptor instead. func (*HttpGetAction) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{43} + return file_atelet_proto_rawDescGZIP(), []int{44} } func (x *HttpGetAction) GetPath() string { @@ -3041,7 +3098,7 @@ type RunResponse struct { func (x *RunResponse) Reset() { *x = RunResponse{} - mi := &file_atelet_proto_msgTypes[44] + mi := &file_atelet_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3053,7 +3110,7 @@ func (x *RunResponse) String() string { func (*RunResponse) ProtoMessage() {} func (x *RunResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[44] + mi := &file_atelet_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3066,7 +3123,7 @@ func (x *RunResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RunResponse.ProtoReflect.Descriptor instead. func (*RunResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{44} + return file_atelet_proto_rawDescGZIP(), []int{45} } type ObjectStorage struct { @@ -3084,7 +3141,7 @@ type ObjectStorage struct { func (x *ObjectStorage) Reset() { *x = ObjectStorage{} - mi := &file_atelet_proto_msgTypes[45] + mi := &file_atelet_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3096,7 +3153,7 @@ func (x *ObjectStorage) String() string { func (*ObjectStorage) ProtoMessage() {} func (x *ObjectStorage) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[45] + mi := &file_atelet_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3109,7 +3166,7 @@ func (x *ObjectStorage) ProtoReflect() protoreflect.Message { // Deprecated: Use ObjectStorage.ProtoReflect.Descriptor instead. func (*ObjectStorage) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{45} + return file_atelet_proto_rawDescGZIP(), []int{46} } func (x *ObjectStorage) GetSnapshotUri() string { @@ -3169,7 +3226,7 @@ type Snapshot struct { func (x *Snapshot) Reset() { *x = Snapshot{} - mi := &file_atelet_proto_msgTypes[46] + mi := &file_atelet_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3181,7 +3238,7 @@ func (x *Snapshot) String() string { func (*Snapshot) ProtoMessage() {} func (x *Snapshot) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[46] + mi := &file_atelet_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3194,7 +3251,7 @@ func (x *Snapshot) ProtoReflect() protoreflect.Message { // Deprecated: Use Snapshot.ProtoReflect.Descriptor instead. func (*Snapshot) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{46} + return file_atelet_proto_rawDescGZIP(), []int{47} } func (x *Snapshot) GetSnapshotUuid() string { @@ -3241,7 +3298,7 @@ type SnapshotReport struct { func (x *SnapshotReport) Reset() { *x = SnapshotReport{} - mi := &file_atelet_proto_msgTypes[47] + mi := &file_atelet_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3253,7 +3310,7 @@ func (x *SnapshotReport) String() string { func (*SnapshotReport) ProtoMessage() {} func (x *SnapshotReport) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[47] + mi := &file_atelet_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3266,7 +3323,7 @@ func (x *SnapshotReport) ProtoReflect() protoreflect.Message { // Deprecated: Use SnapshotReport.ProtoReflect.Descriptor instead. func (*SnapshotReport) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{47} + return file_atelet_proto_rawDescGZIP(), []int{48} } func (x *SnapshotReport) GetSnapshot() *Snapshot { @@ -3321,7 +3378,7 @@ type ActorSnapshotState struct { func (x *ActorSnapshotState) Reset() { *x = ActorSnapshotState{} - mi := &file_atelet_proto_msgTypes[48] + mi := &file_atelet_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3333,7 +3390,7 @@ func (x *ActorSnapshotState) String() string { func (*ActorSnapshotState) ProtoMessage() {} func (x *ActorSnapshotState) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[48] + mi := &file_atelet_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3346,7 +3403,7 @@ func (x *ActorSnapshotState) ProtoReflect() protoreflect.Message { // Deprecated: Use ActorSnapshotState.ProtoReflect.Descriptor instead. func (*ActorSnapshotState) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{48} + return file_atelet_proto_rawDescGZIP(), []int{49} } func (x *ActorSnapshotState) GetLatestLocal() *SnapshotReport { @@ -3390,7 +3447,7 @@ type UploadConfig struct { func (x *UploadConfig) Reset() { *x = UploadConfig{} - mi := &file_atelet_proto_msgTypes[49] + mi := &file_atelet_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3402,7 +3459,7 @@ func (x *UploadConfig) String() string { func (*UploadConfig) ProtoMessage() {} func (x *UploadConfig) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[49] + mi := &file_atelet_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3415,7 +3472,7 @@ func (x *UploadConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use UploadConfig.ProtoReflect.Descriptor instead. func (*UploadConfig) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{49} + return file_atelet_proto_rawDescGZIP(), []int{50} } func (x *UploadConfig) GetMode() UploadMode { @@ -3478,7 +3535,7 @@ type CheckpointRequest struct { func (x *CheckpointRequest) Reset() { *x = CheckpointRequest{} - mi := &file_atelet_proto_msgTypes[50] + mi := &file_atelet_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3490,7 +3547,7 @@ func (x *CheckpointRequest) String() string { func (*CheckpointRequest) ProtoMessage() {} func (x *CheckpointRequest) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[50] + mi := &file_atelet_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3503,7 +3560,7 @@ func (x *CheckpointRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointRequest.ProtoReflect.Descriptor instead. func (*CheckpointRequest) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{50} + return file_atelet_proto_rawDescGZIP(), []int{51} } func (x *CheckpointRequest) GetWorkerPodUid() string { @@ -3582,7 +3639,7 @@ type CheckpointResponse struct { func (x *CheckpointResponse) Reset() { *x = CheckpointResponse{} - mi := &file_atelet_proto_msgTypes[51] + mi := &file_atelet_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3594,7 +3651,7 @@ func (x *CheckpointResponse) String() string { func (*CheckpointResponse) ProtoMessage() {} func (x *CheckpointResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[51] + mi := &file_atelet_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3607,7 +3664,7 @@ func (x *CheckpointResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointResponse.ProtoReflect.Descriptor instead. func (*CheckpointResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{51} + return file_atelet_proto_rawDescGZIP(), []int{52} } func (x *CheckpointResponse) GetState() *ActorSnapshotState { @@ -3682,7 +3739,7 @@ type RestoreRequest struct { func (x *RestoreRequest) Reset() { *x = RestoreRequest{} - mi := &file_atelet_proto_msgTypes[52] + mi := &file_atelet_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3694,7 +3751,7 @@ func (x *RestoreRequest) String() string { func (*RestoreRequest) ProtoMessage() {} func (x *RestoreRequest) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[52] + mi := &file_atelet_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3707,7 +3764,7 @@ func (x *RestoreRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreRequest.ProtoReflect.Descriptor instead. func (*RestoreRequest) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{52} + return file_atelet_proto_rawDescGZIP(), []int{53} } func (x *RestoreRequest) GetWorkerPodUid() string { @@ -3802,7 +3859,7 @@ type RestoreResponse struct { func (x *RestoreResponse) Reset() { *x = RestoreResponse{} - mi := &file_atelet_proto_msgTypes[53] + mi := &file_atelet_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3814,7 +3871,7 @@ func (x *RestoreResponse) String() string { func (*RestoreResponse) ProtoMessage() {} func (x *RestoreResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[53] + mi := &file_atelet_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3827,7 +3884,7 @@ func (x *RestoreResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreResponse.ProtoReflect.Descriptor instead. func (*RestoreResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{53} + return file_atelet_proto_rawDescGZIP(), []int{54} } type PromoteSnapshotRequest struct { @@ -3864,7 +3921,7 @@ type PromoteSnapshotRequest struct { func (x *PromoteSnapshotRequest) Reset() { *x = PromoteSnapshotRequest{} - mi := &file_atelet_proto_msgTypes[54] + mi := &file_atelet_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3876,7 +3933,7 @@ func (x *PromoteSnapshotRequest) String() string { func (*PromoteSnapshotRequest) ProtoMessage() {} func (x *PromoteSnapshotRequest) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[54] + mi := &file_atelet_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3889,7 +3946,7 @@ func (x *PromoteSnapshotRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PromoteSnapshotRequest.ProtoReflect.Descriptor instead. func (*PromoteSnapshotRequest) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{54} + return file_atelet_proto_rawDescGZIP(), []int{55} } func (x *PromoteSnapshotRequest) GetAtespace() string { @@ -3945,7 +4002,7 @@ type PromoteSnapshotResponse struct { func (x *PromoteSnapshotResponse) Reset() { *x = PromoteSnapshotResponse{} - mi := &file_atelet_proto_msgTypes[55] + mi := &file_atelet_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3957,7 +4014,7 @@ func (x *PromoteSnapshotResponse) String() string { func (*PromoteSnapshotResponse) ProtoMessage() {} func (x *PromoteSnapshotResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[55] + mi := &file_atelet_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3970,7 +4027,7 @@ func (x *PromoteSnapshotResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PromoteSnapshotResponse.ProtoReflect.Descriptor instead. func (*PromoteSnapshotResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{55} + return file_atelet_proto_rawDescGZIP(), []int{56} } func (x *PromoteSnapshotResponse) GetState() *ActorSnapshotState { @@ -4088,19 +4145,21 @@ const file_atelet_proto_rawDesc = "" + "pauseImage\x1aM\n" + "\vAssetsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12(\n" + - "\x05value\x18\x02 \x01(\v2\x12.atelet.ArchAssetsR\x05value:\x028\x01\"k\n" + + "\x05value\x18\x02 \x01(\v2\x12.atelet.ArchAssetsR\x05value:\x028\x01\"\x92\x01\n" + "\fWorkloadSpec\x121\n" + "\n" + "containers\x18\x01 \x03(\v2\x11.atelet.ContainerR\n" + "containers\x12(\n" + - "\avolumes\x18\x02 \x03(\v2\x0e.atelet.VolumeR\avolumes\"\x12\n" + - "\x10DurableDirVolume\"\x9b\x03\n" + - "\x14ExternalVolumeSource\x12*\n" + + "\avolumes\x18\x02 \x03(\v2\x0e.atelet.VolumeR\avolumes\x12%\n" + + "\x0eruntime_golden\x18\x03 \x01(\bR\rruntimeGolden\"\x12\n" + + "\x10DurableDirVolume\"\x10\n" + + "\x0eEmptyDirVolume\"\x89\x03\n" + + "\x0eExternalVolume\x12*\n" + "\x11storage_volume_id\x18\x01 \x01(\tR\x0fstorageVolumeId\x12\x1f\n" + "\vvolume_type\x18\x02 \x01(\tR\n" + - "volumeType\x12V\n" + - "\x0evolume_context\x18\x03 \x03(\v2/.atelet.ExternalVolumeSource.VolumeContextEntryR\rvolumeContext\x12Y\n" + - "\x0fpublish_context\x18\x04 \x03(\v20.atelet.ExternalVolumeSource.PublishContextEntryR\x0epublishContext\x1a@\n" + + "volumeType\x12P\n" + + "\x0evolume_context\x18\x03 \x03(\v2).atelet.ExternalVolume.VolumeContextEntryR\rvolumeContext\x12S\n" + + "\x0fpublish_context\x18\x04 \x03(\v2*.atelet.ExternalVolume.PublishContextEntryR\x0epublishContext\x1a@\n" + "\x12VolumeContextEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1aA\n" + @@ -4121,15 +4180,16 @@ const file_atelet_proto_rawDesc = "" + "\x0eactor_metadata\x18\x01 \x01(\v2\x1f.atelet.ActorMetadataDataSourceR\ractorMetadata\x12@\n" + "\ftrust_bundle\x18\x02 \x01(\v2\x1d.atelet.TrustBundleDataSourceR\vtrustBundle\"S\n" + "\x10SystemInfoVolume\x12?\n" + - "\fdata_sources\x18\x01 \x03(\v2\x1c.atelet.SystemInfoDataSourceR\vdataSources\"\xfd\x01\n" + + "\fdata_sources\x18\x01 \x03(\v2\x1c.atelet.SystemInfoDataSourceR\vdataSources\"\xac\x02\n" + "\x06Volume\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x129\n" + "\vdurable_dir\x18\x02 \x01(\v2\x18.atelet.DurableDirVolumeR\n" + - "durableDir\x128\n" + - "\bexternal\x18\x03 \x01(\v2\x1c.atelet.ExternalVolumeSourceR\bexternal\x129\n" + + "durableDir\x122\n" + + "\bexternal\x18\x03 \x01(\v2\x16.atelet.ExternalVolumeR\bexternal\x129\n" + "\vsystem_info\x18\x04 \x01(\v2\x18.atelet.SystemInfoVolumeR\n" + "systemInfo\x12/\n" + - "\x05image\x18\x05 \x01(\v2\x19.atelet.ImageVolumeSourceR\x05image\"@\n" + + "\x05image\x18\x05 \x01(\v2\x19.atelet.ImageVolumeSourceR\x05image\x123\n" + + "\tempty_dir\x18\x06 \x01(\v2\x16.atelet.EmptyDirVolumeR\bemptyDir\"@\n" + "\vVolumeMount\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" + "\n" + @@ -4263,7 +4323,7 @@ func file_atelet_proto_rawDescGZIP() []byte { } var file_atelet_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_atelet_proto_msgTypes = make([]protoimpl.MessageInfo, 60) +var file_atelet_proto_msgTypes = make([]protoimpl.MessageInfo, 61) var file_atelet_proto_goTypes = []any{ (ActorMetadataField)(0), // 0: atelet.ActorMetadataField (SnapshotFidelity)(0), // 1: atelet.SnapshotFidelity @@ -4296,39 +4356,40 @@ var file_atelet_proto_goTypes = []any{ (*SandboxAssets)(nil), // 28: atelet.SandboxAssets (*WorkloadSpec)(nil), // 29: atelet.WorkloadSpec (*DurableDirVolume)(nil), // 30: atelet.DurableDirVolume - (*ExternalVolumeSource)(nil), // 31: atelet.ExternalVolumeSource - (*ImageVolumeSource)(nil), // 32: atelet.ImageVolumeSource - (*ActorMetadataItem)(nil), // 33: atelet.ActorMetadataItem - (*ActorMetadataDataSource)(nil), // 34: atelet.ActorMetadataDataSource - (*TrustBundleDataSource)(nil), // 35: atelet.TrustBundleDataSource - (*SystemInfoDataSource)(nil), // 36: atelet.SystemInfoDataSource - (*SystemInfoVolume)(nil), // 37: atelet.SystemInfoVolume - (*Volume)(nil), // 38: atelet.Volume - (*VolumeMount)(nil), // 39: atelet.VolumeMount - (*Container)(nil), // 40: atelet.Container - (*SecurityContext)(nil), // 41: atelet.SecurityContext - (*Capabilities)(nil), // 42: atelet.Capabilities - (*ResourceLimits)(nil), // 43: atelet.ResourceLimits - (*EnvEntry)(nil), // 44: atelet.EnvEntry - (*WakeupProbe)(nil), // 45: atelet.WakeupProbe - (*HttpGetAction)(nil), // 46: atelet.HttpGetAction - (*RunResponse)(nil), // 47: atelet.RunResponse - (*ObjectStorage)(nil), // 48: atelet.ObjectStorage - (*Snapshot)(nil), // 49: atelet.Snapshot - (*SnapshotReport)(nil), // 50: atelet.SnapshotReport - (*ActorSnapshotState)(nil), // 51: atelet.ActorSnapshotState - (*UploadConfig)(nil), // 52: atelet.UploadConfig - (*CheckpointRequest)(nil), // 53: atelet.CheckpointRequest - (*CheckpointResponse)(nil), // 54: atelet.CheckpointResponse - (*RestoreRequest)(nil), // 55: atelet.RestoreRequest - (*RestoreResponse)(nil), // 56: atelet.RestoreResponse - (*PromoteSnapshotRequest)(nil), // 57: atelet.PromoteSnapshotRequest - (*PromoteSnapshotResponse)(nil), // 58: atelet.PromoteSnapshotResponse - nil, // 59: atelet.ArchAssets.FilesEntry - nil, // 60: atelet.SandboxAssets.AssetsEntry - nil, // 61: atelet.ExternalVolumeSource.VolumeContextEntry - nil, // 62: atelet.ExternalVolumeSource.PublishContextEntry - (*timestamppb.Timestamp)(nil), // 63: google.protobuf.Timestamp + (*EmptyDirVolume)(nil), // 31: atelet.EmptyDirVolume + (*ExternalVolume)(nil), // 32: atelet.ExternalVolume + (*ImageVolumeSource)(nil), // 33: atelet.ImageVolumeSource + (*ActorMetadataItem)(nil), // 34: atelet.ActorMetadataItem + (*ActorMetadataDataSource)(nil), // 35: atelet.ActorMetadataDataSource + (*TrustBundleDataSource)(nil), // 36: atelet.TrustBundleDataSource + (*SystemInfoDataSource)(nil), // 37: atelet.SystemInfoDataSource + (*SystemInfoVolume)(nil), // 38: atelet.SystemInfoVolume + (*Volume)(nil), // 39: atelet.Volume + (*VolumeMount)(nil), // 40: atelet.VolumeMount + (*Container)(nil), // 41: atelet.Container + (*SecurityContext)(nil), // 42: atelet.SecurityContext + (*Capabilities)(nil), // 43: atelet.Capabilities + (*ResourceLimits)(nil), // 44: atelet.ResourceLimits + (*EnvEntry)(nil), // 45: atelet.EnvEntry + (*WakeupProbe)(nil), // 46: atelet.WakeupProbe + (*HttpGetAction)(nil), // 47: atelet.HttpGetAction + (*RunResponse)(nil), // 48: atelet.RunResponse + (*ObjectStorage)(nil), // 49: atelet.ObjectStorage + (*Snapshot)(nil), // 50: atelet.Snapshot + (*SnapshotReport)(nil), // 51: atelet.SnapshotReport + (*ActorSnapshotState)(nil), // 52: atelet.ActorSnapshotState + (*UploadConfig)(nil), // 53: atelet.UploadConfig + (*CheckpointRequest)(nil), // 54: atelet.CheckpointRequest + (*CheckpointResponse)(nil), // 55: atelet.CheckpointResponse + (*RestoreRequest)(nil), // 56: atelet.RestoreRequest + (*RestoreResponse)(nil), // 57: atelet.RestoreResponse + (*PromoteSnapshotRequest)(nil), // 58: atelet.PromoteSnapshotRequest + (*PromoteSnapshotResponse)(nil), // 59: atelet.PromoteSnapshotResponse + nil, // 60: atelet.ArchAssets.FilesEntry + nil, // 61: atelet.SandboxAssets.AssetsEntry + nil, // 62: atelet.ExternalVolume.VolumeContextEntry + nil, // 63: atelet.ExternalVolume.PublishContextEntry + (*timestamppb.Timestamp)(nil), // 64: google.protobuf.Timestamp } var file_atelet_proto_depIdxs = []int32{ 7, // 0: atelet.RegisterWorkerRequest.capacity:type_name -> atelet.WorkerResources @@ -4349,73 +4410,74 @@ var file_atelet_proto_depIdxs = []int32{ 29, // 15: atelet.RunRequest.spec:type_name -> atelet.WorkloadSpec 28, // 16: atelet.RunRequest.sandbox_assets:type_name -> atelet.SandboxAssets 25, // 17: atelet.RunRequest.egress_gateway:type_name -> atelet.EgressGateway - 59, // 18: atelet.ArchAssets.files:type_name -> atelet.ArchAssets.FilesEntry - 60, // 19: atelet.SandboxAssets.assets:type_name -> atelet.SandboxAssets.AssetsEntry - 40, // 20: atelet.WorkloadSpec.containers:type_name -> atelet.Container - 38, // 21: atelet.WorkloadSpec.volumes:type_name -> atelet.Volume - 61, // 22: atelet.ExternalVolumeSource.volume_context:type_name -> atelet.ExternalVolumeSource.VolumeContextEntry - 62, // 23: atelet.ExternalVolumeSource.publish_context:type_name -> atelet.ExternalVolumeSource.PublishContextEntry + 60, // 18: atelet.ArchAssets.files:type_name -> atelet.ArchAssets.FilesEntry + 61, // 19: atelet.SandboxAssets.assets:type_name -> atelet.SandboxAssets.AssetsEntry + 41, // 20: atelet.WorkloadSpec.containers:type_name -> atelet.Container + 39, // 21: atelet.WorkloadSpec.volumes:type_name -> atelet.Volume + 62, // 22: atelet.ExternalVolume.volume_context:type_name -> atelet.ExternalVolume.VolumeContextEntry + 63, // 23: atelet.ExternalVolume.publish_context:type_name -> atelet.ExternalVolume.PublishContextEntry 0, // 24: atelet.ActorMetadataItem.field:type_name -> atelet.ActorMetadataField - 33, // 25: atelet.ActorMetadataDataSource.items:type_name -> atelet.ActorMetadataItem - 34, // 26: atelet.SystemInfoDataSource.actor_metadata:type_name -> atelet.ActorMetadataDataSource - 35, // 27: atelet.SystemInfoDataSource.trust_bundle:type_name -> atelet.TrustBundleDataSource - 36, // 28: atelet.SystemInfoVolume.data_sources:type_name -> atelet.SystemInfoDataSource + 34, // 25: atelet.ActorMetadataDataSource.items:type_name -> atelet.ActorMetadataItem + 35, // 26: atelet.SystemInfoDataSource.actor_metadata:type_name -> atelet.ActorMetadataDataSource + 36, // 27: atelet.SystemInfoDataSource.trust_bundle:type_name -> atelet.TrustBundleDataSource + 37, // 28: atelet.SystemInfoVolume.data_sources:type_name -> atelet.SystemInfoDataSource 30, // 29: atelet.Volume.durable_dir:type_name -> atelet.DurableDirVolume - 31, // 30: atelet.Volume.external:type_name -> atelet.ExternalVolumeSource - 37, // 31: atelet.Volume.system_info:type_name -> atelet.SystemInfoVolume - 32, // 32: atelet.Volume.image:type_name -> atelet.ImageVolumeSource - 44, // 33: atelet.Container.env:type_name -> atelet.EnvEntry - 45, // 34: atelet.Container.wakeup_probe:type_name -> atelet.WakeupProbe - 39, // 35: atelet.Container.volume_mounts:type_name -> atelet.VolumeMount - 41, // 36: atelet.Container.security_context:type_name -> atelet.SecurityContext - 43, // 37: atelet.Container.resources:type_name -> atelet.ResourceLimits - 42, // 38: atelet.SecurityContext.capabilities:type_name -> atelet.Capabilities - 46, // 39: atelet.WakeupProbe.http_get:type_name -> atelet.HttpGetAction - 1, // 40: atelet.Snapshot.fidelity:type_name -> atelet.SnapshotFidelity - 48, // 41: atelet.Snapshot.object:type_name -> atelet.ObjectStorage - 49, // 42: atelet.SnapshotReport.snapshot:type_name -> atelet.Snapshot - 63, // 43: atelet.SnapshotReport.local_commit_time:type_name -> google.protobuf.Timestamp - 63, // 44: atelet.SnapshotReport.durable_complete_time:type_name -> google.protobuf.Timestamp - 50, // 45: atelet.ActorSnapshotState.latest_local:type_name -> atelet.SnapshotReport - 50, // 46: atelet.ActorSnapshotState.uploading:type_name -> atelet.SnapshotReport - 50, // 47: atelet.ActorSnapshotState.latest_durable:type_name -> atelet.SnapshotReport - 63, // 48: atelet.ActorSnapshotState.upload_due_time:type_name -> google.protobuf.Timestamp - 2, // 49: atelet.UploadConfig.mode:type_name -> atelet.UploadMode - 29, // 50: atelet.CheckpointRequest.spec:type_name -> atelet.WorkloadSpec - 49, // 51: atelet.CheckpointRequest.snapshot:type_name -> atelet.Snapshot - 52, // 52: atelet.CheckpointRequest.upload_config:type_name -> atelet.UploadConfig - 51, // 53: atelet.CheckpointResponse.state:type_name -> atelet.ActorSnapshotState - 29, // 54: atelet.RestoreRequest.spec:type_name -> atelet.WorkloadSpec - 49, // 55: atelet.RestoreRequest.snapshot:type_name -> atelet.Snapshot - 25, // 56: atelet.RestoreRequest.egress_gateway:type_name -> atelet.EgressGateway - 28, // 57: atelet.RestoreRequest.sandbox_assets:type_name -> atelet.SandboxAssets - 49, // 58: atelet.PromoteSnapshotRequest.snapshot:type_name -> atelet.Snapshot - 51, // 59: atelet.PromoteSnapshotResponse.state:type_name -> atelet.ActorSnapshotState - 26, // 60: atelet.ArchAssets.FilesEntry.value:type_name -> atelet.AssetFile - 27, // 61: atelet.SandboxAssets.AssetsEntry.value:type_name -> atelet.ArchAssets - 20, // 62: atelet.WorkerSupport.MintActorCertificate:input_type -> atelet.MintActorCertificateRequest - 3, // 63: atelet.WorkerSupport.RegisterWorker:input_type -> atelet.RegisterWorkerRequest - 11, // 64: atelet.WorkerSupport.RequestActorSuspend:input_type -> atelet.RequestActorSuspendRequest - 13, // 65: atelet.WorkerSupport.GetActorEgressPolicy:input_type -> atelet.GetActorEgressPolicyRequest - 24, // 66: atelet.Atelet.Run:input_type -> atelet.RunRequest - 53, // 67: atelet.Atelet.Checkpoint:input_type -> atelet.CheckpointRequest - 55, // 68: atelet.Atelet.Restore:input_type -> atelet.RestoreRequest - 57, // 69: atelet.Atelet.PromoteSnapshot:input_type -> atelet.PromoteSnapshotRequest - 22, // 70: atelet.Atelet.Terminate:input_type -> atelet.TerminateRequest - 21, // 71: atelet.WorkerSupport.MintActorCertificate:output_type -> atelet.MintActorCertificateResponse - 10, // 72: atelet.WorkerSupport.RegisterWorker:output_type -> atelet.RegisterWorkerResponse - 12, // 73: atelet.WorkerSupport.RequestActorSuspend:output_type -> atelet.RequestActorSuspendResponse - 14, // 74: atelet.WorkerSupport.GetActorEgressPolicy:output_type -> atelet.GetActorEgressPolicyResponse - 47, // 75: atelet.Atelet.Run:output_type -> atelet.RunResponse - 54, // 76: atelet.Atelet.Checkpoint:output_type -> atelet.CheckpointResponse - 56, // 77: atelet.Atelet.Restore:output_type -> atelet.RestoreResponse - 58, // 78: atelet.Atelet.PromoteSnapshot:output_type -> atelet.PromoteSnapshotResponse - 23, // 79: atelet.Atelet.Terminate:output_type -> atelet.TerminateResponse - 71, // [71:80] is the sub-list for method output_type - 62, // [62:71] is the sub-list for method input_type - 62, // [62:62] is the sub-list for extension type_name - 62, // [62:62] is the sub-list for extension extendee - 0, // [0:62] is the sub-list for field type_name + 32, // 30: atelet.Volume.external:type_name -> atelet.ExternalVolume + 38, // 31: atelet.Volume.system_info:type_name -> atelet.SystemInfoVolume + 33, // 32: atelet.Volume.image:type_name -> atelet.ImageVolumeSource + 31, // 33: atelet.Volume.empty_dir:type_name -> atelet.EmptyDirVolume + 45, // 34: atelet.Container.env:type_name -> atelet.EnvEntry + 46, // 35: atelet.Container.wakeup_probe:type_name -> atelet.WakeupProbe + 40, // 36: atelet.Container.volume_mounts:type_name -> atelet.VolumeMount + 42, // 37: atelet.Container.security_context:type_name -> atelet.SecurityContext + 44, // 38: atelet.Container.resources:type_name -> atelet.ResourceLimits + 43, // 39: atelet.SecurityContext.capabilities:type_name -> atelet.Capabilities + 47, // 40: atelet.WakeupProbe.http_get:type_name -> atelet.HttpGetAction + 1, // 41: atelet.Snapshot.fidelity:type_name -> atelet.SnapshotFidelity + 49, // 42: atelet.Snapshot.object:type_name -> atelet.ObjectStorage + 50, // 43: atelet.SnapshotReport.snapshot:type_name -> atelet.Snapshot + 64, // 44: atelet.SnapshotReport.local_commit_time:type_name -> google.protobuf.Timestamp + 64, // 45: atelet.SnapshotReport.durable_complete_time:type_name -> google.protobuf.Timestamp + 51, // 46: atelet.ActorSnapshotState.latest_local:type_name -> atelet.SnapshotReport + 51, // 47: atelet.ActorSnapshotState.uploading:type_name -> atelet.SnapshotReport + 51, // 48: atelet.ActorSnapshotState.latest_durable:type_name -> atelet.SnapshotReport + 64, // 49: atelet.ActorSnapshotState.upload_due_time:type_name -> google.protobuf.Timestamp + 2, // 50: atelet.UploadConfig.mode:type_name -> atelet.UploadMode + 29, // 51: atelet.CheckpointRequest.spec:type_name -> atelet.WorkloadSpec + 50, // 52: atelet.CheckpointRequest.snapshot:type_name -> atelet.Snapshot + 53, // 53: atelet.CheckpointRequest.upload_config:type_name -> atelet.UploadConfig + 52, // 54: atelet.CheckpointResponse.state:type_name -> atelet.ActorSnapshotState + 29, // 55: atelet.RestoreRequest.spec:type_name -> atelet.WorkloadSpec + 50, // 56: atelet.RestoreRequest.snapshot:type_name -> atelet.Snapshot + 25, // 57: atelet.RestoreRequest.egress_gateway:type_name -> atelet.EgressGateway + 28, // 58: atelet.RestoreRequest.sandbox_assets:type_name -> atelet.SandboxAssets + 50, // 59: atelet.PromoteSnapshotRequest.snapshot:type_name -> atelet.Snapshot + 52, // 60: atelet.PromoteSnapshotResponse.state:type_name -> atelet.ActorSnapshotState + 26, // 61: atelet.ArchAssets.FilesEntry.value:type_name -> atelet.AssetFile + 27, // 62: atelet.SandboxAssets.AssetsEntry.value:type_name -> atelet.ArchAssets + 20, // 63: atelet.WorkerSupport.MintActorCertificate:input_type -> atelet.MintActorCertificateRequest + 3, // 64: atelet.WorkerSupport.RegisterWorker:input_type -> atelet.RegisterWorkerRequest + 11, // 65: atelet.WorkerSupport.RequestActorSuspend:input_type -> atelet.RequestActorSuspendRequest + 13, // 66: atelet.WorkerSupport.GetActorEgressPolicy:input_type -> atelet.GetActorEgressPolicyRequest + 24, // 67: atelet.Atelet.Run:input_type -> atelet.RunRequest + 54, // 68: atelet.Atelet.Checkpoint:input_type -> atelet.CheckpointRequest + 56, // 69: atelet.Atelet.Restore:input_type -> atelet.RestoreRequest + 58, // 70: atelet.Atelet.PromoteSnapshot:input_type -> atelet.PromoteSnapshotRequest + 22, // 71: atelet.Atelet.Terminate:input_type -> atelet.TerminateRequest + 21, // 72: atelet.WorkerSupport.MintActorCertificate:output_type -> atelet.MintActorCertificateResponse + 10, // 73: atelet.WorkerSupport.RegisterWorker:output_type -> atelet.RegisterWorkerResponse + 12, // 74: atelet.WorkerSupport.RequestActorSuspend:output_type -> atelet.RequestActorSuspendResponse + 14, // 75: atelet.WorkerSupport.GetActorEgressPolicy:output_type -> atelet.GetActorEgressPolicyResponse + 48, // 76: atelet.Atelet.Run:output_type -> atelet.RunResponse + 55, // 77: atelet.Atelet.Checkpoint:output_type -> atelet.CheckpointResponse + 57, // 78: atelet.Atelet.Restore:output_type -> atelet.RestoreResponse + 59, // 79: atelet.Atelet.PromoteSnapshot:output_type -> atelet.PromoteSnapshotResponse + 23, // 80: atelet.Atelet.Terminate:output_type -> atelet.TerminateResponse + 72, // [72:81] is the sub-list for method output_type + 63, // [63:72] is the sub-list for method input_type + 63, // [63:63] is the sub-list for extension type_name + 63, // [63:63] is the sub-list for extension extendee + 0, // [0:63] is the sub-list for field type_name } func init() { file_atelet_proto_init() } @@ -4424,14 +4486,14 @@ func file_atelet_proto_init() { return } file_atelet_proto_msgTypes[21].OneofWrappers = []any{} - file_atelet_proto_msgTypes[52].OneofWrappers = []any{} + file_atelet_proto_msgTypes[53].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_atelet_proto_rawDesc), len(file_atelet_proto_rawDesc)), NumEnums: 3, - NumMessages: 60, + NumMessages: 61, NumExtensions: 0, NumServices: 2, }, diff --git a/internal/proto/ateletpb/atelet.proto b/internal/proto/ateletpb/atelet.proto index 7608234b8e..0d334361d2 100644 --- a/internal/proto/ateletpb/atelet.proto +++ b/internal/proto/ateletpb/atelet.proto @@ -529,22 +529,30 @@ message WorkloadSpec { // +k8s:listType=map // +k8s:listMapKey=name repeated Volume volumes = 2; + + // Capture only the pre-application runtime; golden actors only. + // +k8s:optional + bool runtime_golden = 3; } message DurableDirVolume { } -message ExternalVolumeSource { +// EmptyDirVolume is an empty host directory at the external volume path. +message EmptyDirVolume { +} + +message ExternalVolume { // Required, unlike the actor status counterpart: the control plane only // sends volumes that finished provisioning. // // +k8s:required - // +k8s:maxLength=256 # matches ExternalVolume.storage_volume_id's bound + // +k8s:maxLength=256 # matches ExternalVolumeRef.storage_volume_id's bound // +k8s:customValidation # no control characters string storage_volume_id = 1; // +k8s:optional - // +k8s:maxLength=253 # matches ExternalVolume.volume_type's bound + // +k8s:maxLength=253 # matches ExternalVolumeRef.volume_type's bound // +k8s:customValidation # optional "substrate.io/" prefix + DNS subdomain string volume_type = 2; @@ -665,7 +673,7 @@ message Volume { // +k8s:format=k8s-short-name string name = 1; - // Exactly one of durable_dir / external / system_info / image must be set. + // Exactly one of durable_dir / external / system_info / image / empty_dir must be set. // // +k8s:optional // +k8s:unionMember @@ -673,7 +681,7 @@ message Volume { // +k8s:optional // +k8s:unionMember - ExternalVolumeSource external = 3; + ExternalVolume external = 3; // +k8s:optional // +k8s:unionMember @@ -682,6 +690,10 @@ message Volume { // +k8s:optional // +k8s:unionMember ImageVolumeSource image = 5; + + // +k8s:optional + // +k8s:unionMember + EmptyDirVolume empty_dir = 6; } message VolumeMount { diff --git a/internal/proto/ateworkerpb/ateworker.pb.go b/internal/proto/ateworkerpb/ateworker.pb.go index ee23673a67..60de76b5c5 100644 --- a/internal/proto/ateworkerpb/ateworker.pb.go +++ b/internal/proto/ateworkerpb/ateworker.pb.go @@ -586,8 +586,11 @@ func (x *EgressGateway) GetAddress() string { // WorkloadSpec parallels Pod, but with far fewer configurable fields. type WorkloadSpec struct { - state protoimpl.MessageState `protogen:"open.v1"` - Containers []*Container `protobuf:"bytes,1,rep,name=containers,proto3" json:"containers,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Containers []*Container `protobuf:"bytes,1,rep,name=containers,proto3" json:"containers,omitempty"` + // Boot the runtime without creating a guest sandbox or application processes. + // Valid only for a golden actor; supported by gVisor and microVM. + RuntimeGolden bool `protobuf:"varint,2,opt,name=runtime_golden,json=runtimeGolden,proto3" json:"runtime_golden,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -629,6 +632,13 @@ func (x *WorkloadSpec) GetContainers() []*Container { return nil } +func (x *WorkloadSpec) GetRuntimeGolden() bool { + if x != nil { + return x.RuntimeGolden + } + return false +} + type Container struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -2032,11 +2042,12 @@ const file_ateworker_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x11\n" + "\x0f_egress_gateway\")\n" + "\rEgressGateway\x12\x18\n" + - "\aaddress\x18\x01 \x01(\tR\aaddress\"D\n" + + "\aaddress\x18\x01 \x01(\tR\aaddress\"k\n" + "\fWorkloadSpec\x124\n" + "\n" + "containers\x18\x01 \x03(\v2\x14.ateworker.ContainerR\n" + - "containers\"\xe6\x03\n" + + "containers\x12%\n" + + "\x0eruntime_golden\x18\x02 \x01(\bR\rruntimeGolden\"\xe6\x03\n" + "\tContainer\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x129\n" + "\fwakeup_probe\x18\x02 \x01(\v2\x16.ateworker.WakeupProbeR\vwakeupProbe\x12[\n" + diff --git a/internal/proto/ateworkerpb/ateworker.proto b/internal/proto/ateworkerpb/ateworker.proto index 79b8da40b8..a67ab4e9ad 100644 --- a/internal/proto/ateworkerpb/ateworker.proto +++ b/internal/proto/ateworkerpb/ateworker.proto @@ -18,7 +18,7 @@ package ateworker; option go_package = "github.com/agent-substrate/substrate/internal/proto/ateworkerpb"; -// Worker is the interface to control a single gVisor (or, in the future microVM) +// Worker is the interface to control a single gVisor or microVM // guest inside a worker pod. // // Each worker has two main states, "available" and "executing". @@ -179,6 +179,9 @@ message EgressGateway { // WorkloadSpec parallels Pod, but with far fewer configurable fields. message WorkloadSpec { repeated Container containers = 1; + // Boot the runtime without creating a guest sandbox or application processes. + // Valid only for a golden actor; supported by gVisor and microVM. + bool runtime_golden = 2; } message Container { diff --git a/internal/proto/ateworkerpb/ateworker_grpc.pb.go b/internal/proto/ateworkerpb/ateworker_grpc.pb.go index 2c4854e5a4..7265703bcb 100644 --- a/internal/proto/ateworkerpb/ateworker_grpc.pb.go +++ b/internal/proto/ateworkerpb/ateworker_grpc.pb.go @@ -45,7 +45,7 @@ const ( // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. // -// Worker is the interface to control a single gVisor (or, in the future microVM) +// Worker is the interface to control a single gVisor or microVM // guest inside a worker pod. // // Each worker has two main states, "available" and "executing". @@ -199,7 +199,7 @@ func (c *workerClient) TerminateWorkload(ctx context.Context, in *TerminateWorkl // All implementations must embed UnimplementedWorkerServer // for forward compatibility. // -// Worker is the interface to control a single gVisor (or, in the future microVM) +// Worker is the interface to control a single gVisor or microVM // guest inside a worker pod. // // Each worker has two main states, "available" and "executing". diff --git a/internal/resources/resourceref.go b/internal/resources/resourceref.go index a14d079d75..92ba75186a 100644 --- a/internal/resources/resourceref.go +++ b/internal/resources/resourceref.go @@ -107,3 +107,20 @@ func TagRefFromTag(t *ateapipb.Tag) TagRef { Name: t.GetMetadata().GetName(), } } + +// SharedVolumeRef identifies a SharedVolume by the (atespace, name). +type SharedVolumeRef = ResourceRef[*ateapipb.SharedVolume] + +// SharedVolumeRefFromObjectRef converts an ObjectRef to a SharedVolumeRef. +func SharedVolumeRefFromObjectRef(ref *ateapipb.ObjectRef) SharedVolumeRef { + return resourceRefFromObjectRef[*ateapipb.SharedVolume](ref) +} + +// SharedVolumeRefFromSharedVolume returns the reference addressing the +// given volume. +func SharedVolumeRefFromSharedVolume(v *ateapipb.SharedVolume) SharedVolumeRef { + return SharedVolumeRef{ + Atespace: v.GetMetadata().GetAtespace(), + Name: v.GetMetadata().GetName(), + } +} diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index ad88e8685f..98b3ecb804 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -319,6 +319,167 @@ func (ActorState) EnumDescriptor() ([]byte, []int) { return file_ateapi_proto_rawDescGZIP(), []int{4} } +// DeleteTrigger is what removes a SharedVolume and its storage. +// TODO: add an ATESPACE trigger that removes the volume with its atespace. +type DeleteTrigger int32 + +const ( + DeleteTrigger_DELETE_TRIGGER_UNSPECIFIED DeleteTrigger = 0 + // Only an explicit DeleteSharedVolume removes the volume. + DeleteTrigger_DELETE_TRIGGER_MANUAL DeleteTrigger = 1 + // The volume is removed when the last actor referencing it is deleted. + DeleteTrigger_DELETE_TRIGGER_LAST_ACTOR DeleteTrigger = 2 +) + +// Enum value maps for DeleteTrigger. +var ( + DeleteTrigger_name = map[int32]string{ + 0: "DELETE_TRIGGER_UNSPECIFIED", + 1: "DELETE_TRIGGER_MANUAL", + 2: "DELETE_TRIGGER_LAST_ACTOR", + } + DeleteTrigger_value = map[string]int32{ + "DELETE_TRIGGER_UNSPECIFIED": 0, + "DELETE_TRIGGER_MANUAL": 1, + "DELETE_TRIGGER_LAST_ACTOR": 2, + } +) + +func (x DeleteTrigger) Enum() *DeleteTrigger { + p := new(DeleteTrigger) + *p = x + return p +} + +func (x DeleteTrigger) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DeleteTrigger) Descriptor() protoreflect.EnumDescriptor { + return file_ateapi_proto_enumTypes[5].Descriptor() +} + +func (DeleteTrigger) Type() protoreflect.EnumType { + return &file_ateapi_proto_enumTypes[5] +} + +func (x DeleteTrigger) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DeleteTrigger.Descriptor instead. +func (DeleteTrigger) EnumDescriptor() ([]byte, []int) { + return file_ateapi_proto_rawDescGZIP(), []int{5} +} + +// AccessMode is how many resumed actors may hold the volume at once. +type AccessMode int32 + +const ( + AccessMode_ACCESS_MODE_UNSPECIFIED AccessMode = 0 + // At most one resumed actor holds the volume. + AccessMode_ACCESS_MODE_READ_WRITE_ONCE AccessMode = 1 + // Any number of resumed actors hold the volume, on any node. + AccessMode_ACCESS_MODE_READ_WRITE_MANY AccessMode = 2 +) + +// Enum value maps for AccessMode. +var ( + AccessMode_name = map[int32]string{ + 0: "ACCESS_MODE_UNSPECIFIED", + 1: "ACCESS_MODE_READ_WRITE_ONCE", + 2: "ACCESS_MODE_READ_WRITE_MANY", + } + AccessMode_value = map[string]int32{ + "ACCESS_MODE_UNSPECIFIED": 0, + "ACCESS_MODE_READ_WRITE_ONCE": 1, + "ACCESS_MODE_READ_WRITE_MANY": 2, + } +) + +func (x AccessMode) Enum() *AccessMode { + p := new(AccessMode) + *p = x + return p +} + +func (x AccessMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AccessMode) Descriptor() protoreflect.EnumDescriptor { + return file_ateapi_proto_enumTypes[6].Descriptor() +} + +func (AccessMode) Type() protoreflect.EnumType { + return &file_ateapi_proto_enumTypes[6] +} + +func (x AccessMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AccessMode.Descriptor instead. +func (AccessMode) EnumDescriptor() ([]byte, []int) { + return file_ateapi_proto_rawDescGZIP(), []int{6} +} + +// SharedVolumeState is where a SharedVolume is in its own lifecycle. +type SharedVolumeState int32 + +const ( + SharedVolumeState_SHARED_VOLUME_STATE_UNSPECIFIED SharedVolumeState = 0 + // The name is reserved but no volume handle is known yet. + SharedVolumeState_SHARED_VOLUME_STATE_PENDING SharedVolumeState = 1 + // The volume has a handle and can be referenced. + SharedVolumeState_SHARED_VOLUME_STATE_READY SharedVolumeState = 2 + // New references are forbidden while storage deletion is retried. + SharedVolumeState_SHARED_VOLUME_STATE_DELETING SharedVolumeState = 3 +) + +// Enum value maps for SharedVolumeState. +var ( + SharedVolumeState_name = map[int32]string{ + 0: "SHARED_VOLUME_STATE_UNSPECIFIED", + 1: "SHARED_VOLUME_STATE_PENDING", + 2: "SHARED_VOLUME_STATE_READY", + 3: "SHARED_VOLUME_STATE_DELETING", + } + SharedVolumeState_value = map[string]int32{ + "SHARED_VOLUME_STATE_UNSPECIFIED": 0, + "SHARED_VOLUME_STATE_PENDING": 1, + "SHARED_VOLUME_STATE_READY": 2, + "SHARED_VOLUME_STATE_DELETING": 3, + } +) + +func (x SharedVolumeState) Enum() *SharedVolumeState { + p := new(SharedVolumeState) + *p = x + return p +} + +func (x SharedVolumeState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SharedVolumeState) Descriptor() protoreflect.EnumDescriptor { + return file_ateapi_proto_enumTypes[7].Descriptor() +} + +func (SharedVolumeState) Type() protoreflect.EnumType { + return &file_ateapi_proto_enumTypes[7] +} + +func (x SharedVolumeState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SharedVolumeState.Descriptor instead. +func (SharedVolumeState) EnumDescriptor() ([]byte, []int) { + return file_ateapi_proto_rawDescGZIP(), []int{7} +} + // GoldenSnapshotMode selects whether a template builds a golden snapshot. type GoldenSnapshotMode int32 @@ -326,6 +487,10 @@ const ( GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_UNSPECIFIED GoldenSnapshotMode = 0 GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_ENABLED GoldenSnapshotMode = 1 GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_DISABLED GoldenSnapshotMode = 2 + // Capture the runtime before creating application processes. Supported by + // MICROVM and GVISOR. Restore attaches real volumes before starting + // applications; application warmup is not preserved. + GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_RUNTIME GoldenSnapshotMode = 3 ) // Enum value maps for GoldenSnapshotMode. @@ -334,11 +499,13 @@ var ( 0: "GOLDEN_SNAPSHOT_MODE_UNSPECIFIED", 1: "GOLDEN_SNAPSHOT_MODE_ENABLED", 2: "GOLDEN_SNAPSHOT_MODE_DISABLED", + 3: "GOLDEN_SNAPSHOT_MODE_RUNTIME", } GoldenSnapshotMode_value = map[string]int32{ "GOLDEN_SNAPSHOT_MODE_UNSPECIFIED": 0, "GOLDEN_SNAPSHOT_MODE_ENABLED": 1, "GOLDEN_SNAPSHOT_MODE_DISABLED": 2, + "GOLDEN_SNAPSHOT_MODE_RUNTIME": 3, } ) @@ -353,11 +520,11 @@ func (x GoldenSnapshotMode) String() string { } func (GoldenSnapshotMode) Descriptor() protoreflect.EnumDescriptor { - return file_ateapi_proto_enumTypes[5].Descriptor() + return file_ateapi_proto_enumTypes[8].Descriptor() } func (GoldenSnapshotMode) Type() protoreflect.EnumType { - return &file_ateapi_proto_enumTypes[5] + return &file_ateapi_proto_enumTypes[8] } func (x GoldenSnapshotMode) Number() protoreflect.EnumNumber { @@ -366,7 +533,7 @@ func (x GoldenSnapshotMode) Number() protoreflect.EnumNumber { // Deprecated: Use GoldenSnapshotMode.Descriptor instead. func (GoldenSnapshotMode) EnumDescriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{5} + return file_ateapi_proto_rawDescGZIP(), []int{8} } // ActorMetadataField selects one identity field of the actor. @@ -406,11 +573,11 @@ func (x ActorMetadataField) String() string { } func (ActorMetadataField) Descriptor() protoreflect.EnumDescriptor { - return file_ateapi_proto_enumTypes[6].Descriptor() + return file_ateapi_proto_enumTypes[9].Descriptor() } func (ActorMetadataField) Type() protoreflect.EnumType { - return &file_ateapi_proto_enumTypes[6] + return &file_ateapi_proto_enumTypes[9] } func (x ActorMetadataField) Number() protoreflect.EnumNumber { @@ -419,7 +586,7 @@ func (x ActorMetadataField) Number() protoreflect.EnumNumber { // Deprecated: Use ActorMetadataField.Descriptor instead. func (ActorMetadataField) EnumDescriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{6} + return file_ateapi_proto_rawDescGZIP(), []int{9} } type WorkerState int32 @@ -463,11 +630,11 @@ func (x WorkerState) String() string { } func (WorkerState) Descriptor() protoreflect.EnumDescriptor { - return file_ateapi_proto_enumTypes[7].Descriptor() + return file_ateapi_proto_enumTypes[10].Descriptor() } func (WorkerState) Type() protoreflect.EnumType { - return &file_ateapi_proto_enumTypes[7] + return &file_ateapi_proto_enumTypes[10] } func (x WorkerState) Number() protoreflect.EnumNumber { @@ -476,7 +643,7 @@ func (x WorkerState) Number() protoreflect.EnumNumber { // Deprecated: Use WorkerState.Descriptor instead. func (WorkerState) EnumDescriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{7} + return file_ateapi_proto_rawDescGZIP(), []int{10} } // Scope is whether a top-level resource belongs to an atespace. @@ -516,11 +683,11 @@ func (x Scope) String() string { } func (Scope) Descriptor() protoreflect.EnumDescriptor { - return file_ateapi_proto_enumTypes[8].Descriptor() + return file_ateapi_proto_enumTypes[11].Descriptor() } func (Scope) Type() protoreflect.EnumType { - return &file_ateapi_proto_enumTypes[8] + return &file_ateapi_proto_enumTypes[11] } func (x Scope) Number() protoreflect.EnumNumber { @@ -529,7 +696,7 @@ func (x Scope) Number() protoreflect.EnumNumber { // Deprecated: Use Scope.Descriptor instead. func (Scope) EnumDescriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{8} + return file_ateapi_proto_rawDescGZIP(), []int{11} } // ObjectSnapshot addresses a snapshot held in object storage. @@ -969,7 +1136,8 @@ func (x *ResourceMetadata) GetUpdateTime() *timestamppb.Timestamp { return nil } -type ExternalVolume struct { +// ExternalVolumeRef is one actor's view of an external volume it mounts. +type ExternalVolumeRef struct { state protoimpl.MessageState `protogen:"open.v1"` // Name of the volume specified in the actor template. Template volume // names are DNS labels. @@ -1011,24 +1179,32 @@ type ExternalVolume struct { // +k8s:optional // +k8s:customValidation # at most 40 KiB in total, ten times the CSI limit for map fields VolumeContext map[string]string `protobuf:"bytes,5,rep,name=volume_context,json=volumeContext,proto3" json:"volume_context,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // shared_volume_name names the SharedVolume this mount references, in the + // actor's atespace. Empty when the actor owns the volume itself. + // + // +k8s:optional + // +k8s:format=k8s-short-name + // +k8s:update=NoModify + // +k8s:update=NoUnset + SharedVolumeName string `protobuf:"bytes,6,opt,name=shared_volume_name,json=sharedVolumeName,proto3" json:"shared_volume_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *ExternalVolume) Reset() { - *x = ExternalVolume{} +func (x *ExternalVolumeRef) Reset() { + *x = ExternalVolumeRef{} mi := &file_ateapi_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ExternalVolume) String() string { +func (x *ExternalVolumeRef) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ExternalVolume) ProtoMessage() {} +func (*ExternalVolumeRef) ProtoMessage() {} -func (x *ExternalVolume) ProtoReflect() protoreflect.Message { +func (x *ExternalVolumeRef) ProtoReflect() protoreflect.Message { mi := &file_ateapi_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1040,39 +1216,46 @@ func (x *ExternalVolume) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ExternalVolume.ProtoReflect.Descriptor instead. -func (*ExternalVolume) Descriptor() ([]byte, []int) { +// Deprecated: Use ExternalVolumeRef.ProtoReflect.Descriptor instead. +func (*ExternalVolumeRef) Descriptor() ([]byte, []int) { return file_ateapi_proto_rawDescGZIP(), []int{5} } -func (x *ExternalVolume) GetName() string { +func (x *ExternalVolumeRef) GetName() string { if x != nil { return x.Name } return "" } -func (x *ExternalVolume) GetStorageVolumeId() string { +func (x *ExternalVolumeRef) GetStorageVolumeId() string { if x != nil { return x.StorageVolumeId } return "" } -func (x *ExternalVolume) GetVolumeType() string { +func (x *ExternalVolumeRef) GetVolumeType() string { if x != nil { return x.VolumeType } return "" } -func (x *ExternalVolume) GetVolumeContext() map[string]string { +func (x *ExternalVolumeRef) GetVolumeContext() map[string]string { if x != nil { return x.VolumeContext } return nil } +func (x *ExternalVolumeRef) GetSharedVolumeName() string { + if x != nil { + return x.SharedVolumeName + } + return "" +} + type Actor struct { state protoimpl.MessageState `protogen:"open.v1"` // Common resource metadata: atespace, name, uid, version, timestamps. @@ -1104,9 +1287,19 @@ type Actor struct { // server and ignored on input. It is always present in output. // // +k8s:optional - Status *ActorStatus `protobuf:"bytes,7,opt,name=status,proto3" json:"status,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Status *ActorStatus `protobuf:"bytes,7,opt,name=status,proto3" json:"status,omitempty"` + // shared_volume_bindings maps each unnamed external.shared_volume in the + // template, by volume name, to the SharedVolume this actor mounts. Every + // unnamed ref must be bound; a named ref cannot be rebound. Set at creation. + // + // +k8s:optional + // +k8s:maxProperties=32 + // +k8s:eachKey=+k8s:format=k8s-short-name + // +k8s:eachVal=+k8s:format=k8s-short-name + // +k8s:immutable + SharedVolumeBindings map[string]string `protobuf:"bytes,8,rep,name=shared_volume_bindings,json=sharedVolumeBindings,proto3" json:"shared_volume_bindings,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Actor) Reset() { @@ -1174,6 +1367,13 @@ func (x *Actor) GetStatus() *ActorStatus { return nil } +func (x *Actor) GetSharedVolumeBindings() map[string]string { + if x != nil { + return x.SharedVolumeBindings + } + return nil +} + // EgressPolicy is an egress policy resource nested under an Actor. An Actor has // at most one egress policy resource, named "default". type EgressPolicy struct { @@ -1941,7 +2141,7 @@ type ActorStatus struct { // +k8s:maxItems=32 # matches the template's volumes bound // +k8s:listType=map // +k8s:listMapKey=name - ExternalVolumes []*ExternalVolume `protobuf:"bytes,7,rep,name=external_volumes,json=externalVolumes,proto3" json:"external_volumes,omitempty"` + ExternalVolumes []*ExternalVolumeRef `protobuf:"bytes,7,rep,name=external_volumes,json=externalVolumes,proto3" json:"external_volumes,omitempty"` // crash records why and when the Actor entered CRASHED. It is set with the // CRASHED state and cleared when a revert returns the Actor to SUSPENDED. // @@ -2020,7 +2220,7 @@ func (x *ActorStatus) GetWorkerAssignment() *WorkerAssignment { return nil } -func (x *ActorStatus) GetExternalVolumes() []*ExternalVolume { +func (x *ActorStatus) GetExternalVolumes() []*ExternalVolumeRef { if x != nil { return x.ExternalVolumes } @@ -2435,33 +2635,99 @@ func (x *Tag) GetSourceActor() *ObjectRef { 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 { +// SharedVolume is an atespace-scoped CSI volume whose lifetime is independent +// of any actor. Identity is (atespace, name); the storage-level ID is derived +// from the record's UID. +type SharedVolume struct { state protoimpl.MessageState `protogen:"open.v1"` - // Common resource metadata: name, uid, version, timestamps. + // Common resource metadata: atespace, name, uid, version, timestamps. // // +k8s:required - // +k8s:beta(since: "0.0")=+k8s:subfield(atespace)=+k8s:forbidden # TODO: get rid of beta prefix - Metadata *ResourceMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // +k8s:subfield(atespace)=+k8s:required + Metadata *ResourceMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // delete_trigger decides what removes the volume. Defaults to LAST_ACTOR when + // provisioned and MANUAL when registered. + // + // +k8s:optional + // +k8s:minimum=1 + // +k8s:maximum=2 # keep this in sync with the DeleteTrigger enum + // +k8s:mutable + DeleteTrigger DeleteTrigger `protobuf:"varint,2,opt,name=delete_trigger,json=deleteTrigger,proto3,enum=ateapi.DeleteTrigger" json:"delete_trigger,omitempty"` + // storage_class_name names the StorageClass to provision from, or documents + // the class of a registered volume. + // + // +k8s:optional + // +k8s:format=k8s-long-name + // +k8s:update=NoModify + // +k8s:update=NoUnset + StorageClassName string `protobuf:"bytes,3,opt,name=storage_class_name,json=storageClassName,proto3" json:"storage_class_name,omitempty"` + // volume_id is the storage system's volume ID. Supplied to register an + // existing volume, otherwise stamped once provisioning completes. Bounded and + // checked for control characters only. + // + // +k8s:optional + // +k8s:maxLength=256 + // +k8s:update=NoModify + // +k8s:update=NoUnset + // +k8s:customValidation # no control characters + VolumeId string `protobuf:"bytes,4,opt,name=volume_id,json=volumeId,proto3" json:"volume_id,omitempty"` + // volume_type is the CSI driver name. Required alongside volume_id when no + // StorageClass is named; otherwise taken from the class's provisioner. + // + // +k8s:optional + // +k8s:maxLength=253 + // +k8s:update=NoModify + // +k8s:update=NoUnset + // +k8s:customValidation + VolumeType string `protobuf:"bytes,5,opt,name=volume_type,json=volumeType,proto3" json:"volume_type,omitempty"` + // volume_context is driver metadata needed by the node plugin for mounting. + // Attachment metadata is per-node and deliberately not recorded here. + // + // +k8s:optional + // +k8s:maxProperties=32 + // +k8s:eachKey=+k8s:maxLength=128 + // +k8s:eachVal=+k8s:maxLength=256 + VolumeContext map[string]string `protobuf:"bytes,6,rep,name=volume_context,json=volumeContext,proto3" json:"volume_context,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // status is set by the control plane; it is ignored on write. + // + // +k8s:optional + Status *SharedVolumeStatus `protobuf:"bytes,7,opt,name=status,proto3" json:"status,omitempty"` + // capacity to provision, as a Kubernetes resource.Quantity string (e.g. + // "10Gi"). Required when provisioning, forbidden alongside volume_id. + // + // +k8s:optional + // +k8s:maxLength=64 + // +k8s:update=NoModify + // +k8s:update=NoUnset + // +k8s:customValidation # must parse as a resource.Quantity + Capacity string `protobuf:"bytes,8,opt,name=capacity,proto3" json:"capacity,omitempty"` + // access_mode is fixed at create and defaults to READ_WRITE_ONCE. + // TODO: accept READ_WRITE_MANY once concurrent holders are supported. + // + // +k8s:optional + // +k8s:minimum=1 + // +k8s:maximum=2 # keep this in sync with the AccessMode enum + // +k8s:update=NoModify + // +k8s:update=NoUnset + AccessMode AccessMode `protobuf:"varint,9,opt,name=access_mode,json=accessMode,proto3,enum=ateapi.AccessMode" json:"access_mode,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *Atespace) Reset() { - *x = Atespace{} +func (x *SharedVolume) Reset() { + *x = SharedVolume{} mi := &file_ateapi_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *Atespace) String() string { +func (x *SharedVolume) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Atespace) ProtoMessage() {} +func (*SharedVolume) ProtoMessage() {} -func (x *Atespace) ProtoReflect() protoreflect.Message { +func (x *SharedVolume) ProtoReflect() protoreflect.Message { mi := &file_ateapi_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2473,53 +2739,109 @@ func (x *Atespace) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Atespace.ProtoReflect.Descriptor instead. -func (*Atespace) Descriptor() ([]byte, []int) { +// Deprecated: Use SharedVolume.ProtoReflect.Descriptor instead. +func (*SharedVolume) Descriptor() ([]byte, []int) { return file_ateapi_proto_rawDescGZIP(), []int{22} } -func (x *Atespace) GetMetadata() *ResourceMetadata { +func (x *SharedVolume) GetMetadata() *ResourceMetadata { if x != nil { return x.Metadata } return nil } -// ObjectRef references a Substrate resource by its atespace and name. -type ObjectRef struct { +func (x *SharedVolume) GetDeleteTrigger() DeleteTrigger { + if x != nil { + return x.DeleteTrigger + } + return DeleteTrigger_DELETE_TRIGGER_UNSPECIFIED +} + +func (x *SharedVolume) GetStorageClassName() string { + if x != nil { + return x.StorageClassName + } + return "" +} + +func (x *SharedVolume) GetVolumeId() string { + if x != nil { + return x.VolumeId + } + return "" +} + +func (x *SharedVolume) GetVolumeType() string { + if x != nil { + return x.VolumeType + } + return "" +} + +func (x *SharedVolume) GetVolumeContext() map[string]string { + if x != nil { + return x.VolumeContext + } + return nil +} + +func (x *SharedVolume) GetStatus() *SharedVolumeStatus { + if x != nil { + return x.Status + } + return nil +} + +func (x *SharedVolume) GetCapacity() string { + if x != nil { + return x.Capacity + } + return "" +} + +func (x *SharedVolume) GetAccessMode() AccessMode { + if x != nil { + return x.AccessMode + } + return AccessMode_ACCESS_MODE_UNSPECIFIED +} + +// SharedVolumeStatus is the part of a SharedVolume the control plane owns. +type SharedVolumeStatus struct { state protoimpl.MessageState `protogen:"open.v1"` - // The atespace of the referenced resource. This field should be empty if the - // target resource is global-scoped, but references to objects in the same - // atespace as the referrer must still populate this field. + // state is where the volume is in its lifecycle. // - // References to atespaced resources should add `+k8s:subfield(atespace)=+k8s:required` - // References to non-atespaced resources should add `+k8s:subfield(atespace)=+k8s:forbidden` // +k8s:optional - // +k8s:format=k8s-short-name - Atespace string `protobuf:"bytes,1,opt,name=atespace,proto3" json:"atespace,omitempty"` - // The name of the referenced resource. This field is required. + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the SharedVolumeState enum + State SharedVolumeState `protobuf:"varint,1,opt,name=state,proto3,enum=ateapi.SharedVolumeState" json:"state,omitempty"` + // refs are the actors referencing the volume, resumed or not. + // TODO: move references to their own table before lifting the bound. // - // +k8s:required - // +k8s:format=k8s-short-name - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // +k8s:optional + // +k8s:maxItems=256 + // +k8s:listType=map + // +k8s:listMapKey=actor_uid + Refs []*ActorRef `protobuf:"bytes,2,rep,name=refs,proto3" json:"refs,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ObjectRef) Reset() { - *x = ObjectRef{} +func (x *SharedVolumeStatus) Reset() { + *x = SharedVolumeStatus{} mi := &file_ateapi_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ObjectRef) String() string { +func (x *SharedVolumeStatus) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ObjectRef) ProtoMessage() {} +func (*SharedVolumeStatus) ProtoMessage() {} -func (x *ObjectRef) ProtoReflect() protoreflect.Message { +func (x *SharedVolumeStatus) ProtoReflect() protoreflect.Message { mi := &file_ateapi_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2531,23 +2853,210 @@ func (x *ObjectRef) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ObjectRef.ProtoReflect.Descriptor instead. -func (*ObjectRef) Descriptor() ([]byte, []int) { +// Deprecated: Use SharedVolumeStatus.ProtoReflect.Descriptor instead. +func (*SharedVolumeStatus) Descriptor() ([]byte, []int) { return file_ateapi_proto_rawDescGZIP(), []int{23} } -func (x *ObjectRef) GetAtespace() string { +func (x *SharedVolumeStatus) GetState() SharedVolumeState { if x != nil { - return x.Atespace + return x.State } - return "" + return SharedVolumeState_SHARED_VOLUME_STATE_UNSPECIFIED } -func (x *ObjectRef) GetName() string { +func (x *SharedVolumeStatus) GetRefs() []*ActorRef { if x != nil { - return x.Name + return x.Refs } - return "" + return nil +} + +// ActorRef is one actor's reference to a SharedVolume. +type ActorRef struct { + state protoimpl.MessageState `protogen:"open.v1"` + // actor_uid identifies the actor, so a recreated namesake does not inherit it. + // + // +k8s:required + // +k8s:maxLength=253 + ActorUid string `protobuf:"bytes,1,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` + // actor_name is the actor's name, for readers. + // + // +k8s:required + // +k8s:format=k8s-short-name + ActorName string `protobuf:"bytes,2,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` + // active is true while the actor is resumed and holds the volume mounted. + // + // +k8s:optional + Active bool `protobuf:"varint,3,opt,name=active,proto3" json:"active,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ActorRef) Reset() { + *x = ActorRef{} + mi := &file_ateapi_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ActorRef) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ActorRef) ProtoMessage() {} + +func (x *ActorRef) ProtoReflect() protoreflect.Message { + mi := &file_ateapi_proto_msgTypes[24] + 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 ActorRef.ProtoReflect.Descriptor instead. +func (*ActorRef) Descriptor() ([]byte, []int) { + return file_ateapi_proto_rawDescGZIP(), []int{24} +} + +func (x *ActorRef) GetActorUid() string { + if x != nil { + return x.ActorUid + } + return "" +} + +func (x *ActorRef) GetActorName() string { + if x != nil { + return x.ActorName + } + return "" +} + +func (x *ActorRef) GetActive() bool { + if x != nil { + return x.Active + } + return false +} + +// 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 { + state protoimpl.MessageState `protogen:"open.v1"` + // Common resource metadata: name, uid, version, timestamps. + // + // +k8s:required + // +k8s:beta(since: "0.0")=+k8s:subfield(atespace)=+k8s:forbidden # TODO: get rid of beta prefix + Metadata *ResourceMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Atespace) Reset() { + *x = Atespace{} + mi := &file_ateapi_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Atespace) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Atespace) ProtoMessage() {} + +func (x *Atespace) ProtoReflect() protoreflect.Message { + mi := &file_ateapi_proto_msgTypes[25] + 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 Atespace.ProtoReflect.Descriptor instead. +func (*Atespace) Descriptor() ([]byte, []int) { + return file_ateapi_proto_rawDescGZIP(), []int{25} +} + +func (x *Atespace) GetMetadata() *ResourceMetadata { + if x != nil { + return x.Metadata + } + return nil +} + +// ObjectRef references a Substrate resource by its atespace and name. +type ObjectRef struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The atespace of the referenced resource. This field should be empty if the + // target resource is global-scoped, but references to objects in the same + // atespace as the referrer must still populate this field. + // + // References to atespaced resources should add `+k8s:subfield(atespace)=+k8s:required` + // References to non-atespaced resources should add `+k8s:subfield(atespace)=+k8s:forbidden` + // +k8s:optional + // +k8s:format=k8s-short-name + Atespace string `protobuf:"bytes,1,opt,name=atespace,proto3" json:"atespace,omitempty"` + // The name of the referenced resource. This field is required. + // + // +k8s:required + // +k8s:format=k8s-short-name + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ObjectRef) Reset() { + *x = ObjectRef{} + mi := &file_ateapi_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ObjectRef) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ObjectRef) ProtoMessage() {} + +func (x *ObjectRef) ProtoReflect() protoreflect.Message { + mi := &file_ateapi_proto_msgTypes[26] + 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 ObjectRef.ProtoReflect.Descriptor instead. +func (*ObjectRef) Descriptor() ([]byte, []int) { + return file_ateapi_proto_rawDescGZIP(), []int{26} +} + +func (x *ObjectRef) GetAtespace() string { + if x != nil { + return x.Atespace + } + return "" +} + +func (x *ObjectRef) GetName() string { + if x != nil { + return x.Name + } + return "" } type ActorTemplate struct { @@ -2593,7 +3102,7 @@ type ActorTemplate struct { func (x *ActorTemplate) Reset() { *x = ActorTemplate{} - mi := &file_ateapi_proto_msgTypes[24] + mi := &file_ateapi_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2605,7 +3114,7 @@ func (x *ActorTemplate) String() string { func (*ActorTemplate) ProtoMessage() {} func (x *ActorTemplate) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[24] + mi := &file_ateapi_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2618,7 +3127,7 @@ func (x *ActorTemplate) ProtoReflect() protoreflect.Message { // Deprecated: Use ActorTemplate.ProtoReflect.Descriptor instead. func (*ActorTemplate) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{24} + return file_ateapi_proto_rawDescGZIP(), []int{27} } func (x *ActorTemplate) GetMetadata() *ResourceMetadata { @@ -2694,7 +3203,7 @@ type Resources struct { func (x *Resources) Reset() { *x = Resources{} - mi := &file_ateapi_proto_msgTypes[25] + mi := &file_ateapi_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2706,7 +3215,7 @@ func (x *Resources) String() string { func (*Resources) ProtoMessage() {} func (x *Resources) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[25] + mi := &file_ateapi_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2719,7 +3228,7 @@ func (x *Resources) ProtoReflect() protoreflect.Message { // Deprecated: Use Resources.ProtoReflect.Descriptor instead. func (*Resources) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{25} + return file_ateapi_proto_rawDescGZIP(), []int{28} } func (x *Resources) GetLimits() []*Limits { @@ -2747,7 +3256,7 @@ type Limits struct { func (x *Limits) Reset() { *x = Limits{} - mi := &file_ateapi_proto_msgTypes[26] + mi := &file_ateapi_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2759,7 +3268,7 @@ func (x *Limits) String() string { func (*Limits) ProtoMessage() {} func (x *Limits) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[26] + mi := &file_ateapi_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2772,7 +3281,7 @@ func (x *Limits) ProtoReflect() protoreflect.Message { // Deprecated: Use Limits.ProtoReflect.Descriptor instead. func (*Limits) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{26} + return file_ateapi_proto_rawDescGZIP(), []int{29} } func (x *Limits) GetName() string { @@ -2813,7 +3322,7 @@ type GoldenSnapshotStatus struct { func (x *GoldenSnapshotStatus) Reset() { *x = GoldenSnapshotStatus{} - mi := &file_ateapi_proto_msgTypes[27] + mi := &file_ateapi_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2825,7 +3334,7 @@ func (x *GoldenSnapshotStatus) String() string { func (*GoldenSnapshotStatus) ProtoMessage() {} func (x *GoldenSnapshotStatus) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[27] + mi := &file_ateapi_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2838,7 +3347,7 @@ func (x *GoldenSnapshotStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use GoldenSnapshotStatus.ProtoReflect.Descriptor instead. func (*GoldenSnapshotStatus) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{27} + return file_ateapi_proto_rawDescGZIP(), []int{30} } func (x *GoldenSnapshotStatus) GetGoldenTag() *ObjectRef { @@ -2872,7 +3381,7 @@ type ActorTemplateStatus struct { func (x *ActorTemplateStatus) Reset() { *x = ActorTemplateStatus{} - mi := &file_ateapi_proto_msgTypes[28] + mi := &file_ateapi_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2884,7 +3393,7 @@ func (x *ActorTemplateStatus) String() string { func (*ActorTemplateStatus) ProtoMessage() {} func (x *ActorTemplateStatus) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[28] + mi := &file_ateapi_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2897,7 +3406,7 @@ func (x *ActorTemplateStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use ActorTemplateStatus.ProtoReflect.Descriptor instead. func (*ActorTemplateStatus) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{28} + return file_ateapi_proto_rawDescGZIP(), []int{31} } func (x *ActorTemplateStatus) GetGoldenSnapshotStatus() *GoldenSnapshotStatus { @@ -2929,7 +3438,7 @@ type SandboxConfig struct { func (x *SandboxConfig) Reset() { *x = SandboxConfig{} - mi := &file_ateapi_proto_msgTypes[29] + mi := &file_ateapi_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2941,7 +3450,7 @@ func (x *SandboxConfig) String() string { func (*SandboxConfig) ProtoMessage() {} func (x *SandboxConfig) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[29] + mi := &file_ateapi_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2954,7 +3463,7 @@ func (x *SandboxConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxConfig.ProtoReflect.Descriptor instead. func (*SandboxConfig) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{29} + return file_ateapi_proto_rawDescGZIP(), []int{32} } func (x *SandboxConfig) GetSandboxClass() string { @@ -3007,7 +3516,7 @@ type SnapshotConfig struct { func (x *SnapshotConfig) Reset() { *x = SnapshotConfig{} - mi := &file_ateapi_proto_msgTypes[30] + mi := &file_ateapi_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3019,7 +3528,7 @@ func (x *SnapshotConfig) String() string { func (*SnapshotConfig) ProtoMessage() {} func (x *SnapshotConfig) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[30] + mi := &file_ateapi_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3032,7 +3541,7 @@ func (x *SnapshotConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use SnapshotConfig.ProtoReflect.Descriptor instead. func (*SnapshotConfig) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{30} + return file_ateapi_proto_rawDescGZIP(), []int{33} } func (x *SnapshotConfig) GetStorageLocation() string { @@ -3064,7 +3573,7 @@ type GoldenSnapshotConfig struct { // // +k8s:required // +k8s:minimum=1 - // +k8s:maximum=2 # keep this in sync with the GoldenSnapshotMode enum + // +k8s:maximum=3 # keep this in sync with the GoldenSnapshotMode enum Mode GoldenSnapshotMode `protobuf:"varint,1,opt,name=mode,proto3,enum=ateapi.GoldenSnapshotMode" json:"mode,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -3072,7 +3581,7 @@ type GoldenSnapshotConfig struct { func (x *GoldenSnapshotConfig) Reset() { *x = GoldenSnapshotConfig{} - mi := &file_ateapi_proto_msgTypes[31] + mi := &file_ateapi_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3084,7 +3593,7 @@ func (x *GoldenSnapshotConfig) String() string { func (*GoldenSnapshotConfig) ProtoMessage() {} func (x *GoldenSnapshotConfig) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[31] + mi := &file_ateapi_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3097,7 +3606,7 @@ func (x *GoldenSnapshotConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use GoldenSnapshotConfig.ProtoReflect.Descriptor instead. func (*GoldenSnapshotConfig) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{31} + return file_ateapi_proto_rawDescGZIP(), []int{34} } func (x *GoldenSnapshotConfig) GetMode() GoldenSnapshotMode { @@ -3177,7 +3686,7 @@ type Container struct { func (x *Container) Reset() { *x = Container{} - mi := &file_ateapi_proto_msgTypes[32] + mi := &file_ateapi_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3189,7 +3698,7 @@ func (x *Container) String() string { func (*Container) ProtoMessage() {} func (x *Container) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[32] + mi := &file_ateapi_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3202,7 +3711,7 @@ func (x *Container) ProtoReflect() protoreflect.Message { // Deprecated: Use Container.ProtoReflect.Descriptor instead. func (*Container) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{32} + return file_ateapi_proto_rawDescGZIP(), []int{35} } func (x *Container) GetName() string { @@ -3282,7 +3791,7 @@ type SecurityContext struct { func (x *SecurityContext) Reset() { *x = SecurityContext{} - mi := &file_ateapi_proto_msgTypes[33] + mi := &file_ateapi_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3294,7 +3803,7 @@ func (x *SecurityContext) String() string { func (*SecurityContext) ProtoMessage() {} func (x *SecurityContext) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[33] + mi := &file_ateapi_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3307,7 +3816,7 @@ func (x *SecurityContext) ProtoReflect() protoreflect.Message { // Deprecated: Use SecurityContext.ProtoReflect.Descriptor instead. func (*SecurityContext) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{33} + return file_ateapi_proto_rawDescGZIP(), []int{36} } func (x *SecurityContext) GetCapabilities() *Capabilities { @@ -3345,7 +3854,7 @@ type Capabilities struct { func (x *Capabilities) Reset() { *x = Capabilities{} - mi := &file_ateapi_proto_msgTypes[34] + mi := &file_ateapi_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3357,7 +3866,7 @@ func (x *Capabilities) String() string { func (*Capabilities) ProtoMessage() {} func (x *Capabilities) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[34] + mi := &file_ateapi_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3370,7 +3879,7 @@ func (x *Capabilities) ProtoReflect() protoreflect.Message { // Deprecated: Use Capabilities.ProtoReflect.Descriptor instead. func (*Capabilities) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{34} + return file_ateapi_proto_rawDescGZIP(), []int{37} } func (x *Capabilities) GetAdd() []string { @@ -3408,7 +3917,7 @@ type EnvVar struct { func (x *EnvVar) Reset() { *x = EnvVar{} - mi := &file_ateapi_proto_msgTypes[35] + mi := &file_ateapi_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3420,7 +3929,7 @@ func (x *EnvVar) String() string { func (*EnvVar) ProtoMessage() {} func (x *EnvVar) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[35] + mi := &file_ateapi_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3433,7 +3942,7 @@ func (x *EnvVar) ProtoReflect() protoreflect.Message { // Deprecated: Use EnvVar.ProtoReflect.Descriptor instead. func (*EnvVar) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{35} + return file_ateapi_proto_rawDescGZIP(), []int{38} } func (x *EnvVar) GetName() string { @@ -3473,7 +3982,7 @@ type WakeupProbe struct { func (x *WakeupProbe) Reset() { *x = WakeupProbe{} - mi := &file_ateapi_proto_msgTypes[36] + mi := &file_ateapi_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3485,7 +3994,7 @@ func (x *WakeupProbe) String() string { func (*WakeupProbe) ProtoMessage() {} func (x *WakeupProbe) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[36] + mi := &file_ateapi_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3498,7 +4007,7 @@ func (x *WakeupProbe) ProtoReflect() protoreflect.Message { // Deprecated: Use WakeupProbe.ProtoReflect.Descriptor instead. func (*WakeupProbe) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{36} + return file_ateapi_proto_rawDescGZIP(), []int{39} } func (x *WakeupProbe) GetHttpGet() *HttpGetAction { @@ -3538,7 +4047,7 @@ type HttpGetAction struct { func (x *HttpGetAction) Reset() { *x = HttpGetAction{} - mi := &file_ateapi_proto_msgTypes[37] + mi := &file_ateapi_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3550,7 +4059,7 @@ func (x *HttpGetAction) String() string { func (*HttpGetAction) ProtoMessage() {} func (x *HttpGetAction) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[37] + mi := &file_ateapi_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3563,7 +4072,7 @@ func (x *HttpGetAction) ProtoReflect() protoreflect.Message { // Deprecated: Use HttpGetAction.ProtoReflect.Descriptor instead. func (*HttpGetAction) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{37} + return file_ateapi_proto_rawDescGZIP(), []int{40} } func (x *HttpGetAction) GetPath() string { @@ -3587,17 +4096,20 @@ type Volume struct { // +k8s:required // +k8s:format=k8s-short-name Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Exactly one of durable_dir / external_volume_template / image / - // system_info must be set. + // Exactly one of durable_dir / external / image / system_info must be + // set. // // +k8s:optional // +k8s:unionMember DurableDir *DurableDirVolumeSource `protobuf:"bytes,2,opt,name=durable_dir,json=durableDir,proto3" json:"durable_dir,omitempty"` + // external is a CSI volume: provisioned for this actor from a template, + // or a SharedVolume the actor references. + // // +ate:preview=ExternalVolumes // +k8s:ifDisabled(ExternalVolumes)=+k8s:forbidden // +k8s:optional // +k8s:unionMember - ExternalVolumeTemplate *ExternalVolumeTemplate `protobuf:"bytes,3,opt,name=external_volume_template,json=externalVolumeTemplate,proto3" json:"external_volume_template,omitempty"` + External *ExternalVolumeSource `protobuf:"bytes,7,opt,name=external,proto3" json:"external,omitempty"` // +k8s:optional // +k8s:unionMember SystemInfo *SystemInfoVolumeSource `protobuf:"bytes,5,opt,name=system_info,json=systemInfo,proto3" json:"system_info,omitempty"` @@ -3612,7 +4124,7 @@ type Volume struct { func (x *Volume) Reset() { *x = Volume{} - mi := &file_ateapi_proto_msgTypes[38] + mi := &file_ateapi_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3624,7 +4136,7 @@ func (x *Volume) String() string { func (*Volume) ProtoMessage() {} func (x *Volume) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[38] + mi := &file_ateapi_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3637,7 +4149,7 @@ func (x *Volume) ProtoReflect() protoreflect.Message { // Deprecated: Use Volume.ProtoReflect.Descriptor instead. func (*Volume) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{38} + return file_ateapi_proto_rawDescGZIP(), []int{41} } func (x *Volume) GetName() string { @@ -3654,9 +4166,9 @@ func (x *Volume) GetDurableDir() *DurableDirVolumeSource { return nil } -func (x *Volume) GetExternalVolumeTemplate() *ExternalVolumeTemplate { +func (x *Volume) GetExternal() *ExternalVolumeSource { if x != nil { - return x.ExternalVolumeTemplate + return x.External } return nil } @@ -3675,6 +4187,68 @@ func (x *Volume) GetImage() *ImageVolumeSource { return nil } +// ExternalVolumeSource is a CSI volume: provisioned for the actor from a +// template, or a SharedVolume that already exists. +type ExternalVolumeSource struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Exactly one of template / shared_volume must be set. + // + // +k8s:optional + // +k8s:unionMember + Template *ExternalVolumeTemplate `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"` + // shared_volume mounts a SharedVolume that already exists. + // + // +k8s:optional + // +k8s:unionMember + SharedVolume *SharedVolumeRef `protobuf:"bytes,2,opt,name=shared_volume,json=sharedVolume,proto3" json:"shared_volume,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExternalVolumeSource) Reset() { + *x = ExternalVolumeSource{} + mi := &file_ateapi_proto_msgTypes[42] + 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_ateapi_proto_msgTypes[42] + 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_ateapi_proto_rawDescGZIP(), []int{42} +} + +func (x *ExternalVolumeSource) GetTemplate() *ExternalVolumeTemplate { + if x != nil { + return x.Template + } + return nil +} + +func (x *ExternalVolumeSource) GetSharedVolume() *SharedVolumeRef { + if x != nil { + return x.SharedVolume + } + return nil +} + // ImageVolumeSource mounts the contents of an OCI image, read-only. The // reference must include a digest: changing the image invalidates // snapshots. @@ -3693,7 +4267,7 @@ type ImageVolumeSource struct { func (x *ImageVolumeSource) Reset() { *x = ImageVolumeSource{} - mi := &file_ateapi_proto_msgTypes[39] + mi := &file_ateapi_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3705,7 +4279,7 @@ func (x *ImageVolumeSource) String() string { func (*ImageVolumeSource) ProtoMessage() {} func (x *ImageVolumeSource) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[39] + mi := &file_ateapi_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3718,7 +4292,7 @@ func (x *ImageVolumeSource) ProtoReflect() protoreflect.Message { // Deprecated: Use ImageVolumeSource.ProtoReflect.Descriptor instead. func (*ImageVolumeSource) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{39} + return file_ateapi_proto_rawDescGZIP(), []int{43} } func (x *ImageVolumeSource) GetReference() string { @@ -3738,7 +4312,7 @@ type DurableDirVolumeSource struct { func (x *DurableDirVolumeSource) Reset() { *x = DurableDirVolumeSource{} - mi := &file_ateapi_proto_msgTypes[40] + mi := &file_ateapi_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3750,7 +4324,7 @@ func (x *DurableDirVolumeSource) String() string { func (*DurableDirVolumeSource) ProtoMessage() {} func (x *DurableDirVolumeSource) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[40] + mi := &file_ateapi_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3763,7 +4337,7 @@ func (x *DurableDirVolumeSource) ProtoReflect() protoreflect.Message { // Deprecated: Use DurableDirVolumeSource.ProtoReflect.Descriptor instead. func (*DurableDirVolumeSource) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{40} + return file_ateapi_proto_rawDescGZIP(), []int{44} } // ExternalVolumeTemplate provisions an external volume per actor; the volume @@ -3789,7 +4363,7 @@ type ExternalVolumeTemplate struct { func (x *ExternalVolumeTemplate) Reset() { *x = ExternalVolumeTemplate{} - mi := &file_ateapi_proto_msgTypes[41] + mi := &file_ateapi_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3801,7 +4375,7 @@ func (x *ExternalVolumeTemplate) String() string { func (*ExternalVolumeTemplate) ProtoMessage() {} func (x *ExternalVolumeTemplate) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[41] + mi := &file_ateapi_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3814,7 +4388,7 @@ func (x *ExternalVolumeTemplate) ProtoReflect() protoreflect.Message { // Deprecated: Use ExternalVolumeTemplate.ProtoReflect.Descriptor instead. func (*ExternalVolumeTemplate) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{41} + return file_ateapi_proto_rawDescGZIP(), []int{45} } func (x *ExternalVolumeTemplate) GetCapacity() string { @@ -3831,6 +4405,60 @@ func (x *ExternalVolumeTemplate) GetStorageClassName() string { return "" } +// SharedVolumeRef mounts a SharedVolume in the actor's atespace. The +// volume outlives the actor, so several actors can mount it in turn. +// +// SharedVolume actors cold-boot initially unless their microVM template opts +// into GOLDEN_SNAPSHOT_MODE_RUNTIME. Later resumes use their own snapshots. +type SharedVolumeRef struct { + state protoimpl.MessageState `protogen:"open.v1"` + // name of the SharedVolume. Left empty, the actor names it at creation in + // Actor.shared_volume_bindings. + // + // +k8s:optional + // +k8s:format=k8s-short-name + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SharedVolumeRef) Reset() { + *x = SharedVolumeRef{} + mi := &file_ateapi_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SharedVolumeRef) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SharedVolumeRef) ProtoMessage() {} + +func (x *SharedVolumeRef) ProtoReflect() protoreflect.Message { + mi := &file_ateapi_proto_msgTypes[46] + 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 SharedVolumeRef.ProtoReflect.Descriptor instead. +func (*SharedVolumeRef) Descriptor() ([]byte, []int) { + return file_ateapi_proto_rawDescGZIP(), []int{46} +} + +func (x *SharedVolumeRef) GetName() string { + if x != nil { + return x.Name + } + return "" +} + // SystemInfoVolumeSource is a read-only volume of substrate-generated // per-actor files (identity fields, projected trust bundles), regenerated by // atelet on every Run/Restore. @@ -3851,7 +4479,7 @@ type SystemInfoVolumeSource struct { func (x *SystemInfoVolumeSource) Reset() { *x = SystemInfoVolumeSource{} - mi := &file_ateapi_proto_msgTypes[42] + mi := &file_ateapi_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3863,7 +4491,7 @@ func (x *SystemInfoVolumeSource) String() string { func (*SystemInfoVolumeSource) ProtoMessage() {} func (x *SystemInfoVolumeSource) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[42] + mi := &file_ateapi_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3876,7 +4504,7 @@ func (x *SystemInfoVolumeSource) ProtoReflect() protoreflect.Message { // Deprecated: Use SystemInfoVolumeSource.ProtoReflect.Descriptor instead. func (*SystemInfoVolumeSource) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{42} + return file_ateapi_proto_rawDescGZIP(), []int{47} } func (x *SystemInfoVolumeSource) GetDataSources() []*SystemInfoDataSource { @@ -3902,7 +4530,7 @@ type SystemInfoDataSource struct { func (x *SystemInfoDataSource) Reset() { *x = SystemInfoDataSource{} - mi := &file_ateapi_proto_msgTypes[43] + mi := &file_ateapi_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3914,7 +4542,7 @@ func (x *SystemInfoDataSource) String() string { func (*SystemInfoDataSource) ProtoMessage() {} func (x *SystemInfoDataSource) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[43] + mi := &file_ateapi_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3927,7 +4555,7 @@ func (x *SystemInfoDataSource) ProtoReflect() protoreflect.Message { // Deprecated: Use SystemInfoDataSource.ProtoReflect.Descriptor instead. func (*SystemInfoDataSource) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{43} + return file_ateapi_proto_rawDescGZIP(), []int{48} } func (x *SystemInfoDataSource) GetActorMetadata() *ActorMetadataDataSource { @@ -3964,7 +4592,7 @@ type ActorMetadataDataSource struct { func (x *ActorMetadataDataSource) Reset() { *x = ActorMetadataDataSource{} - mi := &file_ateapi_proto_msgTypes[44] + mi := &file_ateapi_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3976,7 +4604,7 @@ func (x *ActorMetadataDataSource) String() string { func (*ActorMetadataDataSource) ProtoMessage() {} func (x *ActorMetadataDataSource) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[44] + mi := &file_ateapi_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3989,7 +4617,7 @@ func (x *ActorMetadataDataSource) ProtoReflect() protoreflect.Message { // Deprecated: Use ActorMetadataDataSource.ProtoReflect.Descriptor instead. func (*ActorMetadataDataSource) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{44} + return file_ateapi_proto_rawDescGZIP(), []int{49} } func (x *ActorMetadataDataSource) GetItems() []*ActorMetadataItem { @@ -4021,7 +4649,7 @@ type ActorMetadataItem struct { func (x *ActorMetadataItem) Reset() { *x = ActorMetadataItem{} - mi := &file_ateapi_proto_msgTypes[45] + mi := &file_ateapi_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4033,7 +4661,7 @@ func (x *ActorMetadataItem) String() string { func (*ActorMetadataItem) ProtoMessage() {} func (x *ActorMetadataItem) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[45] + mi := &file_ateapi_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4046,7 +4674,7 @@ func (x *ActorMetadataItem) ProtoReflect() protoreflect.Message { // Deprecated: Use ActorMetadataItem.ProtoReflect.Descriptor instead. func (*ActorMetadataItem) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{45} + return file_ateapi_proto_rawDescGZIP(), []int{50} } func (x *ActorMetadataItem) GetField() ActorMetadataField { @@ -4098,7 +4726,7 @@ type TrustBundleDataSource struct { func (x *TrustBundleDataSource) Reset() { *x = TrustBundleDataSource{} - mi := &file_ateapi_proto_msgTypes[46] + mi := &file_ateapi_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4110,7 +4738,7 @@ func (x *TrustBundleDataSource) String() string { func (*TrustBundleDataSource) ProtoMessage() {} func (x *TrustBundleDataSource) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[46] + mi := &file_ateapi_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4123,7 +4751,7 @@ func (x *TrustBundleDataSource) ProtoReflect() protoreflect.Message { // Deprecated: Use TrustBundleDataSource.ProtoReflect.Descriptor instead. func (*TrustBundleDataSource) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{46} + return file_ateapi_proto_rawDescGZIP(), []int{51} } func (x *TrustBundleDataSource) GetNames() []string { @@ -4162,7 +4790,7 @@ type VolumeMount struct { func (x *VolumeMount) Reset() { *x = VolumeMount{} - mi := &file_ateapi_proto_msgTypes[47] + mi := &file_ateapi_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4174,7 +4802,7 @@ func (x *VolumeMount) String() string { func (*VolumeMount) ProtoMessage() {} func (x *VolumeMount) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[47] + mi := &file_ateapi_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4187,7 +4815,7 @@ func (x *VolumeMount) ProtoReflect() protoreflect.Message { // Deprecated: Use VolumeMount.ProtoReflect.Descriptor instead. func (*VolumeMount) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{47} + return file_ateapi_proto_rawDescGZIP(), []int{52} } func (x *VolumeMount) GetName() string { @@ -4216,7 +4844,7 @@ type CreateAtespaceRequest struct { func (x *CreateAtespaceRequest) Reset() { *x = CreateAtespaceRequest{} - mi := &file_ateapi_proto_msgTypes[48] + mi := &file_ateapi_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4228,7 +4856,7 @@ func (x *CreateAtespaceRequest) String() string { func (*CreateAtespaceRequest) ProtoMessage() {} func (x *CreateAtespaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[48] + mi := &file_ateapi_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4241,7 +4869,7 @@ func (x *CreateAtespaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateAtespaceRequest.ProtoReflect.Descriptor instead. func (*CreateAtespaceRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{48} + return file_ateapi_proto_rawDescGZIP(), []int{53} } func (x *CreateAtespaceRequest) GetAtespace() *Atespace { @@ -4262,7 +4890,7 @@ type GetAtespaceRequest struct { func (x *GetAtespaceRequest) Reset() { *x = GetAtespaceRequest{} - mi := &file_ateapi_proto_msgTypes[49] + mi := &file_ateapi_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4274,7 +4902,7 @@ func (x *GetAtespaceRequest) String() string { func (*GetAtespaceRequest) ProtoMessage() {} func (x *GetAtespaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[49] + mi := &file_ateapi_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4287,7 +4915,7 @@ func (x *GetAtespaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetAtespaceRequest.ProtoReflect.Descriptor instead. func (*GetAtespaceRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{49} + return file_ateapi_proto_rawDescGZIP(), []int{54} } func (x *GetAtespaceRequest) GetAtespace() *ObjectRef { @@ -4309,7 +4937,7 @@ type ListAtespacesRequest struct { func (x *ListAtespacesRequest) Reset() { *x = ListAtespacesRequest{} - mi := &file_ateapi_proto_msgTypes[50] + mi := &file_ateapi_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4321,7 +4949,7 @@ func (x *ListAtespacesRequest) String() string { func (*ListAtespacesRequest) ProtoMessage() {} func (x *ListAtespacesRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[50] + mi := &file_ateapi_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4334,7 +4962,7 @@ func (x *ListAtespacesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAtespacesRequest.ProtoReflect.Descriptor instead. func (*ListAtespacesRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{50} + return file_ateapi_proto_rawDescGZIP(), []int{55} } func (x *ListAtespacesRequest) GetOptions() *ListOptions { @@ -4356,7 +4984,7 @@ type ListAtespacesResponse struct { func (x *ListAtespacesResponse) Reset() { *x = ListAtespacesResponse{} - mi := &file_ateapi_proto_msgTypes[51] + mi := &file_ateapi_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4368,7 +4996,7 @@ func (x *ListAtespacesResponse) String() string { func (*ListAtespacesResponse) ProtoMessage() {} func (x *ListAtespacesResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[51] + mi := &file_ateapi_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4381,7 +5009,7 @@ func (x *ListAtespacesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAtespacesResponse.ProtoReflect.Descriptor instead. func (*ListAtespacesResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{51} + return file_ateapi_proto_rawDescGZIP(), []int{56} } func (x *ListAtespacesResponse) GetAtespaces() []*Atespace { @@ -4413,7 +5041,7 @@ type DeleteAtespaceRequest struct { func (x *DeleteAtespaceRequest) Reset() { *x = DeleteAtespaceRequest{} - mi := &file_ateapi_proto_msgTypes[52] + mi := &file_ateapi_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4425,7 +5053,7 @@ func (x *DeleteAtespaceRequest) String() string { func (*DeleteAtespaceRequest) ProtoMessage() {} func (x *DeleteAtespaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[52] + mi := &file_ateapi_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4438,7 +5066,7 @@ func (x *DeleteAtespaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteAtespaceRequest.ProtoReflect.Descriptor instead. func (*DeleteAtespaceRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{52} + return file_ateapi_proto_rawDescGZIP(), []int{57} } func (x *DeleteAtespaceRequest) GetAtespace() *ObjectRef { @@ -4469,7 +5097,7 @@ type CreateActorTemplateRequest struct { func (x *CreateActorTemplateRequest) Reset() { *x = CreateActorTemplateRequest{} - mi := &file_ateapi_proto_msgTypes[53] + mi := &file_ateapi_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4481,7 +5109,7 @@ func (x *CreateActorTemplateRequest) String() string { func (*CreateActorTemplateRequest) ProtoMessage() {} func (x *CreateActorTemplateRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[53] + mi := &file_ateapi_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4494,7 +5122,7 @@ func (x *CreateActorTemplateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateActorTemplateRequest.ProtoReflect.Descriptor instead. func (*CreateActorTemplateRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{53} + return file_ateapi_proto_rawDescGZIP(), []int{58} } func (x *CreateActorTemplateRequest) GetActorTemplate() *ActorTemplate { @@ -4515,7 +5143,7 @@ type GetActorTemplateRequest struct { func (x *GetActorTemplateRequest) Reset() { *x = GetActorTemplateRequest{} - mi := &file_ateapi_proto_msgTypes[54] + mi := &file_ateapi_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4527,7 +5155,7 @@ func (x *GetActorTemplateRequest) String() string { func (*GetActorTemplateRequest) ProtoMessage() {} func (x *GetActorTemplateRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[54] + mi := &file_ateapi_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4540,7 +5168,7 @@ func (x *GetActorTemplateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActorTemplateRequest.ProtoReflect.Descriptor instead. func (*GetActorTemplateRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{54} + return file_ateapi_proto_rawDescGZIP(), []int{59} } func (x *GetActorTemplateRequest) GetActorTemplate() *ObjectRef { @@ -4568,7 +5196,7 @@ type ListActorTemplatesRequest struct { func (x *ListActorTemplatesRequest) Reset() { *x = ListActorTemplatesRequest{} - mi := &file_ateapi_proto_msgTypes[55] + mi := &file_ateapi_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4580,7 +5208,7 @@ func (x *ListActorTemplatesRequest) String() string { func (*ListActorTemplatesRequest) ProtoMessage() {} func (x *ListActorTemplatesRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[55] + mi := &file_ateapi_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4593,7 +5221,7 @@ func (x *ListActorTemplatesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListActorTemplatesRequest.ProtoReflect.Descriptor instead. func (*ListActorTemplatesRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{55} + return file_ateapi_proto_rawDescGZIP(), []int{60} } func (x *ListActorTemplatesRequest) GetAtespace() string { @@ -4623,7 +5251,7 @@ type ListActorTemplatesResponse struct { func (x *ListActorTemplatesResponse) Reset() { *x = ListActorTemplatesResponse{} - mi := &file_ateapi_proto_msgTypes[56] + mi := &file_ateapi_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4635,7 +5263,7 @@ func (x *ListActorTemplatesResponse) String() string { func (*ListActorTemplatesResponse) ProtoMessage() {} func (x *ListActorTemplatesResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[56] + mi := &file_ateapi_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4648,7 +5276,7 @@ func (x *ListActorTemplatesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListActorTemplatesResponse.ProtoReflect.Descriptor instead. func (*ListActorTemplatesResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{56} + return file_ateapi_proto_rawDescGZIP(), []int{61} } func (x *ListActorTemplatesResponse) GetActorTemplates() []*ActorTemplate { @@ -4680,7 +5308,7 @@ type DeleteActorTemplateRequest struct { func (x *DeleteActorTemplateRequest) Reset() { *x = DeleteActorTemplateRequest{} - mi := &file_ateapi_proto_msgTypes[57] + mi := &file_ateapi_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4692,7 +5320,7 @@ func (x *DeleteActorTemplateRequest) String() string { func (*DeleteActorTemplateRequest) ProtoMessage() {} func (x *DeleteActorTemplateRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[57] + mi := &file_ateapi_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4705,7 +5333,7 @@ func (x *DeleteActorTemplateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteActorTemplateRequest.ProtoReflect.Descriptor instead. func (*DeleteActorTemplateRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{57} + return file_ateapi_proto_rawDescGZIP(), []int{62} } func (x *DeleteActorTemplateRequest) GetActorTemplate() *ObjectRef { @@ -4733,7 +5361,7 @@ type GetActorRequest struct { func (x *GetActorRequest) Reset() { *x = GetActorRequest{} - mi := &file_ateapi_proto_msgTypes[58] + mi := &file_ateapi_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4745,7 +5373,7 @@ func (x *GetActorRequest) String() string { func (*GetActorRequest) ProtoMessage() {} func (x *GetActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[58] + mi := &file_ateapi_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4758,7 +5386,7 @@ func (x *GetActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActorRequest.ProtoReflect.Descriptor instead. func (*GetActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{58} + return file_ateapi_proto_rawDescGZIP(), []int{63} } func (x *GetActorRequest) GetActor() *ObjectRef { @@ -4781,7 +5409,7 @@ type CreateActorRequest struct { func (x *CreateActorRequest) Reset() { *x = CreateActorRequest{} - mi := &file_ateapi_proto_msgTypes[59] + mi := &file_ateapi_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4793,7 +5421,7 @@ func (x *CreateActorRequest) String() string { func (*CreateActorRequest) ProtoMessage() {} func (x *CreateActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[59] + mi := &file_ateapi_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4806,7 +5434,7 @@ func (x *CreateActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateActorRequest.ProtoReflect.Descriptor instead. func (*CreateActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{59} + return file_ateapi_proto_rawDescGZIP(), []int{64} } func (x *CreateActorRequest) GetActor() *Actor { @@ -4837,7 +5465,7 @@ type UpdateActorRequest struct { func (x *UpdateActorRequest) Reset() { *x = UpdateActorRequest{} - mi := &file_ateapi_proto_msgTypes[60] + mi := &file_ateapi_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4849,7 +5477,7 @@ func (x *UpdateActorRequest) String() string { func (*UpdateActorRequest) ProtoMessage() {} func (x *UpdateActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[60] + mi := &file_ateapi_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4862,7 +5490,7 @@ func (x *UpdateActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateActorRequest.ProtoReflect.Descriptor instead. func (*UpdateActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{60} + return file_ateapi_proto_rawDescGZIP(), []int{65} } func (x *UpdateActorRequest) GetActor() *Actor { @@ -4883,7 +5511,7 @@ type SuspendActorRequest struct { func (x *SuspendActorRequest) Reset() { *x = SuspendActorRequest{} - mi := &file_ateapi_proto_msgTypes[61] + mi := &file_ateapi_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4895,7 +5523,7 @@ func (x *SuspendActorRequest) String() string { func (*SuspendActorRequest) ProtoMessage() {} func (x *SuspendActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[61] + mi := &file_ateapi_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4908,7 +5536,7 @@ func (x *SuspendActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SuspendActorRequest.ProtoReflect.Descriptor instead. func (*SuspendActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{61} + return file_ateapi_proto_rawDescGZIP(), []int{66} } func (x *SuspendActorRequest) GetActor() *ObjectRef { @@ -4927,7 +5555,7 @@ type SuspendActorResponse struct { func (x *SuspendActorResponse) Reset() { *x = SuspendActorResponse{} - mi := &file_ateapi_proto_msgTypes[62] + mi := &file_ateapi_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4939,7 +5567,7 @@ func (x *SuspendActorResponse) String() string { func (*SuspendActorResponse) ProtoMessage() {} func (x *SuspendActorResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[62] + mi := &file_ateapi_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4952,7 +5580,7 @@ func (x *SuspendActorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SuspendActorResponse.ProtoReflect.Descriptor instead. func (*SuspendActorResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{62} + return file_ateapi_proto_rawDescGZIP(), []int{67} } func (x *SuspendActorResponse) GetActor() *Actor { @@ -4973,7 +5601,7 @@ type PauseActorRequest struct { func (x *PauseActorRequest) Reset() { *x = PauseActorRequest{} - mi := &file_ateapi_proto_msgTypes[63] + mi := &file_ateapi_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4985,7 +5613,7 @@ func (x *PauseActorRequest) String() string { func (*PauseActorRequest) ProtoMessage() {} func (x *PauseActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[63] + mi := &file_ateapi_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4998,7 +5626,7 @@ func (x *PauseActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PauseActorRequest.ProtoReflect.Descriptor instead. func (*PauseActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{63} + return file_ateapi_proto_rawDescGZIP(), []int{68} } func (x *PauseActorRequest) GetActor() *ObjectRef { @@ -5017,7 +5645,7 @@ type PauseActorResponse struct { func (x *PauseActorResponse) Reset() { *x = PauseActorResponse{} - mi := &file_ateapi_proto_msgTypes[64] + mi := &file_ateapi_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5029,7 +5657,7 @@ func (x *PauseActorResponse) String() string { func (*PauseActorResponse) ProtoMessage() {} func (x *PauseActorResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[64] + mi := &file_ateapi_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5042,7 +5670,7 @@ func (x *PauseActorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PauseActorResponse.ProtoReflect.Descriptor instead. func (*PauseActorResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{64} + return file_ateapi_proto_rawDescGZIP(), []int{69} } func (x *PauseActorResponse) GetActor() *Actor { @@ -5063,7 +5691,7 @@ type ResumeActorRequest struct { func (x *ResumeActorRequest) Reset() { *x = ResumeActorRequest{} - mi := &file_ateapi_proto_msgTypes[65] + mi := &file_ateapi_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5075,7 +5703,7 @@ func (x *ResumeActorRequest) String() string { func (*ResumeActorRequest) ProtoMessage() {} func (x *ResumeActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[65] + mi := &file_ateapi_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5088,7 +5716,7 @@ func (x *ResumeActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResumeActorRequest.ProtoReflect.Descriptor instead. func (*ResumeActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{65} + return file_ateapi_proto_rawDescGZIP(), []int{70} } func (x *ResumeActorRequest) GetActor() *ObjectRef { @@ -5110,7 +5738,7 @@ type ResumeActorResponse struct { func (x *ResumeActorResponse) Reset() { *x = ResumeActorResponse{} - mi := &file_ateapi_proto_msgTypes[66] + mi := &file_ateapi_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5122,7 +5750,7 @@ func (x *ResumeActorResponse) String() string { func (*ResumeActorResponse) ProtoMessage() {} func (x *ResumeActorResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[66] + mi := &file_ateapi_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5135,7 +5763,7 @@ func (x *ResumeActorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResumeActorResponse.ProtoReflect.Descriptor instead. func (*ResumeActorResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{66} + return file_ateapi_proto_rawDescGZIP(), []int{71} } func (x *ResumeActorResponse) GetActor() *Actor { @@ -5165,7 +5793,7 @@ type RevertActorRequest struct { func (x *RevertActorRequest) Reset() { *x = RevertActorRequest{} - mi := &file_ateapi_proto_msgTypes[67] + mi := &file_ateapi_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5177,7 +5805,7 @@ func (x *RevertActorRequest) String() string { func (*RevertActorRequest) ProtoMessage() {} func (x *RevertActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[67] + mi := &file_ateapi_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5190,7 +5818,7 @@ func (x *RevertActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RevertActorRequest.ProtoReflect.Descriptor instead. func (*RevertActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{67} + return file_ateapi_proto_rawDescGZIP(), []int{72} } func (x *RevertActorRequest) GetActor() *ObjectRef { @@ -5212,7 +5840,7 @@ type RevertActorResponse struct { func (x *RevertActorResponse) Reset() { *x = RevertActorResponse{} - mi := &file_ateapi_proto_msgTypes[68] + mi := &file_ateapi_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5224,7 +5852,7 @@ func (x *RevertActorResponse) String() string { func (*RevertActorResponse) ProtoMessage() {} func (x *RevertActorResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[68] + mi := &file_ateapi_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5237,7 +5865,7 @@ func (x *RevertActorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RevertActorResponse.ProtoReflect.Descriptor instead. func (*RevertActorResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{68} + return file_ateapi_proto_rawDescGZIP(), []int{73} } func (x *RevertActorResponse) GetActor() *Actor { @@ -5266,7 +5894,7 @@ type DeleteActorRequest struct { func (x *DeleteActorRequest) Reset() { *x = DeleteActorRequest{} - mi := &file_ateapi_proto_msgTypes[69] + mi := &file_ateapi_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5278,7 +5906,7 @@ func (x *DeleteActorRequest) String() string { func (*DeleteActorRequest) ProtoMessage() {} func (x *DeleteActorRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[69] + mi := &file_ateapi_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5291,7 +5919,7 @@ func (x *DeleteActorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteActorRequest.ProtoReflect.Descriptor instead. func (*DeleteActorRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{69} + return file_ateapi_proto_rawDescGZIP(), []int{74} } func (x *DeleteActorRequest) GetActor() *ObjectRef { @@ -5335,7 +5963,7 @@ type GetActorEgressPolicyRequest struct { func (x *GetActorEgressPolicyRequest) Reset() { *x = GetActorEgressPolicyRequest{} - mi := &file_ateapi_proto_msgTypes[70] + mi := &file_ateapi_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5347,7 +5975,7 @@ func (x *GetActorEgressPolicyRequest) String() string { func (*GetActorEgressPolicyRequest) ProtoMessage() {} func (x *GetActorEgressPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[70] + mi := &file_ateapi_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5360,7 +5988,7 @@ func (x *GetActorEgressPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetActorEgressPolicyRequest.ProtoReflect.Descriptor instead. func (*GetActorEgressPolicyRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{70} + return file_ateapi_proto_rawDescGZIP(), []int{75} } func (x *GetActorEgressPolicyRequest) GetActor() *ObjectRef { @@ -5398,7 +6026,7 @@ type CreateActorEgressPolicyRequest struct { func (x *CreateActorEgressPolicyRequest) Reset() { *x = CreateActorEgressPolicyRequest{} - mi := &file_ateapi_proto_msgTypes[71] + mi := &file_ateapi_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5410,7 +6038,7 @@ func (x *CreateActorEgressPolicyRequest) String() string { func (*CreateActorEgressPolicyRequest) ProtoMessage() {} func (x *CreateActorEgressPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[71] + mi := &file_ateapi_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5423,7 +6051,7 @@ func (x *CreateActorEgressPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateActorEgressPolicyRequest.ProtoReflect.Descriptor instead. func (*CreateActorEgressPolicyRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{71} + return file_ateapi_proto_rawDescGZIP(), []int{76} } func (x *CreateActorEgressPolicyRequest) GetActor() *ObjectRef { @@ -5461,7 +6089,7 @@ type UpdateActorEgressPolicyRequest struct { func (x *UpdateActorEgressPolicyRequest) Reset() { *x = UpdateActorEgressPolicyRequest{} - mi := &file_ateapi_proto_msgTypes[72] + mi := &file_ateapi_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5473,7 +6101,7 @@ func (x *UpdateActorEgressPolicyRequest) String() string { func (*UpdateActorEgressPolicyRequest) ProtoMessage() {} func (x *UpdateActorEgressPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[72] + mi := &file_ateapi_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5486,7 +6114,7 @@ func (x *UpdateActorEgressPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateActorEgressPolicyRequest.ProtoReflect.Descriptor instead. func (*UpdateActorEgressPolicyRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{72} + return file_ateapi_proto_rawDescGZIP(), []int{77} } func (x *UpdateActorEgressPolicyRequest) GetActor() *ObjectRef { @@ -5527,7 +6155,7 @@ type DeleteActorEgressPolicyRequest struct { func (x *DeleteActorEgressPolicyRequest) Reset() { *x = DeleteActorEgressPolicyRequest{} - mi := &file_ateapi_proto_msgTypes[73] + mi := &file_ateapi_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5539,7 +6167,7 @@ func (x *DeleteActorEgressPolicyRequest) String() string { func (*DeleteActorEgressPolicyRequest) ProtoMessage() {} func (x *DeleteActorEgressPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[73] + mi := &file_ateapi_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5552,7 +6180,7 @@ func (x *DeleteActorEgressPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteActorEgressPolicyRequest.ProtoReflect.Descriptor instead. func (*DeleteActorEgressPolicyRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{73} + return file_ateapi_proto_rawDescGZIP(), []int{78} } func (x *DeleteActorEgressPolicyRequest) GetActor() *ObjectRef { @@ -5587,7 +6215,7 @@ type GetTagRequest struct { func (x *GetTagRequest) Reset() { *x = GetTagRequest{} - mi := &file_ateapi_proto_msgTypes[74] + mi := &file_ateapi_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5599,7 +6227,7 @@ func (x *GetTagRequest) String() string { func (*GetTagRequest) ProtoMessage() {} func (x *GetTagRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[74] + mi := &file_ateapi_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5612,7 +6240,7 @@ func (x *GetTagRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetTagRequest.ProtoReflect.Descriptor instead. func (*GetTagRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{74} + return file_ateapi_proto_rawDescGZIP(), []int{79} } func (x *GetTagRequest) GetTag() *ObjectRef { @@ -5651,7 +6279,7 @@ type MintActorJwtRequest struct { func (x *MintActorJwtRequest) Reset() { *x = MintActorJwtRequest{} - mi := &file_ateapi_proto_msgTypes[75] + mi := &file_ateapi_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5663,7 +6291,7 @@ func (x *MintActorJwtRequest) String() string { func (*MintActorJwtRequest) ProtoMessage() {} func (x *MintActorJwtRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[75] + mi := &file_ateapi_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5676,7 +6304,7 @@ func (x *MintActorJwtRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MintActorJwtRequest.ProtoReflect.Descriptor instead. func (*MintActorJwtRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{75} + return file_ateapi_proto_rawDescGZIP(), []int{80} } func (x *MintActorJwtRequest) GetActor() *ObjectRef { @@ -5731,7 +6359,7 @@ type MintActorJwtResponse struct { func (x *MintActorJwtResponse) Reset() { *x = MintActorJwtResponse{} - mi := &file_ateapi_proto_msgTypes[76] + mi := &file_ateapi_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5743,7 +6371,7 @@ func (x *MintActorJwtResponse) String() string { func (*MintActorJwtResponse) ProtoMessage() {} func (x *MintActorJwtResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[76] + mi := &file_ateapi_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5756,7 +6384,7 @@ func (x *MintActorJwtResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MintActorJwtResponse.ProtoReflect.Descriptor instead. func (*MintActorJwtResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{76} + return file_ateapi_proto_rawDescGZIP(), []int{81} } func (x *MintActorJwtResponse) GetActorJwt() string { @@ -5801,7 +6429,7 @@ type MintActorCertificateRequest struct { func (x *MintActorCertificateRequest) Reset() { *x = MintActorCertificateRequest{} - mi := &file_ateapi_proto_msgTypes[77] + mi := &file_ateapi_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5813,7 +6441,7 @@ func (x *MintActorCertificateRequest) String() string { func (*MintActorCertificateRequest) ProtoMessage() {} func (x *MintActorCertificateRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[77] + mi := &file_ateapi_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5826,7 +6454,7 @@ func (x *MintActorCertificateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MintActorCertificateRequest.ProtoReflect.Descriptor instead. func (*MintActorCertificateRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{77} + return file_ateapi_proto_rawDescGZIP(), []int{82} } func (x *MintActorCertificateRequest) GetActor() *ObjectRef { @@ -5863,7 +6491,7 @@ type MintActorCertificateResponse struct { func (x *MintActorCertificateResponse) Reset() { *x = MintActorCertificateResponse{} - mi := &file_ateapi_proto_msgTypes[78] + mi := &file_ateapi_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5875,7 +6503,7 @@ func (x *MintActorCertificateResponse) String() string { func (*MintActorCertificateResponse) ProtoMessage() {} func (x *MintActorCertificateResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[78] + mi := &file_ateapi_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5888,7 +6516,7 @@ func (x *MintActorCertificateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MintActorCertificateResponse.ProtoReflect.Descriptor instead. func (*MintActorCertificateResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{78} + return file_ateapi_proto_rawDescGZIP(), []int{83} } func (x *MintActorCertificateResponse) GetActorCertificates() [][]byte { @@ -5915,7 +6543,7 @@ type ListTagsRequest struct { func (x *ListTagsRequest) Reset() { *x = ListTagsRequest{} - mi := &file_ateapi_proto_msgTypes[79] + mi := &file_ateapi_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5927,7 +6555,7 @@ func (x *ListTagsRequest) String() string { func (*ListTagsRequest) ProtoMessage() {} func (x *ListTagsRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[79] + mi := &file_ateapi_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5940,7 +6568,7 @@ func (x *ListTagsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListTagsRequest.ProtoReflect.Descriptor instead. func (*ListTagsRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{79} + return file_ateapi_proto_rawDescGZIP(), []int{84} } func (x *ListTagsRequest) GetAtespace() string { @@ -5967,7 +6595,7 @@ type ListTagsResponse struct { func (x *ListTagsResponse) Reset() { *x = ListTagsResponse{} - mi := &file_ateapi_proto_msgTypes[80] + mi := &file_ateapi_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5979,7 +6607,7 @@ func (x *ListTagsResponse) String() string { func (*ListTagsResponse) ProtoMessage() {} func (x *ListTagsResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[80] + mi := &file_ateapi_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5992,7 +6620,7 @@ func (x *ListTagsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListTagsResponse.ProtoReflect.Descriptor instead. func (*ListTagsResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{80} + return file_ateapi_proto_rawDescGZIP(), []int{85} } func (x *ListTagsResponse) GetTags() []*Tag { @@ -6037,7 +6665,7 @@ type CreateTagRequest struct { func (x *CreateTagRequest) Reset() { *x = CreateTagRequest{} - mi := &file_ateapi_proto_msgTypes[81] + mi := &file_ateapi_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6049,7 +6677,7 @@ func (x *CreateTagRequest) String() string { func (*CreateTagRequest) ProtoMessage() {} func (x *CreateTagRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[81] + mi := &file_ateapi_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6062,7 +6690,7 @@ func (x *CreateTagRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateTagRequest.ProtoReflect.Descriptor instead. func (*CreateTagRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{81} + return file_ateapi_proto_rawDescGZIP(), []int{86} } func (x *CreateTagRequest) GetTag() *Tag { @@ -6092,7 +6720,7 @@ type UpdateTagRequest struct { func (x *UpdateTagRequest) Reset() { *x = UpdateTagRequest{} - mi := &file_ateapi_proto_msgTypes[82] + mi := &file_ateapi_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6104,7 +6732,7 @@ func (x *UpdateTagRequest) String() string { func (*UpdateTagRequest) ProtoMessage() {} func (x *UpdateTagRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[82] + mi := &file_ateapi_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6117,7 +6745,7 @@ func (x *UpdateTagRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateTagRequest.ProtoReflect.Descriptor instead. func (*UpdateTagRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{82} + return file_ateapi_proto_rawDescGZIP(), []int{87} } func (x *UpdateTagRequest) GetTag() *Tag { @@ -6142,7 +6770,7 @@ type DeleteTagRequest struct { func (x *DeleteTagRequest) Reset() { *x = DeleteTagRequest{} - mi := &file_ateapi_proto_msgTypes[83] + mi := &file_ateapi_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6154,7 +6782,7 @@ func (x *DeleteTagRequest) String() string { func (*DeleteTagRequest) ProtoMessage() {} func (x *DeleteTagRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[83] + mi := &file_ateapi_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6167,7 +6795,7 @@ func (x *DeleteTagRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteTagRequest.ProtoReflect.Descriptor instead. func (*DeleteTagRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{83} + return file_ateapi_proto_rawDescGZIP(), []int{88} } func (x *DeleteTagRequest) GetTag() *ObjectRef { @@ -6184,6 +6812,346 @@ func (x *DeleteTagRequest) GetOptions() *DeleteOptions { return nil } +// Request to create a SharedVolume. +// +// +k8s:customValidation # exactly one of storage_class_name and volume_id +type CreateSharedVolumeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The volume to create. Name a storage class to provision a new volume, or + // supply a volume ID to register one that already exists. status is ignored. + // + // +k8s:required + SharedVolume *SharedVolume `protobuf:"bytes,1,opt,name=shared_volume,json=sharedVolume,proto3" json:"shared_volume,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSharedVolumeRequest) Reset() { + *x = CreateSharedVolumeRequest{} + mi := &file_ateapi_proto_msgTypes[89] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSharedVolumeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSharedVolumeRequest) ProtoMessage() {} + +func (x *CreateSharedVolumeRequest) ProtoReflect() protoreflect.Message { + mi := &file_ateapi_proto_msgTypes[89] + 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 CreateSharedVolumeRequest.ProtoReflect.Descriptor instead. +func (*CreateSharedVolumeRequest) Descriptor() ([]byte, []int) { + return file_ateapi_proto_rawDescGZIP(), []int{89} +} + +func (x *CreateSharedVolumeRequest) GetSharedVolume() *SharedVolume { + if x != nil { + return x.SharedVolume + } + return nil +} + +// GetSharedVolumeRequest identifies one SharedVolume. +type GetSharedVolumeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The volume to get. + // + // +k8s:required + // +k8s:subfield(atespace)=+k8s:required + SharedVolume *ObjectRef `protobuf:"bytes,1,opt,name=shared_volume,json=sharedVolume,proto3" json:"shared_volume,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSharedVolumeRequest) Reset() { + *x = GetSharedVolumeRequest{} + mi := &file_ateapi_proto_msgTypes[90] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSharedVolumeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSharedVolumeRequest) ProtoMessage() {} + +func (x *GetSharedVolumeRequest) ProtoReflect() protoreflect.Message { + mi := &file_ateapi_proto_msgTypes[90] + 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 GetSharedVolumeRequest.ProtoReflect.Descriptor instead. +func (*GetSharedVolumeRequest) Descriptor() ([]byte, []int) { + return file_ateapi_proto_rawDescGZIP(), []int{90} +} + +func (x *GetSharedVolumeRequest) GetSharedVolume() *ObjectRef { + if x != nil { + return x.SharedVolume + } + return nil +} + +// ListSharedVolumesRequest selects a page of ExternalVolumes. +type ListSharedVolumesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The atespace to list from. Empty lists across all atespaces. + // + // +k8s:optional + // +k8s:format=k8s-short-name + Atespace string `protobuf:"bytes,1,opt,name=atespace,proto3" json:"atespace,omitempty"` + // Requested page size; values above 1000 are coerced to 1000. + // + // +k8s:optional + // +k8s:minimum=1 + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Pagination token from a previous ListSharedVolumes response. + // + // +k8s:optional + // +k8s:maxLength=256 + PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSharedVolumesRequest) Reset() { + *x = ListSharedVolumesRequest{} + mi := &file_ateapi_proto_msgTypes[91] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSharedVolumesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSharedVolumesRequest) ProtoMessage() {} + +func (x *ListSharedVolumesRequest) ProtoReflect() protoreflect.Message { + mi := &file_ateapi_proto_msgTypes[91] + 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 ListSharedVolumesRequest.ProtoReflect.Descriptor instead. +func (*ListSharedVolumesRequest) Descriptor() ([]byte, []int) { + return file_ateapi_proto_rawDescGZIP(), []int{91} +} + +func (x *ListSharedVolumesRequest) GetAtespace() string { + if x != nil { + return x.Atespace + } + return "" +} + +func (x *ListSharedVolumesRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListSharedVolumesRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +// ListSharedVolumesResponse is one page of ExternalVolumes. +type ListSharedVolumesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The volumes in this page. + SharedVolumes []*SharedVolume `protobuf:"bytes,1,rep,name=shared_volumes,json=sharedVolumes,proto3" json:"shared_volumes,omitempty"` + // Pagination token for the next page. Empty on the last page. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSharedVolumesResponse) Reset() { + *x = ListSharedVolumesResponse{} + mi := &file_ateapi_proto_msgTypes[92] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSharedVolumesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSharedVolumesResponse) ProtoMessage() {} + +func (x *ListSharedVolumesResponse) ProtoReflect() protoreflect.Message { + mi := &file_ateapi_proto_msgTypes[92] + 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 ListSharedVolumesResponse.ProtoReflect.Descriptor instead. +func (*ListSharedVolumesResponse) Descriptor() ([]byte, []int) { + return file_ateapi_proto_rawDescGZIP(), []int{92} +} + +func (x *ListSharedVolumesResponse) GetSharedVolumes() []*SharedVolume { + if x != nil { + return x.SharedVolumes + } + return nil +} + +func (x *ListSharedVolumesResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +// Request to update the mutable fields of a SharedVolume. +type UpdateSharedVolumeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The volume to update. metadata.atespace and metadata.name identify it; + // metadata.version and metadata.uid are required preconditions. + // + // +k8s:required + // +k8s:opaqueType # updates are handled in 2 steps, do not descend + // +k8s:subfield(metadata)=+k8s:required + // +k8s:customValidation # TODO: when we get nested subfields, require metadata.atespace + SharedVolume *SharedVolume `protobuf:"bytes,1,opt,name=shared_volume,json=sharedVolume,proto3" json:"shared_volume,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateSharedVolumeRequest) Reset() { + *x = UpdateSharedVolumeRequest{} + mi := &file_ateapi_proto_msgTypes[93] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateSharedVolumeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateSharedVolumeRequest) ProtoMessage() {} + +func (x *UpdateSharedVolumeRequest) ProtoReflect() protoreflect.Message { + mi := &file_ateapi_proto_msgTypes[93] + 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 UpdateSharedVolumeRequest.ProtoReflect.Descriptor instead. +func (*UpdateSharedVolumeRequest) Descriptor() ([]byte, []int) { + return file_ateapi_proto_rawDescGZIP(), []int{93} +} + +func (x *UpdateSharedVolumeRequest) GetSharedVolume() *SharedVolume { + if x != nil { + return x.SharedVolume + } + return nil +} + +// DeleteSharedVolumeRequest identifies one SharedVolume to delete. +type DeleteSharedVolumeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The volume to delete. + // + // +k8s:required + // +k8s:subfield(atespace)=+k8s:required + SharedVolume *ObjectRef `protobuf:"bytes,1,opt,name=shared_volume,json=sharedVolume,proto3" json:"shared_volume,omitempty"` + // Optional per-delete preconditions. + // + // +k8s:optional + Options *DeleteOptions `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteSharedVolumeRequest) Reset() { + *x = DeleteSharedVolumeRequest{} + mi := &file_ateapi_proto_msgTypes[94] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteSharedVolumeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteSharedVolumeRequest) ProtoMessage() {} + +func (x *DeleteSharedVolumeRequest) ProtoReflect() protoreflect.Message { + mi := &file_ateapi_proto_msgTypes[94] + 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 DeleteSharedVolumeRequest.ProtoReflect.Descriptor instead. +func (*DeleteSharedVolumeRequest) Descriptor() ([]byte, []int) { + return file_ateapi_proto_rawDescGZIP(), []int{94} +} + +func (x *DeleteSharedVolumeRequest) GetSharedVolume() *ObjectRef { + if x != nil { + return x.SharedVolume + } + return nil +} + +func (x *DeleteSharedVolumeRequest) GetOptions() *DeleteOptions { + if x != nil { + return x.Options + } + return nil +} + // DeleteOptions are the optional guards a Delete request may carry. type DeleteOptions struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -6206,7 +7174,7 @@ type DeleteOptions struct { func (x *DeleteOptions) Reset() { *x = DeleteOptions{} - mi := &file_ateapi_proto_msgTypes[84] + mi := &file_ateapi_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6218,7 +7186,7 @@ func (x *DeleteOptions) String() string { func (*DeleteOptions) ProtoMessage() {} func (x *DeleteOptions) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[84] + mi := &file_ateapi_proto_msgTypes[95] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6231,7 +7199,7 @@ func (x *DeleteOptions) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteOptions.ProtoReflect.Descriptor instead. func (*DeleteOptions) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{84} + return file_ateapi_proto_rawDescGZIP(), []int{95} } func (x *DeleteOptions) GetVersion() int64 { @@ -6270,7 +7238,7 @@ type ListOptions struct { func (x *ListOptions) Reset() { *x = ListOptions{} - mi := &file_ateapi_proto_msgTypes[85] + mi := &file_ateapi_proto_msgTypes[96] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6282,7 +7250,7 @@ func (x *ListOptions) String() string { func (*ListOptions) ProtoMessage() {} func (x *ListOptions) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[85] + mi := &file_ateapi_proto_msgTypes[96] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6295,7 +7263,7 @@ func (x *ListOptions) ProtoReflect() protoreflect.Message { // Deprecated: Use ListOptions.ProtoReflect.Descriptor instead. func (*ListOptions) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{85} + return file_ateapi_proto_rawDescGZIP(), []int{96} } func (x *ListOptions) GetPageSize() int32 { @@ -6331,7 +7299,7 @@ type ListWorkerActorAssignmentsRequest struct { func (x *ListWorkerActorAssignmentsRequest) Reset() { *x = ListWorkerActorAssignmentsRequest{} - mi := &file_ateapi_proto_msgTypes[86] + mi := &file_ateapi_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6343,7 +7311,7 @@ func (x *ListWorkerActorAssignmentsRequest) String() string { func (*ListWorkerActorAssignmentsRequest) ProtoMessage() {} func (x *ListWorkerActorAssignmentsRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[86] + mi := &file_ateapi_proto_msgTypes[97] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6356,7 +7324,7 @@ func (x *ListWorkerActorAssignmentsRequest) ProtoReflect() protoreflect.Message // Deprecated: Use ListWorkerActorAssignmentsRequest.ProtoReflect.Descriptor instead. func (*ListWorkerActorAssignmentsRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{86} + return file_ateapi_proto_rawDescGZIP(), []int{97} } func (x *ListWorkerActorAssignmentsRequest) GetWorker() *ObjectRef { @@ -6386,7 +7354,7 @@ type ListWorkerActorAssignmentsResponse struct { func (x *ListWorkerActorAssignmentsResponse) Reset() { *x = ListWorkerActorAssignmentsResponse{} - mi := &file_ateapi_proto_msgTypes[87] + mi := &file_ateapi_proto_msgTypes[98] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6398,7 +7366,7 @@ func (x *ListWorkerActorAssignmentsResponse) String() string { func (*ListWorkerActorAssignmentsResponse) ProtoMessage() {} func (x *ListWorkerActorAssignmentsResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[87] + mi := &file_ateapi_proto_msgTypes[98] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6411,7 +7379,7 @@ func (x *ListWorkerActorAssignmentsResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ListWorkerActorAssignmentsResponse.ProtoReflect.Descriptor instead. func (*ListWorkerActorAssignmentsResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{87} + return file_ateapi_proto_rawDescGZIP(), []int{98} } func (x *ListWorkerActorAssignmentsResponse) GetActorAssignments() []*ActorAssignment { @@ -6440,7 +7408,7 @@ type ListWorkersRequest struct { func (x *ListWorkersRequest) Reset() { *x = ListWorkersRequest{} - mi := &file_ateapi_proto_msgTypes[88] + mi := &file_ateapi_proto_msgTypes[99] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6452,7 +7420,7 @@ func (x *ListWorkersRequest) String() string { func (*ListWorkersRequest) ProtoMessage() {} func (x *ListWorkersRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[88] + mi := &file_ateapi_proto_msgTypes[99] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6465,7 +7433,7 @@ func (x *ListWorkersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkersRequest.ProtoReflect.Descriptor instead. func (*ListWorkersRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{88} + return file_ateapi_proto_rawDescGZIP(), []int{99} } func (x *ListWorkersRequest) GetOptions() *ListOptions { @@ -6487,7 +7455,7 @@ type ListWorkersResponse struct { func (x *ListWorkersResponse) Reset() { *x = ListWorkersResponse{} - mi := &file_ateapi_proto_msgTypes[89] + mi := &file_ateapi_proto_msgTypes[100] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6499,7 +7467,7 @@ func (x *ListWorkersResponse) String() string { func (*ListWorkersResponse) ProtoMessage() {} func (x *ListWorkersResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[89] + mi := &file_ateapi_proto_msgTypes[100] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6512,7 +7480,7 @@ func (x *ListWorkersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkersResponse.ProtoReflect.Descriptor instead. func (*ListWorkersResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{89} + return file_ateapi_proto_rawDescGZIP(), []int{100} } func (x *ListWorkersResponse) GetWorkers() []*Worker { @@ -6542,7 +7510,7 @@ type GetWorkerRequest struct { func (x *GetWorkerRequest) Reset() { *x = GetWorkerRequest{} - mi := &file_ateapi_proto_msgTypes[90] + mi := &file_ateapi_proto_msgTypes[101] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6554,7 +7522,7 @@ func (x *GetWorkerRequest) String() string { func (*GetWorkerRequest) ProtoMessage() {} func (x *GetWorkerRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[90] + mi := &file_ateapi_proto_msgTypes[101] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6567,7 +7535,7 @@ func (x *GetWorkerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkerRequest.ProtoReflect.Descriptor instead. func (*GetWorkerRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{90} + return file_ateapi_proto_rawDescGZIP(), []int{101} } func (x *GetWorkerRequest) GetWorker() *ObjectRef { @@ -6589,7 +7557,7 @@ type CreateWorkerRequest struct { func (x *CreateWorkerRequest) Reset() { *x = CreateWorkerRequest{} - mi := &file_ateapi_proto_msgTypes[91] + mi := &file_ateapi_proto_msgTypes[102] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6601,7 +7569,7 @@ func (x *CreateWorkerRequest) String() string { func (*CreateWorkerRequest) ProtoMessage() {} func (x *CreateWorkerRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[91] + mi := &file_ateapi_proto_msgTypes[102] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6614,7 +7582,7 @@ func (x *CreateWorkerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkerRequest.ProtoReflect.Descriptor instead. func (*CreateWorkerRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{91} + return file_ateapi_proto_rawDescGZIP(), []int{102} } func (x *CreateWorkerRequest) GetWorker() *Worker { @@ -6649,7 +7617,7 @@ type UpdateWorkerRequest struct { func (x *UpdateWorkerRequest) Reset() { *x = UpdateWorkerRequest{} - mi := &file_ateapi_proto_msgTypes[92] + mi := &file_ateapi_proto_msgTypes[103] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6661,7 +7629,7 @@ func (x *UpdateWorkerRequest) String() string { func (*UpdateWorkerRequest) ProtoMessage() {} func (x *UpdateWorkerRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[92] + mi := &file_ateapi_proto_msgTypes[103] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6674,7 +7642,7 @@ func (x *UpdateWorkerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateWorkerRequest.ProtoReflect.Descriptor instead. func (*UpdateWorkerRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{92} + return file_ateapi_proto_rawDescGZIP(), []int{103} } func (x *UpdateWorkerRequest) GetWorker() *Worker { @@ -6701,7 +7669,7 @@ type DeleteWorkerRequest struct { func (x *DeleteWorkerRequest) Reset() { *x = DeleteWorkerRequest{} - mi := &file_ateapi_proto_msgTypes[93] + mi := &file_ateapi_proto_msgTypes[104] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6713,7 +7681,7 @@ func (x *DeleteWorkerRequest) String() string { func (*DeleteWorkerRequest) ProtoMessage() {} func (x *DeleteWorkerRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[93] + mi := &file_ateapi_proto_msgTypes[104] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6726,7 +7694,7 @@ func (x *DeleteWorkerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkerRequest.ProtoReflect.Descriptor instead. func (*DeleteWorkerRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{93} + return file_ateapi_proto_rawDescGZIP(), []int{104} } func (x *DeleteWorkerRequest) GetWorker() *ObjectRef { @@ -6756,7 +7724,7 @@ type DrainWorkerRequest struct { func (x *DrainWorkerRequest) Reset() { *x = DrainWorkerRequest{} - mi := &file_ateapi_proto_msgTypes[94] + mi := &file_ateapi_proto_msgTypes[105] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6768,7 +7736,7 @@ func (x *DrainWorkerRequest) String() string { func (*DrainWorkerRequest) ProtoMessage() {} func (x *DrainWorkerRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[94] + mi := &file_ateapi_proto_msgTypes[105] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6781,7 +7749,7 @@ func (x *DrainWorkerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DrainWorkerRequest.ProtoReflect.Descriptor instead. func (*DrainWorkerRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{94} + return file_ateapi_proto_rawDescGZIP(), []int{105} } func (x *DrainWorkerRequest) GetWorker() *ObjectRef { @@ -6802,7 +7770,7 @@ type DrainWorkerResponse struct { func (x *DrainWorkerResponse) Reset() { *x = DrainWorkerResponse{} - mi := &file_ateapi_proto_msgTypes[95] + mi := &file_ateapi_proto_msgTypes[106] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6814,7 +7782,7 @@ func (x *DrainWorkerResponse) String() string { func (*DrainWorkerResponse) ProtoMessage() {} func (x *DrainWorkerResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[95] + mi := &file_ateapi_proto_msgTypes[106] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6827,7 +7795,7 @@ func (x *DrainWorkerResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DrainWorkerResponse.ProtoReflect.Descriptor instead. func (*DrainWorkerResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{95} + return file_ateapi_proto_rawDescGZIP(), []int{106} } func (x *DrainWorkerResponse) GetWorker() *Worker { @@ -6851,7 +7819,7 @@ type MarkWorkerAvailableRequest struct { func (x *MarkWorkerAvailableRequest) Reset() { *x = MarkWorkerAvailableRequest{} - mi := &file_ateapi_proto_msgTypes[96] + mi := &file_ateapi_proto_msgTypes[107] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6863,7 +7831,7 @@ func (x *MarkWorkerAvailableRequest) String() string { func (*MarkWorkerAvailableRequest) ProtoMessage() {} func (x *MarkWorkerAvailableRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[96] + mi := &file_ateapi_proto_msgTypes[107] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6876,7 +7844,7 @@ func (x *MarkWorkerAvailableRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MarkWorkerAvailableRequest.ProtoReflect.Descriptor instead. func (*MarkWorkerAvailableRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{96} + return file_ateapi_proto_rawDescGZIP(), []int{107} } func (x *MarkWorkerAvailableRequest) GetWorker() *ObjectRef { @@ -6897,7 +7865,7 @@ type MarkWorkerAvailableResponse struct { func (x *MarkWorkerAvailableResponse) Reset() { *x = MarkWorkerAvailableResponse{} - mi := &file_ateapi_proto_msgTypes[97] + mi := &file_ateapi_proto_msgTypes[108] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6909,7 +7877,7 @@ func (x *MarkWorkerAvailableResponse) String() string { func (*MarkWorkerAvailableResponse) ProtoMessage() {} func (x *MarkWorkerAvailableResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[97] + mi := &file_ateapi_proto_msgTypes[108] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6922,7 +7890,7 @@ func (x *MarkWorkerAvailableResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MarkWorkerAvailableResponse.ProtoReflect.Descriptor instead. func (*MarkWorkerAvailableResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{97} + return file_ateapi_proto_rawDescGZIP(), []int{108} } func (x *MarkWorkerAvailableResponse) GetWorker() *Worker { @@ -6946,7 +7914,7 @@ type MarkWorkerUnavailableRequest struct { func (x *MarkWorkerUnavailableRequest) Reset() { *x = MarkWorkerUnavailableRequest{} - mi := &file_ateapi_proto_msgTypes[98] + mi := &file_ateapi_proto_msgTypes[109] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6958,7 +7926,7 @@ func (x *MarkWorkerUnavailableRequest) String() string { func (*MarkWorkerUnavailableRequest) ProtoMessage() {} func (x *MarkWorkerUnavailableRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[98] + mi := &file_ateapi_proto_msgTypes[109] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6971,7 +7939,7 @@ func (x *MarkWorkerUnavailableRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MarkWorkerUnavailableRequest.ProtoReflect.Descriptor instead. func (*MarkWorkerUnavailableRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{98} + return file_ateapi_proto_rawDescGZIP(), []int{109} } func (x *MarkWorkerUnavailableRequest) GetWorker() *ObjectRef { @@ -6992,7 +7960,7 @@ type MarkWorkerUnavailableResponse struct { func (x *MarkWorkerUnavailableResponse) Reset() { *x = MarkWorkerUnavailableResponse{} - mi := &file_ateapi_proto_msgTypes[99] + mi := &file_ateapi_proto_msgTypes[110] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7004,7 +7972,7 @@ func (x *MarkWorkerUnavailableResponse) String() string { func (*MarkWorkerUnavailableResponse) ProtoMessage() {} func (x *MarkWorkerUnavailableResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[99] + mi := &file_ateapi_proto_msgTypes[110] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7017,7 +7985,7 @@ func (x *MarkWorkerUnavailableResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MarkWorkerUnavailableResponse.ProtoReflect.Descriptor instead. func (*MarkWorkerUnavailableResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{99} + return file_ateapi_proto_rawDescGZIP(), []int{110} } func (x *MarkWorkerUnavailableResponse) GetWorker() *Worker { @@ -7046,7 +8014,7 @@ type ListActorsRequest struct { func (x *ListActorsRequest) Reset() { *x = ListActorsRequest{} - mi := &file_ateapi_proto_msgTypes[100] + mi := &file_ateapi_proto_msgTypes[111] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7058,7 +8026,7 @@ func (x *ListActorsRequest) String() string { func (*ListActorsRequest) ProtoMessage() {} func (x *ListActorsRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[100] + mi := &file_ateapi_proto_msgTypes[111] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7071,7 +8039,7 @@ func (x *ListActorsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListActorsRequest.ProtoReflect.Descriptor instead. func (*ListActorsRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{100} + return file_ateapi_proto_rawDescGZIP(), []int{111} } func (x *ListActorsRequest) GetAtespace() string { @@ -7100,7 +8068,7 @@ type ListActorsResponse struct { func (x *ListActorsResponse) Reset() { *x = ListActorsResponse{} - mi := &file_ateapi_proto_msgTypes[101] + mi := &file_ateapi_proto_msgTypes[112] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7112,7 +8080,7 @@ func (x *ListActorsResponse) String() string { func (*ListActorsResponse) ProtoMessage() {} func (x *ListActorsResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[101] + mi := &file_ateapi_proto_msgTypes[112] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7125,7 +8093,7 @@ func (x *ListActorsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListActorsResponse.ProtoReflect.Descriptor instead. func (*ListActorsResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{101} + return file_ateapi_proto_rawDescGZIP(), []int{112} } func (x *ListActorsResponse) GetActors() []*Actor { @@ -7234,7 +8202,7 @@ type Worker struct { func (x *Worker) Reset() { *x = Worker{} - mi := &file_ateapi_proto_msgTypes[102] + mi := &file_ateapi_proto_msgTypes[113] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7246,7 +8214,7 @@ func (x *Worker) String() string { func (*Worker) ProtoMessage() {} func (x *Worker) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[102] + mi := &file_ateapi_proto_msgTypes[113] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7259,7 +8227,7 @@ func (x *Worker) ProtoReflect() protoreflect.Message { // Deprecated: Use Worker.ProtoReflect.Descriptor instead. func (*Worker) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{102} + return file_ateapi_proto_rawDescGZIP(), []int{113} } func (x *Worker) GetMetadata() *ResourceMetadata { @@ -7388,7 +8356,7 @@ type WorkerStatus struct { func (x *WorkerStatus) Reset() { *x = WorkerStatus{} - mi := &file_ateapi_proto_msgTypes[103] + mi := &file_ateapi_proto_msgTypes[114] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7400,7 +8368,7 @@ func (x *WorkerStatus) String() string { func (*WorkerStatus) ProtoMessage() {} func (x *WorkerStatus) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[103] + mi := &file_ateapi_proto_msgTypes[114] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7413,7 +8381,7 @@ func (x *WorkerStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkerStatus.ProtoReflect.Descriptor instead. func (*WorkerStatus) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{103} + return file_ateapi_proto_rawDescGZIP(), []int{114} } func (x *WorkerStatus) GetState() WorkerState { @@ -7481,7 +8449,7 @@ type SandboxRuntime struct { func (x *SandboxRuntime) Reset() { *x = SandboxRuntime{} - mi := &file_ateapi_proto_msgTypes[104] + mi := &file_ateapi_proto_msgTypes[115] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7493,7 +8461,7 @@ func (x *SandboxRuntime) String() string { func (*SandboxRuntime) ProtoMessage() {} func (x *SandboxRuntime) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[104] + mi := &file_ateapi_proto_msgTypes[115] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7506,7 +8474,7 @@ func (x *SandboxRuntime) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxRuntime.ProtoReflect.Descriptor instead. func (*SandboxRuntime) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{104} + return file_ateapi_proto_rawDescGZIP(), []int{115} } func (x *SandboxRuntime) GetSandboxClass() string { @@ -7556,7 +8524,7 @@ type VersionedSandboxCompat struct { func (x *VersionedSandboxCompat) Reset() { *x = VersionedSandboxCompat{} - mi := &file_ateapi_proto_msgTypes[105] + mi := &file_ateapi_proto_msgTypes[116] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7568,7 +8536,7 @@ func (x *VersionedSandboxCompat) String() string { func (*VersionedSandboxCompat) ProtoMessage() {} func (x *VersionedSandboxCompat) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[105] + mi := &file_ateapi_proto_msgTypes[116] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7581,7 +8549,7 @@ func (x *VersionedSandboxCompat) ProtoReflect() protoreflect.Message { // Deprecated: Use VersionedSandboxCompat.ProtoReflect.Descriptor instead. func (*VersionedSandboxCompat) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{105} + return file_ateapi_proto_rawDescGZIP(), []int{116} } func (x *VersionedSandboxCompat) GetSchemaVersion() string { @@ -7617,7 +8585,7 @@ type AttributeEntry struct { func (x *AttributeEntry) Reset() { *x = AttributeEntry{} - mi := &file_ateapi_proto_msgTypes[106] + mi := &file_ateapi_proto_msgTypes[117] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7629,7 +8597,7 @@ func (x *AttributeEntry) String() string { func (*AttributeEntry) ProtoMessage() {} func (x *AttributeEntry) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[106] + mi := &file_ateapi_proto_msgTypes[117] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7642,7 +8610,7 @@ func (x *AttributeEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use AttributeEntry.ProtoReflect.Descriptor instead. func (*AttributeEntry) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{106} + return file_ateapi_proto_rawDescGZIP(), []int{117} } func (x *AttributeEntry) GetKey() string { @@ -7681,7 +8649,7 @@ type WorkerResources struct { func (x *WorkerResources) Reset() { *x = WorkerResources{} - mi := &file_ateapi_proto_msgTypes[107] + mi := &file_ateapi_proto_msgTypes[118] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7693,7 +8661,7 @@ func (x *WorkerResources) String() string { func (*WorkerResources) ProtoMessage() {} func (x *WorkerResources) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[107] + mi := &file_ateapi_proto_msgTypes[118] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7706,7 +8674,7 @@ func (x *WorkerResources) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkerResources.ProtoReflect.Descriptor instead. func (*WorkerResources) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{107} + return file_ateapi_proto_rawDescGZIP(), []int{118} } func (x *WorkerResources) GetResources() *Resources { @@ -7759,7 +8727,7 @@ type ActorAssignment struct { func (x *ActorAssignment) Reset() { *x = ActorAssignment{} - mi := &file_ateapi_proto_msgTypes[108] + mi := &file_ateapi_proto_msgTypes[119] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7771,7 +8739,7 @@ func (x *ActorAssignment) String() string { func (*ActorAssignment) ProtoMessage() {} func (x *ActorAssignment) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[108] + mi := &file_ateapi_proto_msgTypes[119] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7784,7 +8752,7 @@ func (x *ActorAssignment) ProtoReflect() protoreflect.Message { // Deprecated: Use ActorAssignment.ProtoReflect.Descriptor instead. func (*ActorAssignment) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{108} + return file_ateapi_proto_rawDescGZIP(), []int{119} } func (x *ActorAssignment) GetMetadata() *ResourceMetadata { @@ -7855,7 +8823,7 @@ type RegisterWorkerRequest struct { func (x *RegisterWorkerRequest) Reset() { *x = RegisterWorkerRequest{} - mi := &file_ateapi_proto_msgTypes[109] + mi := &file_ateapi_proto_msgTypes[120] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7867,7 +8835,7 @@ func (x *RegisterWorkerRequest) String() string { func (*RegisterWorkerRequest) ProtoMessage() {} func (x *RegisterWorkerRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[109] + mi := &file_ateapi_proto_msgTypes[120] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7880,7 +8848,7 @@ func (x *RegisterWorkerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RegisterWorkerRequest.ProtoReflect.Descriptor instead. func (*RegisterWorkerRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{109} + return file_ateapi_proto_rawDescGZIP(), []int{120} } func (x *RegisterWorkerRequest) GetWorker() *ObjectRef { @@ -7921,7 +8889,7 @@ type RegisterWorkerResponse struct { func (x *RegisterWorkerResponse) Reset() { *x = RegisterWorkerResponse{} - mi := &file_ateapi_proto_msgTypes[110] + mi := &file_ateapi_proto_msgTypes[121] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7933,7 +8901,7 @@ func (x *RegisterWorkerResponse) String() string { func (*RegisterWorkerResponse) ProtoMessage() {} func (x *RegisterWorkerResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[110] + mi := &file_ateapi_proto_msgTypes[121] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7946,7 +8914,7 @@ func (x *RegisterWorkerResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RegisterWorkerResponse.ProtoReflect.Descriptor instead. func (*RegisterWorkerResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{110} + return file_ateapi_proto_rawDescGZIP(), []int{121} } func (x *RegisterWorkerResponse) GetWorker() *Worker { @@ -7985,7 +8953,7 @@ type MintWorkerActorCertificateRequest struct { func (x *MintWorkerActorCertificateRequest) Reset() { *x = MintWorkerActorCertificateRequest{} - mi := &file_ateapi_proto_msgTypes[111] + mi := &file_ateapi_proto_msgTypes[122] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7997,7 +8965,7 @@ func (x *MintWorkerActorCertificateRequest) String() string { func (*MintWorkerActorCertificateRequest) ProtoMessage() {} func (x *MintWorkerActorCertificateRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[111] + mi := &file_ateapi_proto_msgTypes[122] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8010,7 +8978,7 @@ func (x *MintWorkerActorCertificateRequest) ProtoReflect() protoreflect.Message // Deprecated: Use MintWorkerActorCertificateRequest.ProtoReflect.Descriptor instead. func (*MintWorkerActorCertificateRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{111} + return file_ateapi_proto_rawDescGZIP(), []int{122} } func (x *MintWorkerActorCertificateRequest) GetActor() *ObjectRef { @@ -8047,7 +9015,7 @@ type MintWorkerActorCertificateResponse struct { func (x *MintWorkerActorCertificateResponse) Reset() { *x = MintWorkerActorCertificateResponse{} - mi := &file_ateapi_proto_msgTypes[112] + mi := &file_ateapi_proto_msgTypes[123] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8059,7 +9027,7 @@ func (x *MintWorkerActorCertificateResponse) String() string { func (*MintWorkerActorCertificateResponse) ProtoMessage() {} func (x *MintWorkerActorCertificateResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[112] + mi := &file_ateapi_proto_msgTypes[123] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8072,7 +9040,7 @@ func (x *MintWorkerActorCertificateResponse) ProtoReflect() protoreflect.Message // Deprecated: Use MintWorkerActorCertificateResponse.ProtoReflect.Descriptor instead. func (*MintWorkerActorCertificateResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{112} + return file_ateapi_proto_rawDescGZIP(), []int{123} } func (x *MintWorkerActorCertificateResponse) GetActorCertificates() [][]byte { @@ -8113,7 +9081,7 @@ type MintWorkerActorJwtRequest struct { func (x *MintWorkerActorJwtRequest) Reset() { *x = MintWorkerActorJwtRequest{} - mi := &file_ateapi_proto_msgTypes[113] + mi := &file_ateapi_proto_msgTypes[124] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8125,7 +9093,7 @@ func (x *MintWorkerActorJwtRequest) String() string { func (*MintWorkerActorJwtRequest) ProtoMessage() {} func (x *MintWorkerActorJwtRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[113] + mi := &file_ateapi_proto_msgTypes[124] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8138,7 +9106,7 @@ func (x *MintWorkerActorJwtRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MintWorkerActorJwtRequest.ProtoReflect.Descriptor instead. func (*MintWorkerActorJwtRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{113} + return file_ateapi_proto_rawDescGZIP(), []int{124} } func (x *MintWorkerActorJwtRequest) GetActor() *ObjectRef { @@ -8184,7 +9152,7 @@ type MintWorkerActorJwtResponse struct { func (x *MintWorkerActorJwtResponse) Reset() { *x = MintWorkerActorJwtResponse{} - mi := &file_ateapi_proto_msgTypes[114] + mi := &file_ateapi_proto_msgTypes[125] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8196,7 +9164,7 @@ func (x *MintWorkerActorJwtResponse) String() string { func (*MintWorkerActorJwtResponse) ProtoMessage() {} func (x *MintWorkerActorJwtResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[114] + mi := &file_ateapi_proto_msgTypes[125] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8209,7 +9177,7 @@ func (x *MintWorkerActorJwtResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MintWorkerActorJwtResponse.ProtoReflect.Descriptor instead. func (*MintWorkerActorJwtResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{114} + return file_ateapi_proto_rawDescGZIP(), []int{125} } func (x *MintWorkerActorJwtResponse) GetActorJwt() string { @@ -8254,7 +9222,7 @@ type RequestActorSuspendRequest struct { func (x *RequestActorSuspendRequest) Reset() { *x = RequestActorSuspendRequest{} - mi := &file_ateapi_proto_msgTypes[115] + mi := &file_ateapi_proto_msgTypes[126] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8266,7 +9234,7 @@ func (x *RequestActorSuspendRequest) String() string { func (*RequestActorSuspendRequest) ProtoMessage() {} func (x *RequestActorSuspendRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[115] + mi := &file_ateapi_proto_msgTypes[126] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8279,7 +9247,7 @@ func (x *RequestActorSuspendRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestActorSuspendRequest.ProtoReflect.Descriptor instead. func (*RequestActorSuspendRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{115} + return file_ateapi_proto_rawDescGZIP(), []int{126} } func (x *RequestActorSuspendRequest) GetWorker() *ObjectRef { @@ -8313,7 +9281,7 @@ type RequestActorSuspendResponse struct { func (x *RequestActorSuspendResponse) Reset() { *x = RequestActorSuspendResponse{} - mi := &file_ateapi_proto_msgTypes[116] + mi := &file_ateapi_proto_msgTypes[127] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8325,7 +9293,7 @@ func (x *RequestActorSuspendResponse) String() string { func (*RequestActorSuspendResponse) ProtoMessage() {} func (x *RequestActorSuspendResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[116] + mi := &file_ateapi_proto_msgTypes[127] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8338,7 +9306,7 @@ func (x *RequestActorSuspendResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestActorSuspendResponse.ProtoReflect.Descriptor instead. func (*RequestActorSuspendResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{116} + return file_ateapi_proto_rawDescGZIP(), []int{127} } func (x *RequestActorSuspendResponse) GetActor() *Actor { @@ -8369,7 +9337,7 @@ type ReportActorSnapshotStateRequest struct { func (x *ReportActorSnapshotStateRequest) Reset() { *x = ReportActorSnapshotStateRequest{} - mi := &file_ateapi_proto_msgTypes[117] + mi := &file_ateapi_proto_msgTypes[128] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8381,7 +9349,7 @@ func (x *ReportActorSnapshotStateRequest) String() string { func (*ReportActorSnapshotStateRequest) ProtoMessage() {} func (x *ReportActorSnapshotStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[117] + mi := &file_ateapi_proto_msgTypes[128] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8394,7 +9362,7 @@ func (x *ReportActorSnapshotStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportActorSnapshotStateRequest.ProtoReflect.Descriptor instead. func (*ReportActorSnapshotStateRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{117} + return file_ateapi_proto_rawDescGZIP(), []int{128} } func (x *ReportActorSnapshotStateRequest) GetActor() *ObjectRef { @@ -8426,7 +9394,7 @@ type ReportActorSnapshotStateResponse struct { func (x *ReportActorSnapshotStateResponse) Reset() { *x = ReportActorSnapshotStateResponse{} - mi := &file_ateapi_proto_msgTypes[118] + mi := &file_ateapi_proto_msgTypes[129] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8438,7 +9406,7 @@ func (x *ReportActorSnapshotStateResponse) String() string { func (*ReportActorSnapshotStateResponse) ProtoMessage() {} func (x *ReportActorSnapshotStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[118] + mi := &file_ateapi_proto_msgTypes[129] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8451,7 +9419,7 @@ func (x *ReportActorSnapshotStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportActorSnapshotStateResponse.ProtoReflect.Descriptor instead. func (*ReportActorSnapshotStateResponse) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{118} + return file_ateapi_proto_rawDescGZIP(), []int{129} } // ActorSnapshotState is what atelet holds for one Actor on its node. It @@ -8485,7 +9453,7 @@ type ActorSnapshotState struct { func (x *ActorSnapshotState) Reset() { *x = ActorSnapshotState{} - mi := &file_ateapi_proto_msgTypes[119] + mi := &file_ateapi_proto_msgTypes[130] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8497,7 +9465,7 @@ func (x *ActorSnapshotState) String() string { func (*ActorSnapshotState) ProtoMessage() {} func (x *ActorSnapshotState) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[119] + mi := &file_ateapi_proto_msgTypes[130] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8510,7 +9478,7 @@ func (x *ActorSnapshotState) ProtoReflect() protoreflect.Message { // Deprecated: Use ActorSnapshotState.ProtoReflect.Descriptor instead. func (*ActorSnapshotState) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{119} + return file_ateapi_proto_rawDescGZIP(), []int{130} } func (x *ActorSnapshotState) GetLatestLocal() *SnapshotReport { @@ -8582,7 +9550,7 @@ type SnapshotReport struct { func (x *SnapshotReport) Reset() { *x = SnapshotReport{} - mi := &file_ateapi_proto_msgTypes[120] + mi := &file_ateapi_proto_msgTypes[131] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8594,7 +9562,7 @@ func (x *SnapshotReport) String() string { func (*SnapshotReport) ProtoMessage() {} func (x *SnapshotReport) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[120] + mi := &file_ateapi_proto_msgTypes[131] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8607,7 +9575,7 @@ func (x *SnapshotReport) ProtoReflect() protoreflect.Message { // Deprecated: Use SnapshotReport.ProtoReflect.Descriptor instead. func (*SnapshotReport) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{120} + return file_ateapi_proto_rawDescGZIP(), []int{131} } func (x *SnapshotReport) GetUuid() string { @@ -8677,7 +9645,7 @@ type AccessPolicy struct { func (x *AccessPolicy) Reset() { *x = AccessPolicy{} - mi := &file_ateapi_proto_msgTypes[121] + mi := &file_ateapi_proto_msgTypes[132] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8689,7 +9657,7 @@ func (x *AccessPolicy) String() string { func (*AccessPolicy) ProtoMessage() {} func (x *AccessPolicy) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[121] + mi := &file_ateapi_proto_msgTypes[132] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8702,7 +9670,7 @@ func (x *AccessPolicy) ProtoReflect() protoreflect.Message { // Deprecated: Use AccessPolicy.ProtoReflect.Descriptor instead. func (*AccessPolicy) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{121} + return file_ateapi_proto_rawDescGZIP(), []int{132} } func (x *AccessPolicy) GetMetadata() *ResourceMetadata { @@ -8744,7 +9712,7 @@ type Binding struct { func (x *Binding) Reset() { *x = Binding{} - mi := &file_ateapi_proto_msgTypes[122] + mi := &file_ateapi_proto_msgTypes[133] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8756,7 +9724,7 @@ func (x *Binding) String() string { func (*Binding) ProtoMessage() {} func (x *Binding) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[122] + mi := &file_ateapi_proto_msgTypes[133] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8769,7 +9737,7 @@ func (x *Binding) ProtoReflect() protoreflect.Message { // Deprecated: Use Binding.ProtoReflect.Descriptor instead. func (*Binding) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{122} + return file_ateapi_proto_rawDescGZIP(), []int{133} } func (x *Binding) GetRole() string { @@ -8801,7 +9769,7 @@ type GetGlobalAccessPolicyRequest struct { func (x *GetGlobalAccessPolicyRequest) Reset() { *x = GetGlobalAccessPolicyRequest{} - mi := &file_ateapi_proto_msgTypes[123] + mi := &file_ateapi_proto_msgTypes[134] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8813,7 +9781,7 @@ func (x *GetGlobalAccessPolicyRequest) String() string { func (*GetGlobalAccessPolicyRequest) ProtoMessage() {} func (x *GetGlobalAccessPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[123] + mi := &file_ateapi_proto_msgTypes[134] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8826,7 +9794,7 @@ func (x *GetGlobalAccessPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetGlobalAccessPolicyRequest.ProtoReflect.Descriptor instead. func (*GetGlobalAccessPolicyRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{123} + return file_ateapi_proto_rawDescGZIP(), []int{134} } func (x *GetGlobalAccessPolicyRequest) GetName() string { @@ -8850,7 +9818,7 @@ type CreateGlobalAccessPolicyRequest struct { func (x *CreateGlobalAccessPolicyRequest) Reset() { *x = CreateGlobalAccessPolicyRequest{} - mi := &file_ateapi_proto_msgTypes[124] + mi := &file_ateapi_proto_msgTypes[135] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8862,7 +9830,7 @@ func (x *CreateGlobalAccessPolicyRequest) String() string { func (*CreateGlobalAccessPolicyRequest) ProtoMessage() {} func (x *CreateGlobalAccessPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[124] + mi := &file_ateapi_proto_msgTypes[135] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8875,7 +9843,7 @@ func (x *CreateGlobalAccessPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateGlobalAccessPolicyRequest.ProtoReflect.Descriptor instead. func (*CreateGlobalAccessPolicyRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{124} + return file_ateapi_proto_rawDescGZIP(), []int{135} } func (x *CreateGlobalAccessPolicyRequest) GetAccessPolicy() *AccessPolicy { @@ -8899,7 +9867,7 @@ type UpdateGlobalAccessPolicyRequest struct { func (x *UpdateGlobalAccessPolicyRequest) Reset() { *x = UpdateGlobalAccessPolicyRequest{} - mi := &file_ateapi_proto_msgTypes[125] + mi := &file_ateapi_proto_msgTypes[136] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8911,7 +9879,7 @@ func (x *UpdateGlobalAccessPolicyRequest) String() string { func (*UpdateGlobalAccessPolicyRequest) ProtoMessage() {} func (x *UpdateGlobalAccessPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[125] + mi := &file_ateapi_proto_msgTypes[136] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8924,7 +9892,7 @@ func (x *UpdateGlobalAccessPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateGlobalAccessPolicyRequest.ProtoReflect.Descriptor instead. func (*UpdateGlobalAccessPolicyRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{125} + return file_ateapi_proto_rawDescGZIP(), []int{136} } func (x *UpdateGlobalAccessPolicyRequest) GetAccessPolicy() *AccessPolicy { @@ -8954,7 +9922,7 @@ type GetAtespaceAccessPolicyRequest struct { func (x *GetAtespaceAccessPolicyRequest) Reset() { *x = GetAtespaceAccessPolicyRequest{} - mi := &file_ateapi_proto_msgTypes[126] + mi := &file_ateapi_proto_msgTypes[137] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8966,7 +9934,7 @@ func (x *GetAtespaceAccessPolicyRequest) String() string { func (*GetAtespaceAccessPolicyRequest) ProtoMessage() {} func (x *GetAtespaceAccessPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[126] + mi := &file_ateapi_proto_msgTypes[137] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8979,7 +9947,7 @@ func (x *GetAtespaceAccessPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetAtespaceAccessPolicyRequest.ProtoReflect.Descriptor instead. func (*GetAtespaceAccessPolicyRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{126} + return file_ateapi_proto_rawDescGZIP(), []int{137} } func (x *GetAtespaceAccessPolicyRequest) GetAtespace() *ObjectRef { @@ -9015,7 +9983,7 @@ type CreateAtespaceAccessPolicyRequest struct { func (x *CreateAtespaceAccessPolicyRequest) Reset() { *x = CreateAtespaceAccessPolicyRequest{} - mi := &file_ateapi_proto_msgTypes[127] + mi := &file_ateapi_proto_msgTypes[138] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9027,7 +9995,7 @@ func (x *CreateAtespaceAccessPolicyRequest) String() string { func (*CreateAtespaceAccessPolicyRequest) ProtoMessage() {} func (x *CreateAtespaceAccessPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[127] + mi := &file_ateapi_proto_msgTypes[138] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9040,7 +10008,7 @@ func (x *CreateAtespaceAccessPolicyRequest) ProtoReflect() protoreflect.Message // Deprecated: Use CreateAtespaceAccessPolicyRequest.ProtoReflect.Descriptor instead. func (*CreateAtespaceAccessPolicyRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{127} + return file_ateapi_proto_rawDescGZIP(), []int{138} } func (x *CreateAtespaceAccessPolicyRequest) GetAtespace() *ObjectRef { @@ -9076,7 +10044,7 @@ type UpdateAtespaceAccessPolicyRequest struct { func (x *UpdateAtespaceAccessPolicyRequest) Reset() { *x = UpdateAtespaceAccessPolicyRequest{} - mi := &file_ateapi_proto_msgTypes[128] + mi := &file_ateapi_proto_msgTypes[139] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9088,7 +10056,7 @@ func (x *UpdateAtespaceAccessPolicyRequest) String() string { func (*UpdateAtespaceAccessPolicyRequest) ProtoMessage() {} func (x *UpdateAtespaceAccessPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[128] + mi := &file_ateapi_proto_msgTypes[139] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9101,7 +10069,7 @@ func (x *UpdateAtespaceAccessPolicyRequest) ProtoReflect() protoreflect.Message // Deprecated: Use UpdateAtespaceAccessPolicyRequest.ProtoReflect.Descriptor instead. func (*UpdateAtespaceAccessPolicyRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{128} + return file_ateapi_proto_rawDescGZIP(), []int{139} } func (x *UpdateAtespaceAccessPolicyRequest) GetAtespace() *ObjectRef { @@ -9142,7 +10110,7 @@ type DeleteAtespaceAccessPolicyRequest struct { func (x *DeleteAtespaceAccessPolicyRequest) Reset() { *x = DeleteAtespaceAccessPolicyRequest{} - mi := &file_ateapi_proto_msgTypes[129] + mi := &file_ateapi_proto_msgTypes[140] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9154,7 +10122,7 @@ func (x *DeleteAtespaceAccessPolicyRequest) String() string { func (*DeleteAtespaceAccessPolicyRequest) ProtoMessage() {} func (x *DeleteAtespaceAccessPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[129] + mi := &file_ateapi_proto_msgTypes[140] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9167,7 +10135,7 @@ func (x *DeleteAtespaceAccessPolicyRequest) ProtoReflect() protoreflect.Message // Deprecated: Use DeleteAtespaceAccessPolicyRequest.ProtoReflect.Descriptor instead. func (*DeleteAtespaceAccessPolicyRequest) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{129} + return file_ateapi_proto_rawDescGZIP(), []int{140} } func (x *DeleteAtespaceAccessPolicyRequest) GetAtespace() *ObjectRef { @@ -9213,7 +10181,7 @@ type ResourceAnnotation struct { func (x *ResourceAnnotation) Reset() { *x = ResourceAnnotation{} - mi := &file_ateapi_proto_msgTypes[130] + mi := &file_ateapi_proto_msgTypes[141] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9225,7 +10193,7 @@ func (x *ResourceAnnotation) String() string { func (*ResourceAnnotation) ProtoMessage() {} func (x *ResourceAnnotation) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[130] + mi := &file_ateapi_proto_msgTypes[141] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9238,7 +10206,7 @@ func (x *ResourceAnnotation) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceAnnotation.ProtoReflect.Descriptor instead. func (*ResourceAnnotation) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{130} + return file_ateapi_proto_rawDescGZIP(), []int{141} } func (x *ResourceAnnotation) GetParents() []string { @@ -9281,7 +10249,7 @@ type MethodAnnotation struct { func (x *MethodAnnotation) Reset() { *x = MethodAnnotation{} - mi := &file_ateapi_proto_msgTypes[131] + mi := &file_ateapi_proto_msgTypes[142] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9293,7 +10261,7 @@ func (x *MethodAnnotation) String() string { func (*MethodAnnotation) ProtoMessage() {} func (x *MethodAnnotation) ProtoReflect() protoreflect.Message { - mi := &file_ateapi_proto_msgTypes[131] + mi := &file_ateapi_proto_msgTypes[142] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9306,7 +10274,7 @@ func (x *MethodAnnotation) ProtoReflect() protoreflect.Message { // Deprecated: Use MethodAnnotation.ProtoReflect.Descriptor instead. func (*MethodAnnotation) Descriptor() ([]byte, []int) { - return file_ateapi_proto_rawDescGZIP(), []int{131} + return file_ateapi_proto_rawDescGZIP(), []int{142} } func (x *MethodAnnotation) GetResource() string { @@ -9381,23 +10349,28 @@ 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\"\x85\x02\n" + - "\x0eExternalVolume\x12\x12\n" + + "updateTime\"\xb9\x02\n" + + "\x11ExternalVolumeRef\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12*\n" + "\x11storage_volume_id\x18\x02 \x01(\tR\x0fstorageVolumeId\x12\x1f\n" + "\vvolume_type\x18\x03 \x01(\tR\n" + - "volumeType\x12P\n" + - "\x0evolume_context\x18\x05 \x03(\v2).ateapi.ExternalVolume.VolumeContextEntryR\rvolumeContext\x1a@\n" + + "volumeType\x12S\n" + + "\x0evolume_context\x18\x05 \x03(\v2,.ateapi.ExternalVolumeRef.VolumeContextEntryR\rvolumeContext\x12,\n" + + "\x12shared_volume_name\x18\x06 \x01(\tR\x10sharedVolumeName\x1a@\n" + "\x12VolumeContextEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xa1\x02\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xc9\x03\n" + "\x05Actor\x124\n" + "\bmetadata\x18\x01 \x01(\v2\x18.ateapi.ResourceMetadataR\bmetadata\x128\n" + "\x0eactor_template\x18\x04 \x01(\v2\x11.ateapi.ObjectRefR\ractorTemplate\x129\n" + "\x0fworker_selector\x18\x05 \x01(\v2\x10.ateapi.SelectorR\x0eworkerSelector\x120\n" + "\n" + "source_tag\x18\x06 \x01(\v2\x11.ateapi.ObjectRefR\tsourceTag\x12+\n" + - "\x06status\x18\a \x01(\v2\x13.ateapi.ActorStatusR\x06status:\x0e\x82\xb5\x18\n" + + "\x06status\x18\a \x01(\v2\x13.ateapi.ActorStatusR\x06status\x12]\n" + + "\x16shared_volume_bindings\x18\b \x03(\v2'.ateapi.Actor.SharedVolumeBindingsEntryR\x14sharedVolumeBindings\x1aG\n" + + "\x19SharedVolumeBindingsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01:\x0e\x82\xb5\x18\n" + "\x18\x01\"\x06Actors\"\x8d\x01\n" + "\fEgressPolicy\x124\n" + "\bmetadata\x18\x01 \x01(\v2\x18.ateapi.ResourceMetadataR\bmetadata\x12(\n" + @@ -9433,11 +10406,11 @@ const file_ateapi_proto_rawDesc = "" + "\tactor_jwt\x18\x04 \x01(\v2\x16.ateapi.ActorJwtSourceR\bactorJwt\"]\n" + "\x0eActorJwtSource\x12\x1c\n" + "\taudiences\x18\x01 \x03(\tR\taudiences\x12-\n" + - "\x12expiration_seconds\x18\x02 \x01(\x03R\x11expirationSeconds\"\xfa\x02\n" + + "\x12expiration_seconds\x18\x02 \x01(\x03R\x11expirationSeconds\"\xfd\x02\n" + "\vActorStatus\x12(\n" + "\x05state\x18\x01 \x01(\x0e2\x12.ateapi.ActorStateR\x05state\x12E\n" + - "\x11worker_assignment\x18\x02 \x01(\v2\x18.ateapi.WorkerAssignmentR\x10workerAssignment\x12A\n" + - "\x10external_volumes\x18\a \x03(\v2\x16.ateapi.ExternalVolumeR\x0fexternalVolumes\x12(\n" + + "\x11worker_assignment\x18\x02 \x01(\v2\x18.ateapi.WorkerAssignmentR\x10workerAssignment\x12D\n" + + "\x10external_volumes\x18\a \x03(\v2\x19.ateapi.ExternalVolumeRefR\x0fexternalVolumes\x12(\n" + "\x05crash\x18\t \x01(\v2\x12.ateapi.ActorCrashR\x05crash\x12#\n" + "\rassigned_node\x18\n" + " \x01(\tR\fassignedNode\x12.\n" + @@ -9467,7 +10440,30 @@ const file_ateapi_proto_rawDesc = "" + "\bmetadata\x18\x01 \x01(\v2\x18.ateapi.ResourceMetadataR\bmetadata\x12)\n" + "\x06status\x18\x02 \x01(\v2\x11.ateapi.TagStatusR\x06status\x12&\n" + "\x05scope\x18\x03 \x01(\x0e2\x10.ateapi.TagScopeR\x05scope\x124\n" + - "\fsource_actor\x18\x04 \x01(\v2\x11.ateapi.ObjectRefR\vsourceActor:\f\x82\xb5\x18\b\x18\x01\"\x04Tags\"S\n" + + "\fsource_actor\x18\x04 \x01(\v2\x11.ateapi.ObjectRefR\vsourceActor:\f\x82\xb5\x18\b\x18\x01\"\x04Tags\"\x9c\x04\n" + + "\fSharedVolume\x124\n" + + "\bmetadata\x18\x01 \x01(\v2\x18.ateapi.ResourceMetadataR\bmetadata\x12<\n" + + "\x0edelete_trigger\x18\x02 \x01(\x0e2\x15.ateapi.DeleteTriggerR\rdeleteTrigger\x12,\n" + + "\x12storage_class_name\x18\x03 \x01(\tR\x10storageClassName\x12\x1b\n" + + "\tvolume_id\x18\x04 \x01(\tR\bvolumeId\x12\x1f\n" + + "\vvolume_type\x18\x05 \x01(\tR\n" + + "volumeType\x12N\n" + + "\x0evolume_context\x18\x06 \x03(\v2'.ateapi.SharedVolume.VolumeContextEntryR\rvolumeContext\x122\n" + + "\x06status\x18\a \x01(\v2\x1a.ateapi.SharedVolumeStatusR\x06status\x12\x1a\n" + + "\bcapacity\x18\b \x01(\tR\bcapacity\x123\n" + + "\vaccess_mode\x18\t \x01(\x0e2\x12.ateapi.AccessModeR\n" + + "accessMode\x1a@\n" + + "\x12VolumeContextEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01:\x15\x82\xb5\x18\x11\x18\x01\"\rSharedVolumes\"k\n" + + "\x12SharedVolumeStatus\x12/\n" + + "\x05state\x18\x01 \x01(\x0e2\x19.ateapi.SharedVolumeStateR\x05state\x12$\n" + + "\x04refs\x18\x02 \x03(\v2\x10.ateapi.ActorRefR\x04refs\"^\n" + + "\bActorRef\x12\x1b\n" + + "\tactor_uid\x18\x01 \x01(\tR\bactorUid\x12\x1d\n" + + "\n" + + "actor_name\x18\x02 \x01(\tR\tactorName\x12\x16\n" + + "\x06active\x18\x03 \x01(\bR\x06active\"S\n" + "\bAtespace\x124\n" + "\bmetadata\x18\x01 \x01(\v2\x18.ateapi.ResourceMetadataR\bmetadata:\x11\x82\xb5\x18\r\x18\x02\"\tAtespaces\";\n" + "\tObjectRef\x12\x1a\n" + @@ -9529,21 +10525,26 @@ const file_ateapi_proto_rawDesc = "" + "\x0ftimeout_seconds\x18\x02 \x01(\x05R\x0etimeoutSeconds\"7\n" + "\rHttpGetAction\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + - "\x04port\x18\x02 \x01(\x05R\x04port\"\xa9\x02\n" + + "\x04port\x18\x02 \x01(\x05R\x04port\"\x89\x02\n" + "\x06Volume\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12?\n" + "\vdurable_dir\x18\x02 \x01(\v2\x1e.ateapi.DurableDirVolumeSourceR\n" + - "durableDir\x12X\n" + - "\x18external_volume_template\x18\x03 \x01(\v2\x1e.ateapi.ExternalVolumeTemplateR\x16externalVolumeTemplate\x12?\n" + + "durableDir\x128\n" + + "\bexternal\x18\a \x01(\v2\x1c.ateapi.ExternalVolumeSourceR\bexternal\x12?\n" + "\vsystem_info\x18\x05 \x01(\v2\x1e.ateapi.SystemInfoVolumeSourceR\n" + "systemInfo\x12/\n" + - "\x05image\x18\x06 \x01(\v2\x19.ateapi.ImageVolumeSourceR\x05image\"1\n" + + "\x05image\x18\x06 \x01(\v2\x19.ateapi.ImageVolumeSourceR\x05image\"\x90\x01\n" + + "\x14ExternalVolumeSource\x12:\n" + + "\btemplate\x18\x01 \x01(\v2\x1e.ateapi.ExternalVolumeTemplateR\btemplate\x12<\n" + + "\rshared_volume\x18\x02 \x01(\v2\x17.ateapi.SharedVolumeRefR\fsharedVolume\"1\n" + "\x11ImageVolumeSource\x12\x1c\n" + "\treference\x18\x01 \x01(\tR\treference\"\x18\n" + "\x16DurableDirVolumeSource\"b\n" + "\x16ExternalVolumeTemplate\x12\x1a\n" + "\bcapacity\x18\x01 \x01(\tR\bcapacity\x12,\n" + - "\x12storage_class_name\x18\x02 \x01(\tR\x10storageClassName\"Y\n" + + "\x12storage_class_name\x18\x02 \x01(\tR\x10storageClassName\"%\n" + + "\x0fSharedVolumeRef\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"Y\n" + "\x16SystemInfoVolumeSource\x12?\n" + "\fdata_sources\x18\x01 \x03(\v2\x1c.ateapi.SystemInfoDataSourceR\vdataSources\"\xa0\x01\n" + "\x14SystemInfoDataSource\x12F\n" + @@ -9654,6 +10655,23 @@ const file_ateapi_proto_rawDesc = "" + "\x03tag\x18\x01 \x01(\v2\v.ateapi.TagR\x03tag\"h\n" + "\x10DeleteTagRequest\x12#\n" + "\x03tag\x18\x01 \x01(\v2\x11.ateapi.ObjectRefR\x03tag\x12/\n" + + "\aoptions\x18\x02 \x01(\v2\x15.ateapi.DeleteOptionsR\aoptions\"V\n" + + "\x19CreateSharedVolumeRequest\x129\n" + + "\rshared_volume\x18\x01 \x01(\v2\x14.ateapi.SharedVolumeR\fsharedVolume\"P\n" + + "\x16GetSharedVolumeRequest\x126\n" + + "\rshared_volume\x18\x01 \x01(\v2\x11.ateapi.ObjectRefR\fsharedVolume\"r\n" + + "\x18ListSharedVolumesRequest\x12\x1a\n" + + "\batespace\x18\x01 \x01(\tR\batespace\x12\x1b\n" + + "\tpage_size\x18\x02 \x01(\x05R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x03 \x01(\tR\tpageToken\"\x80\x01\n" + + "\x19ListSharedVolumesResponse\x12;\n" + + "\x0eshared_volumes\x18\x01 \x03(\v2\x14.ateapi.SharedVolumeR\rsharedVolumes\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"V\n" + + "\x19UpdateSharedVolumeRequest\x129\n" + + "\rshared_volume\x18\x01 \x01(\v2\x14.ateapi.SharedVolumeR\fsharedVolume\"\x84\x01\n" + + "\x19DeleteSharedVolumeRequest\x126\n" + + "\rshared_volume\x18\x01 \x01(\v2\x11.ateapi.ObjectRefR\fsharedVolume\x12/\n" + "\aoptions\x18\x02 \x01(\v2\x15.ateapi.DeleteOptionsR\aoptions\";\n" + "\rDeleteOptions\x12\x18\n" + "\aversion\x18\x01 \x01(\x03R\aversion\x12\x10\n" + @@ -9851,11 +10869,26 @@ const file_ateapi_proto_rawDesc = "" + "\x15ACTOR_STATE_SUSPENDED\x10\x04\x12\x17\n" + "\x13ACTOR_STATE_CRASHED\x10\a\x12\x18\n" + "\x14ACTOR_STATE_DELETING\x10\b\x12\x19\n" + - "\x15ACTOR_STATE_REVERTING\x10\t*\x7f\n" + + "\x15ACTOR_STATE_REVERTING\x10\t*i\n" + + "\rDeleteTrigger\x12\x1e\n" + + "\x1aDELETE_TRIGGER_UNSPECIFIED\x10\x00\x12\x19\n" + + "\x15DELETE_TRIGGER_MANUAL\x10\x01\x12\x1d\n" + + "\x19DELETE_TRIGGER_LAST_ACTOR\x10\x02*k\n" + + "\n" + + "AccessMode\x12\x1b\n" + + "\x17ACCESS_MODE_UNSPECIFIED\x10\x00\x12\x1f\n" + + "\x1bACCESS_MODE_READ_WRITE_ONCE\x10\x01\x12\x1f\n" + + "\x1bACCESS_MODE_READ_WRITE_MANY\x10\x02*\x9a\x01\n" + + "\x11SharedVolumeState\x12#\n" + + "\x1fSHARED_VOLUME_STATE_UNSPECIFIED\x10\x00\x12\x1f\n" + + "\x1bSHARED_VOLUME_STATE_PENDING\x10\x01\x12\x1d\n" + + "\x19SHARED_VOLUME_STATE_READY\x10\x02\x12 \n" + + "\x1cSHARED_VOLUME_STATE_DELETING\x10\x03*\xa1\x01\n" + "\x12GoldenSnapshotMode\x12$\n" + " GOLDEN_SNAPSHOT_MODE_UNSPECIFIED\x10\x00\x12 \n" + "\x1cGOLDEN_SNAPSHOT_MODE_ENABLED\x10\x01\x12!\n" + - "\x1dGOLDEN_SNAPSHOT_MODE_DISABLED\x10\x02*\x9a\x01\n" + + "\x1dGOLDEN_SNAPSHOT_MODE_DISABLED\x10\x02\x12 \n" + + "\x1cGOLDEN_SNAPSHOT_MODE_RUNTIME\x10\x03*\x9a\x01\n" + "\x12ActorMetadataField\x12$\n" + " ACTOR_METADATA_FIELD_UNSPECIFIED\x10\x00\x12\x1d\n" + "\x19ACTOR_METADATA_FIELD_NAME\x10\x01\x12!\n" + @@ -9869,7 +10902,7 @@ const file_ateapi_proto_rawDesc = "" + "\x05Scope\x12\x15\n" + "\x11SCOPE_UNSPECIFIED\x10\x00\x12\x12\n" + "\x0eSCOPE_ATESPACE\x10\x01\x12\x10\n" + - "\fSCOPE_GLOBAL\x10\x022\xbb\x1f\n" + + "\fSCOPE_GLOBAL\x10\x022\xaf#\n" + "\aControl\x12?\n" + "\bGetActor\x12\x17.ateapi.GetActorRequest\x1a\r.ateapi.Actor\"\v\x82\xb5\x18\a\n" + "\x05Actor\x12E\n" + @@ -9909,7 +10942,17 @@ const file_ateapi_proto_rawDesc = "" + "\tUpdateTag\x12\x18.ateapi.UpdateTagRequest\x1a\v.ateapi.Tag\"\t\x82\xb5\x18\x05\n" + "\x03Tag\x12=\n" + "\tDeleteTag\x12\x18.ateapi.DeleteTagRequest\x1a\v.ateapi.Tag\"\t\x82\xb5\x18\x05\n" + - "\x03Tag\x12T\n" + + "\x03Tag\x12a\n" + + "\x12CreateSharedVolume\x12!.ateapi.CreateSharedVolumeRequest\x1a\x14.ateapi.SharedVolume\"\x12\x82\xb5\x18\x0e\n" + + "\fSharedVolume\x12[\n" + + "\x0fGetSharedVolume\x12\x1e.ateapi.GetSharedVolumeRequest\x1a\x14.ateapi.SharedVolume\"\x12\x82\xb5\x18\x0e\n" + + "\fSharedVolume\x12l\n" + + "\x11ListSharedVolumes\x12 .ateapi.ListSharedVolumesRequest\x1a!.ateapi.ListSharedVolumesResponse\"\x12\x82\xb5\x18\x0e\n" + + "\fSharedVolume\x12a\n" + + "\x12UpdateSharedVolume\x12!.ateapi.UpdateSharedVolumeRequest\x1a\x14.ateapi.SharedVolume\"\x12\x82\xb5\x18\x0e\n" + + "\fSharedVolume\x12a\n" + + "\x12DeleteSharedVolume\x12!.ateapi.DeleteSharedVolumeRequest\x1a\x14.ateapi.SharedVolume\"\x12\x82\xb5\x18\x0e\n" + + "\fSharedVolume\x12T\n" + "\vListWorkers\x12\x1a.ateapi.ListWorkersRequest\x1a\x1b.ateapi.ListWorkersResponse\"\f\x82\xb5\x18\b\n" + "\x06Worker\x12C\n" + "\tGetWorker\x12\x18.ateapi.GetWorkerRequest\x1a\x0e.ateapi.Worker\"\f\x82\xb5\x18\b\n" + @@ -9986,445 +11029,487 @@ func file_ateapi_proto_rawDescGZIP() []byte { return file_ateapi_proto_rawDescData } -var file_ateapi_proto_enumTypes = make([]protoimpl.EnumInfo, 9) -var file_ateapi_proto_msgTypes = make([]protoimpl.MessageInfo, 135) +var file_ateapi_proto_enumTypes = make([]protoimpl.EnumInfo, 12) +var file_ateapi_proto_msgTypes = make([]protoimpl.MessageInfo, 148) var file_ateapi_proto_goTypes = []any{ (SnapshotOwner)(0), // 0: ateapi.SnapshotOwner (SnapshotStorageStatus)(0), // 1: ateapi.SnapshotStorageStatus (SnapshotFidelity)(0), // 2: ateapi.SnapshotFidelity (TagScope)(0), // 3: ateapi.TagScope (ActorState)(0), // 4: ateapi.ActorState - (GoldenSnapshotMode)(0), // 5: ateapi.GoldenSnapshotMode - (ActorMetadataField)(0), // 6: ateapi.ActorMetadataField - (WorkerState)(0), // 7: ateapi.WorkerState - (Scope)(0), // 8: ateapi.Scope - (*ObjectSnapshot)(nil), // 9: ateapi.ObjectSnapshot - (*SnapshotStorage)(nil), // 10: ateapi.SnapshotStorage - (*Snapshot)(nil), // 11: ateapi.Snapshot - (*Selector)(nil), // 12: ateapi.Selector - (*ResourceMetadata)(nil), // 13: ateapi.ResourceMetadata - (*ExternalVolume)(nil), // 14: ateapi.ExternalVolume - (*Actor)(nil), // 15: ateapi.Actor - (*EgressPolicy)(nil), // 16: ateapi.EgressPolicy - (*EgressRule)(nil), // 17: ateapi.EgressRule - (*HttpRule)(nil), // 18: ateapi.HttpRule - (*HttpsRule)(nil), // 19: ateapi.HttpsRule - (*TlsPassthroughRule)(nil), // 20: ateapi.TlsPassthroughRule - (*Ports)(nil), // 21: ateapi.Ports - (*AllPorts)(nil), // 22: ateapi.AllPorts - (*HttpRuleEffects)(nil), // 23: ateapi.HttpRuleEffects - (*CredentialHeader)(nil), // 24: ateapi.CredentialHeader - (*ActorJwtSource)(nil), // 25: ateapi.ActorJwtSource - (*ActorStatus)(nil), // 26: ateapi.ActorStatus - (*ActorCrash)(nil), // 27: ateapi.ActorCrash - (*WorkerAssignment)(nil), // 28: ateapi.WorkerAssignment - (*TagStatus)(nil), // 29: ateapi.TagStatus - (*Tag)(nil), // 30: ateapi.Tag - (*Atespace)(nil), // 31: ateapi.Atespace - (*ObjectRef)(nil), // 32: ateapi.ObjectRef - (*ActorTemplate)(nil), // 33: ateapi.ActorTemplate - (*Resources)(nil), // 34: ateapi.Resources - (*Limits)(nil), // 35: ateapi.Limits - (*GoldenSnapshotStatus)(nil), // 36: ateapi.GoldenSnapshotStatus - (*ActorTemplateStatus)(nil), // 37: ateapi.ActorTemplateStatus - (*SandboxConfig)(nil), // 38: ateapi.SandboxConfig - (*SnapshotConfig)(nil), // 39: ateapi.SnapshotConfig - (*GoldenSnapshotConfig)(nil), // 40: ateapi.GoldenSnapshotConfig - (*Container)(nil), // 41: ateapi.Container - (*SecurityContext)(nil), // 42: ateapi.SecurityContext - (*Capabilities)(nil), // 43: ateapi.Capabilities - (*EnvVar)(nil), // 44: ateapi.EnvVar - (*WakeupProbe)(nil), // 45: ateapi.WakeupProbe - (*HttpGetAction)(nil), // 46: ateapi.HttpGetAction - (*Volume)(nil), // 47: ateapi.Volume - (*ImageVolumeSource)(nil), // 48: ateapi.ImageVolumeSource - (*DurableDirVolumeSource)(nil), // 49: ateapi.DurableDirVolumeSource - (*ExternalVolumeTemplate)(nil), // 50: ateapi.ExternalVolumeTemplate - (*SystemInfoVolumeSource)(nil), // 51: ateapi.SystemInfoVolumeSource - (*SystemInfoDataSource)(nil), // 52: ateapi.SystemInfoDataSource - (*ActorMetadataDataSource)(nil), // 53: ateapi.ActorMetadataDataSource - (*ActorMetadataItem)(nil), // 54: ateapi.ActorMetadataItem - (*TrustBundleDataSource)(nil), // 55: ateapi.TrustBundleDataSource - (*VolumeMount)(nil), // 56: ateapi.VolumeMount - (*CreateAtespaceRequest)(nil), // 57: ateapi.CreateAtespaceRequest - (*GetAtespaceRequest)(nil), // 58: ateapi.GetAtespaceRequest - (*ListAtespacesRequest)(nil), // 59: ateapi.ListAtespacesRequest - (*ListAtespacesResponse)(nil), // 60: ateapi.ListAtespacesResponse - (*DeleteAtespaceRequest)(nil), // 61: ateapi.DeleteAtespaceRequest - (*CreateActorTemplateRequest)(nil), // 62: ateapi.CreateActorTemplateRequest - (*GetActorTemplateRequest)(nil), // 63: ateapi.GetActorTemplateRequest - (*ListActorTemplatesRequest)(nil), // 64: ateapi.ListActorTemplatesRequest - (*ListActorTemplatesResponse)(nil), // 65: ateapi.ListActorTemplatesResponse - (*DeleteActorTemplateRequest)(nil), // 66: ateapi.DeleteActorTemplateRequest - (*GetActorRequest)(nil), // 67: ateapi.GetActorRequest - (*CreateActorRequest)(nil), // 68: ateapi.CreateActorRequest - (*UpdateActorRequest)(nil), // 69: ateapi.UpdateActorRequest - (*SuspendActorRequest)(nil), // 70: ateapi.SuspendActorRequest - (*SuspendActorResponse)(nil), // 71: ateapi.SuspendActorResponse - (*PauseActorRequest)(nil), // 72: ateapi.PauseActorRequest - (*PauseActorResponse)(nil), // 73: ateapi.PauseActorResponse - (*ResumeActorRequest)(nil), // 74: ateapi.ResumeActorRequest - (*ResumeActorResponse)(nil), // 75: ateapi.ResumeActorResponse - (*RevertActorRequest)(nil), // 76: ateapi.RevertActorRequest - (*RevertActorResponse)(nil), // 77: ateapi.RevertActorResponse - (*DeleteActorRequest)(nil), // 78: ateapi.DeleteActorRequest - (*GetActorEgressPolicyRequest)(nil), // 79: ateapi.GetActorEgressPolicyRequest - (*CreateActorEgressPolicyRequest)(nil), // 80: ateapi.CreateActorEgressPolicyRequest - (*UpdateActorEgressPolicyRequest)(nil), // 81: ateapi.UpdateActorEgressPolicyRequest - (*DeleteActorEgressPolicyRequest)(nil), // 82: ateapi.DeleteActorEgressPolicyRequest - (*GetTagRequest)(nil), // 83: ateapi.GetTagRequest - (*MintActorJwtRequest)(nil), // 84: ateapi.MintActorJwtRequest - (*MintActorJwtResponse)(nil), // 85: ateapi.MintActorJwtResponse - (*MintActorCertificateRequest)(nil), // 86: ateapi.MintActorCertificateRequest - (*MintActorCertificateResponse)(nil), // 87: ateapi.MintActorCertificateResponse - (*ListTagsRequest)(nil), // 88: ateapi.ListTagsRequest - (*ListTagsResponse)(nil), // 89: ateapi.ListTagsResponse - (*CreateTagRequest)(nil), // 90: ateapi.CreateTagRequest - (*UpdateTagRequest)(nil), // 91: ateapi.UpdateTagRequest - (*DeleteTagRequest)(nil), // 92: ateapi.DeleteTagRequest - (*DeleteOptions)(nil), // 93: ateapi.DeleteOptions - (*ListOptions)(nil), // 94: ateapi.ListOptions - (*ListWorkerActorAssignmentsRequest)(nil), // 95: ateapi.ListWorkerActorAssignmentsRequest - (*ListWorkerActorAssignmentsResponse)(nil), // 96: ateapi.ListWorkerActorAssignmentsResponse - (*ListWorkersRequest)(nil), // 97: ateapi.ListWorkersRequest - (*ListWorkersResponse)(nil), // 98: ateapi.ListWorkersResponse - (*GetWorkerRequest)(nil), // 99: ateapi.GetWorkerRequest - (*CreateWorkerRequest)(nil), // 100: ateapi.CreateWorkerRequest - (*UpdateWorkerRequest)(nil), // 101: ateapi.UpdateWorkerRequest - (*DeleteWorkerRequest)(nil), // 102: ateapi.DeleteWorkerRequest - (*DrainWorkerRequest)(nil), // 103: ateapi.DrainWorkerRequest - (*DrainWorkerResponse)(nil), // 104: ateapi.DrainWorkerResponse - (*MarkWorkerAvailableRequest)(nil), // 105: ateapi.MarkWorkerAvailableRequest - (*MarkWorkerAvailableResponse)(nil), // 106: ateapi.MarkWorkerAvailableResponse - (*MarkWorkerUnavailableRequest)(nil), // 107: ateapi.MarkWorkerUnavailableRequest - (*MarkWorkerUnavailableResponse)(nil), // 108: ateapi.MarkWorkerUnavailableResponse - (*ListActorsRequest)(nil), // 109: ateapi.ListActorsRequest - (*ListActorsResponse)(nil), // 110: ateapi.ListActorsResponse - (*Worker)(nil), // 111: ateapi.Worker - (*WorkerStatus)(nil), // 112: ateapi.WorkerStatus - (*SandboxRuntime)(nil), // 113: ateapi.SandboxRuntime - (*VersionedSandboxCompat)(nil), // 114: ateapi.VersionedSandboxCompat - (*AttributeEntry)(nil), // 115: ateapi.AttributeEntry - (*WorkerResources)(nil), // 116: ateapi.WorkerResources - (*ActorAssignment)(nil), // 117: ateapi.ActorAssignment - (*RegisterWorkerRequest)(nil), // 118: ateapi.RegisterWorkerRequest - (*RegisterWorkerResponse)(nil), // 119: ateapi.RegisterWorkerResponse - (*MintWorkerActorCertificateRequest)(nil), // 120: ateapi.MintWorkerActorCertificateRequest - (*MintWorkerActorCertificateResponse)(nil), // 121: ateapi.MintWorkerActorCertificateResponse - (*MintWorkerActorJwtRequest)(nil), // 122: ateapi.MintWorkerActorJwtRequest - (*MintWorkerActorJwtResponse)(nil), // 123: ateapi.MintWorkerActorJwtResponse - (*RequestActorSuspendRequest)(nil), // 124: ateapi.RequestActorSuspendRequest - (*RequestActorSuspendResponse)(nil), // 125: ateapi.RequestActorSuspendResponse - (*ReportActorSnapshotStateRequest)(nil), // 126: ateapi.ReportActorSnapshotStateRequest - (*ReportActorSnapshotStateResponse)(nil), // 127: ateapi.ReportActorSnapshotStateResponse - (*ActorSnapshotState)(nil), // 128: ateapi.ActorSnapshotState - (*SnapshotReport)(nil), // 129: ateapi.SnapshotReport - (*AccessPolicy)(nil), // 130: ateapi.AccessPolicy - (*Binding)(nil), // 131: ateapi.Binding - (*GetGlobalAccessPolicyRequest)(nil), // 132: ateapi.GetGlobalAccessPolicyRequest - (*CreateGlobalAccessPolicyRequest)(nil), // 133: ateapi.CreateGlobalAccessPolicyRequest - (*UpdateGlobalAccessPolicyRequest)(nil), // 134: ateapi.UpdateGlobalAccessPolicyRequest - (*GetAtespaceAccessPolicyRequest)(nil), // 135: ateapi.GetAtespaceAccessPolicyRequest - (*CreateAtespaceAccessPolicyRequest)(nil), // 136: ateapi.CreateAtespaceAccessPolicyRequest - (*UpdateAtespaceAccessPolicyRequest)(nil), // 137: ateapi.UpdateAtespaceAccessPolicyRequest - (*DeleteAtespaceAccessPolicyRequest)(nil), // 138: ateapi.DeleteAtespaceAccessPolicyRequest - (*ResourceAnnotation)(nil), // 139: ateapi.ResourceAnnotation - (*MethodAnnotation)(nil), // 140: ateapi.MethodAnnotation - nil, // 141: ateapi.Selector.MatchLabelsEntry - nil, // 142: ateapi.ExternalVolume.VolumeContextEntry - nil, // 143: ateapi.Worker.LabelsEntry - (*timestamppb.Timestamp)(nil), // 144: google.protobuf.Timestamp - (*descriptorpb.MessageOptions)(nil), // 145: google.protobuf.MessageOptions - (*descriptorpb.MethodOptions)(nil), // 146: google.protobuf.MethodOptions + (DeleteTrigger)(0), // 5: ateapi.DeleteTrigger + (AccessMode)(0), // 6: ateapi.AccessMode + (SharedVolumeState)(0), // 7: ateapi.SharedVolumeState + (GoldenSnapshotMode)(0), // 8: ateapi.GoldenSnapshotMode + (ActorMetadataField)(0), // 9: ateapi.ActorMetadataField + (WorkerState)(0), // 10: ateapi.WorkerState + (Scope)(0), // 11: ateapi.Scope + (*ObjectSnapshot)(nil), // 12: ateapi.ObjectSnapshot + (*SnapshotStorage)(nil), // 13: ateapi.SnapshotStorage + (*Snapshot)(nil), // 14: ateapi.Snapshot + (*Selector)(nil), // 15: ateapi.Selector + (*ResourceMetadata)(nil), // 16: ateapi.ResourceMetadata + (*ExternalVolumeRef)(nil), // 17: ateapi.ExternalVolumeRef + (*Actor)(nil), // 18: ateapi.Actor + (*EgressPolicy)(nil), // 19: ateapi.EgressPolicy + (*EgressRule)(nil), // 20: ateapi.EgressRule + (*HttpRule)(nil), // 21: ateapi.HttpRule + (*HttpsRule)(nil), // 22: ateapi.HttpsRule + (*TlsPassthroughRule)(nil), // 23: ateapi.TlsPassthroughRule + (*Ports)(nil), // 24: ateapi.Ports + (*AllPorts)(nil), // 25: ateapi.AllPorts + (*HttpRuleEffects)(nil), // 26: ateapi.HttpRuleEffects + (*CredentialHeader)(nil), // 27: ateapi.CredentialHeader + (*ActorJwtSource)(nil), // 28: ateapi.ActorJwtSource + (*ActorStatus)(nil), // 29: ateapi.ActorStatus + (*ActorCrash)(nil), // 30: ateapi.ActorCrash + (*WorkerAssignment)(nil), // 31: ateapi.WorkerAssignment + (*TagStatus)(nil), // 32: ateapi.TagStatus + (*Tag)(nil), // 33: ateapi.Tag + (*SharedVolume)(nil), // 34: ateapi.SharedVolume + (*SharedVolumeStatus)(nil), // 35: ateapi.SharedVolumeStatus + (*ActorRef)(nil), // 36: ateapi.ActorRef + (*Atespace)(nil), // 37: ateapi.Atespace + (*ObjectRef)(nil), // 38: ateapi.ObjectRef + (*ActorTemplate)(nil), // 39: ateapi.ActorTemplate + (*Resources)(nil), // 40: ateapi.Resources + (*Limits)(nil), // 41: ateapi.Limits + (*GoldenSnapshotStatus)(nil), // 42: ateapi.GoldenSnapshotStatus + (*ActorTemplateStatus)(nil), // 43: ateapi.ActorTemplateStatus + (*SandboxConfig)(nil), // 44: ateapi.SandboxConfig + (*SnapshotConfig)(nil), // 45: ateapi.SnapshotConfig + (*GoldenSnapshotConfig)(nil), // 46: ateapi.GoldenSnapshotConfig + (*Container)(nil), // 47: ateapi.Container + (*SecurityContext)(nil), // 48: ateapi.SecurityContext + (*Capabilities)(nil), // 49: ateapi.Capabilities + (*EnvVar)(nil), // 50: ateapi.EnvVar + (*WakeupProbe)(nil), // 51: ateapi.WakeupProbe + (*HttpGetAction)(nil), // 52: ateapi.HttpGetAction + (*Volume)(nil), // 53: ateapi.Volume + (*ExternalVolumeSource)(nil), // 54: ateapi.ExternalVolumeSource + (*ImageVolumeSource)(nil), // 55: ateapi.ImageVolumeSource + (*DurableDirVolumeSource)(nil), // 56: ateapi.DurableDirVolumeSource + (*ExternalVolumeTemplate)(nil), // 57: ateapi.ExternalVolumeTemplate + (*SharedVolumeRef)(nil), // 58: ateapi.SharedVolumeRef + (*SystemInfoVolumeSource)(nil), // 59: ateapi.SystemInfoVolumeSource + (*SystemInfoDataSource)(nil), // 60: ateapi.SystemInfoDataSource + (*ActorMetadataDataSource)(nil), // 61: ateapi.ActorMetadataDataSource + (*ActorMetadataItem)(nil), // 62: ateapi.ActorMetadataItem + (*TrustBundleDataSource)(nil), // 63: ateapi.TrustBundleDataSource + (*VolumeMount)(nil), // 64: ateapi.VolumeMount + (*CreateAtespaceRequest)(nil), // 65: ateapi.CreateAtespaceRequest + (*GetAtespaceRequest)(nil), // 66: ateapi.GetAtespaceRequest + (*ListAtespacesRequest)(nil), // 67: ateapi.ListAtespacesRequest + (*ListAtespacesResponse)(nil), // 68: ateapi.ListAtespacesResponse + (*DeleteAtespaceRequest)(nil), // 69: ateapi.DeleteAtespaceRequest + (*CreateActorTemplateRequest)(nil), // 70: ateapi.CreateActorTemplateRequest + (*GetActorTemplateRequest)(nil), // 71: ateapi.GetActorTemplateRequest + (*ListActorTemplatesRequest)(nil), // 72: ateapi.ListActorTemplatesRequest + (*ListActorTemplatesResponse)(nil), // 73: ateapi.ListActorTemplatesResponse + (*DeleteActorTemplateRequest)(nil), // 74: ateapi.DeleteActorTemplateRequest + (*GetActorRequest)(nil), // 75: ateapi.GetActorRequest + (*CreateActorRequest)(nil), // 76: ateapi.CreateActorRequest + (*UpdateActorRequest)(nil), // 77: ateapi.UpdateActorRequest + (*SuspendActorRequest)(nil), // 78: ateapi.SuspendActorRequest + (*SuspendActorResponse)(nil), // 79: ateapi.SuspendActorResponse + (*PauseActorRequest)(nil), // 80: ateapi.PauseActorRequest + (*PauseActorResponse)(nil), // 81: ateapi.PauseActorResponse + (*ResumeActorRequest)(nil), // 82: ateapi.ResumeActorRequest + (*ResumeActorResponse)(nil), // 83: ateapi.ResumeActorResponse + (*RevertActorRequest)(nil), // 84: ateapi.RevertActorRequest + (*RevertActorResponse)(nil), // 85: ateapi.RevertActorResponse + (*DeleteActorRequest)(nil), // 86: ateapi.DeleteActorRequest + (*GetActorEgressPolicyRequest)(nil), // 87: ateapi.GetActorEgressPolicyRequest + (*CreateActorEgressPolicyRequest)(nil), // 88: ateapi.CreateActorEgressPolicyRequest + (*UpdateActorEgressPolicyRequest)(nil), // 89: ateapi.UpdateActorEgressPolicyRequest + (*DeleteActorEgressPolicyRequest)(nil), // 90: ateapi.DeleteActorEgressPolicyRequest + (*GetTagRequest)(nil), // 91: ateapi.GetTagRequest + (*MintActorJwtRequest)(nil), // 92: ateapi.MintActorJwtRequest + (*MintActorJwtResponse)(nil), // 93: ateapi.MintActorJwtResponse + (*MintActorCertificateRequest)(nil), // 94: ateapi.MintActorCertificateRequest + (*MintActorCertificateResponse)(nil), // 95: ateapi.MintActorCertificateResponse + (*ListTagsRequest)(nil), // 96: ateapi.ListTagsRequest + (*ListTagsResponse)(nil), // 97: ateapi.ListTagsResponse + (*CreateTagRequest)(nil), // 98: ateapi.CreateTagRequest + (*UpdateTagRequest)(nil), // 99: ateapi.UpdateTagRequest + (*DeleteTagRequest)(nil), // 100: ateapi.DeleteTagRequest + (*CreateSharedVolumeRequest)(nil), // 101: ateapi.CreateSharedVolumeRequest + (*GetSharedVolumeRequest)(nil), // 102: ateapi.GetSharedVolumeRequest + (*ListSharedVolumesRequest)(nil), // 103: ateapi.ListSharedVolumesRequest + (*ListSharedVolumesResponse)(nil), // 104: ateapi.ListSharedVolumesResponse + (*UpdateSharedVolumeRequest)(nil), // 105: ateapi.UpdateSharedVolumeRequest + (*DeleteSharedVolumeRequest)(nil), // 106: ateapi.DeleteSharedVolumeRequest + (*DeleteOptions)(nil), // 107: ateapi.DeleteOptions + (*ListOptions)(nil), // 108: ateapi.ListOptions + (*ListWorkerActorAssignmentsRequest)(nil), // 109: ateapi.ListWorkerActorAssignmentsRequest + (*ListWorkerActorAssignmentsResponse)(nil), // 110: ateapi.ListWorkerActorAssignmentsResponse + (*ListWorkersRequest)(nil), // 111: ateapi.ListWorkersRequest + (*ListWorkersResponse)(nil), // 112: ateapi.ListWorkersResponse + (*GetWorkerRequest)(nil), // 113: ateapi.GetWorkerRequest + (*CreateWorkerRequest)(nil), // 114: ateapi.CreateWorkerRequest + (*UpdateWorkerRequest)(nil), // 115: ateapi.UpdateWorkerRequest + (*DeleteWorkerRequest)(nil), // 116: ateapi.DeleteWorkerRequest + (*DrainWorkerRequest)(nil), // 117: ateapi.DrainWorkerRequest + (*DrainWorkerResponse)(nil), // 118: ateapi.DrainWorkerResponse + (*MarkWorkerAvailableRequest)(nil), // 119: ateapi.MarkWorkerAvailableRequest + (*MarkWorkerAvailableResponse)(nil), // 120: ateapi.MarkWorkerAvailableResponse + (*MarkWorkerUnavailableRequest)(nil), // 121: ateapi.MarkWorkerUnavailableRequest + (*MarkWorkerUnavailableResponse)(nil), // 122: ateapi.MarkWorkerUnavailableResponse + (*ListActorsRequest)(nil), // 123: ateapi.ListActorsRequest + (*ListActorsResponse)(nil), // 124: ateapi.ListActorsResponse + (*Worker)(nil), // 125: ateapi.Worker + (*WorkerStatus)(nil), // 126: ateapi.WorkerStatus + (*SandboxRuntime)(nil), // 127: ateapi.SandboxRuntime + (*VersionedSandboxCompat)(nil), // 128: ateapi.VersionedSandboxCompat + (*AttributeEntry)(nil), // 129: ateapi.AttributeEntry + (*WorkerResources)(nil), // 130: ateapi.WorkerResources + (*ActorAssignment)(nil), // 131: ateapi.ActorAssignment + (*RegisterWorkerRequest)(nil), // 132: ateapi.RegisterWorkerRequest + (*RegisterWorkerResponse)(nil), // 133: ateapi.RegisterWorkerResponse + (*MintWorkerActorCertificateRequest)(nil), // 134: ateapi.MintWorkerActorCertificateRequest + (*MintWorkerActorCertificateResponse)(nil), // 135: ateapi.MintWorkerActorCertificateResponse + (*MintWorkerActorJwtRequest)(nil), // 136: ateapi.MintWorkerActorJwtRequest + (*MintWorkerActorJwtResponse)(nil), // 137: ateapi.MintWorkerActorJwtResponse + (*RequestActorSuspendRequest)(nil), // 138: ateapi.RequestActorSuspendRequest + (*RequestActorSuspendResponse)(nil), // 139: ateapi.RequestActorSuspendResponse + (*ReportActorSnapshotStateRequest)(nil), // 140: ateapi.ReportActorSnapshotStateRequest + (*ReportActorSnapshotStateResponse)(nil), // 141: ateapi.ReportActorSnapshotStateResponse + (*ActorSnapshotState)(nil), // 142: ateapi.ActorSnapshotState + (*SnapshotReport)(nil), // 143: ateapi.SnapshotReport + (*AccessPolicy)(nil), // 144: ateapi.AccessPolicy + (*Binding)(nil), // 145: ateapi.Binding + (*GetGlobalAccessPolicyRequest)(nil), // 146: ateapi.GetGlobalAccessPolicyRequest + (*CreateGlobalAccessPolicyRequest)(nil), // 147: ateapi.CreateGlobalAccessPolicyRequest + (*UpdateGlobalAccessPolicyRequest)(nil), // 148: ateapi.UpdateGlobalAccessPolicyRequest + (*GetAtespaceAccessPolicyRequest)(nil), // 149: ateapi.GetAtespaceAccessPolicyRequest + (*CreateAtespaceAccessPolicyRequest)(nil), // 150: ateapi.CreateAtespaceAccessPolicyRequest + (*UpdateAtespaceAccessPolicyRequest)(nil), // 151: ateapi.UpdateAtespaceAccessPolicyRequest + (*DeleteAtespaceAccessPolicyRequest)(nil), // 152: ateapi.DeleteAtespaceAccessPolicyRequest + (*ResourceAnnotation)(nil), // 153: ateapi.ResourceAnnotation + (*MethodAnnotation)(nil), // 154: ateapi.MethodAnnotation + nil, // 155: ateapi.Selector.MatchLabelsEntry + nil, // 156: ateapi.ExternalVolumeRef.VolumeContextEntry + nil, // 157: ateapi.Actor.SharedVolumeBindingsEntry + nil, // 158: ateapi.SharedVolume.VolumeContextEntry + nil, // 159: ateapi.Worker.LabelsEntry + (*timestamppb.Timestamp)(nil), // 160: google.protobuf.Timestamp + (*descriptorpb.MessageOptions)(nil), // 161: google.protobuf.MessageOptions + (*descriptorpb.MethodOptions)(nil), // 162: google.protobuf.MethodOptions } var file_ateapi_proto_depIdxs = []int32{ 1, // 0: ateapi.SnapshotStorage.status:type_name -> ateapi.SnapshotStorageStatus - 9, // 1: ateapi.SnapshotStorage.object:type_name -> ateapi.ObjectSnapshot + 12, // 1: ateapi.SnapshotStorage.object:type_name -> ateapi.ObjectSnapshot 0, // 2: ateapi.Snapshot.owner:type_name -> ateapi.SnapshotOwner 2, // 3: ateapi.Snapshot.fidelity:type_name -> ateapi.SnapshotFidelity - 10, // 4: ateapi.Snapshot.durable_snapshot:type_name -> ateapi.SnapshotStorage - 113, // 5: ateapi.Snapshot.sandbox_runtime:type_name -> ateapi.SandboxRuntime - 141, // 6: ateapi.Selector.match_labels:type_name -> ateapi.Selector.MatchLabelsEntry - 144, // 7: ateapi.ResourceMetadata.create_time:type_name -> google.protobuf.Timestamp - 144, // 8: ateapi.ResourceMetadata.update_time:type_name -> google.protobuf.Timestamp - 142, // 9: ateapi.ExternalVolume.volume_context:type_name -> ateapi.ExternalVolume.VolumeContextEntry - 13, // 10: ateapi.Actor.metadata:type_name -> ateapi.ResourceMetadata - 32, // 11: ateapi.Actor.actor_template:type_name -> ateapi.ObjectRef - 12, // 12: ateapi.Actor.worker_selector:type_name -> ateapi.Selector - 32, // 13: ateapi.Actor.source_tag:type_name -> ateapi.ObjectRef - 26, // 14: ateapi.Actor.status:type_name -> ateapi.ActorStatus - 13, // 15: ateapi.EgressPolicy.metadata:type_name -> ateapi.ResourceMetadata - 17, // 16: ateapi.EgressPolicy.rules:type_name -> ateapi.EgressRule - 18, // 17: ateapi.EgressRule.http:type_name -> ateapi.HttpRule - 19, // 18: ateapi.EgressRule.https:type_name -> ateapi.HttpsRule - 20, // 19: ateapi.EgressRule.tls_passthrough:type_name -> ateapi.TlsPassthroughRule - 21, // 20: ateapi.HttpRule.ports:type_name -> ateapi.Ports - 23, // 21: ateapi.HttpRule.effects:type_name -> ateapi.HttpRuleEffects - 21, // 22: ateapi.HttpsRule.ports:type_name -> ateapi.Ports - 23, // 23: ateapi.HttpsRule.effects:type_name -> ateapi.HttpRuleEffects - 21, // 24: ateapi.TlsPassthroughRule.ports:type_name -> ateapi.Ports - 22, // 25: ateapi.Ports.all:type_name -> ateapi.AllPorts - 24, // 26: ateapi.HttpRuleEffects.replace_headers:type_name -> ateapi.CredentialHeader - 25, // 27: ateapi.CredentialHeader.actor_jwt:type_name -> ateapi.ActorJwtSource - 4, // 28: ateapi.ActorStatus.state:type_name -> ateapi.ActorState - 28, // 29: ateapi.ActorStatus.worker_assignment:type_name -> ateapi.WorkerAssignment - 14, // 30: ateapi.ActorStatus.external_volumes:type_name -> ateapi.ExternalVolume - 27, // 31: ateapi.ActorStatus.crash:type_name -> ateapi.ActorCrash - 11, // 32: ateapi.ActorStatus.snapshots:type_name -> ateapi.Snapshot - 144, // 33: ateapi.ActorCrash.crash_time:type_name -> google.protobuf.Timestamp - 32, // 34: ateapi.WorkerAssignment.worker:type_name -> ateapi.ObjectRef - 113, // 35: ateapi.WorkerAssignment.sandbox_runtime:type_name -> ateapi.SandboxRuntime - 11, // 36: ateapi.TagStatus.snapshot:type_name -> ateapi.Snapshot - 13, // 37: ateapi.Tag.metadata:type_name -> ateapi.ResourceMetadata - 29, // 38: ateapi.Tag.status:type_name -> ateapi.TagStatus - 3, // 39: ateapi.Tag.scope:type_name -> ateapi.TagScope - 32, // 40: ateapi.Tag.source_actor:type_name -> ateapi.ObjectRef - 13, // 41: ateapi.Atespace.metadata:type_name -> ateapi.ResourceMetadata - 13, // 42: ateapi.ActorTemplate.metadata:type_name -> ateapi.ResourceMetadata - 12, // 43: ateapi.ActorTemplate.worker_selector:type_name -> ateapi.Selector - 41, // 44: ateapi.ActorTemplate.containers:type_name -> ateapi.Container - 47, // 45: ateapi.ActorTemplate.volumes:type_name -> ateapi.Volume - 39, // 46: ateapi.ActorTemplate.snapshot_config:type_name -> ateapi.SnapshotConfig - 38, // 47: ateapi.ActorTemplate.sandbox_config:type_name -> ateapi.SandboxConfig - 34, // 48: ateapi.ActorTemplate.resources:type_name -> ateapi.Resources - 37, // 49: ateapi.ActorTemplate.status:type_name -> ateapi.ActorTemplateStatus - 35, // 50: ateapi.Resources.limits:type_name -> ateapi.Limits - 32, // 51: ateapi.GoldenSnapshotStatus.golden_tag:type_name -> ateapi.ObjectRef - 144, // 52: ateapi.GoldenSnapshotStatus.snapshot_time:type_name -> google.protobuf.Timestamp - 36, // 53: ateapi.ActorTemplateStatus.golden_snapshot_status:type_name -> ateapi.GoldenSnapshotStatus - 2, // 54: ateapi.SnapshotConfig.preferred_fidelity:type_name -> ateapi.SnapshotFidelity - 40, // 55: ateapi.SnapshotConfig.golden_snapshot_config:type_name -> ateapi.GoldenSnapshotConfig - 5, // 56: ateapi.GoldenSnapshotConfig.mode:type_name -> ateapi.GoldenSnapshotMode - 44, // 57: ateapi.Container.env:type_name -> ateapi.EnvVar - 45, // 58: ateapi.Container.wakeup_probe:type_name -> ateapi.WakeupProbe - 56, // 59: ateapi.Container.volume_mounts:type_name -> ateapi.VolumeMount - 42, // 60: ateapi.Container.security_context:type_name -> ateapi.SecurityContext - 34, // 61: ateapi.Container.resources:type_name -> ateapi.Resources - 43, // 62: ateapi.SecurityContext.capabilities:type_name -> ateapi.Capabilities - 46, // 63: ateapi.WakeupProbe.http_get:type_name -> ateapi.HttpGetAction - 49, // 64: ateapi.Volume.durable_dir:type_name -> ateapi.DurableDirVolumeSource - 50, // 65: ateapi.Volume.external_volume_template:type_name -> ateapi.ExternalVolumeTemplate - 51, // 66: ateapi.Volume.system_info:type_name -> ateapi.SystemInfoVolumeSource - 48, // 67: ateapi.Volume.image:type_name -> ateapi.ImageVolumeSource - 52, // 68: ateapi.SystemInfoVolumeSource.data_sources:type_name -> ateapi.SystemInfoDataSource - 53, // 69: ateapi.SystemInfoDataSource.actor_metadata:type_name -> ateapi.ActorMetadataDataSource - 55, // 70: ateapi.SystemInfoDataSource.trust_bundle:type_name -> ateapi.TrustBundleDataSource - 54, // 71: ateapi.ActorMetadataDataSource.items:type_name -> ateapi.ActorMetadataItem - 6, // 72: ateapi.ActorMetadataItem.field:type_name -> ateapi.ActorMetadataField - 31, // 73: ateapi.CreateAtespaceRequest.atespace:type_name -> ateapi.Atespace - 32, // 74: ateapi.GetAtespaceRequest.atespace:type_name -> ateapi.ObjectRef - 94, // 75: ateapi.ListAtespacesRequest.options:type_name -> ateapi.ListOptions - 31, // 76: ateapi.ListAtespacesResponse.atespaces:type_name -> ateapi.Atespace - 32, // 77: ateapi.DeleteAtespaceRequest.atespace:type_name -> ateapi.ObjectRef - 93, // 78: ateapi.DeleteAtespaceRequest.options:type_name -> ateapi.DeleteOptions - 33, // 79: ateapi.CreateActorTemplateRequest.actor_template:type_name -> ateapi.ActorTemplate - 32, // 80: ateapi.GetActorTemplateRequest.actor_template:type_name -> ateapi.ObjectRef - 94, // 81: ateapi.ListActorTemplatesRequest.options:type_name -> ateapi.ListOptions - 33, // 82: ateapi.ListActorTemplatesResponse.actor_templates:type_name -> ateapi.ActorTemplate - 32, // 83: ateapi.DeleteActorTemplateRequest.actor_template:type_name -> ateapi.ObjectRef - 93, // 84: ateapi.DeleteActorTemplateRequest.options:type_name -> ateapi.DeleteOptions - 32, // 85: ateapi.GetActorRequest.actor:type_name -> ateapi.ObjectRef - 15, // 86: ateapi.CreateActorRequest.actor:type_name -> ateapi.Actor - 15, // 87: ateapi.UpdateActorRequest.actor:type_name -> ateapi.Actor - 32, // 88: ateapi.SuspendActorRequest.actor:type_name -> ateapi.ObjectRef - 15, // 89: ateapi.SuspendActorResponse.actor:type_name -> ateapi.Actor - 32, // 90: ateapi.PauseActorRequest.actor:type_name -> ateapi.ObjectRef - 15, // 91: ateapi.PauseActorResponse.actor:type_name -> ateapi.Actor - 32, // 92: ateapi.ResumeActorRequest.actor:type_name -> ateapi.ObjectRef - 15, // 93: ateapi.ResumeActorResponse.actor:type_name -> ateapi.Actor - 32, // 94: ateapi.RevertActorRequest.actor:type_name -> ateapi.ObjectRef - 15, // 95: ateapi.RevertActorResponse.actor:type_name -> ateapi.Actor - 32, // 96: ateapi.DeleteActorRequest.actor:type_name -> ateapi.ObjectRef - 93, // 97: ateapi.DeleteActorRequest.options:type_name -> ateapi.DeleteOptions - 32, // 98: ateapi.GetActorEgressPolicyRequest.actor:type_name -> ateapi.ObjectRef - 32, // 99: ateapi.CreateActorEgressPolicyRequest.actor:type_name -> ateapi.ObjectRef - 16, // 100: ateapi.CreateActorEgressPolicyRequest.egress_policy:type_name -> ateapi.EgressPolicy - 32, // 101: ateapi.UpdateActorEgressPolicyRequest.actor:type_name -> ateapi.ObjectRef - 16, // 102: ateapi.UpdateActorEgressPolicyRequest.egress_policy:type_name -> ateapi.EgressPolicy - 32, // 103: ateapi.DeleteActorEgressPolicyRequest.actor:type_name -> ateapi.ObjectRef - 93, // 104: ateapi.DeleteActorEgressPolicyRequest.options:type_name -> ateapi.DeleteOptions - 32, // 105: ateapi.GetTagRequest.tag:type_name -> ateapi.ObjectRef - 32, // 106: ateapi.MintActorJwtRequest.actor:type_name -> ateapi.ObjectRef - 144, // 107: ateapi.MintActorJwtResponse.expire_time:type_name -> google.protobuf.Timestamp - 32, // 108: ateapi.MintActorCertificateRequest.actor:type_name -> ateapi.ObjectRef - 94, // 109: ateapi.ListTagsRequest.options:type_name -> ateapi.ListOptions - 30, // 110: ateapi.ListTagsResponse.tags:type_name -> ateapi.Tag - 30, // 111: ateapi.CreateTagRequest.tag:type_name -> ateapi.Tag - 30, // 112: ateapi.UpdateTagRequest.tag:type_name -> ateapi.Tag - 32, // 113: ateapi.DeleteTagRequest.tag:type_name -> ateapi.ObjectRef - 93, // 114: ateapi.DeleteTagRequest.options:type_name -> ateapi.DeleteOptions - 32, // 115: ateapi.ListWorkerActorAssignmentsRequest.worker:type_name -> ateapi.ObjectRef - 94, // 116: ateapi.ListWorkerActorAssignmentsRequest.options:type_name -> ateapi.ListOptions - 117, // 117: ateapi.ListWorkerActorAssignmentsResponse.actor_assignments:type_name -> ateapi.ActorAssignment - 94, // 118: ateapi.ListWorkersRequest.options:type_name -> ateapi.ListOptions - 111, // 119: ateapi.ListWorkersResponse.workers:type_name -> ateapi.Worker - 32, // 120: ateapi.GetWorkerRequest.worker:type_name -> ateapi.ObjectRef - 111, // 121: ateapi.CreateWorkerRequest.worker:type_name -> ateapi.Worker - 111, // 122: ateapi.UpdateWorkerRequest.worker:type_name -> ateapi.Worker - 32, // 123: ateapi.DeleteWorkerRequest.worker:type_name -> ateapi.ObjectRef - 93, // 124: ateapi.DeleteWorkerRequest.options:type_name -> ateapi.DeleteOptions - 32, // 125: ateapi.DrainWorkerRequest.worker:type_name -> ateapi.ObjectRef - 111, // 126: ateapi.DrainWorkerResponse.worker:type_name -> ateapi.Worker - 32, // 127: ateapi.MarkWorkerAvailableRequest.worker:type_name -> ateapi.ObjectRef - 111, // 128: ateapi.MarkWorkerAvailableResponse.worker:type_name -> ateapi.Worker - 32, // 129: ateapi.MarkWorkerUnavailableRequest.worker:type_name -> ateapi.ObjectRef - 111, // 130: ateapi.MarkWorkerUnavailableResponse.worker:type_name -> ateapi.Worker - 94, // 131: ateapi.ListActorsRequest.options:type_name -> ateapi.ListOptions - 15, // 132: ateapi.ListActorsResponse.actors:type_name -> ateapi.Actor - 13, // 133: ateapi.Worker.metadata:type_name -> ateapi.ResourceMetadata - 143, // 134: ateapi.Worker.labels:type_name -> ateapi.Worker.LabelsEntry - 112, // 135: ateapi.Worker.status:type_name -> ateapi.WorkerStatus - 7, // 136: ateapi.WorkerStatus.state:type_name -> ateapi.WorkerState - 116, // 137: ateapi.WorkerStatus.capacity:type_name -> ateapi.WorkerResources - 116, // 138: ateapi.WorkerStatus.allocated:type_name -> ateapi.WorkerResources - 113, // 139: ateapi.WorkerStatus.default_runtime:type_name -> ateapi.SandboxRuntime - 113, // 140: ateapi.WorkerStatus.restorable_runtimes:type_name -> ateapi.SandboxRuntime - 114, // 141: ateapi.SandboxRuntime.version:type_name -> ateapi.VersionedSandboxCompat - 115, // 142: ateapi.VersionedSandboxCompat.attributes:type_name -> ateapi.AttributeEntry - 34, // 143: ateapi.WorkerResources.resources:type_name -> ateapi.Resources - 13, // 144: ateapi.ActorAssignment.metadata:type_name -> ateapi.ResourceMetadata - 32, // 145: ateapi.ActorAssignment.actor:type_name -> ateapi.ObjectRef - 34, // 146: ateapi.ActorAssignment.resources:type_name -> ateapi.Resources - 32, // 147: ateapi.RegisterWorkerRequest.worker:type_name -> ateapi.ObjectRef - 116, // 148: ateapi.RegisterWorkerRequest.capacity:type_name -> ateapi.WorkerResources - 113, // 149: ateapi.RegisterWorkerRequest.default_runtime:type_name -> ateapi.SandboxRuntime - 113, // 150: ateapi.RegisterWorkerRequest.restorable_runtimes:type_name -> ateapi.SandboxRuntime - 111, // 151: ateapi.RegisterWorkerResponse.worker:type_name -> ateapi.Worker - 32, // 152: ateapi.MintWorkerActorCertificateRequest.actor:type_name -> ateapi.ObjectRef - 32, // 153: ateapi.MintWorkerActorJwtRequest.actor:type_name -> ateapi.ObjectRef - 144, // 154: ateapi.MintWorkerActorJwtResponse.expire_time:type_name -> google.protobuf.Timestamp - 32, // 155: ateapi.RequestActorSuspendRequest.worker:type_name -> ateapi.ObjectRef - 32, // 156: ateapi.RequestActorSuspendRequest.actor:type_name -> ateapi.ObjectRef - 15, // 157: ateapi.RequestActorSuspendResponse.actor:type_name -> ateapi.Actor - 32, // 158: ateapi.ReportActorSnapshotStateRequest.actor:type_name -> ateapi.ObjectRef - 128, // 159: ateapi.ReportActorSnapshotStateRequest.state:type_name -> ateapi.ActorSnapshotState - 129, // 160: ateapi.ActorSnapshotState.latest_local:type_name -> ateapi.SnapshotReport - 129, // 161: ateapi.ActorSnapshotState.uploading:type_name -> ateapi.SnapshotReport - 129, // 162: ateapi.ActorSnapshotState.latest_durable:type_name -> ateapi.SnapshotReport - 144, // 163: ateapi.ActorSnapshotState.upload_due_time:type_name -> google.protobuf.Timestamp - 2, // 164: ateapi.SnapshotReport.fidelity:type_name -> ateapi.SnapshotFidelity - 9, // 165: ateapi.SnapshotReport.object:type_name -> ateapi.ObjectSnapshot - 144, // 166: ateapi.SnapshotReport.local_commit_time:type_name -> google.protobuf.Timestamp - 144, // 167: ateapi.SnapshotReport.durable_complete_time:type_name -> google.protobuf.Timestamp - 13, // 168: ateapi.AccessPolicy.metadata:type_name -> ateapi.ResourceMetadata - 131, // 169: ateapi.AccessPolicy.bindings:type_name -> ateapi.Binding - 130, // 170: ateapi.CreateGlobalAccessPolicyRequest.access_policy:type_name -> ateapi.AccessPolicy - 130, // 171: ateapi.UpdateGlobalAccessPolicyRequest.access_policy:type_name -> ateapi.AccessPolicy - 32, // 172: ateapi.GetAtespaceAccessPolicyRequest.atespace:type_name -> ateapi.ObjectRef - 32, // 173: ateapi.CreateAtespaceAccessPolicyRequest.atespace:type_name -> ateapi.ObjectRef - 130, // 174: ateapi.CreateAtespaceAccessPolicyRequest.access_policy:type_name -> ateapi.AccessPolicy - 32, // 175: ateapi.UpdateAtespaceAccessPolicyRequest.atespace:type_name -> ateapi.ObjectRef - 130, // 176: ateapi.UpdateAtespaceAccessPolicyRequest.access_policy:type_name -> ateapi.AccessPolicy - 32, // 177: ateapi.DeleteAtespaceAccessPolicyRequest.atespace:type_name -> ateapi.ObjectRef - 93, // 178: ateapi.DeleteAtespaceAccessPolicyRequest.options:type_name -> ateapi.DeleteOptions - 8, // 179: ateapi.ResourceAnnotation.scope:type_name -> ateapi.Scope - 145, // 180: ateapi.resource:extendee -> google.protobuf.MessageOptions - 146, // 181: ateapi.method:extendee -> google.protobuf.MethodOptions - 139, // 182: ateapi.resource:type_name -> ateapi.ResourceAnnotation - 140, // 183: ateapi.method:type_name -> ateapi.MethodAnnotation - 67, // 184: ateapi.Control.GetActor:input_type -> ateapi.GetActorRequest - 68, // 185: ateapi.Control.CreateActor:input_type -> ateapi.CreateActorRequest - 69, // 186: ateapi.Control.UpdateActor:input_type -> ateapi.UpdateActorRequest - 70, // 187: ateapi.Control.SuspendActor:input_type -> ateapi.SuspendActorRequest - 72, // 188: ateapi.Control.PauseActor:input_type -> ateapi.PauseActorRequest - 74, // 189: ateapi.Control.ResumeActor:input_type -> ateapi.ResumeActorRequest - 76, // 190: ateapi.Control.RevertActor:input_type -> ateapi.RevertActorRequest - 78, // 191: ateapi.Control.DeleteActor:input_type -> ateapi.DeleteActorRequest - 79, // 192: ateapi.Control.GetActorEgressPolicy:input_type -> ateapi.GetActorEgressPolicyRequest - 80, // 193: ateapi.Control.CreateActorEgressPolicy:input_type -> ateapi.CreateActorEgressPolicyRequest - 81, // 194: ateapi.Control.UpdateActorEgressPolicy:input_type -> ateapi.UpdateActorEgressPolicyRequest - 82, // 195: ateapi.Control.DeleteActorEgressPolicy:input_type -> ateapi.DeleteActorEgressPolicyRequest - 84, // 196: ateapi.Control.MintActorJwt:input_type -> ateapi.MintActorJwtRequest - 86, // 197: ateapi.Control.MintActorCertificate:input_type -> ateapi.MintActorCertificateRequest - 90, // 198: ateapi.Control.CreateTag:input_type -> ateapi.CreateTagRequest - 83, // 199: ateapi.Control.GetTag:input_type -> ateapi.GetTagRequest - 88, // 200: ateapi.Control.ListTags:input_type -> ateapi.ListTagsRequest - 91, // 201: ateapi.Control.UpdateTag:input_type -> ateapi.UpdateTagRequest - 92, // 202: ateapi.Control.DeleteTag:input_type -> ateapi.DeleteTagRequest - 97, // 203: ateapi.Control.ListWorkers:input_type -> ateapi.ListWorkersRequest - 99, // 204: ateapi.Control.GetWorker:input_type -> ateapi.GetWorkerRequest - 100, // 205: ateapi.Control.CreateWorker:input_type -> ateapi.CreateWorkerRequest - 101, // 206: ateapi.Control.UpdateWorker:input_type -> ateapi.UpdateWorkerRequest - 102, // 207: ateapi.Control.DeleteWorker:input_type -> ateapi.DeleteWorkerRequest - 103, // 208: ateapi.Control.DrainWorker:input_type -> ateapi.DrainWorkerRequest - 105, // 209: ateapi.Control.MarkWorkerAvailable:input_type -> ateapi.MarkWorkerAvailableRequest - 107, // 210: ateapi.Control.MarkWorkerUnavailable:input_type -> ateapi.MarkWorkerUnavailableRequest - 95, // 211: ateapi.Control.ListWorkerActorAssignments:input_type -> ateapi.ListWorkerActorAssignmentsRequest - 109, // 212: ateapi.Control.ListActors:input_type -> ateapi.ListActorsRequest - 57, // 213: ateapi.Control.CreateAtespace:input_type -> ateapi.CreateAtespaceRequest - 58, // 214: ateapi.Control.GetAtespace:input_type -> ateapi.GetAtespaceRequest - 59, // 215: ateapi.Control.ListAtespaces:input_type -> ateapi.ListAtespacesRequest - 61, // 216: ateapi.Control.DeleteAtespace:input_type -> ateapi.DeleteAtespaceRequest - 62, // 217: ateapi.Control.CreateActorTemplate:input_type -> ateapi.CreateActorTemplateRequest - 63, // 218: ateapi.Control.GetActorTemplate:input_type -> ateapi.GetActorTemplateRequest - 64, // 219: ateapi.Control.ListActorTemplates:input_type -> ateapi.ListActorTemplatesRequest - 66, // 220: ateapi.Control.DeleteActorTemplate:input_type -> ateapi.DeleteActorTemplateRequest - 132, // 221: ateapi.Control.GetGlobalAccessPolicy:input_type -> ateapi.GetGlobalAccessPolicyRequest - 133, // 222: ateapi.Control.CreateGlobalAccessPolicy:input_type -> ateapi.CreateGlobalAccessPolicyRequest - 134, // 223: ateapi.Control.UpdateGlobalAccessPolicy:input_type -> ateapi.UpdateGlobalAccessPolicyRequest - 135, // 224: ateapi.Control.GetAtespaceAccessPolicy:input_type -> ateapi.GetAtespaceAccessPolicyRequest - 136, // 225: ateapi.Control.CreateAtespaceAccessPolicy:input_type -> ateapi.CreateAtespaceAccessPolicyRequest - 137, // 226: ateapi.Control.UpdateAtespaceAccessPolicy:input_type -> ateapi.UpdateAtespaceAccessPolicyRequest - 138, // 227: ateapi.Control.DeleteAtespaceAccessPolicy:input_type -> ateapi.DeleteAtespaceAccessPolicyRequest - 118, // 228: ateapi.WorkerService.RegisterWorker:input_type -> ateapi.RegisterWorkerRequest - 120, // 229: ateapi.WorkerService.MintWorkerActorCertificate:input_type -> ateapi.MintWorkerActorCertificateRequest - 122, // 230: ateapi.WorkerService.MintWorkerActorJwt:input_type -> ateapi.MintWorkerActorJwtRequest - 124, // 231: ateapi.WorkerService.RequestActorSuspend:input_type -> ateapi.RequestActorSuspendRequest - 126, // 232: ateapi.WorkerService.ReportActorSnapshotState:input_type -> ateapi.ReportActorSnapshotStateRequest - 15, // 233: ateapi.Control.GetActor:output_type -> ateapi.Actor - 15, // 234: ateapi.Control.CreateActor:output_type -> ateapi.Actor - 15, // 235: ateapi.Control.UpdateActor:output_type -> ateapi.Actor - 71, // 236: ateapi.Control.SuspendActor:output_type -> ateapi.SuspendActorResponse - 73, // 237: ateapi.Control.PauseActor:output_type -> ateapi.PauseActorResponse - 75, // 238: ateapi.Control.ResumeActor:output_type -> ateapi.ResumeActorResponse - 77, // 239: ateapi.Control.RevertActor:output_type -> ateapi.RevertActorResponse - 15, // 240: ateapi.Control.DeleteActor:output_type -> ateapi.Actor - 16, // 241: ateapi.Control.GetActorEgressPolicy:output_type -> ateapi.EgressPolicy - 16, // 242: ateapi.Control.CreateActorEgressPolicy:output_type -> ateapi.EgressPolicy - 16, // 243: ateapi.Control.UpdateActorEgressPolicy:output_type -> ateapi.EgressPolicy - 16, // 244: ateapi.Control.DeleteActorEgressPolicy:output_type -> ateapi.EgressPolicy - 85, // 245: ateapi.Control.MintActorJwt:output_type -> ateapi.MintActorJwtResponse - 87, // 246: ateapi.Control.MintActorCertificate:output_type -> ateapi.MintActorCertificateResponse - 30, // 247: ateapi.Control.CreateTag:output_type -> ateapi.Tag - 30, // 248: ateapi.Control.GetTag:output_type -> ateapi.Tag - 89, // 249: ateapi.Control.ListTags:output_type -> ateapi.ListTagsResponse - 30, // 250: ateapi.Control.UpdateTag:output_type -> ateapi.Tag - 30, // 251: ateapi.Control.DeleteTag:output_type -> ateapi.Tag - 98, // 252: ateapi.Control.ListWorkers:output_type -> ateapi.ListWorkersResponse - 111, // 253: ateapi.Control.GetWorker:output_type -> ateapi.Worker - 111, // 254: ateapi.Control.CreateWorker:output_type -> ateapi.Worker - 111, // 255: ateapi.Control.UpdateWorker:output_type -> ateapi.Worker - 111, // 256: ateapi.Control.DeleteWorker:output_type -> ateapi.Worker - 104, // 257: ateapi.Control.DrainWorker:output_type -> ateapi.DrainWorkerResponse - 106, // 258: ateapi.Control.MarkWorkerAvailable:output_type -> ateapi.MarkWorkerAvailableResponse - 108, // 259: ateapi.Control.MarkWorkerUnavailable:output_type -> ateapi.MarkWorkerUnavailableResponse - 96, // 260: ateapi.Control.ListWorkerActorAssignments:output_type -> ateapi.ListWorkerActorAssignmentsResponse - 110, // 261: ateapi.Control.ListActors:output_type -> ateapi.ListActorsResponse - 31, // 262: ateapi.Control.CreateAtespace:output_type -> ateapi.Atespace - 31, // 263: ateapi.Control.GetAtespace:output_type -> ateapi.Atespace - 60, // 264: ateapi.Control.ListAtespaces:output_type -> ateapi.ListAtespacesResponse - 31, // 265: ateapi.Control.DeleteAtespace:output_type -> ateapi.Atespace - 33, // 266: ateapi.Control.CreateActorTemplate:output_type -> ateapi.ActorTemplate - 33, // 267: ateapi.Control.GetActorTemplate:output_type -> ateapi.ActorTemplate - 65, // 268: ateapi.Control.ListActorTemplates:output_type -> ateapi.ListActorTemplatesResponse - 33, // 269: ateapi.Control.DeleteActorTemplate:output_type -> ateapi.ActorTemplate - 130, // 270: ateapi.Control.GetGlobalAccessPolicy:output_type -> ateapi.AccessPolicy - 130, // 271: ateapi.Control.CreateGlobalAccessPolicy:output_type -> ateapi.AccessPolicy - 130, // 272: ateapi.Control.UpdateGlobalAccessPolicy:output_type -> ateapi.AccessPolicy - 130, // 273: ateapi.Control.GetAtespaceAccessPolicy:output_type -> ateapi.AccessPolicy - 130, // 274: ateapi.Control.CreateAtespaceAccessPolicy:output_type -> ateapi.AccessPolicy - 130, // 275: ateapi.Control.UpdateAtespaceAccessPolicy:output_type -> ateapi.AccessPolicy - 130, // 276: ateapi.Control.DeleteAtespaceAccessPolicy:output_type -> ateapi.AccessPolicy - 119, // 277: ateapi.WorkerService.RegisterWorker:output_type -> ateapi.RegisterWorkerResponse - 121, // 278: ateapi.WorkerService.MintWorkerActorCertificate:output_type -> ateapi.MintWorkerActorCertificateResponse - 123, // 279: ateapi.WorkerService.MintWorkerActorJwt:output_type -> ateapi.MintWorkerActorJwtResponse - 125, // 280: ateapi.WorkerService.RequestActorSuspend:output_type -> ateapi.RequestActorSuspendResponse - 127, // 281: ateapi.WorkerService.ReportActorSnapshotState:output_type -> ateapi.ReportActorSnapshotStateResponse - 233, // [233:282] is the sub-list for method output_type - 184, // [184:233] is the sub-list for method input_type - 182, // [182:184] is the sub-list for extension type_name - 180, // [180:182] is the sub-list for extension extendee - 0, // [0:180] is the sub-list for field type_name + 13, // 4: ateapi.Snapshot.durable_snapshot:type_name -> ateapi.SnapshotStorage + 127, // 5: ateapi.Snapshot.sandbox_runtime:type_name -> ateapi.SandboxRuntime + 155, // 6: ateapi.Selector.match_labels:type_name -> ateapi.Selector.MatchLabelsEntry + 160, // 7: ateapi.ResourceMetadata.create_time:type_name -> google.protobuf.Timestamp + 160, // 8: ateapi.ResourceMetadata.update_time:type_name -> google.protobuf.Timestamp + 156, // 9: ateapi.ExternalVolumeRef.volume_context:type_name -> ateapi.ExternalVolumeRef.VolumeContextEntry + 16, // 10: ateapi.Actor.metadata:type_name -> ateapi.ResourceMetadata + 38, // 11: ateapi.Actor.actor_template:type_name -> ateapi.ObjectRef + 15, // 12: ateapi.Actor.worker_selector:type_name -> ateapi.Selector + 38, // 13: ateapi.Actor.source_tag:type_name -> ateapi.ObjectRef + 29, // 14: ateapi.Actor.status:type_name -> ateapi.ActorStatus + 157, // 15: ateapi.Actor.shared_volume_bindings:type_name -> ateapi.Actor.SharedVolumeBindingsEntry + 16, // 16: ateapi.EgressPolicy.metadata:type_name -> ateapi.ResourceMetadata + 20, // 17: ateapi.EgressPolicy.rules:type_name -> ateapi.EgressRule + 21, // 18: ateapi.EgressRule.http:type_name -> ateapi.HttpRule + 22, // 19: ateapi.EgressRule.https:type_name -> ateapi.HttpsRule + 23, // 20: ateapi.EgressRule.tls_passthrough:type_name -> ateapi.TlsPassthroughRule + 24, // 21: ateapi.HttpRule.ports:type_name -> ateapi.Ports + 26, // 22: ateapi.HttpRule.effects:type_name -> ateapi.HttpRuleEffects + 24, // 23: ateapi.HttpsRule.ports:type_name -> ateapi.Ports + 26, // 24: ateapi.HttpsRule.effects:type_name -> ateapi.HttpRuleEffects + 24, // 25: ateapi.TlsPassthroughRule.ports:type_name -> ateapi.Ports + 25, // 26: ateapi.Ports.all:type_name -> ateapi.AllPorts + 27, // 27: ateapi.HttpRuleEffects.replace_headers:type_name -> ateapi.CredentialHeader + 28, // 28: ateapi.CredentialHeader.actor_jwt:type_name -> ateapi.ActorJwtSource + 4, // 29: ateapi.ActorStatus.state:type_name -> ateapi.ActorState + 31, // 30: ateapi.ActorStatus.worker_assignment:type_name -> ateapi.WorkerAssignment + 17, // 31: ateapi.ActorStatus.external_volumes:type_name -> ateapi.ExternalVolumeRef + 30, // 32: ateapi.ActorStatus.crash:type_name -> ateapi.ActorCrash + 14, // 33: ateapi.ActorStatus.snapshots:type_name -> ateapi.Snapshot + 160, // 34: ateapi.ActorCrash.crash_time:type_name -> google.protobuf.Timestamp + 38, // 35: ateapi.WorkerAssignment.worker:type_name -> ateapi.ObjectRef + 127, // 36: ateapi.WorkerAssignment.sandbox_runtime:type_name -> ateapi.SandboxRuntime + 14, // 37: ateapi.TagStatus.snapshot:type_name -> ateapi.Snapshot + 16, // 38: ateapi.Tag.metadata:type_name -> ateapi.ResourceMetadata + 32, // 39: ateapi.Tag.status:type_name -> ateapi.TagStatus + 3, // 40: ateapi.Tag.scope:type_name -> ateapi.TagScope + 38, // 41: ateapi.Tag.source_actor:type_name -> ateapi.ObjectRef + 16, // 42: ateapi.SharedVolume.metadata:type_name -> ateapi.ResourceMetadata + 5, // 43: ateapi.SharedVolume.delete_trigger:type_name -> ateapi.DeleteTrigger + 158, // 44: ateapi.SharedVolume.volume_context:type_name -> ateapi.SharedVolume.VolumeContextEntry + 35, // 45: ateapi.SharedVolume.status:type_name -> ateapi.SharedVolumeStatus + 6, // 46: ateapi.SharedVolume.access_mode:type_name -> ateapi.AccessMode + 7, // 47: ateapi.SharedVolumeStatus.state:type_name -> ateapi.SharedVolumeState + 36, // 48: ateapi.SharedVolumeStatus.refs:type_name -> ateapi.ActorRef + 16, // 49: ateapi.Atespace.metadata:type_name -> ateapi.ResourceMetadata + 16, // 50: ateapi.ActorTemplate.metadata:type_name -> ateapi.ResourceMetadata + 15, // 51: ateapi.ActorTemplate.worker_selector:type_name -> ateapi.Selector + 47, // 52: ateapi.ActorTemplate.containers:type_name -> ateapi.Container + 53, // 53: ateapi.ActorTemplate.volumes:type_name -> ateapi.Volume + 45, // 54: ateapi.ActorTemplate.snapshot_config:type_name -> ateapi.SnapshotConfig + 44, // 55: ateapi.ActorTemplate.sandbox_config:type_name -> ateapi.SandboxConfig + 40, // 56: ateapi.ActorTemplate.resources:type_name -> ateapi.Resources + 43, // 57: ateapi.ActorTemplate.status:type_name -> ateapi.ActorTemplateStatus + 41, // 58: ateapi.Resources.limits:type_name -> ateapi.Limits + 38, // 59: ateapi.GoldenSnapshotStatus.golden_tag:type_name -> ateapi.ObjectRef + 160, // 60: ateapi.GoldenSnapshotStatus.snapshot_time:type_name -> google.protobuf.Timestamp + 42, // 61: ateapi.ActorTemplateStatus.golden_snapshot_status:type_name -> ateapi.GoldenSnapshotStatus + 2, // 62: ateapi.SnapshotConfig.preferred_fidelity:type_name -> ateapi.SnapshotFidelity + 46, // 63: ateapi.SnapshotConfig.golden_snapshot_config:type_name -> ateapi.GoldenSnapshotConfig + 8, // 64: ateapi.GoldenSnapshotConfig.mode:type_name -> ateapi.GoldenSnapshotMode + 50, // 65: ateapi.Container.env:type_name -> ateapi.EnvVar + 51, // 66: ateapi.Container.wakeup_probe:type_name -> ateapi.WakeupProbe + 64, // 67: ateapi.Container.volume_mounts:type_name -> ateapi.VolumeMount + 48, // 68: ateapi.Container.security_context:type_name -> ateapi.SecurityContext + 40, // 69: ateapi.Container.resources:type_name -> ateapi.Resources + 49, // 70: ateapi.SecurityContext.capabilities:type_name -> ateapi.Capabilities + 52, // 71: ateapi.WakeupProbe.http_get:type_name -> ateapi.HttpGetAction + 56, // 72: ateapi.Volume.durable_dir:type_name -> ateapi.DurableDirVolumeSource + 54, // 73: ateapi.Volume.external:type_name -> ateapi.ExternalVolumeSource + 59, // 74: ateapi.Volume.system_info:type_name -> ateapi.SystemInfoVolumeSource + 55, // 75: ateapi.Volume.image:type_name -> ateapi.ImageVolumeSource + 57, // 76: ateapi.ExternalVolumeSource.template:type_name -> ateapi.ExternalVolumeTemplate + 58, // 77: ateapi.ExternalVolumeSource.shared_volume:type_name -> ateapi.SharedVolumeRef + 60, // 78: ateapi.SystemInfoVolumeSource.data_sources:type_name -> ateapi.SystemInfoDataSource + 61, // 79: ateapi.SystemInfoDataSource.actor_metadata:type_name -> ateapi.ActorMetadataDataSource + 63, // 80: ateapi.SystemInfoDataSource.trust_bundle:type_name -> ateapi.TrustBundleDataSource + 62, // 81: ateapi.ActorMetadataDataSource.items:type_name -> ateapi.ActorMetadataItem + 9, // 82: ateapi.ActorMetadataItem.field:type_name -> ateapi.ActorMetadataField + 37, // 83: ateapi.CreateAtespaceRequest.atespace:type_name -> ateapi.Atespace + 38, // 84: ateapi.GetAtespaceRequest.atespace:type_name -> ateapi.ObjectRef + 108, // 85: ateapi.ListAtespacesRequest.options:type_name -> ateapi.ListOptions + 37, // 86: ateapi.ListAtespacesResponse.atespaces:type_name -> ateapi.Atespace + 38, // 87: ateapi.DeleteAtespaceRequest.atespace:type_name -> ateapi.ObjectRef + 107, // 88: ateapi.DeleteAtespaceRequest.options:type_name -> ateapi.DeleteOptions + 39, // 89: ateapi.CreateActorTemplateRequest.actor_template:type_name -> ateapi.ActorTemplate + 38, // 90: ateapi.GetActorTemplateRequest.actor_template:type_name -> ateapi.ObjectRef + 108, // 91: ateapi.ListActorTemplatesRequest.options:type_name -> ateapi.ListOptions + 39, // 92: ateapi.ListActorTemplatesResponse.actor_templates:type_name -> ateapi.ActorTemplate + 38, // 93: ateapi.DeleteActorTemplateRequest.actor_template:type_name -> ateapi.ObjectRef + 107, // 94: ateapi.DeleteActorTemplateRequest.options:type_name -> ateapi.DeleteOptions + 38, // 95: ateapi.GetActorRequest.actor:type_name -> ateapi.ObjectRef + 18, // 96: ateapi.CreateActorRequest.actor:type_name -> ateapi.Actor + 18, // 97: ateapi.UpdateActorRequest.actor:type_name -> ateapi.Actor + 38, // 98: ateapi.SuspendActorRequest.actor:type_name -> ateapi.ObjectRef + 18, // 99: ateapi.SuspendActorResponse.actor:type_name -> ateapi.Actor + 38, // 100: ateapi.PauseActorRequest.actor:type_name -> ateapi.ObjectRef + 18, // 101: ateapi.PauseActorResponse.actor:type_name -> ateapi.Actor + 38, // 102: ateapi.ResumeActorRequest.actor:type_name -> ateapi.ObjectRef + 18, // 103: ateapi.ResumeActorResponse.actor:type_name -> ateapi.Actor + 38, // 104: ateapi.RevertActorRequest.actor:type_name -> ateapi.ObjectRef + 18, // 105: ateapi.RevertActorResponse.actor:type_name -> ateapi.Actor + 38, // 106: ateapi.DeleteActorRequest.actor:type_name -> ateapi.ObjectRef + 107, // 107: ateapi.DeleteActorRequest.options:type_name -> ateapi.DeleteOptions + 38, // 108: ateapi.GetActorEgressPolicyRequest.actor:type_name -> ateapi.ObjectRef + 38, // 109: ateapi.CreateActorEgressPolicyRequest.actor:type_name -> ateapi.ObjectRef + 19, // 110: ateapi.CreateActorEgressPolicyRequest.egress_policy:type_name -> ateapi.EgressPolicy + 38, // 111: ateapi.UpdateActorEgressPolicyRequest.actor:type_name -> ateapi.ObjectRef + 19, // 112: ateapi.UpdateActorEgressPolicyRequest.egress_policy:type_name -> ateapi.EgressPolicy + 38, // 113: ateapi.DeleteActorEgressPolicyRequest.actor:type_name -> ateapi.ObjectRef + 107, // 114: ateapi.DeleteActorEgressPolicyRequest.options:type_name -> ateapi.DeleteOptions + 38, // 115: ateapi.GetTagRequest.tag:type_name -> ateapi.ObjectRef + 38, // 116: ateapi.MintActorJwtRequest.actor:type_name -> ateapi.ObjectRef + 160, // 117: ateapi.MintActorJwtResponse.expire_time:type_name -> google.protobuf.Timestamp + 38, // 118: ateapi.MintActorCertificateRequest.actor:type_name -> ateapi.ObjectRef + 108, // 119: ateapi.ListTagsRequest.options:type_name -> ateapi.ListOptions + 33, // 120: ateapi.ListTagsResponse.tags:type_name -> ateapi.Tag + 33, // 121: ateapi.CreateTagRequest.tag:type_name -> ateapi.Tag + 33, // 122: ateapi.UpdateTagRequest.tag:type_name -> ateapi.Tag + 38, // 123: ateapi.DeleteTagRequest.tag:type_name -> ateapi.ObjectRef + 107, // 124: ateapi.DeleteTagRequest.options:type_name -> ateapi.DeleteOptions + 34, // 125: ateapi.CreateSharedVolumeRequest.shared_volume:type_name -> ateapi.SharedVolume + 38, // 126: ateapi.GetSharedVolumeRequest.shared_volume:type_name -> ateapi.ObjectRef + 34, // 127: ateapi.ListSharedVolumesResponse.shared_volumes:type_name -> ateapi.SharedVolume + 34, // 128: ateapi.UpdateSharedVolumeRequest.shared_volume:type_name -> ateapi.SharedVolume + 38, // 129: ateapi.DeleteSharedVolumeRequest.shared_volume:type_name -> ateapi.ObjectRef + 107, // 130: ateapi.DeleteSharedVolumeRequest.options:type_name -> ateapi.DeleteOptions + 38, // 131: ateapi.ListWorkerActorAssignmentsRequest.worker:type_name -> ateapi.ObjectRef + 108, // 132: ateapi.ListWorkerActorAssignmentsRequest.options:type_name -> ateapi.ListOptions + 131, // 133: ateapi.ListWorkerActorAssignmentsResponse.actor_assignments:type_name -> ateapi.ActorAssignment + 108, // 134: ateapi.ListWorkersRequest.options:type_name -> ateapi.ListOptions + 125, // 135: ateapi.ListWorkersResponse.workers:type_name -> ateapi.Worker + 38, // 136: ateapi.GetWorkerRequest.worker:type_name -> ateapi.ObjectRef + 125, // 137: ateapi.CreateWorkerRequest.worker:type_name -> ateapi.Worker + 125, // 138: ateapi.UpdateWorkerRequest.worker:type_name -> ateapi.Worker + 38, // 139: ateapi.DeleteWorkerRequest.worker:type_name -> ateapi.ObjectRef + 107, // 140: ateapi.DeleteWorkerRequest.options:type_name -> ateapi.DeleteOptions + 38, // 141: ateapi.DrainWorkerRequest.worker:type_name -> ateapi.ObjectRef + 125, // 142: ateapi.DrainWorkerResponse.worker:type_name -> ateapi.Worker + 38, // 143: ateapi.MarkWorkerAvailableRequest.worker:type_name -> ateapi.ObjectRef + 125, // 144: ateapi.MarkWorkerAvailableResponse.worker:type_name -> ateapi.Worker + 38, // 145: ateapi.MarkWorkerUnavailableRequest.worker:type_name -> ateapi.ObjectRef + 125, // 146: ateapi.MarkWorkerUnavailableResponse.worker:type_name -> ateapi.Worker + 108, // 147: ateapi.ListActorsRequest.options:type_name -> ateapi.ListOptions + 18, // 148: ateapi.ListActorsResponse.actors:type_name -> ateapi.Actor + 16, // 149: ateapi.Worker.metadata:type_name -> ateapi.ResourceMetadata + 159, // 150: ateapi.Worker.labels:type_name -> ateapi.Worker.LabelsEntry + 126, // 151: ateapi.Worker.status:type_name -> ateapi.WorkerStatus + 10, // 152: ateapi.WorkerStatus.state:type_name -> ateapi.WorkerState + 130, // 153: ateapi.WorkerStatus.capacity:type_name -> ateapi.WorkerResources + 130, // 154: ateapi.WorkerStatus.allocated:type_name -> ateapi.WorkerResources + 127, // 155: ateapi.WorkerStatus.default_runtime:type_name -> ateapi.SandboxRuntime + 127, // 156: ateapi.WorkerStatus.restorable_runtimes:type_name -> ateapi.SandboxRuntime + 128, // 157: ateapi.SandboxRuntime.version:type_name -> ateapi.VersionedSandboxCompat + 129, // 158: ateapi.VersionedSandboxCompat.attributes:type_name -> ateapi.AttributeEntry + 40, // 159: ateapi.WorkerResources.resources:type_name -> ateapi.Resources + 16, // 160: ateapi.ActorAssignment.metadata:type_name -> ateapi.ResourceMetadata + 38, // 161: ateapi.ActorAssignment.actor:type_name -> ateapi.ObjectRef + 40, // 162: ateapi.ActorAssignment.resources:type_name -> ateapi.Resources + 38, // 163: ateapi.RegisterWorkerRequest.worker:type_name -> ateapi.ObjectRef + 130, // 164: ateapi.RegisterWorkerRequest.capacity:type_name -> ateapi.WorkerResources + 127, // 165: ateapi.RegisterWorkerRequest.default_runtime:type_name -> ateapi.SandboxRuntime + 127, // 166: ateapi.RegisterWorkerRequest.restorable_runtimes:type_name -> ateapi.SandboxRuntime + 125, // 167: ateapi.RegisterWorkerResponse.worker:type_name -> ateapi.Worker + 38, // 168: ateapi.MintWorkerActorCertificateRequest.actor:type_name -> ateapi.ObjectRef + 38, // 169: ateapi.MintWorkerActorJwtRequest.actor:type_name -> ateapi.ObjectRef + 160, // 170: ateapi.MintWorkerActorJwtResponse.expire_time:type_name -> google.protobuf.Timestamp + 38, // 171: ateapi.RequestActorSuspendRequest.worker:type_name -> ateapi.ObjectRef + 38, // 172: ateapi.RequestActorSuspendRequest.actor:type_name -> ateapi.ObjectRef + 18, // 173: ateapi.RequestActorSuspendResponse.actor:type_name -> ateapi.Actor + 38, // 174: ateapi.ReportActorSnapshotStateRequest.actor:type_name -> ateapi.ObjectRef + 142, // 175: ateapi.ReportActorSnapshotStateRequest.state:type_name -> ateapi.ActorSnapshotState + 143, // 176: ateapi.ActorSnapshotState.latest_local:type_name -> ateapi.SnapshotReport + 143, // 177: ateapi.ActorSnapshotState.uploading:type_name -> ateapi.SnapshotReport + 143, // 178: ateapi.ActorSnapshotState.latest_durable:type_name -> ateapi.SnapshotReport + 160, // 179: ateapi.ActorSnapshotState.upload_due_time:type_name -> google.protobuf.Timestamp + 2, // 180: ateapi.SnapshotReport.fidelity:type_name -> ateapi.SnapshotFidelity + 12, // 181: ateapi.SnapshotReport.object:type_name -> ateapi.ObjectSnapshot + 160, // 182: ateapi.SnapshotReport.local_commit_time:type_name -> google.protobuf.Timestamp + 160, // 183: ateapi.SnapshotReport.durable_complete_time:type_name -> google.protobuf.Timestamp + 16, // 184: ateapi.AccessPolicy.metadata:type_name -> ateapi.ResourceMetadata + 145, // 185: ateapi.AccessPolicy.bindings:type_name -> ateapi.Binding + 144, // 186: ateapi.CreateGlobalAccessPolicyRequest.access_policy:type_name -> ateapi.AccessPolicy + 144, // 187: ateapi.UpdateGlobalAccessPolicyRequest.access_policy:type_name -> ateapi.AccessPolicy + 38, // 188: ateapi.GetAtespaceAccessPolicyRequest.atespace:type_name -> ateapi.ObjectRef + 38, // 189: ateapi.CreateAtespaceAccessPolicyRequest.atespace:type_name -> ateapi.ObjectRef + 144, // 190: ateapi.CreateAtespaceAccessPolicyRequest.access_policy:type_name -> ateapi.AccessPolicy + 38, // 191: ateapi.UpdateAtespaceAccessPolicyRequest.atespace:type_name -> ateapi.ObjectRef + 144, // 192: ateapi.UpdateAtespaceAccessPolicyRequest.access_policy:type_name -> ateapi.AccessPolicy + 38, // 193: ateapi.DeleteAtespaceAccessPolicyRequest.atespace:type_name -> ateapi.ObjectRef + 107, // 194: ateapi.DeleteAtespaceAccessPolicyRequest.options:type_name -> ateapi.DeleteOptions + 11, // 195: ateapi.ResourceAnnotation.scope:type_name -> ateapi.Scope + 161, // 196: ateapi.resource:extendee -> google.protobuf.MessageOptions + 162, // 197: ateapi.method:extendee -> google.protobuf.MethodOptions + 153, // 198: ateapi.resource:type_name -> ateapi.ResourceAnnotation + 154, // 199: ateapi.method:type_name -> ateapi.MethodAnnotation + 75, // 200: ateapi.Control.GetActor:input_type -> ateapi.GetActorRequest + 76, // 201: ateapi.Control.CreateActor:input_type -> ateapi.CreateActorRequest + 77, // 202: ateapi.Control.UpdateActor:input_type -> ateapi.UpdateActorRequest + 78, // 203: ateapi.Control.SuspendActor:input_type -> ateapi.SuspendActorRequest + 80, // 204: ateapi.Control.PauseActor:input_type -> ateapi.PauseActorRequest + 82, // 205: ateapi.Control.ResumeActor:input_type -> ateapi.ResumeActorRequest + 84, // 206: ateapi.Control.RevertActor:input_type -> ateapi.RevertActorRequest + 86, // 207: ateapi.Control.DeleteActor:input_type -> ateapi.DeleteActorRequest + 87, // 208: ateapi.Control.GetActorEgressPolicy:input_type -> ateapi.GetActorEgressPolicyRequest + 88, // 209: ateapi.Control.CreateActorEgressPolicy:input_type -> ateapi.CreateActorEgressPolicyRequest + 89, // 210: ateapi.Control.UpdateActorEgressPolicy:input_type -> ateapi.UpdateActorEgressPolicyRequest + 90, // 211: ateapi.Control.DeleteActorEgressPolicy:input_type -> ateapi.DeleteActorEgressPolicyRequest + 92, // 212: ateapi.Control.MintActorJwt:input_type -> ateapi.MintActorJwtRequest + 94, // 213: ateapi.Control.MintActorCertificate:input_type -> ateapi.MintActorCertificateRequest + 98, // 214: ateapi.Control.CreateTag:input_type -> ateapi.CreateTagRequest + 91, // 215: ateapi.Control.GetTag:input_type -> ateapi.GetTagRequest + 96, // 216: ateapi.Control.ListTags:input_type -> ateapi.ListTagsRequest + 99, // 217: ateapi.Control.UpdateTag:input_type -> ateapi.UpdateTagRequest + 100, // 218: ateapi.Control.DeleteTag:input_type -> ateapi.DeleteTagRequest + 101, // 219: ateapi.Control.CreateSharedVolume:input_type -> ateapi.CreateSharedVolumeRequest + 102, // 220: ateapi.Control.GetSharedVolume:input_type -> ateapi.GetSharedVolumeRequest + 103, // 221: ateapi.Control.ListSharedVolumes:input_type -> ateapi.ListSharedVolumesRequest + 105, // 222: ateapi.Control.UpdateSharedVolume:input_type -> ateapi.UpdateSharedVolumeRequest + 106, // 223: ateapi.Control.DeleteSharedVolume:input_type -> ateapi.DeleteSharedVolumeRequest + 111, // 224: ateapi.Control.ListWorkers:input_type -> ateapi.ListWorkersRequest + 113, // 225: ateapi.Control.GetWorker:input_type -> ateapi.GetWorkerRequest + 114, // 226: ateapi.Control.CreateWorker:input_type -> ateapi.CreateWorkerRequest + 115, // 227: ateapi.Control.UpdateWorker:input_type -> ateapi.UpdateWorkerRequest + 116, // 228: ateapi.Control.DeleteWorker:input_type -> ateapi.DeleteWorkerRequest + 117, // 229: ateapi.Control.DrainWorker:input_type -> ateapi.DrainWorkerRequest + 119, // 230: ateapi.Control.MarkWorkerAvailable:input_type -> ateapi.MarkWorkerAvailableRequest + 121, // 231: ateapi.Control.MarkWorkerUnavailable:input_type -> ateapi.MarkWorkerUnavailableRequest + 109, // 232: ateapi.Control.ListWorkerActorAssignments:input_type -> ateapi.ListWorkerActorAssignmentsRequest + 123, // 233: ateapi.Control.ListActors:input_type -> ateapi.ListActorsRequest + 65, // 234: ateapi.Control.CreateAtespace:input_type -> ateapi.CreateAtespaceRequest + 66, // 235: ateapi.Control.GetAtespace:input_type -> ateapi.GetAtespaceRequest + 67, // 236: ateapi.Control.ListAtespaces:input_type -> ateapi.ListAtespacesRequest + 69, // 237: ateapi.Control.DeleteAtespace:input_type -> ateapi.DeleteAtespaceRequest + 70, // 238: ateapi.Control.CreateActorTemplate:input_type -> ateapi.CreateActorTemplateRequest + 71, // 239: ateapi.Control.GetActorTemplate:input_type -> ateapi.GetActorTemplateRequest + 72, // 240: ateapi.Control.ListActorTemplates:input_type -> ateapi.ListActorTemplatesRequest + 74, // 241: ateapi.Control.DeleteActorTemplate:input_type -> ateapi.DeleteActorTemplateRequest + 146, // 242: ateapi.Control.GetGlobalAccessPolicy:input_type -> ateapi.GetGlobalAccessPolicyRequest + 147, // 243: ateapi.Control.CreateGlobalAccessPolicy:input_type -> ateapi.CreateGlobalAccessPolicyRequest + 148, // 244: ateapi.Control.UpdateGlobalAccessPolicy:input_type -> ateapi.UpdateGlobalAccessPolicyRequest + 149, // 245: ateapi.Control.GetAtespaceAccessPolicy:input_type -> ateapi.GetAtespaceAccessPolicyRequest + 150, // 246: ateapi.Control.CreateAtespaceAccessPolicy:input_type -> ateapi.CreateAtespaceAccessPolicyRequest + 151, // 247: ateapi.Control.UpdateAtespaceAccessPolicy:input_type -> ateapi.UpdateAtespaceAccessPolicyRequest + 152, // 248: ateapi.Control.DeleteAtespaceAccessPolicy:input_type -> ateapi.DeleteAtespaceAccessPolicyRequest + 132, // 249: ateapi.WorkerService.RegisterWorker:input_type -> ateapi.RegisterWorkerRequest + 134, // 250: ateapi.WorkerService.MintWorkerActorCertificate:input_type -> ateapi.MintWorkerActorCertificateRequest + 136, // 251: ateapi.WorkerService.MintWorkerActorJwt:input_type -> ateapi.MintWorkerActorJwtRequest + 138, // 252: ateapi.WorkerService.RequestActorSuspend:input_type -> ateapi.RequestActorSuspendRequest + 140, // 253: ateapi.WorkerService.ReportActorSnapshotState:input_type -> ateapi.ReportActorSnapshotStateRequest + 18, // 254: ateapi.Control.GetActor:output_type -> ateapi.Actor + 18, // 255: ateapi.Control.CreateActor:output_type -> ateapi.Actor + 18, // 256: ateapi.Control.UpdateActor:output_type -> ateapi.Actor + 79, // 257: ateapi.Control.SuspendActor:output_type -> ateapi.SuspendActorResponse + 81, // 258: ateapi.Control.PauseActor:output_type -> ateapi.PauseActorResponse + 83, // 259: ateapi.Control.ResumeActor:output_type -> ateapi.ResumeActorResponse + 85, // 260: ateapi.Control.RevertActor:output_type -> ateapi.RevertActorResponse + 18, // 261: ateapi.Control.DeleteActor:output_type -> ateapi.Actor + 19, // 262: ateapi.Control.GetActorEgressPolicy:output_type -> ateapi.EgressPolicy + 19, // 263: ateapi.Control.CreateActorEgressPolicy:output_type -> ateapi.EgressPolicy + 19, // 264: ateapi.Control.UpdateActorEgressPolicy:output_type -> ateapi.EgressPolicy + 19, // 265: ateapi.Control.DeleteActorEgressPolicy:output_type -> ateapi.EgressPolicy + 93, // 266: ateapi.Control.MintActorJwt:output_type -> ateapi.MintActorJwtResponse + 95, // 267: ateapi.Control.MintActorCertificate:output_type -> ateapi.MintActorCertificateResponse + 33, // 268: ateapi.Control.CreateTag:output_type -> ateapi.Tag + 33, // 269: ateapi.Control.GetTag:output_type -> ateapi.Tag + 97, // 270: ateapi.Control.ListTags:output_type -> ateapi.ListTagsResponse + 33, // 271: ateapi.Control.UpdateTag:output_type -> ateapi.Tag + 33, // 272: ateapi.Control.DeleteTag:output_type -> ateapi.Tag + 34, // 273: ateapi.Control.CreateSharedVolume:output_type -> ateapi.SharedVolume + 34, // 274: ateapi.Control.GetSharedVolume:output_type -> ateapi.SharedVolume + 104, // 275: ateapi.Control.ListSharedVolumes:output_type -> ateapi.ListSharedVolumesResponse + 34, // 276: ateapi.Control.UpdateSharedVolume:output_type -> ateapi.SharedVolume + 34, // 277: ateapi.Control.DeleteSharedVolume:output_type -> ateapi.SharedVolume + 112, // 278: ateapi.Control.ListWorkers:output_type -> ateapi.ListWorkersResponse + 125, // 279: ateapi.Control.GetWorker:output_type -> ateapi.Worker + 125, // 280: ateapi.Control.CreateWorker:output_type -> ateapi.Worker + 125, // 281: ateapi.Control.UpdateWorker:output_type -> ateapi.Worker + 125, // 282: ateapi.Control.DeleteWorker:output_type -> ateapi.Worker + 118, // 283: ateapi.Control.DrainWorker:output_type -> ateapi.DrainWorkerResponse + 120, // 284: ateapi.Control.MarkWorkerAvailable:output_type -> ateapi.MarkWorkerAvailableResponse + 122, // 285: ateapi.Control.MarkWorkerUnavailable:output_type -> ateapi.MarkWorkerUnavailableResponse + 110, // 286: ateapi.Control.ListWorkerActorAssignments:output_type -> ateapi.ListWorkerActorAssignmentsResponse + 124, // 287: ateapi.Control.ListActors:output_type -> ateapi.ListActorsResponse + 37, // 288: ateapi.Control.CreateAtespace:output_type -> ateapi.Atespace + 37, // 289: ateapi.Control.GetAtespace:output_type -> ateapi.Atespace + 68, // 290: ateapi.Control.ListAtespaces:output_type -> ateapi.ListAtespacesResponse + 37, // 291: ateapi.Control.DeleteAtespace:output_type -> ateapi.Atespace + 39, // 292: ateapi.Control.CreateActorTemplate:output_type -> ateapi.ActorTemplate + 39, // 293: ateapi.Control.GetActorTemplate:output_type -> ateapi.ActorTemplate + 73, // 294: ateapi.Control.ListActorTemplates:output_type -> ateapi.ListActorTemplatesResponse + 39, // 295: ateapi.Control.DeleteActorTemplate:output_type -> ateapi.ActorTemplate + 144, // 296: ateapi.Control.GetGlobalAccessPolicy:output_type -> ateapi.AccessPolicy + 144, // 297: ateapi.Control.CreateGlobalAccessPolicy:output_type -> ateapi.AccessPolicy + 144, // 298: ateapi.Control.UpdateGlobalAccessPolicy:output_type -> ateapi.AccessPolicy + 144, // 299: ateapi.Control.GetAtespaceAccessPolicy:output_type -> ateapi.AccessPolicy + 144, // 300: ateapi.Control.CreateAtespaceAccessPolicy:output_type -> ateapi.AccessPolicy + 144, // 301: ateapi.Control.UpdateAtespaceAccessPolicy:output_type -> ateapi.AccessPolicy + 144, // 302: ateapi.Control.DeleteAtespaceAccessPolicy:output_type -> ateapi.AccessPolicy + 133, // 303: ateapi.WorkerService.RegisterWorker:output_type -> ateapi.RegisterWorkerResponse + 135, // 304: ateapi.WorkerService.MintWorkerActorCertificate:output_type -> ateapi.MintWorkerActorCertificateResponse + 137, // 305: ateapi.WorkerService.MintWorkerActorJwt:output_type -> ateapi.MintWorkerActorJwtResponse + 139, // 306: ateapi.WorkerService.RequestActorSuspend:output_type -> ateapi.RequestActorSuspendResponse + 141, // 307: ateapi.WorkerService.ReportActorSnapshotState:output_type -> ateapi.ReportActorSnapshotStateResponse + 254, // [254:308] is the sub-list for method output_type + 200, // [200:254] is the sub-list for method input_type + 198, // [198:200] is the sub-list for extension type_name + 196, // [196:198] is the sub-list for extension extendee + 0, // [0:196] is the sub-list for field type_name } func init() { file_ateapi_proto_init() } @@ -10437,8 +11522,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: 9, - NumMessages: 135, + NumEnums: 12, + NumMessages: 148, NumExtensions: 2, NumServices: 2, }, diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 233b3fb63e..ddce10a0a1 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -135,6 +135,31 @@ service Control { option (method).resource = "Tag"; } + // Create a SharedVolume, provisioning it or registering an existing volume. + rpc CreateSharedVolume(CreateSharedVolumeRequest) returns (SharedVolume) { + option (method).resource = "SharedVolume"; + } + + // Get a SharedVolume. + rpc GetSharedVolume(GetSharedVolumeRequest) returns (SharedVolume) { + option (method).resource = "SharedVolume"; + } + + // List ExternalVolumes. + rpc ListSharedVolumes(ListSharedVolumesRequest) returns (ListSharedVolumesResponse) { + option (method).resource = "SharedVolume"; + } + + // Update a SharedVolume's delete trigger. + rpc UpdateSharedVolume(UpdateSharedVolumeRequest) returns (SharedVolume) { + option (method).resource = "SharedVolume"; + } + + // Delete a SharedVolume and its storage. Rejected while any Actor references it. + rpc DeleteSharedVolume(DeleteSharedVolumeRequest) returns (SharedVolume) { + option (method).resource = "SharedVolume"; + } + // List Workers. rpc ListWorkers(ListWorkersRequest) returns (ListWorkersResponse) { option (method).resource = "Worker"; @@ -483,7 +508,8 @@ message ResourceMetadata { google.protobuf.Timestamp update_time = 6; } -message ExternalVolume { +// ExternalVolumeRef is one actor's view of an external volume it mounts. +message ExternalVolumeRef { // Name of the volume specified in the actor template. Template volume // names are DNS labels. // @@ -527,6 +553,15 @@ message ExternalVolume { // +k8s:optional // +k8s:customValidation # at most 40 KiB in total, ten times the CSI limit for map fields map volume_context = 5; + + // shared_volume_name names the SharedVolume this mount references, in the + // actor's atespace. Empty when the actor owns the volume itself. + // + // +k8s:optional + // +k8s:format=k8s-short-name + // +k8s:update=NoModify + // +k8s:update=NoUnset + string shared_volume_name = 6; } message Actor { @@ -569,6 +604,17 @@ message Actor { // // +k8s:optional ActorStatus status = 7; + + // shared_volume_bindings maps each unnamed external.shared_volume in the + // template, by volume name, to the SharedVolume this actor mounts. Every + // unnamed ref must be bound; a named ref cannot be rebound. Set at creation. + // + // +k8s:optional + // +k8s:maxProperties=32 + // +k8s:eachKey=+k8s:format=k8s-short-name + // +k8s:eachVal=+k8s:format=k8s-short-name + // +k8s:immutable + map shared_volume_bindings = 8; } // EgressPolicy is an egress policy resource nested under an Actor. An Actor has @@ -886,7 +932,7 @@ message ActorStatus { // +k8s:maxItems=32 # matches the template's volumes bound // +k8s:listType=map // +k8s:listMapKey=name - repeated ExternalVolume external_volumes = 7; + repeated ExternalVolumeRef external_volumes = 7; // crash records why and when the Actor entered CRASHED. It is set with the // CRASHED state and cleared when a revert returns the Actor to SUSPENDED. @@ -1070,6 +1116,164 @@ message Tag { ObjectRef source_actor = 4; } +// DeleteTrigger is what removes a SharedVolume and its storage. +// TODO: add an ATESPACE trigger that removes the volume with its atespace. +enum DeleteTrigger { + DELETE_TRIGGER_UNSPECIFIED = 0; + // Only an explicit DeleteSharedVolume removes the volume. + DELETE_TRIGGER_MANUAL = 1; + // The volume is removed when the last actor referencing it is deleted. + DELETE_TRIGGER_LAST_ACTOR = 2; +} + +// AccessMode is how many resumed actors may hold the volume at once. +enum AccessMode { + ACCESS_MODE_UNSPECIFIED = 0; + // At most one resumed actor holds the volume. + ACCESS_MODE_READ_WRITE_ONCE = 1; + // Any number of resumed actors hold the volume, on any node. + ACCESS_MODE_READ_WRITE_MANY = 2; +} + +// SharedVolumeState is where a SharedVolume is in its own lifecycle. +enum SharedVolumeState { + SHARED_VOLUME_STATE_UNSPECIFIED = 0; + // The name is reserved but no volume handle is known yet. + SHARED_VOLUME_STATE_PENDING = 1; + // The volume has a handle and can be referenced. + SHARED_VOLUME_STATE_READY = 2; + // New references are forbidden while storage deletion is retried. + SHARED_VOLUME_STATE_DELETING = 3; +} + +// SharedVolume is an atespace-scoped CSI volume whose lifetime is independent +// of any actor. Identity is (atespace, name); the storage-level ID is derived +// from the record's UID. +message SharedVolume { + option (resource) = { + scope : SCOPE_ATESPACE + plural : "SharedVolumes" + }; + + // Common resource metadata: atespace, name, uid, version, timestamps. + // + // +k8s:required + // +k8s:subfield(atespace)=+k8s:required + ResourceMetadata metadata = 1; + + // delete_trigger decides what removes the volume. Defaults to LAST_ACTOR when + // provisioned and MANUAL when registered. + // + // +k8s:optional + // +k8s:minimum=1 + // +k8s:maximum=2 # keep this in sync with the DeleteTrigger enum + // +k8s:mutable + DeleteTrigger delete_trigger = 2; + + // storage_class_name names the StorageClass to provision from, or documents + // the class of a registered volume. + // + // +k8s:optional + // +k8s:format=k8s-long-name + // +k8s:update=NoModify + // +k8s:update=NoUnset + string storage_class_name = 3; + + // volume_id is the storage system's volume ID. Supplied to register an + // existing volume, otherwise stamped once provisioning completes. Bounded and + // checked for control characters only. + // + // +k8s:optional + // +k8s:maxLength=256 + // +k8s:update=NoModify + // +k8s:update=NoUnset + // +k8s:customValidation # no control characters + string volume_id = 4; + + // volume_type is the CSI driver name. Required alongside volume_id when no + // StorageClass is named; otherwise taken from the class's provisioner. + // + // +k8s:optional + // +k8s:maxLength=253 + // +k8s:update=NoModify + // +k8s:update=NoUnset + // +k8s:customValidation + string volume_type = 5; + + // volume_context is driver metadata needed by the node plugin for mounting. + // Attachment metadata is per-node and deliberately not recorded here. + // + // +k8s:optional + // +k8s:maxProperties=32 + // +k8s:eachKey=+k8s:maxLength=128 + // +k8s:eachVal=+k8s:maxLength=256 + map volume_context = 6; + + // status is set by the control plane; it is ignored on write. + // + // +k8s:optional + SharedVolumeStatus status = 7; + + // capacity to provision, as a Kubernetes resource.Quantity string (e.g. + // "10Gi"). Required when provisioning, forbidden alongside volume_id. + // + // +k8s:optional + // +k8s:maxLength=64 + // +k8s:update=NoModify + // +k8s:update=NoUnset + // +k8s:customValidation # must parse as a resource.Quantity + string capacity = 8; + + // access_mode is fixed at create and defaults to READ_WRITE_ONCE. + // TODO: accept READ_WRITE_MANY once concurrent holders are supported. + // + // +k8s:optional + // +k8s:minimum=1 + // +k8s:maximum=2 # keep this in sync with the AccessMode enum + // +k8s:update=NoModify + // +k8s:update=NoUnset + AccessMode access_mode = 9; +} + +// SharedVolumeStatus is the part of a SharedVolume the control plane owns. +message SharedVolumeStatus { + // state is where the volume is in its lifecycle. + // + // +k8s:optional + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the SharedVolumeState enum + SharedVolumeState state = 1; + + // refs are the actors referencing the volume, resumed or not. + // TODO: move references to their own table before lifting the bound. + // + // +k8s:optional + // +k8s:maxItems=256 + // +k8s:listType=map + // +k8s:listMapKey=actor_uid + repeated ActorRef refs = 2; +} + +// ActorRef is one actor's reference to a SharedVolume. +message ActorRef { + // actor_uid identifies the actor, so a recreated namesake does not inherit it. + // + // +k8s:required + // +k8s:maxLength=253 + string actor_uid = 1; + + // actor_name is the actor's name, for readers. + // + // +k8s:required + // +k8s:format=k8s-short-name + string actor_name = 2; + + // active is true while the actor is resumed and holds the volume mounted. + // + // +k8s:optional + bool active = 3; +} + // Atespace is the isolation boundary an Actor is created into. Global-scoped: // metadata.atespace is always empty; the atespace's identity is metadata.name. message Atespace { @@ -1262,6 +1466,10 @@ enum GoldenSnapshotMode { GOLDEN_SNAPSHOT_MODE_UNSPECIFIED = 0; GOLDEN_SNAPSHOT_MODE_ENABLED = 1; GOLDEN_SNAPSHOT_MODE_DISABLED = 2; + // Capture the runtime before creating application processes. Supported by + // MICROVM and GVISOR. Restore attaches real volumes before starting + // applications; application warmup is not preserved. + GOLDEN_SNAPSHOT_MODE_RUNTIME = 3; } // GoldenSnapshotConfig controls automated golden snapshot creation for a template. @@ -1271,7 +1479,7 @@ message GoldenSnapshotConfig { // // +k8s:required // +k8s:minimum=1 - // +k8s:maximum=2 # keep this in sync with the GoldenSnapshotMode enum + // +k8s:maximum=3 # keep this in sync with the GoldenSnapshotMode enum GoldenSnapshotMode mode = 1; } @@ -1443,18 +1651,21 @@ message Volume { // +k8s:format=k8s-short-name string name = 1; - // Exactly one of durable_dir / external_volume_template / image / - // system_info must be set. + // Exactly one of durable_dir / external / image / system_info must be + // set. // // +k8s:optional // +k8s:unionMember DurableDirVolumeSource durable_dir = 2; + // external is a CSI volume: provisioned for this actor from a template, + // or a SharedVolume the actor references. + // // +ate:preview=ExternalVolumes // +k8s:ifDisabled(ExternalVolumes)=+k8s:forbidden // +k8s:optional // +k8s:unionMember - ExternalVolumeTemplate external_volume_template = 3; + ExternalVolumeSource external = 7; // +k8s:optional // +k8s:unionMember @@ -1467,6 +1678,22 @@ message Volume { ImageVolumeSource image = 6; } +// ExternalVolumeSource is a CSI volume: provisioned for the actor from a +// template, or a SharedVolume that already exists. +message ExternalVolumeSource { + // Exactly one of template / shared_volume must be set. + // + // +k8s:optional + // +k8s:unionMember + ExternalVolumeTemplate template = 1; + + // shared_volume mounts a SharedVolume that already exists. + // + // +k8s:optional + // +k8s:unionMember + SharedVolumeRef shared_volume = 2; +} + // ImageVolumeSource mounts the contents of an OCI image, read-only. The // reference must include a digest: changing the image invalidates // snapshots. @@ -1503,6 +1730,20 @@ message ExternalVolumeTemplate { string storage_class_name = 2; } +// SharedVolumeRef mounts a SharedVolume in the actor's atespace. The +// volume outlives the actor, so several actors can mount it in turn. +// +// SharedVolume actors cold-boot initially unless their microVM template opts +// into GOLDEN_SNAPSHOT_MODE_RUNTIME. Later resumes use their own snapshots. +message SharedVolumeRef { + // name of the SharedVolume. Left empty, the actor names it at creation in + // Actor.shared_volume_bindings. + // + // +k8s:optional + // +k8s:format=k8s-short-name + string name = 1; +} + // SystemInfoVolumeSource is a read-only volume of substrate-generated // per-actor files (identity fields, projected trust bundles), regenerated by // atelet on every Run/Restore. @@ -2037,6 +2278,82 @@ message DeleteTagRequest { DeleteOptions options = 2; } +// Request to create a SharedVolume. +// +// +k8s:customValidation # exactly one of storage_class_name and volume_id +message CreateSharedVolumeRequest { + // The volume to create. Name a storage class to provision a new volume, or + // supply a volume ID to register one that already exists. status is ignored. + // + // +k8s:required + SharedVolume shared_volume = 1; +} + +// GetSharedVolumeRequest identifies one SharedVolume. +message GetSharedVolumeRequest { + // The volume to get. + // + // +k8s:required + // +k8s:subfield(atespace)=+k8s:required + ObjectRef shared_volume = 1; +} + +// ListSharedVolumesRequest selects a page of ExternalVolumes. +message ListSharedVolumesRequest { + // The atespace to list from. Empty lists across all atespaces. + // + // +k8s:optional + // +k8s:format=k8s-short-name + string atespace = 1; + + // Requested page size; values above 1000 are coerced to 1000. + // + // +k8s:optional + // +k8s:minimum=1 + int32 page_size = 2; + + // Pagination token from a previous ListSharedVolumes response. + // + // +k8s:optional + // +k8s:maxLength=256 + string page_token = 3; +} + +// ListSharedVolumesResponse is one page of ExternalVolumes. +message ListSharedVolumesResponse { + // The volumes in this page. + repeated SharedVolume shared_volumes = 1; + + // Pagination token for the next page. Empty on the last page. + string next_page_token = 2; +} + +// Request to update the mutable fields of a SharedVolume. +message UpdateSharedVolumeRequest { + // The volume to update. metadata.atespace and metadata.name identify it; + // metadata.version and metadata.uid are required preconditions. + // + // +k8s:required + // +k8s:opaqueType # updates are handled in 2 steps, do not descend + // +k8s:subfield(metadata)=+k8s:required + // +k8s:customValidation # TODO: when we get nested subfields, require metadata.atespace + SharedVolume shared_volume = 1; +} + +// DeleteSharedVolumeRequest identifies one SharedVolume to delete. +message DeleteSharedVolumeRequest { + // The volume to delete. + // + // +k8s:required + // +k8s:subfield(atespace)=+k8s:required + ObjectRef shared_volume = 1; + + // Optional per-delete preconditions. + // + // +k8s:optional + DeleteOptions options = 2; +} + // DeleteOptions are the optional guards a Delete request may carry. message DeleteOptions { // Delete only if the resource is still at this version, that is, unchanged diff --git a/pkg/proto/ateapipb/ateapi_grpc.pb.go b/pkg/proto/ateapipb/ateapi_grpc.pb.go index 4043f05a5a..cd95597ad6 100644 --- a/pkg/proto/ateapipb/ateapi_grpc.pb.go +++ b/pkg/proto/ateapipb/ateapi_grpc.pb.go @@ -52,6 +52,11 @@ const ( Control_ListTags_FullMethodName = "/ateapi.Control/ListTags" Control_UpdateTag_FullMethodName = "/ateapi.Control/UpdateTag" Control_DeleteTag_FullMethodName = "/ateapi.Control/DeleteTag" + Control_CreateSharedVolume_FullMethodName = "/ateapi.Control/CreateSharedVolume" + Control_GetSharedVolume_FullMethodName = "/ateapi.Control/GetSharedVolume" + Control_ListSharedVolumes_FullMethodName = "/ateapi.Control/ListSharedVolumes" + Control_UpdateSharedVolume_FullMethodName = "/ateapi.Control/UpdateSharedVolume" + Control_DeleteSharedVolume_FullMethodName = "/ateapi.Control/DeleteSharedVolume" Control_ListWorkers_FullMethodName = "/ateapi.Control/ListWorkers" Control_GetWorker_FullMethodName = "/ateapi.Control/GetWorker" Control_CreateWorker_FullMethodName = "/ateapi.Control/CreateWorker" @@ -140,6 +145,16 @@ type ControlClient interface { // tag that have not yet been suspended still point at that external snapshot // and become unrecoverable, so do not delete a tag while such Actors exist. DeleteTag(ctx context.Context, in *DeleteTagRequest, opts ...grpc.CallOption) (*Tag, error) + // Create a SharedVolume, provisioning it or registering an existing volume. + CreateSharedVolume(ctx context.Context, in *CreateSharedVolumeRequest, opts ...grpc.CallOption) (*SharedVolume, error) + // Get a SharedVolume. + GetSharedVolume(ctx context.Context, in *GetSharedVolumeRequest, opts ...grpc.CallOption) (*SharedVolume, error) + // List ExternalVolumes. + ListSharedVolumes(ctx context.Context, in *ListSharedVolumesRequest, opts ...grpc.CallOption) (*ListSharedVolumesResponse, error) + // Update a SharedVolume's delete trigger. + UpdateSharedVolume(ctx context.Context, in *UpdateSharedVolumeRequest, opts ...grpc.CallOption) (*SharedVolume, error) + // Delete a SharedVolume and its storage. Rejected while any Actor references it. + DeleteSharedVolume(ctx context.Context, in *DeleteSharedVolumeRequest, opts ...grpc.CallOption) (*SharedVolume, error) // List Workers. ListWorkers(ctx context.Context, in *ListWorkersRequest, opts ...grpc.CallOption) (*ListWorkersResponse, error) // Get a Worker. @@ -406,6 +421,56 @@ func (c *controlClient) DeleteTag(ctx context.Context, in *DeleteTagRequest, opt return out, nil } +func (c *controlClient) CreateSharedVolume(ctx context.Context, in *CreateSharedVolumeRequest, opts ...grpc.CallOption) (*SharedVolume, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SharedVolume) + err := c.cc.Invoke(ctx, Control_CreateSharedVolume_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *controlClient) GetSharedVolume(ctx context.Context, in *GetSharedVolumeRequest, opts ...grpc.CallOption) (*SharedVolume, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SharedVolume) + err := c.cc.Invoke(ctx, Control_GetSharedVolume_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *controlClient) ListSharedVolumes(ctx context.Context, in *ListSharedVolumesRequest, opts ...grpc.CallOption) (*ListSharedVolumesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListSharedVolumesResponse) + err := c.cc.Invoke(ctx, Control_ListSharedVolumes_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *controlClient) UpdateSharedVolume(ctx context.Context, in *UpdateSharedVolumeRequest, opts ...grpc.CallOption) (*SharedVolume, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SharedVolume) + err := c.cc.Invoke(ctx, Control_UpdateSharedVolume_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *controlClient) DeleteSharedVolume(ctx context.Context, in *DeleteSharedVolumeRequest, opts ...grpc.CallOption) (*SharedVolume, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SharedVolume) + err := c.cc.Invoke(ctx, Control_DeleteSharedVolume_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *controlClient) ListWorkers(ctx context.Context, in *ListWorkersRequest, opts ...grpc.CallOption) (*ListWorkersResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListWorkersResponse) @@ -717,6 +782,16 @@ type ControlServer interface { // tag that have not yet been suspended still point at that external snapshot // and become unrecoverable, so do not delete a tag while such Actors exist. DeleteTag(context.Context, *DeleteTagRequest) (*Tag, error) + // Create a SharedVolume, provisioning it or registering an existing volume. + CreateSharedVolume(context.Context, *CreateSharedVolumeRequest) (*SharedVolume, error) + // Get a SharedVolume. + GetSharedVolume(context.Context, *GetSharedVolumeRequest) (*SharedVolume, error) + // List ExternalVolumes. + ListSharedVolumes(context.Context, *ListSharedVolumesRequest) (*ListSharedVolumesResponse, error) + // Update a SharedVolume's delete trigger. + UpdateSharedVolume(context.Context, *UpdateSharedVolumeRequest) (*SharedVolume, error) + // Delete a SharedVolume and its storage. Rejected while any Actor references it. + DeleteSharedVolume(context.Context, *DeleteSharedVolumeRequest) (*SharedVolume, error) // List Workers. ListWorkers(context.Context, *ListWorkersRequest) (*ListWorkersResponse, error) // Get a Worker. @@ -850,6 +925,21 @@ func (UnimplementedControlServer) UpdateTag(context.Context, *UpdateTagRequest) func (UnimplementedControlServer) DeleteTag(context.Context, *DeleteTagRequest) (*Tag, error) { return nil, status.Error(codes.Unimplemented, "method DeleteTag not implemented") } +func (UnimplementedControlServer) CreateSharedVolume(context.Context, *CreateSharedVolumeRequest) (*SharedVolume, error) { + return nil, status.Error(codes.Unimplemented, "method CreateSharedVolume not implemented") +} +func (UnimplementedControlServer) GetSharedVolume(context.Context, *GetSharedVolumeRequest) (*SharedVolume, error) { + return nil, status.Error(codes.Unimplemented, "method GetSharedVolume not implemented") +} +func (UnimplementedControlServer) ListSharedVolumes(context.Context, *ListSharedVolumesRequest) (*ListSharedVolumesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListSharedVolumes not implemented") +} +func (UnimplementedControlServer) UpdateSharedVolume(context.Context, *UpdateSharedVolumeRequest) (*SharedVolume, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateSharedVolume not implemented") +} +func (UnimplementedControlServer) DeleteSharedVolume(context.Context, *DeleteSharedVolumeRequest) (*SharedVolume, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteSharedVolume not implemented") +} func (UnimplementedControlServer) ListWorkers(context.Context, *ListWorkersRequest) (*ListWorkersResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListWorkers not implemented") } @@ -1288,6 +1378,96 @@ func _Control_DeleteTag_Handler(srv interface{}, ctx context.Context, dec func(i return interceptor(ctx, in, info, handler) } +func _Control_CreateSharedVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateSharedVolumeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ControlServer).CreateSharedVolume(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Control_CreateSharedVolume_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ControlServer).CreateSharedVolume(ctx, req.(*CreateSharedVolumeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Control_GetSharedVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSharedVolumeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ControlServer).GetSharedVolume(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Control_GetSharedVolume_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ControlServer).GetSharedVolume(ctx, req.(*GetSharedVolumeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Control_ListSharedVolumes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListSharedVolumesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ControlServer).ListSharedVolumes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Control_ListSharedVolumes_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ControlServer).ListSharedVolumes(ctx, req.(*ListSharedVolumesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Control_UpdateSharedVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateSharedVolumeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ControlServer).UpdateSharedVolume(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Control_UpdateSharedVolume_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ControlServer).UpdateSharedVolume(ctx, req.(*UpdateSharedVolumeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Control_DeleteSharedVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteSharedVolumeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ControlServer).DeleteSharedVolume(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Control_DeleteSharedVolume_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ControlServer).DeleteSharedVolume(ctx, req.(*DeleteSharedVolumeRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Control_ListWorkers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListWorkersRequest) if err := dec(in); err != nil { @@ -1821,6 +2001,26 @@ var Control_ServiceDesc = grpc.ServiceDesc{ MethodName: "DeleteTag", Handler: _Control_DeleteTag_Handler, }, + { + MethodName: "CreateSharedVolume", + Handler: _Control_CreateSharedVolume_Handler, + }, + { + MethodName: "GetSharedVolume", + Handler: _Control_GetSharedVolume_Handler, + }, + { + MethodName: "ListSharedVolumes", + Handler: _Control_ListSharedVolumes_Handler, + }, + { + MethodName: "UpdateSharedVolume", + Handler: _Control_UpdateSharedVolume_Handler, + }, + { + MethodName: "DeleteSharedVolume", + Handler: _Control_DeleteSharedVolume_Handler, + }, { MethodName: "ListWorkers", Handler: _Control_ListWorkers_Handler, diff --git a/tools/apitool/exemptions.json b/tools/apitool/exemptions.json index 1664fa468b..12f12ea9e4 100644 --- a/tools/apitool/exemptions.json +++ b/tools/apitool/exemptions.json @@ -359,11 +359,6 @@ "subject": "ateapi.Volume", "message": "message has no doc comment" }, - { - "rule": "documented", - "subject": "ateapi.Volume.external_volume_template", - "message": "field has no doc comment" - }, { "rule": "documented", "subject": "ateapi.Volume.system_info", diff --git a/tools/apitool/internal/model/model.go b/tools/apitool/internal/model/model.go index 38760d86d6..1a91077767 100644 --- a/tools/apitool/internal/model/model.go +++ b/tools/apitool/internal/model/model.go @@ -171,13 +171,13 @@ type Field struct { // Repeated is true for a `repeated` field, not a map - see // MapValueKind for those. For example, true for - // `repeated ExternalVolume external_volumes = 7;` on ActorStatus; false + // `repeated ExternalVolumeRef external_volumes = 7;` on ActorStatus; false // for ResourceMetadata.atespace. Repeated bool // TypeDisplay is always a ready-to-render string. For example, "string" // for ResourceMetadata.name, "int64" for ResourceMetadata.version, - // "repeated ExternalVolume" for ActorStatus.external_volumes, + // "repeated ExternalVolumeRef" for ActorStatus.external_volumes, // "map" for SandboxAssets.assets, "ActorState" for // ActorStatus.state, or "google.protobuf.Timestamp" for // ResourceMetadata.create_time.