diff --git a/cmd/ateapi/internal/controlapi/actor_template.go b/cmd/ateapi/internal/controlapi/actor_template.go index adbf44c2e9..bfe6bc4aff 100644 --- a/cmd/ateapi/internal/controlapi/actor_template.go +++ b/cmd/ateapi/internal/controlapi/actor_template.go @@ -273,6 +273,16 @@ func ValidateCustom_Resources_Limits(_ context.Context, _ operation.Operation, f return errs } +// ValidateCustom_SnapshotsConfig_StorageLocation ensures an +// ActorTemplate's snapshotsConfig.location is a well-formed +// URI with a bucket, so a bad location fails fast. +func ValidateCustom_SnapshotsConfig_StorageLocation(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + if err := resources.ValidateSnapshotLocation(*value); err != nil { + return field.ErrorList{field.Invalid(fldPath, *value, err.Error())} + } + return nil +} + // ValidateCustom_SnapshotsConfig requires on_commit to be a // subset of on_pause. UNSPECIFIED means FULL, so an unset on_commit over a // DATA on_pause is rejected too. diff --git a/cmd/ateapi/internal/controlapi/actor_template_test.go b/cmd/ateapi/internal/controlapi/actor_template_test.go index 84d44cd48f..f14873e6b3 100644 --- a/cmd/ateapi/internal/controlapi/actor_template_test.go +++ b/cmd/ateapi/internal/controlapi/actor_template_test.go @@ -136,6 +136,18 @@ func TestValidateCreateActorTemplateRequest(t *testing.T) { tmpl.SnapshotsConfig.StorageLocation = "" })}, field.ErrorList{field.Required(field.NewPath("actor_template", "snapshots_config", "storage_location"), "")}, + }, { + "storage_location without a bucket", + &ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(tmpl *ateapipb.ActorTemplate) { + tmpl.SnapshotsConfig.StorageLocation = "my-bucket/snapshots" + })}, + field.ErrorList{field.Invalid(field.NewPath("actor_template", "snapshots_config", "storage_location"), "my-bucket/snapshots", "")}, + }, { + "storage_location with a query", + &ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(tmpl *ateapipb.ActorTemplate) { + tmpl.SnapshotsConfig.StorageLocation = "gs://my-bucket/snapshots?versions=true" + })}, + field.ErrorList{field.Invalid(field.NewPath("actor_template", "snapshots_config", "storage_location"), "gs://my-bucket/snapshots?versions=true", "")}, }, { "on_commit broader than on_pause", &ateapipb.CreateActorTemplateRequest{ActorTemplate: validActorTemplate(func(tmpl *ateapipb.ActorTemplate) { diff --git a/cmd/ateapi/internal/controlapi/zz_generated.validation.go b/cmd/ateapi/internal/controlapi/zz_generated.validation.go index 435446bd8c..ee2d96d51e 100644 --- a/cmd/ateapi/internal/controlapi/zz_generated.validation.go +++ b/cmd/ateapi/internal/controlapi/zz_generated.validation.go @@ -5605,6 +5605,10 @@ func Validate_SnapshotsConfig( if earlyReturn { return // do not proceed } + // custom validation + if e := ValidateCustom_SnapshotsConfig_StorageLocation(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 1024); len(e) != 0 { errs = append(errs, e...) } diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index 4ef399a2ca..989ffdb514 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -2545,8 +2545,8 @@ type SnapshotsConfig struct { // this version are stored under. Required. // // +k8s:required - // +k8s:maxLength=1024 # bound only; object-storage URI formats vary - // TODO: validate that this is a well-formed object-storage URI + // +k8s:maxLength=1024 + // +k8s:customValidation # Validate URI StorageLocation string `protobuf:"bytes,4,opt,name=storage_location,json=storageLocation,proto3" json:"storage_location,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index ffadb9e7bf..acd5975c79 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -852,8 +852,8 @@ message SnapshotsConfig { // this version are stored under. Required. // // +k8s:required - // +k8s:maxLength=1024 # bound only; object-storage URI formats vary - // TODO: validate that this is a well-formed object-storage URI + // +k8s:maxLength=1024 + // +k8s:customValidation # Validate URI string storage_location = 4; }