diff --git a/cmd/ateapi/internal/controlapi/validation_test.go b/cmd/ateapi/internal/controlapi/validation_test.go index cbb2982fd0..d86de7b1bb 100644 --- a/cmd/ateapi/internal/controlapi/validation_test.go +++ b/cmd/ateapi/internal/controlapi/validation_test.go @@ -776,3 +776,87 @@ func TestValidateTrustBundleDataSource(t *testing.T) { }) } } + +func TestValidateDeleteOptions(t *testing.T) { + valid := func(mutate ...func(*ateapipb.DeleteOptions)) *ateapipb.DeleteOptions { + tb := &ateapipb.DeleteOptions{} + for _, m := range mutate { + m(tb) + } + return tb + } + + tests := []struct { + name string + obj *ateapipb.DeleteOptions + want field.ErrorList + }{{ + name: "valid", + obj: valid(), // all optional fields + }, { + name: "valid version", + obj: valid(func(do *ateapipb.DeleteOptions) { do.Version = 1 }), + want: nil, + }, { + name: "invalid version", + obj: valid(func(do *ateapipb.DeleteOptions) { do.Version = -1 }), + want: field.ErrorList{field.Invalid(field.NewPath("version"), nil, "").WithOrigin("minimum")}, + }, { + name: "valid uid", + obj: valid(func(do *ateapipb.DeleteOptions) { do.Uid = "11111111-2222-3333-4444-555555555555" }), + want: nil, + }, { + name: "invalid uid", + obj: valid(func(do *ateapipb.DeleteOptions) { do.Uid = "not a uid" }), + want: field.ErrorList{field.Invalid(field.NewPath("uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + op := operation.Operation{Type: operation.Create} + matcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin() + matcher.Test(t, tt.want, Validate_DeleteOptions(context.Background(), op, nil, tt.obj, nil)) + }) + } +} + +func TestValidateKubeNamespacedObjectRef(t *testing.T) { + valid := func(mutate ...func(*ateapipb.KubeNamespacedObjectRef)) *ateapipb.KubeNamespacedObjectRef { + tb := &ateapipb.KubeNamespacedObjectRef{Namespace: "ns", Name: "nm"} + for _, m := range mutate { + m(tb) + } + return tb + } + + tests := []struct { + name string + obj *ateapipb.KubeNamespacedObjectRef + want field.ErrorList + }{{ + name: "valid", + obj: valid(), + }, { + name: "missing namespace", + obj: valid(func(or *ateapipb.KubeNamespacedObjectRef) { or.Namespace = "" }), + want: field.ErrorList{field.Required(field.NewPath("namespace"), "")}, + }, { + name: "invalid namespace", + obj: valid(func(or *ateapipb.KubeNamespacedObjectRef) { or.Namespace = "not a namespace" }), + want: field.ErrorList{field.Invalid(field.NewPath("namespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing name", + obj: valid(func(or *ateapipb.KubeNamespacedObjectRef) { or.Name = "" }), + want: field.ErrorList{field.Required(field.NewPath("name"), "")}, + }, { + name: "invalid name", + obj: valid(func(or *ateapipb.KubeNamespacedObjectRef) { or.Name = "not a name" }), + want: field.ErrorList{field.Invalid(field.NewPath("name"), nil, "").WithOrigin("format=k8s-long-name")}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + op := operation.Operation{Type: operation.Create} + matcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin() + matcher.Test(t, tt.want, Validate_KubeNamespacedObjectRef(context.Background(), op, nil, tt.obj, nil)) + }) + } +} diff --git a/cmd/ateapi/internal/controlapi/worker.go b/cmd/ateapi/internal/controlapi/worker.go index 7841245c50..d130a8a04d 100644 --- a/cmd/ateapi/internal/controlapi/worker.go +++ b/cmd/ateapi/internal/controlapi/worker.go @@ -20,17 +20,18 @@ import ( "fmt" "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "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" "google.golang.org/protobuf/proto" - "k8s.io/apimachinery/pkg/api/validate/content" + "k8s.io/apimachinery/pkg/api/operation" + "k8s.io/apimachinery/pkg/api/validate" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" ) func (s *RPCService) ListWorkers(ctx context.Context, req *ateapipb.ListWorkersRequest) (*ateapipb.ListWorkersResponse, error) { - if errs := validateListWorkersRequest(req); len(errs) > 0 { + if errs := validateListWorkersRequest(ctx, req); len(errs) > 0 { return nil, toGRPCStatusError(errs) } @@ -45,23 +46,17 @@ func (s *RPCService) ListWorkers(ctx context.Context, req *ateapipb.ListWorkersR } func (s *ServiceImpl) ListWorkers(ctx context.Context, opts store.ListOptions) (store.ListResponse[*ateapipb.Worker], error) { - // TODO: implement this return s.store.ListWorkers(ctx, opts) } -func validateListWorkersRequest(req *ateapipb.ListWorkersRequest) field.ErrorList { - var fldPath *field.Path - var errs field.ErrorList - - if val, fldPath := req.PageSize, fldPath.Child("page_size"); val < 0 { - errs = append(errs, field.Invalid(fldPath, val, "must be greater than or equal to 0")) - } - - return errs +func validateListWorkersRequest(ctx context.Context, req *ateapipb.ListWorkersRequest) field.ErrorList { + // Call the generated validation. + op := operation.Operation{Type: operation.Create} + return Validate_ListWorkersRequest(ctx, op, nil, req, nil) } func (s *RPCService) GetWorker(ctx context.Context, req *ateapipb.GetWorkerRequest) (*ateapipb.Worker, error) { - if errs := validateGetWorkerRequest(req); len(errs) > 0 { + if errs := validateGetWorkerRequest(ctx, req); len(errs) > 0 { return nil, toGRPCStatusError(errs) } name := req.GetWorker().GetName() @@ -77,68 +72,86 @@ func (s *RPCService) GetWorker(ctx context.Context, req *ateapipb.GetWorkerReque } func (s *ServiceImpl) GetWorker(ctx context.Context, name string) (*ateapipb.Worker, error) { - // TODO: implement this return s.store.GetWorker(ctx, name) } -func validateGetWorkerRequest(req *ateapipb.GetWorkerRequest) field.ErrorList { - var fldPath *field.Path - return resources.ValidateGlobalObjectRef(req.GetWorker(), fldPath.Child("worker")) +func validateGetWorkerRequest(ctx context.Context, req *ateapipb.GetWorkerRequest) field.ErrorList { + // Call the generated validation. + op := operation.Operation{Type: operation.Create} + return Validate_GetWorkerRequest(ctx, op, nil, req, nil) } func (s *RPCService) CreateWorker(ctx context.Context, req *ateapipb.CreateWorkerRequest) (*ateapipb.Worker, error) { - if errs := validateCreateWorkerRequest(req); len(errs) > 0 { + // First scrub any fields that callers are not allowed to set. status is + // output-only, so whatever the request carried there is replaced rather + // than rejected. + inWorker := req.Worker + if inWorker != nil { // otherwise validation will flag it + scrubResourceMetadataForCreate(inWorker.Metadata) + inWorker.Status = nil + } + + // Validate the request, including the object within it. + if errs := validateCreateWorkerRequest(ctx, req); len(errs) > 0 { return nil, toGRPCStatusError(errs) } - name := req.GetWorker().GetMetadata().GetName() - // status is output-only, so whatever the request carried there is replaced - // rather than rejected. - worker := proto.Clone(req.GetWorker()).(*ateapipb.Worker) - worker.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE} + // Handle the creation, including validation of the final stored object. + return s.impl.CreateWorker(ctx, inWorker) +} + +func (s *ServiceImpl) CreateWorker(ctx context.Context, inWorker *ateapipb.Worker) (*ateapipb.Worker, error) { + // A Worker is registered only once its pod is Ready and has an IP, which + // makes ACTIVE the only state it can be born in. + outWorker := proto.CloneOf(inWorker) + outWorker.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE} - created, err := s.impl.CreateWorker(ctx, worker) - if errors.Is(err, store.ErrAlreadyExists) { - return nil, status.Errorf(codes.AlreadyExists, "Worker %s already exists", name) + // Verify that the result is properly valid before storing it. + if errs := validateWorkerUpdate(ctx, field.NewPath("worker"), outWorker, inWorker, true); len(errs) > 0 { + return nil, toGRPCInternalError(errs) } + + // Save the data in the storage layer. + created, err := s.store.CreateWorker(ctx, outWorker) if err != nil { + if errors.Is(err, store.ErrAlreadyExists) { + return nil, status.Errorf(codes.AlreadyExists, "Worker %s already exists", inWorker.GetMetadata().GetName()) + } return nil, fmt.Errorf("while creating worker: %w", err) } return created, nil } -func (s *ServiceImpl) CreateWorker(ctx context.Context, worker *ateapipb.Worker) (*ateapipb.Worker, error) { - // TODO: implement this - return s.store.CreateWorker(ctx, worker) -} - -func validateCreateWorkerRequest(req *ateapipb.CreateWorkerRequest) field.ErrorList { - var fldPath *field.Path - - worker, workerPath := req.GetWorker(), fldPath.Child("worker") - if worker == nil { - return field.ErrorList{field.Required(workerPath, "")} - } - return validateWorker(worker, workerPath) +func validateCreateWorkerRequest(ctx context.Context, req *ateapipb.CreateWorkerRequest) field.ErrorList { + // Call the generated validation. + op := operation.Operation{Type: operation.Create} + return Validate_CreateWorkerRequest(ctx, op, nil, req, nil) } // UpdateWorker replaces the stored Worker with the one the request carries. // Only sandbox_class and labels are the caller's to change; a request that // alters an immutable field — including by leaving it unset, which would clear -// it — is rejected. The store enforces that, since only it holds the stored -// worker to compare against. +// it — is rejected. The service layer enforces that with declarative +// validation against the stored worker inside the update transaction. func (s *RPCService) UpdateWorker(ctx context.Context, req *ateapipb.UpdateWorkerRequest) (*ateapipb.Worker, error) { - if errs := validateUpdateWorkerRequest(req); len(errs) > 0 { + // First scrub any fields that callers are not allowed to set. + inWorker := req.Worker + if inWorker != nil { // otherwise validation will flag it + scrubResourceMetadataForUpdate(inWorker.Metadata) + inWorker.Status = nil + } + + // Validate the request. + if errs := validateUpdateWorkerRequest(ctx, req); len(errs) > 0 { return nil, toGRPCStatusError(errs) } - in := req.GetWorker() - return s.mutateWorker(ctx, in.GetMetadata().GetName(), store.PreconditionFrom(in), func(toUpdate *ateapipb.Worker) error { + return s.mutateWorker(ctx, inWorker.GetMetadata().GetName(), store.PreconditionFrom(inWorker), func(toUpdate *ateapipb.Worker) error { // Status and metadata are server-owned fields. status, metadata := toUpdate.GetStatus(), toUpdate.GetMetadata() // Reset + merge from the input worker. proto.Reset(toUpdate) - proto.Merge(toUpdate, in) + proto.Merge(toUpdate, inWorker) // Restore status and metadata from the server. toUpdate.Status = status toUpdate.Metadata = metadata @@ -147,26 +160,44 @@ func (s *RPCService) UpdateWorker(ctx context.Context, req *ateapipb.UpdateWorke } func (s *ServiceImpl) UpdateWorker(ctx context.Context, name string, precondition store.Precondition, mutate func(toUpdate *ateapipb.Worker) error) (*ateapipb.Worker, error) { - // TODO: implement this - return s.store.UpdateWorker(ctx, name, precondition, mutate) -} + return s.store.UpdateWorker(ctx, name, precondition, func(toUpdate *ateapipb.Worker) error { + // Apply the mutation function to the stored value. + oldVal := proto.CloneOf(toUpdate) + if err := mutate(toUpdate); err != nil { + return err + } + newVal := toUpdate -func validateUpdateWorkerRequest(req *ateapipb.UpdateWorkerRequest) field.ErrorList { - var fldPath *field.Path + // Validate the mutated value before doing any further work. This is + // what enforces the immutable fields, since only the stored worker + // gives declarative validation an old value to compare against. + if errs := validateWorkerUpdate(ctx, field.NewPath("worker"), newVal, oldVal, false); len(errs) > 0 { + return toGRPCStatusError(errs) + } - worker, workerPath := req.GetWorker(), fldPath.Child("worker") - if worker == nil { - return field.ErrorList{field.Required(workerPath, "")} - } + // Do any further work on the resource. + + // Validate the final value before storing it. + if errs := validateWorkerUpdate(ctx, field.NewPath("worker"), newVal, oldVal, true); len(errs) > 0 { + return toGRPCInternalError(errs) + } - // Only the metadata guards are checked here. The rest of the worker is - // pinned to what create stored — validateWorker already passed on it, and - // an update that changed any of it does not get written. - return resources.ValidateGlobalUpdateMetadataRef(worker.GetMetadata(), workerPath.Child("metadata")) + return nil + }) +} + +func validateUpdateWorkerRequest(ctx context.Context, req *ateapipb.UpdateWorkerRequest) field.ErrorList { + // Call the generated validation. + // We model this as a create rather than an update because updates assume + // the existence of a "current" value, which we do not have yet. This is + // validating the request itself. The result will be validated later, after + // we have a current value to compare against. + op := operation.Operation{Type: operation.Create} + return Validate_UpdateWorkerRequest(ctx, op, nil, req, nil) } func (s *RPCService) DeleteWorker(ctx context.Context, req *ateapipb.DeleteWorkerRequest) (*ateapipb.Worker, error) { - if errs := validateDeleteWorkerRequest(req); len(errs) > 0 { + if errs := validateDeleteWorkerRequest(ctx, req); len(errs) > 0 { return nil, toGRPCStatusError(errs) } // The delete releases the Actor bound to this Worker before removing the @@ -178,31 +209,19 @@ func (s *RPCService) DeleteWorker(ctx context.Context, req *ateapipb.DeleteWorke } func (s *ServiceImpl) DeleteWorker(ctx context.Context, name string, pre store.DeletePreconditions) (*ateapipb.Worker, error) { - // TODO: implement this return s.store.DeleteWorker(ctx, name, pre) } -func validateDeleteWorkerRequest(req *ateapipb.DeleteWorkerRequest) field.ErrorList { - var fldPath *field.Path - - errs := resources.ValidateGlobalObjectRef(req.GetWorker(), fldPath.Child("worker")) - - // Delete carries its preconditions in options, and each is optional: a zero - // value waives that guard. Absent options waive both, so nil needs no - // special case. - opts, optsPath := req.GetOptions(), fldPath.Child("options") - if val, p := opts.GetUid(), optsPath.Child("uid"); val != "" { - errs = append(errs, resources.ValidateUUID(val, p)...) - } - if val, p := opts.GetVersion(), optsPath.Child("version"); val < 0 { - errs = append(errs, field.Invalid(p, val, "must not be negative")) - } - - return errs +func validateDeleteWorkerRequest(ctx context.Context, req *ateapipb.DeleteWorkerRequest) field.ErrorList { + // Call the generated validation. The preconditions in options are each + // optional: a zero value waives that guard, so only non-zero values are + // checked for shape. + op := operation.Operation{Type: operation.Create} + return Validate_DeleteWorkerRequest(ctx, op, nil, req, nil) } func (s *RPCService) DrainWorker(ctx context.Context, req *ateapipb.DrainWorkerRequest) (*ateapipb.Worker, error) { - if errs := validateDrainWorkerRequest(req); len(errs) > 0 { + if errs := validateDrainWorkerRequest(ctx, req); len(errs) > 0 { return nil, toGRPCStatusError(errs) } name := req.GetWorker().GetName() @@ -232,9 +251,10 @@ func (s *RPCService) DrainWorker(ctx context.Context, req *ateapipb.DrainWorkerR }) } -func validateDrainWorkerRequest(req *ateapipb.DrainWorkerRequest) field.ErrorList { - var fldPath *field.Path - return resources.ValidateGlobalObjectRef(req.GetWorker(), fldPath.Child("worker")) +func validateDrainWorkerRequest(ctx context.Context, req *ateapipb.DrainWorkerRequest) field.ErrorList { + // Call the generated validation. + op := operation.Operation{Type: operation.Create} + return Validate_DrainWorkerRequest(ctx, op, nil, req, nil) } // mutateWorker runs mutate against the named Worker and translates what comes @@ -257,8 +277,6 @@ func (s *RPCService) mutateWorker(ctx context.Context, name string, precondition return nil, status.Errorf(codes.Aborted, "Worker %s is not the one the request describes", name) case errors.Is(err, store.ErrVersionConflict): return nil, status.Error(codes.Aborted, "concurrent update conflict, please retry") - case errors.Is(err, store.ErrImmutableField): - return nil, status.Errorf(codes.InvalidArgument, "while updating worker %s: %v", name, err) case errors.Is(err, store.ErrPreconditionRequired): return nil, status.Errorf(codes.InvalidArgument, "while updating worker %s: %v", name, err) } @@ -276,73 +294,44 @@ type workerUnchanged struct { func (u *workerUnchanged) Error() string { return "worker is already in the requested state" } -// validateWorker checks that the caller-controlled fields of a Worker are -// well-formed. It is the create-time check: every field it covers is immutable -// afterwards, so no update path re-runs it. -func validateWorker(worker *ateapipb.Worker, fldPath *field.Path) field.ErrorList { - var errs field.ErrorList - - // Worker is global-scoped: metadata.atespace must be empty, name required + - // valid. uid and version are server-assigned, so a create ignores whatever - // the request carried in them. - metaPath := fldPath.Child("metadata") - if val, p := worker.GetMetadata().GetAtespace(), metaPath.Child("atespace"); val != "" { - errs = append(errs, field.Invalid(p, val, "must be empty for a global-scoped resource")) - } - if val, p := worker.GetMetadata().GetName(), metaPath.Child("name"); val == "" { - errs = append(errs, field.Required(p, "")) - } else { - errs = append(errs, resources.ValidateResourceName(val, p)...) - } - - if val, fldPath := worker.GetWorkerNamespace(), fldPath.Child("worker_namespace"); val == "" { - errs = append(errs, field.Required(fldPath, "")) - } else { - for _, msg := range content.IsDNS1123Label(val) { - errs = append(errs, field.Invalid(fldPath, val, msg)) - } - } - - if val, fldPath := worker.GetWorkerPool(), fldPath.Child("worker_pool"); val == "" { - errs = append(errs, field.Required(fldPath, "")) - } else { - for _, msg := range content.IsDNS1123Subdomain(val) { - errs = append(errs, field.Invalid(fldPath, val, msg)) - } - } - - if val, fldPath := worker.GetWorkerPod(), fldPath.Child("worker_pod"); val == "" { - errs = append(errs, field.Required(fldPath, "")) - } else { - for _, msg := range content.IsDNS1123Subdomain(val) { - errs = append(errs, field.Invalid(fldPath, val, msg)) - } +// validateWorkerUpdate validates a Worker against the previous stored value. +// It is what enforces the immutable fields, which need an old value to compare +// against. +func validateWorkerUpdate(ctx context.Context, fldPath *field.Path, newVal, oldVal *ateapipb.Worker, requireStatus bool) field.ErrorList { + // Call the generated validation. + op := operation.Operation{Type: operation.Update} + errs := Validate_Worker(ctx, op, fldPath, newVal, oldVal) + if requireStatus { + // 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"), newVal.GetStatus(), nil)...) } + return errs +} - if val, fldPath := worker.GetIp(), fldPath.Child("ip"); val == "" { - errs = append(errs, field.Required(fldPath, "")) - } else { - errs = append(errs, resources.ValidateIP(val, fldPath)...) - } +func (s *ServiceImpl) WatchWorkers(ctx context.Context) (*store.WorkerWatch, error) { + return s.store.WatchWorkers(ctx) +} - if val, fldPath := worker.GetWorkerPodUid(), fldPath.Child("worker_pod_uid"); val == "" { - errs = append(errs, field.Required(fldPath, "")) - } else { - errs = append(errs, resources.ValidateUUID(val, fldPath)...) - } +// This is needed because DV doesn't have a standard format for IP addresses yet. +func ValidateCustom_Worker_Ip(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return validation.IsValidIP(fldPath, *value) +} - if val, fldPath := worker.GetNodeName(), fldPath.Child("node_name"); val == "" { - errs = append(errs, field.Required(fldPath, "")) - } else { - for _, msg := range content.IsDNS1123Subdomain(val) { - errs = append(errs, field.Invalid(fldPath, val, msg)) - } +// This exists only because nested subfield tags are not supported yet. +func ValidateCustom_UpdateWorkerRequest_Worker(ctx context.Context, op operation.Operation, fldPath *field.Path, worker, _ *ateapipb.Worker) field.ErrorList { + if worker == nil || worker.Metadata == nil { + return nil // handled by DV } + // Updates are validated in 2 steps: first the update request and then the + // resource itself. DV for the request doesn't descend into the resource + // metadata. Once DV supports nested subfield tags, this can be changed to + // something like: + // +k8s:subfield(metadata)=+k8s:subfield(atespace)=+k8s:forbidden + // Workers are global-scoped, so metadata.atespace must be empty. + errs := Validate_ResourceMetadata(ctx, op, fldPath.Child("metadata"), worker.Metadata, nil) + errs = append(errs, validate.ForbiddenValue(ctx, op, fldPath.Child("metadata", "atespace"), &worker.Metadata.Atespace, nil)...) return errs } - -func (s *ServiceImpl) WatchWorkers(ctx context.Context) (*store.WorkerWatch, error) { - // TODO: implement this - return s.store.WatchWorkers(ctx) -} diff --git a/cmd/ateapi/internal/controlapi/worker_test.go b/cmd/ateapi/internal/controlapi/worker_test.go index e4bcab8dbc..e0e114a56e 100644 --- a/cmd/ateapi/internal/controlapi/worker_test.go +++ b/cmd/ateapi/internal/controlapi/worker_test.go @@ -16,6 +16,7 @@ package controlapi import ( "context" + "fmt" "strings" "testing" @@ -36,10 +37,10 @@ const ( apiOtherWorkerName = "1a7e4c83-6d20-4f95-b3c8-9e0a2f6d4b17" ) -// newAPIWorker returns a Worker in the shape CreateWorker accepts: named, with +// validWorker returns a Worker in the shape CreateWorker accepts: named, with // its pod coordinates filled in and no status — status is output-only. -func newAPIWorker(name string) *ateapipb.Worker { - return &ateapipb.Worker{ +func validWorker(name string, mods ...func(*ateapipb.Worker)) *ateapipb.Worker { + w := &ateapipb.Worker{ Metadata: &ateapipb.ResourceMetadata{Name: name}, WorkerNamespace: "ate-system", WorkerPool: "pool-1", @@ -50,6 +51,29 @@ func newAPIWorker(name string) *ateapipb.Worker { SandboxClass: "gvisor", Capacity: &ateapipb.WorkerCapacity{CpuMilli: 2000, MemoryBytes: 4 << 30}, } + for _, m := range mods { + m(w) + } + return w +} + +// withWorkerMetadata returns a modifier func (see validWorker) which sets +// the worker's resource metadata to a valid value. +func withWorkerMetadata(mutate func(*ateapipb.ResourceMetadata)) func(*ateapipb.Worker) { + return func(a *ateapipb.Worker) { mutate(a.Metadata) } +} + +// withWorkerStatus returns a modifier func (see validWorker) which sets the +// actor's status to a valid value. +func withWorkerStatus(mods ...func(*ateapipb.WorkerStatus)) func(*ateapipb.Worker) { + return func(a *ateapipb.Worker) { + a.Status = &ateapipb.WorkerStatus{ + State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, + } + for _, m := range mods { + m(a.Status) + } + } } func newAPIAssignment(actorUID string) *ateapipb.ActorAssignment { @@ -67,7 +91,8 @@ func newWorkerAPIService(t *testing.T) (*RPCService, store.Interface) { t.Helper() persistence, cleanup := storetest.SetupTestStore(t) t.Cleanup(cleanup) - return &RPCService{impl: persistence, workerWorkflow: NewWorkerWorkflow(persistence)}, persistence + impl := newServiceImpl(persistence, nil, nil) + return &RPCService{impl: impl, workerWorkflow: NewWorkerWorkflow(impl)}, persistence } // seedAPIWorker registers a worker directly through the store and returns it as @@ -136,11 +161,19 @@ func TestValidateListWorkersRequest(t *testing.T) { }, { "negative page_size", &ateapipb.ListWorkersRequest{PageSize: -1}, - field.ErrorList{field.Invalid(field.NewPath("page_size"), int32(-1), "")}, + field.ErrorList{field.Invalid(field.NewPath("page_size"), int32(-1), "").WithOrigin("minimum")}, + }, { + "valid page_token", + &ateapipb.ListWorkersRequest{PageToken: strings.Repeat("x", 256)}, + nil, + }, { + "too-large page_token", + &ateapipb.ListWorkersRequest{PageToken: strings.Repeat("x", 257)}, + field.ErrorList{field.TooLongCharacters(field.NewPath("page_token"), "", 256).WithOrigin("maxLength")}, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assertValidateErr(t, validateListWorkersRequest(tt.req), tt.want) + assertValidateErr(t, validateListWorkersRequest(context.Background(), tt.req), tt.want) }) } } @@ -148,7 +181,7 @@ func TestValidateListWorkersRequest(t *testing.T) { func TestGetWorker(t *testing.T) { ctx := context.Background() svc, persistence := newWorkerAPIService(t) - want := seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + want := seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) got, err := svc.GetWorker(ctx, &ateapipb.GetWorkerRequest{Worker: workerRef(apiWorkerName)}) if err != nil { @@ -162,7 +195,7 @@ func TestGetWorker(t *testing.T) { func TestGetWorker_Errors(t *testing.T) { ctx := context.Background() svc, persistence := newWorkerAPIService(t) - seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) tests := []struct { name string @@ -190,7 +223,7 @@ func TestCreateWorker(t *testing.T) { ctx := context.Background() svc, persistence := newWorkerAPIService(t) - got, err := svc.CreateWorker(ctx, &ateapipb.CreateWorkerRequest{Worker: newAPIWorker(apiWorkerName)}) + got, err := svc.CreateWorker(ctx, &ateapipb.CreateWorkerRequest{Worker: validWorker(apiWorkerName)}) if err != nil { t.Fatalf("CreateWorker() failed: %v", err) } @@ -221,7 +254,7 @@ func TestCreateWorker_IgnoresRequestStatus(t *testing.T) { ctx := context.Background() svc, _ := newWorkerAPIService(t) - in := newAPIWorker(apiWorkerName) + in := validWorker(apiWorkerName) in.Status = &ateapipb.WorkerStatus{ State: ateapipb.WorkerState_WORKER_STATE_DRAINING, Assignment: newAPIAssignment("actor-uid-1"), @@ -240,9 +273,9 @@ func TestCreateWorker_IgnoresRequestStatus(t *testing.T) { func TestCreateWorker_AlreadyExists(t *testing.T) { ctx := context.Background() svc, persistence := newWorkerAPIService(t) - seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) - _, err := svc.CreateWorker(ctx, &ateapipb.CreateWorkerRequest{Worker: newAPIWorker(apiWorkerName)}) + _, err := svc.CreateWorker(ctx, &ateapipb.CreateWorkerRequest{Worker: validWorker(apiWorkerName)}) if got := status.Code(err); got != codes.AlreadyExists { t.Errorf("CreateWorker() code = %v (err %v), want %v", got, err, codes.AlreadyExists) } @@ -270,7 +303,7 @@ func TestCreateWorker_InvalidArgument(t *testing.T) { t.Run(tc.name, func(t *testing.T) { req := &ateapipb.CreateWorkerRequest{} if tc.mutate != nil { - worker := newAPIWorker(apiWorkerName) + worker := validWorker(apiWorkerName) tc.mutate(worker) req.Worker = worker } @@ -285,7 +318,7 @@ func TestCreateWorker_InvalidArgument(t *testing.T) { func TestUpdateWorker(t *testing.T) { ctx := context.Background() svc, persistence := newWorkerAPIService(t) - seeded := seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seeded := seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) got, err := svc.UpdateWorker(ctx, &ateapipb.UpdateWorkerRequest{ Worker: updateFrom(seeded, func(w *ateapipb.Worker) { @@ -315,7 +348,7 @@ func TestUpdateWorker(t *testing.T) { func TestUpdateWorker_OmittedMutableFieldIsCleared(t *testing.T) { ctx := context.Background() svc, persistence := newWorkerAPIService(t) - labelled := newAPIWorker(apiWorkerName) + labelled := validWorker(apiWorkerName) labelled.Labels = map[string]string{"tier": "batch"} seeded := seedAPIWorker(t, ctx, persistence, labelled) @@ -342,7 +375,7 @@ func TestUpdateWorker_OmittedMutableFieldIsCleared(t *testing.T) { func TestUpdateWorker_LeavesStatusAlone(t *testing.T) { ctx := context.Background() svc, persistence := newWorkerAPIService(t) - seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) assigned := assignAPIWorker(t, ctx, persistence, apiWorkerName, "actor-uid-1") got, err := svc.UpdateWorker(ctx, &ateapipb.UpdateWorkerRequest{ @@ -364,7 +397,7 @@ func TestUpdateWorker_LeavesStatusAlone(t *testing.T) { func TestUpdateWorker_Preconditions(t *testing.T) { ctx := context.Background() svc, persistence := newWorkerAPIService(t) - seeded := seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seeded := seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) update := func(bend func(*ateapipb.ResourceMetadata)) error { _, err := svc.UpdateWorker(ctx, &ateapipb.UpdateWorkerRequest{ @@ -416,7 +449,7 @@ func TestUpdateWorker_Preconditions(t *testing.T) { func TestUpdateWorker_Errors(t *testing.T) { ctx := context.Background() svc, persistence := newWorkerAPIService(t) - seeded := seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seeded := seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) // Every case below carries the guards an update requires and the worker as // stored, so the rule it is named for is the one that rejects it. @@ -460,7 +493,7 @@ func TestUpdateWorker_Errors(t *testing.T) { func TestUpdateWorker_DrainingWorkerKeepsOtherFieldsMutable(t *testing.T) { ctx := context.Background() svc, persistence := newWorkerAPIService(t) - seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) drained, err := svc.DrainWorker(ctx, &ateapipb.DrainWorkerRequest{Worker: workerRef(apiWorkerName)}) if err != nil { t.Fatalf("DrainWorker() failed: %v", err) @@ -483,7 +516,7 @@ func TestUpdateWorker_DrainingWorkerKeepsOtherFieldsMutable(t *testing.T) { func TestDeleteWorker(t *testing.T) { ctx := context.Background() svc, persistence := newWorkerAPIService(t) - seeded := seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seeded := seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) got, err := svc.DeleteWorker(ctx, &ateapipb.DeleteWorkerRequest{Worker: workerRef(apiWorkerName)}) if err != nil { @@ -513,7 +546,7 @@ func TestDeleteWorker_Absent(t *testing.T) { func TestDeleteWorker_Preconditions(t *testing.T) { ctx := context.Background() svc, persistence := newWorkerAPIService(t) - seeded := seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seeded := seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) t.Run("stale version", func(t *testing.T) { _, err := svc.DeleteWorker(ctx, &ateapipb.DeleteWorkerRequest{ @@ -556,7 +589,7 @@ func TestDeleteWorker_Preconditions(t *testing.T) { func TestDrainWorker(t *testing.T) { ctx := context.Background() svc, persistence := newWorkerAPIService(t) - seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) got, err := svc.DrainWorker(ctx, &ateapipb.DrainWorkerRequest{Worker: workerRef(apiWorkerName)}) if err != nil { @@ -585,7 +618,7 @@ func TestDrainWorker(t *testing.T) { func TestDrainWorker_KeepsAssignment(t *testing.T) { ctx := context.Background() svc, persistence := newWorkerAPIService(t) - seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) assignAPIWorker(t, ctx, persistence, apiWorkerName, "actor-uid-1") got, err := svc.DrainWorker(ctx, &ateapipb.DrainWorkerRequest{Worker: workerRef(apiWorkerName)}) @@ -608,6 +641,7 @@ func TestDrainWorker_Errors(t *testing.T) { }{ {"absent", &ateapipb.DrainWorkerRequest{Worker: workerRef(apiWorkerName)}, codes.NotFound}, {"no ref", &ateapipb.DrainWorkerRequest{}, codes.InvalidArgument}, + {"no name", &ateapipb.DrainWorkerRequest{Worker: &ateapipb.ObjectRef{}}, codes.InvalidArgument}, {"atespace set", &ateapipb.DrainWorkerRequest{Worker: &ateapipb.ObjectRef{Atespace: "team-a", Name: apiWorkerName}}, codes.InvalidArgument}, } for _, tc := range tests { @@ -623,101 +657,457 @@ func TestDrainWorker_Errors(t *testing.T) { // TestValidateWorker pins the field paths validateWorker reports. // TestCreateWorker_InvalidArgument drives the same rules through the RPC, but // only observes the status code. -func TestValidateWorker(t *testing.T) { +func TestValidateCreateWorkerRequest(t *testing.T) { + // This test verifies validation of user input for creation. The RPC scrubs + // status before validating, so status is absent from the valid shape; when + // a request does carry one, it is validated like any other field. + validReq := func(actor *ateapipb.Worker, mods ...func(actor *ateapipb.CreateWorkerRequest)) *ateapipb.CreateWorkerRequest { + req := &ateapipb.CreateWorkerRequest{ + Worker: actor, + } + for _, m := range mods { + m(req) + } + return req + } + withStatus := withWorkerStatus + withMetadata := withWorkerMetadata + tests := []struct { - name string - mutate func(*ateapipb.Worker) // nil leaves the worker valid - wantMsg string // empty means valid + name string + req *ateapipb.CreateWorkerRequest + want field.ErrorList }{{ name: "valid unassigned worker", + req: validReq(validWorker(apiWorkerName)), + }, { + name: "valid with status", + req: validReq(validWorker(apiWorkerName, withStatus())), + }, { + name: "missing worker", + req: &ateapipb.CreateWorkerRequest{Worker: nil}, + want: field.ErrorList{field.Required(field.NewPath("worker"), "")}, + }, { + name: "missing metadata", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.Metadata = nil })), + want: field.ErrorList{field.Required(field.NewPath("worker", "metadata"), "")}, + }, { + name: "missing metadata.name", + req: validReq(validWorker(apiWorkerName, withMetadata(func(m *ateapipb.ResourceMetadata) { m.Name = "" }))), + want: field.ErrorList{field.Required(field.NewPath("worker", "metadata", "name"), "")}, + }, { + name: "invalid metadata.name", + req: validReq(validWorker(apiWorkerName, withMetadata(func(m *ateapipb.ResourceMetadata) { m.Name = "not a name" }))), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "metadata", "name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "metadata.atespace set on a global-scoped Worker", + req: validReq(validWorker(apiWorkerName, withMetadata(func(m *ateapipb.ResourceMetadata) { m.Atespace = "team-a" }))), + want: field.ErrorList{field.Forbidden(field.NewPath("worker", "metadata", "atespace"), "")}, + }, { + name: "missing worker_namespace", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.WorkerNamespace = "" })), + want: field.ErrorList{field.Required(field.NewPath("worker", "worker_namespace"), "")}, + }, { + name: "invalid worker_namespace", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.WorkerNamespace = "NS-1" })), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "worker_namespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing worker_pool", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.WorkerPool = "" })), + want: field.ErrorList{field.Required(field.NewPath("worker", "worker_pool"), "")}, + }, { + name: "invalid worker_pool", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.WorkerPool = "POOL_1" })), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "worker_pool"), nil, "").WithOrigin("format=k8s-long-name")}, + }, { + name: "missing worker_pod", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.WorkerPod = "" })), + want: field.ErrorList{field.Required(field.NewPath("worker", "worker_pod"), "")}, + }, { + name: "invalid worker_pod", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.WorkerPod = "POD_1" })), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "worker_pod"), nil, "").WithOrigin("format=k8s-long-name")}, + }, { + name: "missing worker_pod_uid", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.WorkerPodUid = "" })), + want: field.ErrorList{field.Required(field.NewPath("worker", "worker_pod_uid"), "")}, + }, { + name: "invalid worker_pod_uid", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.WorkerPodUid = "INVALID-UUID" })), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "worker_pod_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "missing node_name", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.NodeName = "" })), + want: field.ErrorList{field.Required(field.NewPath("worker", "node_name"), "")}, + }, { + name: "invalid node_name", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.NodeName = "NODE_NAME" })), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "node_name"), nil, "").WithOrigin("format=k8s-long-name")}, + }, { + name: "missing ip", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.Ip = "" })), + want: field.ErrorList{field.Required(field.NewPath("worker", "ip"), "")}, + }, { + name: "invalid ip", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.Ip = "not-an-ip" })), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "ip"), nil, "").WithOrigin("format=ip-strict")}, + }, { + name: "sandbox_class too long", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.SandboxClass = strings.Repeat("x", 64) })), + want: field.ErrorList{field.TooLong(field.NewPath("worker", "sandbox_class"), nil, 63).WithOrigin("maxLength")}, }, { - // status is output-only and every caller sets it itself, so it is not - // validated at all: a thoroughly malformed one still passes. - name: "status is not validated", - mutate: func(w *ateapipb.Worker) { - w.Status = &ateapipb.WorkerStatus{ - State: ateapipb.WorkerState(99), - Assignment: &ateapipb.ActorAssignment{Actor: &ateapipb.ObjectRef{Name: "actor"}}, + name: "valid labels", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { + w.Labels = map[string]string{"tier": "batch", "pool.ate.io/zone": "us-west1-c"} + })), + }, { + name: "too many labels", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { + labels := make(map[string]string, 65) + for i := 0; i < 65; i++ { + labels[fmt.Sprintf("key-%d", i)] = "v" } - }, + w.Labels = labels + })), + want: field.ErrorList{field.TooMany(field.NewPath("worker", "labels"), 65, 64).WithOrigin("maxProperties")}, + }, { + name: "invalid label key", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.Labels = map[string]string{"bad key!": "batch"} })), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "labels"), "bad key!", "").WithOrigin("format=k8s-label-key")}, + }, { + name: "invalid label value", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.Labels = map[string]string{"tier": "not valid!"} })), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "labels").Key("tier"), "not valid!", "").WithOrigin("format=k8s-label-value")}, + }, { + name: "absent capacity is allowed", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.Capacity = nil })), }, { - name: "missing worker_namespace", - mutate: func(w *ateapipb.Worker) { w.WorkerNamespace = "" }, - wantMsg: "worker.worker_namespace: Required value", + name: "valid capacity", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.Capacity = &ateapipb.WorkerCapacity{CpuMilli: 2000, MemoryBytes: 4 << 30} })), }, { - name: "invalid worker_namespace", - mutate: func(w *ateapipb.Worker) { w.WorkerNamespace = "NS-1" }, - wantMsg: "worker.worker_namespace: Invalid value", + name: "negative capacity.cpu_milli", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.Capacity = &ateapipb.WorkerCapacity{CpuMilli: -1, MemoryBytes: 4 << 30} })), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "capacity", "cpu_milli"), nil, "").WithOrigin("minimum")}, }, { - name: "missing worker_pool", - mutate: func(w *ateapipb.Worker) { w.WorkerPool = "" }, - wantMsg: "worker.worker_pool: Required value", + name: "negative capacity.memory_bytes", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.Capacity = &ateapipb.WorkerCapacity{CpuMilli: 2000, MemoryBytes: -1} })), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "capacity", "memory_bytes"), nil, "").WithOrigin("minimum")}, }, { - name: "missing worker_pod", - mutate: func(w *ateapipb.Worker) { w.WorkerPod = "" }, - wantMsg: "worker.worker_pod: Required value", + name: "status needs a state", + req: validReq(validWorker(apiWorkerName, withStatus(func(s *ateapipb.WorkerStatus) { s.State = 0 }))), + want: field.ErrorList{field.Required(field.NewPath("worker", "status", "state"), "")}, }, { - name: "missing ip", - mutate: func(w *ateapipb.Worker) { w.Ip = "" }, - wantMsg: "worker.ip: Required value", + name: "status invalid state (too small)", + req: validReq(validWorker(apiWorkerName, withStatus(func(s *ateapipb.WorkerStatus) { s.State = -1 }))), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "status", "state"), nil, "").WithOrigin("minimum")}, }, { - name: "invalid ip", - mutate: func(w *ateapipb.Worker) { w.Ip = "not-an-ip" }, - wantMsg: "worker.ip: Invalid value", + name: "status invalid state (too large)", + req: validReq(validWorker(apiWorkerName, withStatus(func(s *ateapipb.WorkerStatus) { s.State = 99 }))), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "status", "state"), nil, "").WithOrigin("maximum")}, }, { - name: "missing worker_pod_uid", - mutate: func(w *ateapipb.Worker) { w.WorkerPodUid = "" }, - wantMsg: "worker.worker_pod_uid: Required value", + name: "valid assignment, when carried, passes", + req: validReq(validWorker(apiWorkerName, withStatus(func(s *ateapipb.WorkerStatus) { + s.Assignment = newAPIAssignment(apiOtherWorkerName) + }))), }, { - name: "invalid worker_pod_uid", - mutate: func(w *ateapipb.Worker) { w.WorkerPodUid = "INVALID-UUID" }, - wantMsg: "worker.worker_pod_uid: Invalid value", + name: "assignment actor_uid must be a uuid", + req: validReq(validWorker(apiWorkerName, withStatus(func(s *ateapipb.WorkerStatus) { + s.Assignment = newAPIAssignment("not a uuid") + }))), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "status", "assignment", "actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, }, { - name: "missing node_name", - mutate: func(w *ateapipb.Worker) { w.NodeName = "" }, - wantMsg: "worker.node_name: Required value", + name: "assignment actor ref needs an atespace", + req: validReq(validWorker(apiWorkerName, withStatus(func(s *ateapipb.WorkerStatus) { + s.Assignment = newAPIAssignment(apiOtherWorkerName) + s.Assignment.Actor.Atespace = "" + }))), + want: field.ErrorList{field.Required(field.NewPath("worker", "status", "assignment", "actor", "atespace"), "")}, }, { - name: "invalid node_name", - mutate: func(w *ateapipb.Worker) { w.NodeName = "NODE_NAME" }, - wantMsg: "worker.node_name: Invalid value", + name: "assignment with a template resource ref passes", + req: validReq(validWorker(apiWorkerName, withStatus(func(s *ateapipb.WorkerStatus) { + s.Assignment = newAPIAssignment(apiOtherWorkerName) + s.Assignment.ActorTemplate = nil + s.Assignment.ActorTemplateRef = &ateapipb.ObjectRef{Atespace: "team-a", Name: "tmpl"} + }))), }, { - name: "missing metadata", - mutate: func(w *ateapipb.Worker) { w.Metadata = nil }, - wantMsg: "worker.metadata.name: Required value", + name: "assignment template ref needs an atespace", + req: validReq(validWorker(apiWorkerName, withStatus(func(s *ateapipb.WorkerStatus) { + s.Assignment = newAPIAssignment(apiOtherWorkerName) + s.Assignment.ActorTemplate = nil + s.Assignment.ActorTemplateRef = &ateapipb.ObjectRef{Name: "tmpl"} + }))), + want: field.ErrorList{field.Required(field.NewPath("worker", "status", "assignment", "actor_template_ref", "atespace"), "")}, }, { - name: "missing metadata.name", - mutate: func(w *ateapipb.Worker) { w.Metadata = &ateapipb.ResourceMetadata{} }, - wantMsg: "worker.metadata.name: Required value", + name: "assignment must name its template exactly once: both set", + req: validReq(validWorker(apiWorkerName, withStatus(func(s *ateapipb.WorkerStatus) { + s.Assignment = newAPIAssignment(apiOtherWorkerName) + s.Assignment.ActorTemplateRef = &ateapipb.ObjectRef{Atespace: "team-a", Name: "tmpl"} + }))), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "status", "assignment"), nil, "").WithOrigin("union")}, }, { - name: "invalid metadata.name", - mutate: func(w *ateapipb.Worker) { w.Metadata.Name = "Not A Name" }, - wantMsg: "worker.metadata.name: Invalid value", + name: "assignment must name its template exactly once: neither set", + req: validReq(validWorker(apiWorkerName, withStatus(func(s *ateapipb.WorkerStatus) { + s.Assignment = newAPIAssignment(apiOtherWorkerName) + s.Assignment.ActorTemplate = nil + }))), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "status", "assignment"), nil, "").WithOrigin("union")}, }, { - name: "metadata.atespace set on a global-scoped Worker", - mutate: func(w *ateapipb.Worker) { w.Metadata.Atespace = "team-a" }, - wantMsg: "worker.metadata.atespace: Invalid value", + name: "assignment template name must be a long name", + req: validReq(validWorker(apiWorkerName, withStatus(func(s *ateapipb.WorkerStatus) { + s.Assignment = newAPIAssignment(apiOtherWorkerName) + s.Assignment.ActorTemplate.Name = "TMPL_1" + }))), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "status", "assignment", "actor_template", "name"), nil, "").WithOrigin("format=k8s-long-name")}, }} for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - worker := newAPIWorker(apiWorkerName) - if tc.mutate != nil { - tc.mutate(worker) + assertValidateErr(t, validateCreateWorkerRequest(context.Background(), tc.req), tc.want) + }) + } +} + +// TestServiceImplUpdateWorker_ImmutableFields pins the immutable-field rule at +// the layer that now owns it: declarative validation in ServiceImpl, which +// every write path shares. It moved up from the store contract when the store +// stopped enforcing immutability itself. +func TestServiceImplUpdateWorker_ImmutableFields(t *testing.T) { + ctx := context.Background() + persistence, cleanup := storetest.SetupTestStore(t) + defer cleanup() + impl := newServiceImpl(persistence, nil, nil) + + // Every case below is rejected, so nothing writes and this stays the + // current incarnation for all of them. + created := seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) + + for _, tc := range []struct { + name string + field string + mutate func(*ateapipb.Worker) + }{ + {"worker_namespace", "worker_namespace", func(w *ateapipb.Worker) { w.WorkerNamespace = "other-ns" }}, + {"worker_pool", "worker_pool", func(w *ateapipb.Worker) { w.WorkerPool = "other-pool" }}, + {"worker_pod", "worker_pod", func(w *ateapipb.Worker) { w.WorkerPod = "other-pod" }}, + {"worker_pod_uid", "worker_pod_uid", func(w *ateapipb.Worker) { w.WorkerPodUid = apiOtherWorkerName }}, + {"node_name", "node_name", func(w *ateapipb.Worker) { w.NodeName = "other-node" }}, + {"ip", "ip", func(w *ateapipb.Worker) { w.Ip = "10.0.0.9" }}, + {"capacity_changed", "capacity", func(w *ateapipb.Worker) { w.Capacity.CpuMilli = 4000 }}, + // An update replaces the worker, so a caller that leaves capacity + // out is asking to clear it. That is a change like any other. + {"capacity_cleared", "capacity", func(w *ateapipb.Worker) { w.Capacity = nil }}, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := impl.UpdateWorker(ctx, apiWorkerName, store.PreconditionFrom(created), func(toUpdate *ateapipb.Worker) error { + tc.mutate(toUpdate) + return nil + }) + if got := status.Code(err); got != codes.InvalidArgument { + t.Fatalf("changing %s returned %v (err %v), want %v", tc.field, got, err, codes.InvalidArgument) + } + if !strings.Contains(err.Error(), tc.field) { + t.Errorf("error %v does not name the offending field %s", err, tc.field) } - errs := validateWorker(worker, field.NewPath("worker")) - if tc.wantMsg == "" { - if len(errs) > 0 { - t.Fatalf("validateWorker() = %v, want no errors", errs) - } - return + got, err := persistence.GetWorker(ctx, apiWorkerName) + if err != nil { + t.Fatalf("GetWorker failed: %v", err) } - // Any error may match: a case can trip more than one rule, so the - // wanted error is not always the first one reported. - for _, err := range errs { - if strings.Contains(err.Error(), tc.wantMsg) { - return - } + if got.GetMetadata().GetVersion() != 1 { + t.Errorf("rejected mutation bumped the version to %d, want 1", got.GetMetadata().GetVersion()) } - t.Errorf("validateWorker() = %v, want an error containing %q", errs, tc.wantMsg) }) } } + +func TestValidateDeleteWorkerRequest(t *testing.T) { + tests := []struct { + name string + req *ateapipb.DeleteWorkerRequest + want field.ErrorList + }{{ + "valid, no options", + &ateapipb.DeleteWorkerRequest{Worker: workerRef(apiWorkerName)}, + nil, + }, { + "valid, both guards", + &ateapipb.DeleteWorkerRequest{ + Worker: workerRef(apiWorkerName), + Options: &ateapipb.DeleteOptions{Uid: apiOtherWorkerName, Version: 3}, + }, + nil, + }, { + "missing worker", + &ateapipb.DeleteWorkerRequest{}, + field.ErrorList{field.Required(field.NewPath("worker"), "")}, + }, { + "missing worker.name", + &ateapipb.DeleteWorkerRequest{Worker: &ateapipb.ObjectRef{}}, + field.ErrorList{field.Required(field.NewPath("worker", "name"), "")}, + }, { + "worker.atespace must be empty", + &ateapipb.DeleteWorkerRequest{Worker: &ateapipb.ObjectRef{Atespace: "team-a", Name: apiWorkerName}}, + field.ErrorList{field.Forbidden(field.NewPath("worker", "atespace"), "")}, + }, { + "invalid options.uid", + &ateapipb.DeleteWorkerRequest{ + Worker: workerRef(apiWorkerName), + Options: &ateapipb.DeleteOptions{Uid: "not-a-uuid"}, + }, + field.ErrorList{field.Invalid(field.NewPath("options", "uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + "negative options.version", + &ateapipb.DeleteWorkerRequest{ + Worker: workerRef(apiWorkerName), + Options: &ateapipb.DeleteOptions{Version: -1}, + }, + field.ErrorList{field.Invalid(field.NewPath("options", "version"), nil, "").WithOrigin("minimum")}, + }, { + // Zero values waive the guards, so they are never validated for shape. + "zero options are waived, not validated", + &ateapipb.DeleteWorkerRequest{ + Worker: workerRef(apiWorkerName), + Options: &ateapipb.DeleteOptions{}, + }, + nil, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, validateDeleteWorkerRequest(context.Background(), tt.req), tt.want) + }) + } +} + +func TestValidateUpdateWorkerRequest(t *testing.T) { + // This test verifies validation of user input for update. The worker body + // is deliberately not descended into here (updates are validated in two + // steps); only the metadata that addresses the resource is checked. + validReq := func(mods ...func(w *ateapipb.Worker)) *ateapipb.UpdateWorkerRequest { + worker := validWorker(apiWorkerName) + worker.Metadata.Uid = apiOtherWorkerName + worker.Metadata.Version = 3 + for _, m := range mods { + m(worker) + } + return &ateapipb.UpdateWorkerRequest{Worker: worker} + } + + tests := []struct { + name string + req *ateapipb.UpdateWorkerRequest + want field.ErrorList + }{{ + "valid", + validReq(), + nil, + }, { + // uid and version are preconditions the store requires; the request + // validation deliberately leaves their presence to the store. + "missing uid and version pass request validation", + validReq(func(w *ateapipb.Worker) { w.Metadata.Uid = ""; w.Metadata.Version = 0 }), + nil, + }, { + "missing worker", + &ateapipb.UpdateWorkerRequest{}, + field.ErrorList{field.Required(field.NewPath("worker"), "")}, + }, { + "missing metadata", + validReq(func(w *ateapipb.Worker) { w.Metadata = nil }), + field.ErrorList{field.Required(field.NewPath("worker", "metadata"), "")}, + }, { + "missing metadata.name", + validReq(func(w *ateapipb.Worker) { w.Metadata.Name = "" }), + field.ErrorList{field.Required(field.NewPath("worker", "metadata", "name"), "")}, + }, { + "invalid metadata.name", + validReq(func(w *ateapipb.Worker) { w.Metadata.Name = "Not A Name" }), + field.ErrorList{field.Invalid(field.NewPath("worker", "metadata", "name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + "invalid metadata.uid", + validReq(func(w *ateapipb.Worker) { w.Metadata.Uid = "not-a-uuid" }), + field.ErrorList{field.Invalid(field.NewPath("worker", "metadata", "uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + "metadata.atespace set on a global-scoped Worker", + validReq(func(w *ateapipb.Worker) { w.Metadata.Atespace = "team-a" }), + field.ErrorList{field.Forbidden(field.NewPath("worker", "metadata", "atespace"), "")}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, validateUpdateWorkerRequest(context.Background(), tt.req), tt.want) + }) + } +} + +// TestValidateWorkerUpdate_RequireStatus pins the final-object check that the +// RPC path cannot reach: the server always sets status before storing, so only +// a direct call shows the guard catching a worker without one. +func TestValidateWorkerUpdate_RequireStatus(t *testing.T) { + oldVal := validWorker(apiWorkerName) + oldVal.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE} + newVal := proto.Clone(oldVal).(*ateapipb.Worker) + newVal.Status = nil + + want := field.ErrorList{field.Required(field.NewPath("worker", "status"), "")} + assertValidateErr(t, validateWorkerUpdate(context.Background(), field.NewPath("worker"), newVal, oldVal, true), want) + + // Without requireStatus the same worker passes: status is optional in the + // schema, and clearing it is not otherwise constrained. + assertValidateErr(t, validateWorkerUpdate(context.Background(), field.NewPath("worker"), newVal, oldVal, false), nil) +} + +// Server-assigned metadata carried on a create request is scrubbed rather than +// rejected: the fields are documented as ignored on input, so even garbage in +// them must not fail validation. +func TestCreateWorker_IgnoresRequestMetadataServerFields(t *testing.T) { + ctx := context.Background() + svc, _ := newWorkerAPIService(t) + + in := validWorker(apiWorkerName) + in.Metadata.Uid = "not-a-uuid" + in.Metadata.Version = -5 + + got, err := svc.CreateWorker(ctx, &ateapipb.CreateWorkerRequest{Worker: in}) + if err != nil { + t.Fatalf("CreateWorker() failed: %v", err) + } + if got.GetMetadata().GetUid() == "" || got.GetMetadata().GetUid() == "not-a-uuid" { + t.Errorf("created worker uid = %q, want a server-assigned uid", got.GetMetadata().GetUid()) + } + if got.GetMetadata().GetVersion() != 1 { + t.Errorf("created worker version = %d, want 1", got.GetMetadata().GetVersion()) + } +} + +// TestServiceImplUpdateWorker_ValidatesAssignment pins that assignment writes +// — which reach the store through ServiceImpl, the way the resume workflow +// binds an Actor — are validated like any other worker update. +func TestServiceImplUpdateWorker_ValidatesAssignment(t *testing.T) { + ctx := context.Background() + persistence, cleanup := storetest.SetupTestStore(t) + t.Cleanup(cleanup) + impl := newServiceImpl(persistence, nil, nil) + created := seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) + + // A malformed assignment must not land. + _, err := impl.UpdateWorker(ctx, apiWorkerName, store.PreconditionFrom(created), func(toUpdate *ateapipb.Worker) error { + toUpdate.Status.Assignment = newAPIAssignment("not-a-uuid") + return nil + }) + if got := status.Code(err); got != codes.InvalidArgument { + t.Fatalf("assigning a malformed uid returned %v (err %v), want %v", got, err, codes.InvalidArgument) + } + + // A well-formed assignment lands, and releasing it lands too: assignment + // is optional, so clearing is not otherwise constrained. + assigned, err := impl.UpdateWorker(ctx, apiWorkerName, store.PreconditionFrom(created), func(toUpdate *ateapipb.Worker) error { + toUpdate.Status.Assignment = newAPIAssignment(apiOtherWorkerName) + return nil + }) + if err != nil { + t.Fatalf("assigning a valid assignment failed: %v", err) + } + if _, err := impl.UpdateWorker(ctx, apiWorkerName, store.PreconditionFrom(assigned), func(toUpdate *ateapipb.Worker) error { + toUpdate.Status.Assignment = nil + return nil + }); err != nil { + t.Fatalf("releasing the assignment failed: %v", err) + } +} diff --git a/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go b/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go index d71f00db28..4b318c0316 100644 --- a/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go @@ -45,7 +45,7 @@ var apiActorRef = resources.ActorRef{Atespace: "team-a", Name: "actor-1"} // seedAPIActor stores an Actor bound to apiWorkerName in the given state — the // shape the delete's release step acts on. Its coordinates line up with -// newAPIWorker and newAPIAssignment, so the two seeds agree about who is bound +// validWorker and newAPIAssignment, so the two seeds agree about who is bound // to whom. func seedAPIActor(t *testing.T, ctx context.Context, persistence store.Interface, state ateapipb.ActorState, opts ...func(*ateapipb.Actor)) *ateapipb.Actor { t.Helper() @@ -79,7 +79,7 @@ func seedAPIActor(t *testing.T, ctx context.Context, persistence store.Interface func TestDeleteWorkerWorkflow_ReleasesBoundActor(t *testing.T) { ctx := context.Background() wf, persistence := newWorkerDeleteWorkflow(t) - seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) actor := seedAPIActor(t, ctx, persistence, ateapipb.ActorState_ACTOR_STATE_RUNNING, func(a *ateapipb.Actor) { // Both in-progress checkpoints are set so the assertion covers the // shared crash path, which cannot know which workflow was in flight. @@ -142,7 +142,7 @@ func TestDeleteWorkerWorkflow_ReleasedActorStateTransitions(t *testing.T) { ctx := context.Background() wf, persistence := newWorkerDeleteWorkflow(t) - seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) actor := seedAPIActor(t, ctx, persistence, tc.start) assignAPIWorker(t, ctx, persistence, apiWorkerName, actor.GetMetadata().GetUid()) @@ -175,7 +175,7 @@ func TestDeleteWorkerWorkflow_ReleasedActorStateTransitions(t *testing.T) { func TestDeleteWorkerWorkflow_IgnoresStaleIncarnationAssignment(t *testing.T) { ctx := context.Background() wf, persistence := newWorkerDeleteWorkflow(t) - seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) seedAPIActor(t, ctx, persistence, ateapipb.ActorState_ACTOR_STATE_RUNNING) assignAPIWorker(t, ctx, persistence, apiWorkerName, "old-incarnation-uid") @@ -197,7 +197,7 @@ func TestDeleteWorkerWorkflow_IgnoresStaleIncarnationAssignment(t *testing.T) { func TestDeleteWorkerWorkflow_IgnoresActorMovedElsewhere(t *testing.T) { ctx := context.Background() wf, persistence := newWorkerDeleteWorkflow(t) - seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) actor := seedAPIActor(t, ctx, persistence, ateapipb.ActorState_ACTOR_STATE_RUNNING, func(a *ateapipb.Actor) { a.Status.WorkerAssignment.Worker = workerRef(apiOtherWorkerName) }) @@ -225,7 +225,7 @@ func TestDeleteWorkerWorkflow_IgnoresActorMovedElsewhere(t *testing.T) { func TestDeleteWorkerWorkflow_AssignedToAbsentActorDeletesAnyway(t *testing.T) { ctx := context.Background() wf, persistence := newWorkerDeleteWorkflow(t) - seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) assignAPIWorker(t, ctx, persistence, apiWorkerName, "actor-uid-1") got, err := wf.DeleteWorker(ctx, apiWorkerName, store.DeletePreconditions{}) @@ -271,7 +271,7 @@ func TestDeleteWorkerWorkflow_FailedReleaseKeepsWorker(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() _, persistence := newWorkerDeleteWorkflow(t) - seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) actor := seedAPIActor(t, ctx, persistence, ateapipb.ActorState_ACTOR_STATE_RUNNING) assignAPIWorker(t, ctx, persistence, apiWorkerName, actor.GetMetadata().GetUid()) @@ -304,7 +304,7 @@ func TestDeleteWorkerWorkflow_FailedReleaseKeepsWorker(t *testing.T) { func TestDeleteWorkerWorkflow_ActorDeletedDuringRelease(t *testing.T) { ctx := context.Background() _, persistence := newWorkerDeleteWorkflow(t) - seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) actor := seedAPIActor(t, ctx, persistence, ateapipb.ActorState_ACTOR_STATE_RUNNING) assignAPIWorker(t, ctx, persistence, apiWorkerName, actor.GetMetadata().GetUid()) diff --git a/cmd/ateapi/internal/controlapi/zz_generated.validation.go b/cmd/ateapi/internal/controlapi/zz_generated.validation.go index 3e04ef47a4..e5dcdc9e6c 100644 --- a/cmd/ateapi/internal/controlapi/zz_generated.validation.go +++ b/cmd/ateapi/internal/controlapi/zz_generated.validation.go @@ -318,6 +318,186 @@ func Validate_Actor( return errs } +var unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_ActorAssignment_ = validate.NewUnionMembership(validate.NewUnionMember("actor_template"), validate.NewUnionMember("actor_template_ref")) + +// Validate_ActorAssignment validates an instance of ActorAssignment according +// to declarative validation rules in the API schema. +func Validate_ActorAssignment( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.ActorAssignment) (errs field.ErrorList) { + + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_ActorAssignment_, + func(obj *ateapipb.ActorAssignment) bool { + if obj == nil { + return false + } + return obj.ActorTemplate != nil + }, + func(obj *ateapipb.ActorAssignment) bool { + if obj == nil { + return false + } + return obj.ActorTemplateRef != nil + }); len(e) != 0 { + errs = append(errs, e...) + } + + { // field ateapipb.ActorAssignment.ActorTemplate + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.KubeNamespacedObjectRef, + 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_KubeNamespacedObjectRef(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ActorAssignment) *ateapipb.KubeNamespacedObjectRef { + return oldObj.ActorTemplate + }) + errs = append(errs, fn(fldPath.Child("actor_template"), obj.ActorTemplate, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ActorAssignment.Actor + 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.ActorAssignment) *ateapipb.ObjectRef { + return oldObj.Actor + }) + errs = append(errs, fn(fldPath.Child("actor"), obj.Actor, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ActorAssignment.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.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ActorAssignment) *string { + return &oldObj.ActorUid + }) + errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ActorAssignment.ActorTemplateRef + 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.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + 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.ActorAssignment) *ateapipb.ObjectRef { + return oldObj.ActorTemplateRef + }) + errs = append(errs, fn(fldPath.Child("actor_template_ref"), obj.ActorTemplateRef, oldVal, oldObj != nil)...) + } + + return errs +} + // Validate_ActorMetadataDataSource validates an instance of ActorMetadataDataSource according // to declarative validation rules in the API schema. func Validate_ActorMetadataDataSource( @@ -862,6 +1042,46 @@ func Validate_CreateAtespaceRequest( return errs } +// Validate_CreateWorkerRequest validates an instance of CreateWorkerRequest according +// to declarative validation rules in the API schema. +func Validate_CreateWorkerRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.CreateWorkerRequest) (errs field.ErrorList) { + + { // field ateapipb.CreateWorkerRequest.Worker + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.Worker, + 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_Worker(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.CreateWorkerRequest) *ateapipb.Worker { + return oldObj.Worker + }) + errs = append(errs, fn(fldPath.Child("worker"), obj.Worker, oldVal, oldObj != nil)...) + } + + return errs +} + // Validate_CredentialHeaderInjection validates an instance of CredentialHeaderInjection according // to declarative validation rules in the API schema. func Validate_CredentialHeaderInjection( @@ -1083,110 +1303,329 @@ func Validate_DeleteAtespaceRequest( return errs } -// Validate_EgressPolicy validates an instance of EgressPolicy according +// Validate_DeleteOptions validates an instance of DeleteOptions according // to declarative validation rules in the API schema. -func Validate_EgressPolicy( +func Validate_DeleteOptions( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.EgressPolicy) (errs field.ErrorList) { + obj, oldObj *ateapipb.DeleteOptions) (errs field.ErrorList) { - { // field ateapipb.EgressPolicy.Metadata + { // field ateapipb.DeleteOptions.Version fn := func( fldPath *field.Path, - obj, oldObj *ateapipb.ResourceMetadata, + obj, oldObj *int64, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { - if ateDeepEqual(obj, oldObj) { + if obj == oldObj || (obj != nil && oldObj != nil && *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...) + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { earlyReturn = true } if earlyReturn { return // do not proceed } - // custom validation - if e := ValidateCustom_EgressPolicy_Metadata(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { errs = append(errs, e...) } - 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.EgressPolicy) *ateapipb.ResourceMetadata { - return oldObj.Metadata + func(oldObj *ateapipb.DeleteOptions) *int64 { + return &oldObj.Version }) - errs = append(errs, fn(fldPath.Child("metadata"), obj.Metadata, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("version"), &obj.Version, oldVal, oldObj != nil)...) } - { // field ateapipb.EgressPolicy.Rules + { // field ateapipb.DeleteOptions.Uid fn := func( fldPath *field.Path, - obj, oldObj []*ateapipb.EgressRule, + obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { - if ateDeepEqual(obj, oldObj) { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.PtrSliceNoNils[ateapipb.EgressRule](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 { + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { earlyReturn = true } if earlyReturn { return // do not proceed } - // iterate the list and call the type's validation function - if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_EgressRule); len(e) != 0 { + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.EgressPolicy) []*ateapipb.EgressRule { - return oldObj.Rules + func(oldObj *ateapipb.DeleteOptions) *string { + return &oldObj.Uid }) - errs = append(errs, fn(fldPath.Child("rules"), obj.Rules, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("uid"), &obj.Uid, oldVal, oldObj != nil)...) } return errs } -var unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_EgressRule_ = validate.NewUnionMembership(validate.NewUnionMember("hostnames"), validate.NewUnionMember("ip_blocks"), validate.NewUnionMember("all")) +// Validate_DeleteWorkerRequest validates an instance of DeleteWorkerRequest according +// to declarative validation rules in the API schema. +func Validate_DeleteWorkerRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.DeleteWorkerRequest) (errs field.ErrorList) { + + { // field ateapipb.DeleteWorkerRequest.Worker + 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.ForbiddenValue).MarkBeta().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).MarkBeta().MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ObjectRef) *string { return &o.Atespace }, validate.DirectEqual, validate.OptionalValue).MarkBeta().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.DeleteWorkerRequest) *ateapipb.ObjectRef { + return oldObj.Worker + }) + errs = append(errs, fn(fldPath.Child("worker"), obj.Worker, oldVal, oldObj != nil)...) + } + + { // field ateapipb.DeleteWorkerRequest.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.DeleteWorkerRequest) *ateapipb.DeleteOptions { + return oldObj.Options + }) + errs = append(errs, fn(fldPath.Child("options"), obj.Options, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_DrainWorkerRequest validates an instance of DrainWorkerRequest according +// to declarative validation rules in the API schema. +func Validate_DrainWorkerRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.DrainWorkerRequest) (errs field.ErrorList) { + + { // field ateapipb.DrainWorkerRequest.Worker + 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.ForbiddenValue).MarkBeta().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).MarkBeta().MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ObjectRef) *string { return &o.Atespace }, validate.DirectEqual, validate.OptionalValue).MarkBeta().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.DrainWorkerRequest) *ateapipb.ObjectRef { + return oldObj.Worker + }) + errs = append(errs, fn(fldPath.Child("worker"), obj.Worker, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_EgressPolicy validates an instance of EgressPolicy according +// to declarative validation rules in the API schema. +func Validate_EgressPolicy( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.EgressPolicy) (errs field.ErrorList) { + + { // field ateapipb.EgressPolicy.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 + } + // custom validation + if e := ValidateCustom_EgressPolicy_Metadata(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + 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.EgressPolicy) *ateapipb.ResourceMetadata { + return oldObj.Metadata + }) + errs = append(errs, fn(fldPath.Child("metadata"), obj.Metadata, oldVal, oldObj != nil)...) + } + + { // field ateapipb.EgressPolicy.Rules + fn := func( + fldPath *field.Path, + obj, oldObj []*ateapipb.EgressRule, + 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.EgressRule](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 + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_EgressRule); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.EgressPolicy) []*ateapipb.EgressRule { + return oldObj.Rules + }) + errs = append(errs, fn(fldPath.Child("rules"), obj.Rules, oldVal, oldObj != nil)...) + } + + return errs +} + +var unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_EgressRule_ = validate.NewUnionMembership(validate.NewUnionMember("hostnames"), validate.NewUnionMember("ip_blocks"), validate.NewUnionMember("all")) // Validate_EgressRule validates an instance of EgressRule according // to declarative validation rules in the API schema. @@ -1483,16 +1922,16 @@ func Validate_GetAtespaceRequest( return errs } -// Validate_HostnameRule validates an instance of HostnameRule according +// Validate_GetWorkerRequest validates an instance of GetWorkerRequest according // to declarative validation rules in the API schema. -func Validate_HostnameRule( +func Validate_GetWorkerRequest( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.HostnameRule) (errs field.ErrorList) { + obj, oldObj *ateapipb.GetWorkerRequest) (errs field.ErrorList) { - { // field ateapipb.HostnameRule.Patterns + { // field ateapipb.GetWorkerRequest.Worker fn := func( fldPath *field.Path, - obj, oldObj []string, + obj, oldObj *ateapipb.ObjectRef, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -1502,25 +1941,84 @@ func Validate_HostnameRule( } // call field-attached validations earlyReturn := false - if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 256).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) - earlyReturn = true - } - if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + 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_HostnameRule_Patterns(ctx, op, fldPath, obj, oldObj); len(e) != 0 { - errs = append(errs, e...) - } - // lists with set semantics require unique values - if e := validate.ValSliceUnique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual); len(e) != 0 { - errs = append(errs, e...) - } + 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.ForbiddenValue).MarkBeta().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).MarkBeta().MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ObjectRef) *string { return &o.Atespace }, validate.DirectEqual, validate.OptionalValue).MarkBeta().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.GetWorkerRequest) *ateapipb.ObjectRef { + return oldObj.Worker + }) + errs = append(errs, fn(fldPath.Child("worker"), obj.Worker, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_HostnameRule validates an instance of HostnameRule according +// to declarative validation rules in the API schema. +func Validate_HostnameRule( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.HostnameRule) (errs field.ErrorList) { + + { // field ateapipb.HostnameRule.Patterns + 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 ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 256).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.RequiredSlice(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_HostnameRule_Patterns(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + // lists with set semantics require unique values + if e := validate.ValSliceUnique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual); len(e) != 0 { + errs = append(errs, e...) + } return } oldVal := safe.Field(oldObj, @@ -1613,6 +2111,79 @@ func Validate_IPBlockRule( return errs } +// Validate_KubeNamespacedObjectRef validates an instance of KubeNamespacedObjectRef according +// to declarative validation rules in the API schema. +func Validate_KubeNamespacedObjectRef( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.KubeNamespacedObjectRef) (errs field.ErrorList) { + + { // field ateapipb.KubeNamespacedObjectRef.Namespace + 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.KubeNamespacedObjectRef) *string { + return &oldObj.Namespace + }) + errs = append(errs, fn(fldPath.Child("namespace"), &obj.Namespace, oldVal, oldObj != nil)...) + } + + { // field ateapipb.KubeNamespacedObjectRef.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.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.LongName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.KubeNamespacedObjectRef) *string { + return &oldObj.Name + }) + errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) + } + + return errs +} + // Validate_ListAtespacesRequest validates an instance of ListAtespacesRequest according // to declarative validation rules in the API schema. func Validate_ListAtespacesRequest( @@ -1684,6 +2255,77 @@ func Validate_ListAtespacesRequest( return errs } +// Validate_ListWorkersRequest validates an instance of ListWorkersRequest according +// to declarative validation rules in the API schema. +func Validate_ListWorkersRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.ListWorkersRequest) (errs field.ErrorList) { + + { // field ateapipb.ListWorkersRequest.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.ListWorkersRequest) *int32 { + return &oldObj.PageSize + }) + errs = append(errs, fn(fldPath.Child("page_size"), &obj.PageSize, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ListWorkersRequest.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.ListWorkersRequest) *string { + return &oldObj.PageToken + }) + errs = append(errs, fn(fldPath.Child("page_token"), &obj.PageToken, oldVal, oldObj != nil)...) + } + + return errs +} + // Validate_MintCertRequest validates an instance of MintCertRequest according // to declarative validation rules in the API schema. func Validate_MintCertRequest( @@ -2516,10 +3158,530 @@ func Validate_TrustBundleDataSource( func(oldObj *ateapipb.TrustBundleDataSource) *string { return &oldObj.Name }) - errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) + } + + { // field ateapipb.TrustBundleDataSource.Path + 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, 255); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MinLength(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.TrustBundleDataSource) *string { + return &oldObj.Path + }) + errs = append(errs, fn(fldPath.Child("path"), &obj.Path, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_UpdateActorEgressPolicyRequest validates an instance of UpdateActorEgressPolicyRequest according +// to declarative validation rules in the API schema. +func Validate_UpdateActorEgressPolicyRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.UpdateActorEgressPolicyRequest) (errs field.ErrorList) { + + // custom validation + if e := ValidateCustom_UpdateActorEgressPolicyRequest(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + + { // field ateapipb.UpdateActorEgressPolicyRequest.Actor + 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.UpdateActorEgressPolicyRequest) *ateapipb.ObjectRef { + return oldObj.Actor + }) + errs = append(errs, fn(fldPath.Child("actor"), obj.Actor, oldVal, oldObj != nil)...) + } + + { // field ateapipb.UpdateActorEgressPolicyRequest.EgressPolicy + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.EgressPolicy, + 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_EgressPolicy(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.UpdateActorEgressPolicyRequest) *ateapipb.EgressPolicy { + return oldObj.EgressPolicy + }) + errs = append(errs, fn(fldPath.Child("egress_policy"), obj.EgressPolicy, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_UpdateActorRequest validates an instance of UpdateActorRequest according +// to declarative validation rules in the API schema. +func Validate_UpdateActorRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.UpdateActorRequest) (errs field.ErrorList) { + + { // field ateapipb.UpdateActorRequest.Actor + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.Actor, + 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_UpdateActorRequest_Actor(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.Actor) *ateapipb.ResourceMetadata { return o.Metadata }, deepEqualImpl_, 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.UpdateActorRequest) *ateapipb.Actor { + return oldObj.Actor + }) + errs = append(errs, fn(fldPath.Child("actor"), obj.Actor, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_UpdateWorkerRequest validates an instance of UpdateWorkerRequest according +// to declarative validation rules in the API schema. +func Validate_UpdateWorkerRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.UpdateWorkerRequest) (errs field.ErrorList) { + + { // field ateapipb.UpdateWorkerRequest.Worker + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.Worker, + 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_UpdateWorkerRequest_Worker(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.Worker) *ateapipb.ResourceMetadata { return o.Metadata }, deepEqualImpl_, 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.UpdateWorkerRequest) *ateapipb.Worker { + return oldObj.Worker + }) + errs = append(errs, fn(fldPath.Child("worker"), obj.Worker, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_Volume validates an instance of Volume according +// to declarative validation rules in the API schema. +func Validate_Volume( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.Volume) (errs field.ErrorList) { + + // field ateapipb.Volume.Name has no validation + // field ateapipb.Volume.DurableDir has no validation + // field ateapipb.Volume.ExternalVolumeTemplate has no validation + + { // field ateapipb.Volume.SystemInfo + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.SystemInfoVolumeSource, + 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_SystemInfoVolumeSource(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.Volume) *ateapipb.SystemInfoVolumeSource { + return oldObj.SystemInfo + }) + errs = append(errs, fn(fldPath.Child("system_info"), obj.SystemInfo, oldVal, oldObj != nil)...) + } + + // field ateapipb.Volume.Image has no validation + // field ateapipb.Volume.Type has no validation + return errs +} + +// Validate_Worker validates an instance of Worker according +// to declarative validation rules in the API schema. +func Validate_Worker( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.Worker) (errs field.ErrorList) { + + { // field ateapipb.Worker.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.ForbiddenValue).MarkBeta().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.OptionalValue).MarkBeta().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.Immutable).MarkBeta().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).MarkBeta().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.Worker) *ateapipb.ResourceMetadata { + return oldObj.Metadata + }) + errs = append(errs, fn(fldPath.Child("metadata"), obj.Metadata, oldVal, oldObj != nil)...) + } + + { // field ateapipb.Worker.WorkerNamespace + 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.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.Worker) *string { + return &oldObj.WorkerNamespace + }) + errs = append(errs, fn(fldPath.Child("worker_namespace"), &obj.WorkerNamespace, oldVal, oldObj != nil)...) + } + + { // field ateapipb.Worker.WorkerPool + 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.LongName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.Worker) *string { + return &oldObj.WorkerPool + }) + errs = append(errs, fn(fldPath.Child("worker_pool"), &obj.WorkerPool, oldVal, oldObj != nil)...) + } + + { // field ateapipb.Worker.WorkerPod + 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.LongName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.Worker) *string { + return &oldObj.WorkerPod + }) + errs = append(errs, fn(fldPath.Child("worker_pod"), &obj.WorkerPod, oldVal, oldObj != nil)...) + } + + { // field ateapipb.Worker.WorkerPodUid + 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.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.Worker) *string { + return &oldObj.WorkerPodUid + }) + errs = append(errs, fn(fldPath.Child("worker_pod_uid"), &obj.WorkerPodUid, oldVal, oldObj != nil)...) + } + + { // field ateapipb.Worker.NodeName + 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.LongName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.Worker) *string { + return &oldObj.NodeName + }) + errs = append(errs, fn(fldPath.Child("node_name"), &obj.NodeName, oldVal, oldObj != nil)...) } - { // field ateapipb.TrustBundleDataSource.Path + { // field ateapipb.Worker.Ip fn := func( fldPath *field.Path, obj, oldObj *string, @@ -2532,6 +3694,10 @@ func Validate_TrustBundleDataSource( } // 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 @@ -2539,85 +3705,54 @@ func Validate_TrustBundleDataSource( if earlyReturn { return // do not proceed } - if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 255); len(e) != 0 { - errs = append(errs, e...) - } - if e := validate.MinLength(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + // custom validation + if e := ValidateCustom_Worker_Ip(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.TrustBundleDataSource) *string { - return &oldObj.Path + func(oldObj *ateapipb.Worker) *string { + return &oldObj.Ip }) - errs = append(errs, fn(fldPath.Child("path"), &obj.Path, oldVal, oldObj != nil)...) - } - - return errs -} - -// Validate_UpdateActorEgressPolicyRequest validates an instance of UpdateActorEgressPolicyRequest according -// to declarative validation rules in the API schema. -func Validate_UpdateActorEgressPolicyRequest( - ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.UpdateActorEgressPolicyRequest) (errs field.ErrorList) { - - // custom validation - if e := ValidateCustom_UpdateActorEgressPolicyRequest(ctx, op, fldPath, obj, oldObj); len(e) != 0 { - errs = append(errs, e...) + errs = append(errs, fn(fldPath.Child("ip"), &obj.Ip, oldVal, oldObj != nil)...) } - { // field ateapipb.UpdateActorEgressPolicyRequest.Actor + { // field ateapipb.Worker.SandboxClass fn := func( fldPath *field.Path, - obj, oldObj *ateapipb.ObjectRef, + obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { - if ateDeepEqual(obj, oldObj) { + if obj == oldObj || (obj != nil && oldObj != nil && *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...) + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { 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)...) + 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.UpdateActorEgressPolicyRequest) *ateapipb.ObjectRef { - return oldObj.Actor + func(oldObj *ateapipb.Worker) *string { + return &oldObj.SandboxClass }) - errs = append(errs, fn(fldPath.Child("actor"), obj.Actor, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("sandbox_class"), &obj.SandboxClass, oldVal, oldObj != nil)...) } - { // field ateapipb.UpdateActorEgressPolicyRequest.EgressPolicy + { // field ateapipb.Worker.Labels fn := func( fldPath *field.Path, - obj, oldObj *ateapipb.EgressPolicy, + obj, oldObj map[string]string, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -2627,37 +3762,35 @@ func Validate_UpdateActorEgressPolicyRequest( } // call field-attached validations earlyReturn := false - if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.MaxProperties(ctx, op, fldPath, obj, oldObj, 64).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 } - // call the type's validation function - errs = append(errs, Validate_EgressPolicy(ctx, op, fldPath, obj, oldObj)...) + if e := validate.EachMapKey(ctx, op, fldPath, obj, oldObj, validate.LabelKey); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.EachMapVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, validate.LabelValue); len(e) != 0 { + errs = append(errs, e...) + } return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.UpdateActorEgressPolicyRequest) *ateapipb.EgressPolicy { - return oldObj.EgressPolicy + func(oldObj *ateapipb.Worker) map[string]string { + return oldObj.Labels }) - errs = append(errs, fn(fldPath.Child("egress_policy"), obj.EgressPolicy, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("labels"), obj.Labels, oldVal, oldObj != nil)...) } - return errs -} - -// Validate_UpdateActorRequest validates an instance of UpdateActorRequest according -// to declarative validation rules in the API schema. -func Validate_UpdateActorRequest( - ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.UpdateActorRequest) (errs field.ErrorList) { - - { // field ateapipb.UpdateActorRequest.Actor + { // field ateapipb.Worker.Capacity fn := func( fldPath *field.Path, - obj, oldObj *ateapipb.Actor, + obj, oldObj *ateapipb.WorkerCapacity, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -2667,54 +3800,31 @@ func Validate_UpdateActorRequest( } // call field-attached validations earlyReturn := false - if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.Immutable(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } if earlyReturn { return // do not proceed } - // custom validation - if e := ValidateCustom_UpdateActorRequest_Actor(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.Actor) *ateapipb.ResourceMetadata { return o.Metadata }, deepEqualImpl_, validate.RequiredPointer).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_WorkerCapacity(ctx, op, fldPath, obj, oldObj)...) return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.UpdateActorRequest) *ateapipb.Actor { - return oldObj.Actor + func(oldObj *ateapipb.Worker) *ateapipb.WorkerCapacity { + return oldObj.Capacity }) - errs = append(errs, fn(fldPath.Child("actor"), obj.Actor, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("capacity"), obj.Capacity, oldVal, oldObj != nil)...) } - return errs -} - -// Validate_Volume validates an instance of Volume according -// to declarative validation rules in the API schema. -func Validate_Volume( - ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.Volume) (errs field.ErrorList) { - - // field ateapipb.Volume.Name has no validation - // field ateapipb.Volume.DurableDir has no validation - // field ateapipb.Volume.ExternalVolumeTemplate has no validation - - { // field ateapipb.Volume.SystemInfo + { // field ateapipb.Worker.Status fn := func( fldPath *field.Path, - obj, oldObj *ateapipb.SystemInfoVolumeSource, + obj, oldObj *ateapipb.WorkerStatus, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -2731,18 +3841,16 @@ func Validate_Volume( return // do not proceed } // call the type's validation function - errs = append(errs, Validate_SystemInfoVolumeSource(ctx, op, fldPath, obj, oldObj)...) + errs = append(errs, Validate_WorkerStatus(ctx, op, fldPath, obj, oldObj)...) return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.Volume) *ateapipb.SystemInfoVolumeSource { - return oldObj.SystemInfo + func(oldObj *ateapipb.Worker) *ateapipb.WorkerStatus { + return oldObj.Status }) - errs = append(errs, fn(fldPath.Child("system_info"), obj.SystemInfo, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("status"), obj.Status, oldVal, oldObj != nil)...) } - // field ateapipb.Volume.Image has no validation - // field ateapipb.Volume.Type has no validation return errs } @@ -2966,6 +4074,151 @@ func Validate_WorkerAssignment( return errs } +// Validate_WorkerCapacity validates an instance of WorkerCapacity according +// to declarative validation rules in the API schema. +func Validate_WorkerCapacity( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.WorkerCapacity) (errs field.ErrorList) { + + { // field ateapipb.WorkerCapacity.CpuMilli + fn := func( + fldPath *field.Path, + obj, oldObj *int64, + 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.WorkerCapacity) *int64 { + return &oldObj.CpuMilli + }) + errs = append(errs, fn(fldPath.Child("cpu_milli"), &obj.CpuMilli, oldVal, oldObj != nil)...) + } + + { // field ateapipb.WorkerCapacity.MemoryBytes + fn := func( + fldPath *field.Path, + obj, oldObj *int64, + 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.WorkerCapacity) *int64 { + return &oldObj.MemoryBytes + }) + errs = append(errs, fn(fldPath.Child("memory_bytes"), &obj.MemoryBytes, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_WorkerStatus validates an instance of WorkerStatus according +// to declarative validation rules in the API schema. +func Validate_WorkerStatus( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.WorkerStatus) (errs field.ErrorList) { + + { // field ateapipb.WorkerStatus.State + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.WorkerState, + 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.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.WorkerStatus) *ateapipb.WorkerState { + return &oldObj.State + }) + errs = append(errs, fn(fldPath.Child("state"), &obj.State, oldVal, oldObj != nil)...) + } + + { // field ateapipb.WorkerStatus.Assignment + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.ActorAssignment, + 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_ActorAssignment(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.WorkerStatus) *ateapipb.ActorAssignment { + return oldObj.Assignment + }) + errs = append(errs, fn(fldPath.Child("assignment"), obj.Assignment, oldVal, oldObj != nil)...) + } + + return errs +} + // deepEqualImpl_ is a validate.MatchFunc which allows the implementation of deep-equality to be defined at codegen time. func deepEqualImpl_[T any](a, b T) bool { return ateDeepEqual(a, b) diff --git a/cmd/ateapi/internal/store/atepg/atepg.go b/cmd/ateapi/internal/store/atepg/atepg.go index 3b3145c201..72fd575d2f 100644 --- a/cmd/ateapi/internal/store/atepg/atepg.go +++ b/cmd/ateapi/internal/store/atepg/atepg.go @@ -1280,8 +1280,10 @@ func (p *Persistence) DeleteActorSnapshotTag(ctx context.Context, tagRef resourc func (p *Persistence) CreateWorker(ctx context.Context, worker *ateapipb.Worker) (*ateapipb.Worker, error) { dbWorker := proto.Clone(worker).(*ateapipb.Worker) - // Workers are global-scoped, so the atespace is always empty. - dbWorker.Metadata = newCreateMetadata("", worker.GetMetadata().GetName()) + if dbWorker.Metadata == nil { + dbWorker.Metadata = &ateapipb.ResourceMetadata{} + } + setCreateMetadata(dbWorker.Metadata) protoBytes, err := proto.Marshal(dbWorker) if err != nil { @@ -1363,18 +1365,16 @@ func (p *Persistence) UpdateWorker(ctx context.Context, name string, preconditio return nil, err } - // Snapshot the stored state before handing the worker to mutate. - // mutate is free to edit anything it is given. - workerBeforeMutation := proto.Clone(dbWorker).(*ateapipb.Worker) + // Snapshot the stored metadata before handing the worker to mutate. + // mutate is free to edit anything it is given; immutable fields are + // the service layer's to enforce, via declarative validation. + oldMeta := proto.CloneOf(dbWorker.GetMetadata()) if err := mutate(dbWorker); err != nil { return nil, err } - if err := store.CheckWorkerMutation(workerBeforeMutation, dbWorker); err != nil { - return nil, err - } // Stored metadata is authoritative; discard any metadata edits made by // the closure and derive the next revision from the row we locked. - dbWorker.Metadata = newUpdateMetadata(workerBeforeMutation.GetMetadata()) + setUpdateMetadata(dbWorker.Metadata, oldMeta) protoBytes, err := proto.Marshal(dbWorker) if err != nil { diff --git a/cmd/ateapi/internal/store/store.go b/cmd/ateapi/internal/store/store.go index ac5e02f85c..967ceb4860 100644 --- a/cmd/ateapi/internal/store/store.go +++ b/cmd/ateapi/internal/store/store.go @@ -23,7 +23,6 @@ import ( "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "google.golang.org/protobuf/proto" ) var ( @@ -282,45 +281,6 @@ func (p DeletePreconditions) Check(md *ateapipb.ResourceMetadata) error { return nil } -// CheckWorkerMutation reports whether an UpdateWorker mutation left the -// worker's immutable identity fields alone. A backend calls it between running -// the mutation and writing the result. It lives here, above any one backend, -// so the rule is stated once and a second backend inherits it rather than -// restating it. -// -// metadata is not checked: a backend re-stamps it from the object it read, so -// whatever the mutation made of it is discarded either way. -// -// capacity is checked along with the rest because UpdateWorker replaces the -// worker rather than patching it: a request that omits capacity is asking to -// clear it, and silently losing a worker's compute capacity is worse than -// rejecting the write. A future pod resize has to relax this rule first. -// -// A rejection wraps ErrImmutableField, so a backend can return it as-is and -// callers still get the sentinel they map to INVALID_ARGUMENT. -func CheckWorkerMutation(stored, mutated *ateapipb.Worker) error { - for _, f := range []struct { - name string - stored string - mutated string - }{ - {"worker_namespace", stored.GetWorkerNamespace(), mutated.GetWorkerNamespace()}, - {"worker_pool", stored.GetWorkerPool(), mutated.GetWorkerPool()}, - {"worker_pod", stored.GetWorkerPod(), mutated.GetWorkerPod()}, - {"worker_pod_uid", stored.GetWorkerPodUid(), mutated.GetWorkerPodUid()}, - {"node_name", stored.GetNodeName(), mutated.GetNodeName()}, - {"ip", stored.GetIp(), mutated.GetIp()}, - } { - if f.stored != f.mutated { - return fmt.Errorf("%w: %s changed from %q to %q", ErrImmutableField, f.name, f.stored, f.mutated) - } - } - if !proto.Equal(stored.GetCapacity(), mutated.GetCapacity()) { - return fmt.Errorf("%w: capacity changed from %v to %v", ErrImmutableField, stored.GetCapacity(), mutated.GetCapacity()) - } - return nil -} - // hasResourceMetadata is an object the store addresses by atespace and name, // and whose identity a caller can guard with a Precondition. type hasResourceMetadata interface { diff --git a/cmd/ateapi/internal/store/storecontract/contract.go b/cmd/ateapi/internal/store/storecontract/contract.go index 9a9337723f..05e620bf34 100644 --- a/cmd/ateapi/internal/store/storecontract/contract.go +++ b/cmd/ateapi/internal/store/storecontract/contract.go @@ -1443,58 +1443,6 @@ func runWorkerContractTests(t *testing.T, setup func(t *testing.T) store.Interfa } }) - // Every backend must reject a mutation that touches an immutable field, and - // must name the field it rejected on. This is the case that holds any new - // backend to that. - t.Run("UpdateWorker_ImmutableFields", func(t *testing.T) { - s := setup(t) - ctx := context.Background() - - // Every case below is rejected, so nothing writes and this stays the - // current incarnation for all of them. - created, err := s.CreateWorker(ctx, newTestWorker(testWorkerName, "pod-1")) - if err != nil { - t.Fatalf("CreateWorker failed: %v", err) - } - - for _, tc := range []struct { - name string - field string - mutate func(*ateapipb.Worker) - }{ - {"worker_namespace", "worker_namespace", func(w *ateapipb.Worker) { w.WorkerNamespace = "other-ns" }}, - {"worker_pool", "worker_pool", func(w *ateapipb.Worker) { w.WorkerPool = "other-pool" }}, - {"worker_pod", "worker_pod", func(w *ateapipb.Worker) { w.WorkerPod = "other-pod" }}, - {"worker_pod_uid", "worker_pod_uid", func(w *ateapipb.Worker) { w.WorkerPodUid = otherTestWorkerName }}, - {"node_name", "node_name", func(w *ateapipb.Worker) { w.NodeName = "other-node" }}, - {"ip", "ip", func(w *ateapipb.Worker) { w.Ip = "10.0.0.9" }}, - {"capacity_changed", "capacity", func(w *ateapipb.Worker) { w.Capacity.CpuMilli = 4000 }}, - // An update replaces the worker, so a caller that leaves capacity - // out is asking to clear it. That is a change like any other. - {"capacity_cleared", "capacity", func(w *ateapipb.Worker) { w.Capacity = nil }}, - } { - t.Run(tc.name, func(t *testing.T) { - _, err := s.UpdateWorker(ctx, testWorkerName, store.PreconditionFrom(created), func(toUpdate *ateapipb.Worker) error { - tc.mutate(toUpdate) - return nil - }) - if !errors.Is(err, store.ErrImmutableField) { - t.Fatalf("changing %s returned %v, want ErrImmutableField", tc.field, err) - } - if !strings.Contains(err.Error(), tc.field) { - t.Errorf("error %v does not name the offending field %s", err, tc.field) - } - got, err := s.GetWorker(ctx, testWorkerName) - if err != nil { - t.Fatalf("GetWorker failed: %v", err) - } - if got.GetMetadata().GetVersion() != 1 { - t.Errorf("rejected mutation bumped the version to %d, want 1", got.GetMetadata().GetVersion()) - } - }) - } - }) - // Claimants that all observed the same free worker must not all win. Two // things keep that true and this exercises both: the precondition rejects // every claimant whose read the winner has since invalidated, and the diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index f84c62275e..0dd8222e6c 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -375,7 +375,7 @@ const ( // Ready; schedulable. WorkerState_WORKER_STATE_ACTIVE WorkerState = 1 // Pod terminating. Not schedulable. - WorkerState_WORKER_STATE_DRAINING WorkerState = 2 + WorkerState_WORKER_STATE_DRAINING WorkerState = 2 // Keep this in sync with WorkerStatus.state's maximum. ) // Enum value maps for WorkerState. @@ -5087,9 +5087,15 @@ func (x *DeleteActorSnapshotTagRequest) GetActorSnapshotTag() *ObjectRef { type DeleteOptions struct { state protoimpl.MessageState `protogen:"open.v1"` // If non-zero, delete only if the server's current version matches. + // + // +k8s:optional + // +k8s:minimum=1 Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` // If non-empty, delete only if the server's current uid matches. Guards // against name reuse across lifecycles. + // + // +k8s:optional + // +k8s:format=k8s-uuid Uid string `protobuf:"bytes,2,opt,name=uid,proto3" json:"uid,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -5144,9 +5150,15 @@ type ListWorkersRequest struct { // Requested page size; the server may return fewer, or occasionally // slightly more. If unspecified, defaults to a server-chosen value; // values above 1000 are coerced to 1000. + // + // +k8s:optional + // +k8s:minimum=1 PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` // Pagination token from a previous ListWorkers response. // Omit or leave empty for the first request. + // + // +k8s:optional + // +k8s:maxLength=256 PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -5253,7 +5265,9 @@ func (x *ListWorkersResponse) GetNextPageToken() string { type GetWorkerRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The Worker to fetch. atespace is always empty; Workers are global-scoped. - // +k8s:opaqueType + // + // +k8s:required + // +k8s:beta(since: "0.0")=+k8s:subfield(atespace)=+k8s:forbidden # TODO: get rid of beta prefix Worker *ObjectRef `protobuf:"bytes,1,opt,name=worker,proto3" json:"worker,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -5299,6 +5313,8 @@ func (x *GetWorkerRequest) GetWorker() *ObjectRef { type CreateWorkerRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The Worker to register. + // + // +k8s:required Worker *Worker `protobuf:"bytes,1,opt,name=worker,proto3" json:"worker,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -5354,6 +5370,11 @@ type UpdateWorkerRequest struct { // send the whole thing back. A request that alters an immutable field, by // changing it or by omitting it, returns INVALID_ARGUMENT naming the field. // status is output-only and whatever it carries is ignored. + // + // +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, forbid metadata.atespace Worker *Worker `protobuf:"bytes,1,opt,name=worker,proto3" json:"worker,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -5399,9 +5420,13 @@ func (x *UpdateWorkerRequest) GetWorker() *Worker { type DeleteWorkerRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The Worker to deregister. - // +k8s:opaqueType + // + // +k8s:required + // +k8s:beta(since: "0.0")=+k8s:subfield(atespace)=+k8s:forbidden # TODO: get rid of beta prefix Worker *ObjectRef `protobuf:"bytes,1,opt,name=worker,proto3" json:"worker,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 @@ -5454,7 +5479,9 @@ func (x *DeleteWorkerRequest) GetOptions() *DeleteOptions { type DrainWorkerRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The Worker to mark as terminating. - // +k8s:opaqueType + // + // +k8s:required + // +k8s:beta(since: "0.0")=+k8s:subfield(atespace)=+k8s:forbidden # TODO: get rid of beta prefix Worker *ObjectRef `protobuf:"bytes,1,opt,name=worker,proto3" json:"worker,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -5633,24 +5660,65 @@ type Worker struct { state protoimpl.MessageState `protogen:"open.v1"` // Output-only: name, uid, version and timestamps are all server-assigned. // uid and version are echoed back on UpdateWorker as preconditions. - // +k8s:opaqueType + // + // +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"` // Kubernetes coordinates. Immutable, set at creation. + // + // +k8s:required + // +k8s:format=k8s-short-name + // +k8s:immutable WorkerNamespace string `protobuf:"bytes,2,opt,name=worker_namespace,json=workerNamespace,proto3" json:"worker_namespace,omitempty"` - WorkerPool string `protobuf:"bytes,3,opt,name=worker_pool,json=workerPool,proto3" json:"worker_pool,omitempty"` - WorkerPod string `protobuf:"bytes,4,opt,name=worker_pod,json=workerPod,proto3" json:"worker_pod,omitempty"` - WorkerPodUid string `protobuf:"bytes,5,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` - NodeName string `protobuf:"bytes,6,opt,name=node_name,json=nodeName,proto3" json:"node_name,omitempty"` - Ip string `protobuf:"bytes,7,opt,name=ip,proto3" json:"ip,omitempty"` - // Mutable. - SandboxClass string `protobuf:"bytes,8,opt,name=sandbox_class,json=sandboxClass,proto3" json:"sandbox_class,omitempty"` - Labels map[string]string `protobuf:"bytes,9,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // +k8s:required + // +k8s:format=k8s-long-name + // +k8s:immutable + WorkerPool string `protobuf:"bytes,3,opt,name=worker_pool,json=workerPool,proto3" json:"worker_pool,omitempty"` + // +k8s:required + // +k8s:format=k8s-long-name + // +k8s:immutable + WorkerPod string `protobuf:"bytes,4,opt,name=worker_pod,json=workerPod,proto3" json:"worker_pod,omitempty"` + // +k8s:required + // +k8s:format=k8s-uuid + // +k8s:immutable + WorkerPodUid string `protobuf:"bytes,5,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` + // +k8s:required + // +k8s:format=k8s-long-name + // +k8s:immutable + NodeName string `protobuf:"bytes,6,opt,name=node_name,json=nodeName,proto3" json:"node_name,omitempty"` + // +k8s:required + // +k8s:customValidation # until `format=k8s-ip` is supported + // +k8s:immutable + Ip string `protobuf:"bytes,7,opt,name=ip,proto3" json:"ip,omitempty"` + // sandbox_class mirrors the WorkerPool's sandboxClass; its values are the + // CRD's own vocabulary, so it is only bounded, not validated. Mutable. + // + // +k8s:optional + // +k8s:maxLength=63 + SandboxClass string `protobuf:"bytes,8,opt,name=sandbox_class,json=sandboxClass,proto3" json:"sandbox_class,omitempty"` + // labels mirror the WorkerPool object's Kubernetes labels, which selectors + // match against. + // Kubernetes does not cap an object's label count, so the bound below is + // a generous guardrail, not a mirror of an upstream limit. + // + // +k8s:optional + // +k8s:maxProperties=64 + // +k8s:eachKey=+k8s:format=k8s-label-key + // +k8s:eachVal=+k8s:format=k8s-label-value + Labels map[string]string `protobuf:"bytes,9,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // The compute capacity this worker can give an actor sandbox. Immutable, set - // at creation: a worker pod's limits are fixed for its lifetime. + // at creation: a worker pod's limits are fixed for its lifetime. An update + // replaces the worker rather than patching it, so a request that omits + // capacity is asking to clear it, which the immutability rule rejects. + // + // +k8s:optional + // +k8s:immutable Capacity *WorkerCapacity `protobuf:"bytes,10,opt,name=capacity,proto3" json:"capacity,omitempty"` // Output-only server-managed state. Absent from Create/Update request // payloads; whatever a request carries here is ignored. DrainWorker is the // only way a client moves state, and assignment is the scheduler's. + // + // +k8s:optional Status *WorkerStatus `protobuf:"bytes,11,opt,name=status,proto3" json:"status,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -5765,8 +5833,13 @@ func (x *Worker) GetStatus() *WorkerStatus { type WorkerStatus struct { state protoimpl.MessageState `protogen:"open.v1"` - State WorkerState `protobuf:"varint,1,opt,name=state,proto3,enum=ateapi.WorkerState" json:"state,omitempty"` + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=2 # keep this in sync with the WorkerState enum + State WorkerState `protobuf:"varint,1,opt,name=state,proto3,enum=ateapi.WorkerState" json:"state,omitempty"` // The Actor currently bound to this Worker, if any. + // + // +k8s:optional Assignment *ActorAssignment `protobuf:"bytes,2,opt,name=assignment,proto3" json:"assignment,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -5823,9 +5896,17 @@ func (x *WorkerStatus) GetAssignment() *ActorAssignment { // "unknown/unset" for that dimension: treated as unconstrained so placement is // not blocked (matching the pre-capacity behavior). type WorkerCapacity struct { - state protoimpl.MessageState `protogen:"open.v1"` - CpuMilli int64 `protobuf:"varint,1,opt,name=cpu_milli,json=cpuMilli,proto3" json:"cpu_milli,omitempty"` // CPU capacity in millicores (1000 = one core). - MemoryBytes int64 `protobuf:"varint,2,opt,name=memory_bytes,json=memoryBytes,proto3" json:"memory_bytes,omitempty"` // Memory capacity in bytes. + state protoimpl.MessageState `protogen:"open.v1"` + // CPU capacity in millicores (1000 = one core). + // + // +k8s:optional + // +k8s:minimum=1 + CpuMilli int64 `protobuf:"varint,1,opt,name=cpu_milli,json=cpuMilli,proto3" json:"cpu_milli,omitempty"` + // Memory capacity in bytes. + // + // +k8s:optional + // +k8s:minimum=1 + MemoryBytes int64 `protobuf:"varint,2,opt,name=memory_bytes,json=memoryBytes,proto3" json:"memory_bytes,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -5882,11 +5963,19 @@ type ActorAssignment struct { // of an actor created with the legacy CRD path // actor_template_ref names the substrate ActorTemplate resource. // Exactly one is set. + // + // +k8s:optional + // +k8s:unionMember ActorTemplate *KubeNamespacedObjectRef `protobuf:"bytes,1,opt,name=actor_template,json=actorTemplate,proto3" json:"actor_template,omitempty"` - // +k8s:opaqueType - Actor *ObjectRef `protobuf:"bytes,2,opt,name=actor,proto3" json:"actor,omitempty"` - ActorUid string `protobuf:"bytes,3,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` - // +k8s:opaqueType + // +k8s:required + // +k8s:subfield(atespace)=+k8s:required + Actor *ObjectRef `protobuf:"bytes,2,opt,name=actor,proto3" json:"actor,omitempty"` + // +k8s:required + // +k8s:format=k8s-uuid + ActorUid string `protobuf:"bytes,3,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` + // +k8s:optional + // +k8s:unionMember + // +k8s:subfield(atespace)=+k8s:required ActorTemplateRef *ObjectRef `protobuf:"bytes,4,opt,name=actor_template_ref,json=actorTemplateRef,proto3" json:"actor_template_ref,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -5951,9 +6040,13 @@ func (x *ActorAssignment) GetActorTemplateRef() *ObjectRef { } type KubeNamespacedObjectRef struct { - state protoimpl.MessageState `protogen:"open.v1"` - Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // +k8s:required + // +k8s:format=k8s-short-name + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + // +k8s:required + // +k8s:format=k8s-long-name + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 6ca24e44fc..e19be73dbd 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -1198,10 +1198,16 @@ message DeleteActorSnapshotTagRequest { // DeleteWorkerRequest carries it so far. message DeleteOptions { // If non-zero, delete only if the server's current version matches. + // + // +k8s:optional + // +k8s:minimum=1 int64 version = 1; // If non-empty, delete only if the server's current uid matches. Guards // against name reuse across lifecycles. + // + // +k8s:optional + // +k8s:format=k8s-uuid string uid = 2; } @@ -1209,10 +1215,16 @@ message ListWorkersRequest { // Requested page size; the server may return fewer, or occasionally // slightly more. If unspecified, defaults to a server-chosen value; // values above 1000 are coerced to 1000. + // + // +k8s:optional + // +k8s:minimum=1 int32 page_size = 1; // Pagination token from a previous ListWorkers response. // Omit or leave empty for the first request. + // + // +k8s:optional + // +k8s:maxLength=256 string page_token = 2; } @@ -1230,12 +1242,16 @@ message ListWorkersResponse { message GetWorkerRequest { // The Worker to fetch. atespace is always empty; Workers are global-scoped. - // +k8s:opaqueType + // + // +k8s:required + // +k8s:beta(since: "0.0")=+k8s:subfield(atespace)=+k8s:forbidden # TODO: get rid of beta prefix ObjectRef worker = 1; } message CreateWorkerRequest { // The Worker to register. + // + // +k8s:required Worker worker = 1; } @@ -1251,21 +1267,32 @@ message UpdateWorkerRequest { // send the whole thing back. A request that alters an immutable field, by // changing it or by omitting it, returns INVALID_ARGUMENT naming the field. // status is output-only and whatever it carries is ignored. + // + // +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, forbid metadata.atespace Worker worker = 1; } message DeleteWorkerRequest { // The Worker to deregister. - // +k8s:opaqueType + // + // +k8s:required + // +k8s:beta(since: "0.0")=+k8s:subfield(atespace)=+k8s:forbidden # TODO: get rid of beta prefix ObjectRef worker = 1; // Optional per-delete preconditions. + // + // +k8s:optional DeleteOptions options = 2; } message DrainWorkerRequest { // The Worker to mark as terminating. - // +k8s:opaqueType + // + // +k8s:required + // +k8s:beta(since: "0.0")=+k8s:subfield(atespace)=+k8s:forbidden # TODO: get rid of beta prefix ObjectRef worker = 1; } @@ -1306,28 +1333,70 @@ message ListActorsResponse { message Worker { // Output-only: name, uid, version and timestamps are all server-assigned. // uid and version are echoed back on UpdateWorker as preconditions. - // +k8s:opaqueType + // + // +k8s:required + // +k8s:beta(since: "0.0")=+k8s:subfield(atespace)=+k8s:forbidden # TODO: get rid of beta prefix ResourceMetadata metadata = 1; // Kubernetes coordinates. Immutable, set at creation. + // + // +k8s:required + // +k8s:format=k8s-short-name + // +k8s:immutable string worker_namespace = 2; + // +k8s:required + // +k8s:format=k8s-long-name + // +k8s:immutable string worker_pool = 3; + // +k8s:required + // +k8s:format=k8s-long-name + // +k8s:immutable string worker_pod = 4; + // +k8s:required + // +k8s:format=k8s-uuid + // +k8s:immutable string worker_pod_uid = 5; + // +k8s:required + // +k8s:format=k8s-long-name + // +k8s:immutable string node_name = 6; + // +k8s:required + // +k8s:customValidation # until `format=k8s-ip` is supported + // +k8s:immutable string ip = 7; - // Mutable. + // sandbox_class mirrors the WorkerPool's sandboxClass; its values are the + // CRD's own vocabulary, so it is only bounded, not validated. Mutable. + // + // +k8s:optional + // +k8s:maxLength=63 string sandbox_class = 8; + + // labels mirror the WorkerPool object's Kubernetes labels, which selectors + // match against. + // Kubernetes does not cap an object's label count, so the bound below is + // a generous guardrail, not a mirror of an upstream limit. + // + // +k8s:optional + // +k8s:maxProperties=64 + // +k8s:eachKey=+k8s:format=k8s-label-key + // +k8s:eachVal=+k8s:format=k8s-label-value map labels = 9; // The compute capacity this worker can give an actor sandbox. Immutable, set - // at creation: a worker pod's limits are fixed for its lifetime. + // at creation: a worker pod's limits are fixed for its lifetime. An update + // replaces the worker rather than patching it, so a request that omits + // capacity is asking to clear it, which the immutability rule rejects. + // + // +k8s:optional + // +k8s:immutable WorkerCapacity capacity = 10; // Output-only server-managed state. Absent from Create/Update request // payloads; whatever a request carries here is ignored. DrainWorker is the // only way a client moves state, and assignment is the scheduler's. + // + // +k8s:optional WorkerStatus status = 11; } @@ -1337,12 +1406,18 @@ enum WorkerState { WORKER_STATE_ACTIVE = 1; // Pod terminating. Not schedulable. WORKER_STATE_DRAINING = 2; + // Keep this in sync with WorkerStatus.state's maximum. } message WorkerStatus { + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=2 # keep this in sync with the WorkerState enum WorkerState state = 1; // The Actor currently bound to this Worker, if any. + // + // +k8s:optional ActorAssignment assignment = 2; } @@ -1353,8 +1428,17 @@ message WorkerStatus { // "unknown/unset" for that dimension: treated as unconstrained so placement is // not blocked (matching the pre-capacity behavior). message WorkerCapacity { - int64 cpu_milli = 1; // CPU capacity in millicores (1000 = one core). - int64 memory_bytes = 2; // Memory capacity in bytes. + // CPU capacity in millicores (1000 = one core). + // + // +k8s:optional + // +k8s:minimum=1 + int64 cpu_milli = 1; + + // Memory capacity in bytes. + // + // +k8s:optional + // +k8s:minimum=1 + int64 memory_bytes = 2; } // ActorAssignment names the Actor currently bound to a Worker — the inverse of @@ -1364,16 +1448,32 @@ message ActorAssignment { // of an actor created with the legacy CRD path // actor_template_ref names the substrate ActorTemplate resource. // Exactly one is set. + // + // +k8s:optional + // +k8s:unionMember KubeNamespacedObjectRef actor_template = 1; - // +k8s:opaqueType + + // +k8s:required + // +k8s:subfield(atespace)=+k8s:required ObjectRef actor = 2; + + // +k8s:required + // +k8s:format=k8s-uuid string actor_uid = 3; - // +k8s:opaqueType + + // +k8s:optional + // +k8s:unionMember + // +k8s:subfield(atespace)=+k8s:required ObjectRef actor_template_ref = 4; } message KubeNamespacedObjectRef { + // +k8s:required + // +k8s:format=k8s-short-name string namespace = 1; + + // +k8s:required + // +k8s:format=k8s-long-name string name = 2; } diff --git a/tools/apitool/exemptions.json b/tools/apitool/exemptions.json index 39eeb34e4e..bc4b2e315a 100644 --- a/tools/apitool/exemptions.json +++ b/tools/apitool/exemptions.json @@ -579,11 +579,6 @@ "subject": "ateapi.Worker.ip", "message": "field has no doc comment" }, - { - "rule": "documented", - "subject": "ateapi.Worker.labels", - "message": "field has no doc comment" - }, { "rule": "documented", "subject": "ateapi.Worker.node_name", @@ -604,16 +599,6 @@ "subject": "ateapi.Worker.worker_pool", "message": "field has no doc comment" }, - { - "rule": "documented", - "subject": "ateapi.WorkerCapacity.cpu_milli", - "message": "field has no doc comment" - }, - { - "rule": "documented", - "subject": "ateapi.WorkerCapacity.memory_bytes", - "message": "field has no doc comment" - }, { "rule": "documented", "subject": "ateapi.WorkerState",