From 0fe82947dbf1314d0ba6c638edf36ee9bf3d4ea7 Mon Sep 17 00:00:00 2001 From: msau42 Date: Thu, 27 Aug 2026 22:15:09 +0000 Subject: [PATCH] Add validation to ateapi ExternalVolume --- cmd/ateapi/internal/controlapi/actor.go | 13 +- cmd/ateapi/internal/controlapi/validate.go | 33 +++ .../internal/controlapi/validation_test.go | 191 ++++++++++++++++++ cmd/ateapi/internal/controlapi/volumes.go | 3 + .../internal/controlapi/volumes_test.go | 7 + .../controlapi/zz_generated.validation.go | 168 ++++++++++++++- .../v1alpha1/actortemplate_validation_test.go | 38 ++++ pkg/proto/ateapipb/ateapi.pb.go | 27 ++- pkg/proto/ateapipb/ateapi.proto | 27 ++- 9 files changed, 499 insertions(+), 8 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/actor.go b/cmd/ateapi/internal/controlapi/actor.go index ff4f9d59a2..d278c03314 100644 --- a/cmd/ateapi/internal/controlapi/actor.go +++ b/cmd/ateapi/internal/controlapi/actor.go @@ -103,7 +103,7 @@ func (s *ServiceImpl) CreateActor(ctx context.Context, inActor *ateapipb.Actor) LatestSnapshot: sourceSnapshotStatus.GetSnapshot(), SourceSnapshot: sourceSnapshotStatus, } - if errs := validateActorUpdate(ctx, field.NewPath("actor"), outActor, inActor, true); len(errs) > 0 { + if errs := validateActorCreate(ctx, field.NewPath("actor"), outActor); len(errs) > 0 { return nil, toGRPCInternalError(errs) } @@ -481,6 +481,17 @@ func validateSuspendActorRequest(req *ateapipb.SuspendActorRequest) field.ErrorL return errs } +func validateActorCreate(ctx context.Context, fldPath *field.Path, val *ateapipb.Actor) field.ErrorList { + // Call the generated validation. + op := operation.Operation{Type: operation.Create} + errs := Validate_Actor(ctx, op, fldPath, val, nil) + // Status is optional in the schema, but is actually required to be set + // by the server. If it was specified, it was already validated above, + // but if it was not specified we need to flag that as an error. + errs = append(errs, validate.RequiredPointer(ctx, op, fldPath.Child("status"), val.GetStatus(), nil)...) + return errs +} + func validateActorUpdate(ctx context.Context, fldPath *field.Path, newVal, oldVal *ateapipb.Actor, requireStatus bool) field.ErrorList { // Call the generated validation. op := operation.Operation{Type: operation.Update} diff --git a/cmd/ateapi/internal/controlapi/validate.go b/cmd/ateapi/internal/controlapi/validate.go index 68358d20ee..543cdec856 100644 --- a/cmd/ateapi/internal/controlapi/validate.go +++ b/cmd/ateapi/internal/controlapi/validate.go @@ -17,6 +17,7 @@ package controlapi import ( "context" "reflect" + "strings" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" @@ -97,3 +98,35 @@ func ValidateCustom_UpdateActorRequest_Actor(ctx context.Context, op operation.O func ValidateCustom_WorkerAssignment_WorkerPodIp(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { return validation.IsValidIP(fldPath, *value) } + +// ValidateCustom_ExternalVolume_VolumeType checks that a volume type string is well-formed. +// It allows an optional "substrate.io/" prefix, followed by a valid DNS-1123 subdomain. +func ValidateCustom_ExternalVolume_VolumeType(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + if value == nil || *value == "" { + return nil + } + var errs field.ErrorList + valToValidate := strings.TrimPrefix(*value, "substrate.io/") + for _, msg := range validation.IsDNS1123Subdomain(valToValidate) { + errs = append(errs, field.Invalid(fldPath, *value, msg)) + } + return errs +} + +// ValidateCustom_ExternalVolume_StorageVolumeId checks that an external volume's storage ID does not +// contain control characters (U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F). +func ValidateCustom_ExternalVolume_StorageVolumeId(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + if value == nil || *value == "" { + return nil + } + for _, r := range *value { + if (r >= 0x0000 && r <= 0x0008) || + r == 0x000B || + r == 0x000C || + (r >= 0x000E && r <= 0x001F) || + (r >= 0x007F && r <= 0x009F) { + return field.ErrorList{field.Invalid(fldPath, *value, "must not contain control characters (U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F)")} + } + } + return nil +} diff --git a/cmd/ateapi/internal/controlapi/validation_test.go b/cmd/ateapi/internal/controlapi/validation_test.go index cbb2982fd0..43c5803f36 100644 --- a/cmd/ateapi/internal/controlapi/validation_test.go +++ b/cmd/ateapi/internal/controlapi/validation_test.go @@ -776,3 +776,194 @@ func TestValidateTrustBundleDataSource(t *testing.T) { }) } } + +func validExternalVolume(mutate ...func(*ateapipb.ExternalVolume)) *ateapipb.ExternalVolume { + v := &ateapipb.ExternalVolume{ + VolumeName: "my-vol", + StorageVolumeId: "valid-storage-id", + VolumeType: "mock", + Status: ateapipb.ExternalVolume_STATUS_CREATED, + } + for _, m := range mutate { + m(v) + } + return v +} + +func TestValidateExternalVolume(t *testing.T) { + valid := validExternalVolume + + tests := []struct { + name string + obj *ateapipb.ExternalVolume + want field.ErrorList + }{ + { + name: "valid external volume", + obj: valid(), + }, + { + name: "valid external volume with empty storage volume id", + obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "" }), + }, + { + name: "invalid storage volume id with null U+0000", + obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol\x00id" }), + want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "")}, + }, + { + name: "invalid storage volume id with unit separator U+001F", + obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol\x1fid" }), + want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "")}, + }, + { + name: "invalid storage volume id with DEL U+007F", + obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol\x7fid" }), + want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "")}, + }, + { + name: "invalid storage volume id with C1 control U+0080", + obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol\u0080id" }), + want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "")}, + }, + { + name: "invalid storage volume id with C1 control U+009F", + obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol\u009fid" }), + want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "")}, + }, + { + name: "valid volume type", + obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "mock" }), + }, + { + name: "valid csi volume type", + obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "pd.csi.storage.gke.io" }), + }, + { + name: "missing volume type", + obj: valid(func(v *ateapipb.ExternalVolume) { 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" }), + 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) }), + }, + { + name: "invalid volume type exceeding 253 characters", + obj: valid(func(v *ateapipb.ExternalVolume) { 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" }), + }, + { + name: "invalid volume type with empty plugin after substrate.io prefix", + obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "substrate.io/" }), + want: field.ErrorList{field.Invalid(field.NewPath("volume_type"), nil, "")}, + }, + { + name: "invalid volume type with invalid plugin name after substrate.io prefix", + obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "substrate.io/Mock_Plugin" }), + want: field.ErrorList{field.Invalid(field.NewPath("volume_type"), nil, "")}, + }, + { + name: "missing volume name", + obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeName = "" }), + want: field.ErrorList{field.Required(field.NewPath("volume_name"), "")}, + }, + { + name: "invalid volume name exceeding 63 characters", + obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeName = strings.Repeat("a", 64) }), + want: field.ErrorList{field.TooLong(field.NewPath("volume_name"), nil, 63).WithOrigin("maxLength")}, + }, + { + name: "invalid volume type with non-substrate prefix", + obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "other.io/mock" }), + want: field.ErrorList{field.Invalid(field.NewPath("volume_type"), nil, "")}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + obj := proto.CloneOf(tt.obj) + op := operation.Operation{Type: operation.Create} + matcher := field.ErrorMatcher{}.ByType().ByField() + matcher.Test(t, tt.want, Validate_ExternalVolume(context.Background(), op, nil, obj, nil)) + }) + } +} + +func TestValidateExternalVolume_Update(t *testing.T) { + valid := validExternalVolume + + tests := []struct { + name string + oldObj *ateapipb.ExternalVolume + newObj *ateapipb.ExternalVolume + want field.ErrorList + }{ + { + name: "unchanged volume is valid", + oldObj: valid(), + newObj: valid(), + want: nil, + }, + { + name: "volume_name changed is invalid", + oldObj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeName = "vol1" }), + newObj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeName = "vol2" }), + want: field.ErrorList{field.Invalid(field.NewPath("volume_name"), nil, "").WithOrigin("immutable")}, + }, + { + 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" }), + want: nil, + }, + { + 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" }), + 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 = "" }), + 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" }), + want: field.ErrorList{field.Invalid(field.NewPath("volume_type"), nil, "").WithOrigin("immutable")}, + }, + { + name: "status and volume_context changed is valid", + oldObj: valid(func(v *ateapipb.ExternalVolume) { + v.Status = ateapipb.ExternalVolume_STATUS_PENDING + v.VolumeContext = nil + }), + newObj: valid(func(v *ateapipb.ExternalVolume) { + v.Status = ateapipb.ExternalVolume_STATUS_CREATED + v.VolumeContext = map[string]string{"foo": "bar"} + }), + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + op := operation.Operation{Type: operation.Update} + matcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin() + matcher.Test(t, tt.want, Validate_ExternalVolume(context.Background(), op, nil, tt.newObj, tt.oldObj)) + }) + } +} diff --git a/cmd/ateapi/internal/controlapi/volumes.go b/cmd/ateapi/internal/controlapi/volumes.go index d0dcb6ad15..14680245b1 100644 --- a/cmd/ateapi/internal/controlapi/volumes.go +++ b/cmd/ateapi/internal/controlapi/volumes.go @@ -30,6 +30,9 @@ import ( // initialActorVolumes constructs initial volume objects in PENDING state before volume creation. func initialActorVolumes(ctx context.Context, scLister storagev1listers.StorageClassLister, template *ateapipb.ActorTemplate) ([]*ateapipb.ExternalVolume, error) { + if template == nil { + return nil, status.Error(codes.InvalidArgument, "template is required") + } var volumes []*ateapipb.ExternalVolume for _, vol := range template.GetVolumes() { if vol.GetExternalVolumeTemplate() != nil { diff --git a/cmd/ateapi/internal/controlapi/volumes_test.go b/cmd/ateapi/internal/controlapi/volumes_test.go index e7842a25d1..be1285a257 100644 --- a/cmd/ateapi/internal/controlapi/volumes_test.go +++ b/cmd/ateapi/internal/controlapi/volumes_test.go @@ -117,6 +117,13 @@ func TestInitialActorVolumes_PendingState(t *testing.T) { } } +func TestInitialActorVolumes_NilTemplate(t *testing.T) { + _, err := initialActorVolumes(context.Background(), nil, nil) + if err == nil { + t.Fatal("expected error for nil template, got nil") + } +} + func TestCreateActorVolumes(t *testing.T) { ctx := context.Background() diff --git a/cmd/ateapi/internal/controlapi/zz_generated.validation.go b/cmd/ateapi/internal/controlapi/zz_generated.validation.go index c15eaf25c3..0b9a0815cf 100644 --- a/cmd/ateapi/internal/controlapi/zz_generated.validation.go +++ b/cmd/ateapi/internal/controlapi/zz_generated.validation.go @@ -529,7 +529,49 @@ func Validate_ActorStatus( // field ateapipb.ActorStatus.LatestSnapshot has no validation // field ateapipb.ActorStatus.LocalSnapshotInfo has no validation // field ateapipb.ActorStatus.InProgressSnapshotSourceActorVersion has no validation - // field ateapipb.ActorStatus.ActorVolumes has no validation + + { // field ateapipb.ActorStatus.ActorVolumes + fn := func( + fldPath *field.Path, + obj, oldObj []*ateapipb.ExternalVolume, + 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.ExternalVolume](ctx, op, fldPath, obj, oldObj).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.ExternalVolume, b *ateapipb.ExternalVolume) bool { return a.VolumeName == b.VolumeName }); 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.VolumeName == b.VolumeName }, deepEqualImpl_, Validate_ExternalVolume); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ActorStatus) []*ateapipb.ExternalVolume { + return oldObj.ActorVolumes + }) + errs = append(errs, fn(fldPath.Child("actor_volumes"), obj.ActorVolumes, oldVal, oldObj != nil)...) + } + // field ateapipb.ActorStatus.InProgressLocalSnapshotName has no validation // field ateapipb.ActorStatus.SourceSnapshot has no validation return errs @@ -1369,6 +1411,130 @@ func Validate_EgressRuleEffects( return errs } +// Validate_ExternalVolume validates an instance of ExternalVolume according +// to declarative validation rules in the API schema. +func Validate_ExternalVolume( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.ExternalVolume) (errs field.ErrorList) { + + { // field ateapipb.ExternalVolume.VolumeName + 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.Immutable(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + 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, 63); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ExternalVolume) *string { + return &oldObj.VolumeName + }) + errs = append(errs, fn(fldPath.Child("volume_name"), &obj.VolumeName, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ExternalVolume.StorageVolumeId + 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_ExternalVolume_StorageVolumeId(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ExternalVolume) *string { + return &oldObj.StorageVolumeId + }) + errs = append(errs, fn(fldPath.Child("storage_volume_id"), &obj.StorageVolumeId, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ExternalVolume.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.Immutable(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + 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_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 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ExternalVolume) *string { + return &oldObj.VolumeType + }) + errs = append(errs, fn(fldPath.Child("volume_type"), &obj.VolumeType, oldVal, oldObj != nil)...) + } + + // field ateapipb.ExternalVolume.Status has no validation + // field ateapipb.ExternalVolume.VolumeContext has no validation + return errs +} + // Validate_GetActorEgressPolicyRequest validates an instance of GetActorEgressPolicyRequest according // to declarative validation rules in the API schema. func Validate_GetActorEgressPolicyRequest( diff --git a/pkg/api/v1alpha1/actortemplate_validation_test.go b/pkg/api/v1alpha1/actortemplate_validation_test.go index 6bd26b8c59..4510928f93 100644 --- a/pkg/api/v1alpha1/actortemplate_validation_test.go +++ b/pkg/api/v1alpha1/actortemplate_validation_test.go @@ -1533,6 +1533,44 @@ func TestActorTemplateValidation(t *testing.T) { }, wantErr: true, errMsg: "Name must be a valid DNS label", + }, { + name: "Volumes: Volume Name empty is invalid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + {Name: "", VolumeSource: VolumeSource{DurableDir: &DurableDirVolumeSource{}}}, + } + }, + wantErr: true, + errMsg: "spec.volumes[0].name", + }, { + name: "Volumes: duplicate Volume Name is invalid", + mutate: func(at *ActorTemplate) { + at.Spec.Volumes = []Volume{ + { + Name: "vol1", + VolumeSource: VolumeSource{ + ExternalVolumeTemplate: &ExternalVolumeTemplate{ + Capacity: resource.MustParse("10Gi"), + StorageClassName: "standard", + }, + }, + }, + { + Name: "vol1", + VolumeSource: VolumeSource{ + ExternalVolumeTemplate: &ExternalVolumeTemplate{ + Capacity: resource.MustParse("20Gi"), + StorageClassName: "standard", + }, + }, + }, + } + at.Spec.Containers[0].VolumeMounts = []VolumeMount{ + {Name: "vol1", MountPath: "/mnt/vol1"}, + } + }, + wantErr: true, + errMsg: `Duplicate value: {"name":"vol1"}`, }, { name: "Volumes: VolumeMount Name with uppercase is invalid", mutate: func(at *ActorTemplate) { diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index c9adccd53f..dd6a4a410b 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -773,11 +773,29 @@ func (x *ResourceMetadata) GetUpdateTime() *timestamppb.Timestamp { type ExternalVolume struct { state protoimpl.MessageState `protogen:"open.v1"` // Name of the volume specified in the actor template. + // + // +k8s:required + // +k8s:maxLength=63 + // +k8s:immutable VolumeName string `protobuf:"bytes,1,opt,name=volume_name,json=volumeName,proto3" json:"volume_name,omitempty"` // The globally unique volume_id returned from the storage system. - // This will be initially empty during volume creation + // This will be initially empty during volume creation. Must not contain + // disallowed unicode characters (U+0000-U+0008, U+000B, U+000C, + // U+000E-U+001F, U+007F-U+009F). + // + // +k8s:optional + // +k8s:update=NoModify + // +k8s:update=NoUnset + // +k8s:customValidation StorageVolumeId string `protobuf:"bytes,2,opt,name=storage_volume_id,json=storageVolumeId,proto3" json:"storage_volume_id,omitempty"` - // Internal volume plugin name or CSI driver name. + // Internal volume plugin name or CSI driver name. Required. Must be a valid + // DNS-1123 subdomain (optionally prefixed with "substrate.io/"), max 253 + // characters. + // + // +k8s:required + // +k8s:maxLength=253 + // +k8s:immutable + // +k8s:customValidation VolumeType string `protobuf:"bytes,3,opt,name=volume_type,json=volumeType,proto3" json:"volume_type,omitempty"` Status ExternalVolume_Status `protobuf:"varint,4,opt,name=status,proto3,enum=ateapi.ExternalVolume_Status" json:"status,omitempty"` // volume_context contains metadata returned by the CSI driver during volume @@ -1413,7 +1431,10 @@ type ActorStatus struct { InProgressSnapshotSourceActorVersion int64 `protobuf:"varint,6,opt,name=in_progress_snapshot_source_actor_version,json=inProgressSnapshotSourceActorVersion,proto3" json:"in_progress_snapshot_source_actor_version,omitempty"` // Volumes attached to the actor. These volumes only live as long as the actor. // They are deleted when the actor is deleted. - // TODO: Add DV (optional, need to recurse into this type, immutable?) + // + // +k8s:optional + // +k8s:listType=map + // +k8s:listMapKey=volume_name ActorVolumes []*ExternalVolume `protobuf:"bytes,7,rep,name=actor_volumes,json=actorVolumes,proto3" json:"actor_volumes,omitempty"` // TODO: Add DV (optional, maxLength?) InProgressLocalSnapshotName string `protobuf:"bytes,8,opt,name=in_progress_local_snapshot_name,json=inProgressLocalSnapshotName,proto3" json:"in_progress_local_snapshot_name,omitempty"` diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 9bf5a80064..f2077deffa 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -232,13 +232,31 @@ message ResourceMetadata { message ExternalVolume { // Name of the volume specified in the actor template. + // + // +k8s:required + // +k8s:maxLength=63 + // +k8s:immutable string volume_name = 1; // The globally unique volume_id returned from the storage system. - // This will be initially empty during volume creation + // This will be initially empty during volume creation. Must not contain + // disallowed unicode characters (U+0000-U+0008, U+000B, U+000C, + // U+000E-U+001F, U+007F-U+009F). + // + // +k8s:optional + // +k8s:update=NoModify + // +k8s:update=NoUnset + // +k8s:customValidation string storage_volume_id = 2; - // Internal volume plugin name or CSI driver name. + // Internal volume plugin name or CSI driver name. Required. Must be a valid + // DNS-1123 subdomain (optionally prefixed with "substrate.io/"), max 253 + // characters. + // + // +k8s:required + // +k8s:maxLength=253 + // +k8s:immutable + // +k8s:customValidation string volume_type = 3; enum Status { @@ -483,7 +501,10 @@ message ActorStatus { // Volumes attached to the actor. These volumes only live as long as the actor. // They are deleted when the actor is deleted. - // TODO: Add DV (optional, need to recurse into this type, immutable?) + // + // +k8s:optional + // +k8s:listType=map + // +k8s:listMapKey=volume_name repeated ExternalVolume actor_volumes = 7; // TODO: Add DV (optional, maxLength?)