From de48ad6a61873a5cd4cc6a70f91d30902b447386 Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Wed, 26 Aug 2026 16:07:54 -0700 Subject: [PATCH 01/11] update worker dv --- cmd/ateapi/internal/controlapi/syncer.go | 8 +- cmd/ateapi/internal/controlapi/validate.go | 22 + cmd/ateapi/internal/controlapi/worker.go | 266 ++-- cmd/ateapi/internal/controlapi/worker_test.go | 360 ++++- .../controlapi/zz_generated.validation.go | 1197 ++++++++++++++--- cmd/ateapi/internal/store/atepg/atepg.go | 12 +- cmd/ateapi/internal/store/store.go | 40 - .../internal/store/storecontract/contract.go | 52 - pkg/proto/ateapipb/ateapi.pb.go | 84 +- pkg/proto/ateapipb/ateapi.proto | 70 +- 10 files changed, 1611 insertions(+), 500 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/syncer.go b/cmd/ateapi/internal/controlapi/syncer.go index be87fe75f6..23e7384e0c 100644 --- a/cmd/ateapi/internal/controlapi/syncer.go +++ b/cmd/ateapi/internal/controlapi/syncer.go @@ -263,10 +263,10 @@ func (s *WorkerPoolSyncer) createOrUpdateWorker(ctx context.Context, key workerK State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, }, } - // TODO: validateWorker now lives next to CreateWorker, which applies it - // too. Once this path calls the RPC instead of the store, the check - // here goes away and the errors below arrive as INVALID_ARGUMENT. - if errs := validateWorker(worker, nil); len(errs) > 0 { + // TODO: CreateWorker applies the same declarative validation. Once + // this path calls the RPC instead of the store, the check here goes + // away and the errors below arrive as INVALID_ARGUMENT. + if errs := validateWorkerCreate(ctx, nil, worker); len(errs) > 0 { // Terminal: the inputs are deterministic, retrying cannot help. A // future pod event re-enqueues the key. slog.ErrorContext(ctx, "Invalid worker", append(key.logAttrs(), slog.Any("err", errs.ToAggregate()))...) diff --git a/cmd/ateapi/internal/controlapi/validate.go b/cmd/ateapi/internal/controlapi/validate.go index 6fa32c5abd..dee19ebe22 100644 --- a/cmd/ateapi/internal/controlapi/validate.go +++ b/cmd/ateapi/internal/controlapi/validate.go @@ -144,3 +144,25 @@ func ValidateCustom_UpdateActorRequest_Actor(ctx context.Context, op operation.O func ValidateCustom_WorkerAssignment_WorkerPodIp(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { return validation.IsValidIP(fldPath, *value) } + +// 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) +} + +// 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 +} diff --git a/cmd/ateapi/internal/controlapi/worker.go b/cmd/ateapi/internal/controlapi/worker.go index 1d4873d119..6242886b48 100644 --- a/cmd/ateapi/internal/controlapi/worker.go +++ b/cmd/ateapi/internal/controlapi/worker.go @@ -20,17 +20,17 @@ 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/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) } @@ -49,19 +49,14 @@ func (s *ServiceImpl) ListWorkers(ctx context.Context, opts store.ListOptions) ( 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() @@ -81,64 +76,85 @@ func (s *ServiceImpl) GetWorker(ctx context.Context, name string) (*ateapipb.Wor 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} + errs := Validate_CreateWorkerRequest(ctx, op, nil, req, nil) + errs = append(errs, validateNoUnknownFields(req.Worker, field.NewPath("worker"))...) + return errs } // 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 +163,46 @@ 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} + errs := Validate_UpdateWorkerRequest(ctx, op, nil, req, nil) + errs = append(errs, validateNoUnknownFields(req.Worker, field.NewPath("worker"))...) + return errs } 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) } name := req.GetWorker().GetName() @@ -194,27 +230,16 @@ func (s *ServiceImpl) DeleteWorker(ctx context.Context, name string, pre store.D 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() @@ -244,9 +269,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 @@ -269,8 +295,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) } @@ -288,69 +312,27 @@ 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)) - } - } - - if val, fldPath := worker.GetIp(), fldPath.Child("ip"); val == "" { - errs = append(errs, field.Required(fldPath, "")) - } else { - errs = append(errs, resources.ValidateIP(val, fldPath)...) - } - - 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)...) - } +// validateWorkerCreate validates a complete Worker as a standalone object, +// with no previous value to compare against. +func validateWorkerCreate(ctx context.Context, fldPath *field.Path, worker *ateapipb.Worker) field.ErrorList { + // Call the generated validation. + op := operation.Operation{Type: operation.Create} + return Validate_Worker(ctx, op, fldPath, worker, nil) +} - 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)) - } +// 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 } diff --git a/cmd/ateapi/internal/controlapi/worker_test.go b/cmd/ateapi/internal/controlapi/worker_test.go index f4e3fd1383..e69967df2e 100644 --- a/cmd/ateapi/internal/controlapi/worker_test.go +++ b/cmd/ateapi/internal/controlapi/worker_test.go @@ -67,7 +67,7 @@ func newWorkerAPIService(t *testing.T) (*RPCService, store.Interface) { t.Helper() persistence, cleanup := storetest.SetupTestStore(t) t.Cleanup(cleanup) - return &RPCService{impl: persistence}, persistence + return &RPCService{impl: newServiceImpl(persistence, nil, nil)}, persistence } // seedAPIWorker registers a worker directly through the store and returns it as @@ -136,11 +136,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) }) } } @@ -625,6 +633,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 { @@ -640,79 +649,95 @@ 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. tests := []struct { - name string - mutate func(*ateapipb.Worker) // nil leaves the worker valid - wantMsg string // empty means valid + name string + mutate func(*ateapipb.Worker) // nil leaves the worker valid + want field.ErrorList }{{ name: "valid unassigned worker", }, { - // 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", + name: "status, when carried, is validated", mutate: func(w *ateapipb.Worker) { - w.Status = &ateapipb.WorkerStatus{ - State: ateapipb.WorkerState(99), - Assignment: &ateapipb.ActorAssignment{Actor: &ateapipb.ObjectRef{Name: "actor"}}, - } + w.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState(99)} + }, + want: field.ErrorList{field.Invalid(field.NewPath("worker", "status", "state"), nil, "").WithOrigin("maximum")}, + }, { + name: "status, when carried, needs a state", + mutate: func(w *ateapipb.Worker) { + w.Status = &ateapipb.WorkerStatus{} }, + want: field.ErrorList{field.Required(field.NewPath("worker", "status", "state"), "")}, + }, { + name: "absent capacity is allowed", + mutate: 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: "missing worker_namespace", + mutate: func(w *ateapipb.Worker) { w.WorkerNamespace = "" }, + want: field.ErrorList{field.Required(field.NewPath("worker", "worker_namespace"), "")}, }, { - name: "invalid worker_namespace", - mutate: func(w *ateapipb.Worker) { w.WorkerNamespace = "NS-1" }, - wantMsg: "worker.worker_namespace: Invalid value", + name: "invalid worker_namespace", + mutate: 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", - mutate: func(w *ateapipb.Worker) { w.WorkerPool = "" }, - wantMsg: "worker.worker_pool: Required value", + name: "missing worker_pool", + mutate: func(w *ateapipb.Worker) { w.WorkerPool = "" }, + want: field.ErrorList{field.Required(field.NewPath("worker", "worker_pool"), "")}, }, { - name: "missing worker_pod", - mutate: func(w *ateapipb.Worker) { w.WorkerPod = "" }, - wantMsg: "worker.worker_pod: Required value", + name: "invalid worker_pool", + mutate: 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 ip", - mutate: func(w *ateapipb.Worker) { w.Ip = "" }, - wantMsg: "worker.ip: Required value", + name: "missing worker_pod", + mutate: func(w *ateapipb.Worker) { w.WorkerPod = "" }, + want: field.ErrorList{field.Required(field.NewPath("worker", "worker_pod"), "")}, }, { - name: "invalid ip", - mutate: func(w *ateapipb.Worker) { w.Ip = "not-an-ip" }, - wantMsg: "worker.ip: Invalid value", + name: "invalid worker_pod", + mutate: 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", - mutate: func(w *ateapipb.Worker) { w.WorkerPodUid = "" }, - wantMsg: "worker.worker_pod_uid: Required value", + name: "missing ip", + mutate: func(w *ateapipb.Worker) { w.Ip = "" }, + want: field.ErrorList{field.Required(field.NewPath("worker", "ip"), "")}, }, { - name: "invalid worker_pod_uid", - mutate: func(w *ateapipb.Worker) { w.WorkerPodUid = "INVALID-UUID" }, - wantMsg: "worker.worker_pod_uid: Invalid value", + name: "invalid ip", + mutate: func(w *ateapipb.Worker) { w.Ip = "not-an-ip" }, + want: field.ErrorList{field.Invalid(field.NewPath("worker", "ip"), nil, "").WithOrigin("format=ip-strict")}, }, { - name: "missing node_name", - mutate: func(w *ateapipb.Worker) { w.NodeName = "" }, - wantMsg: "worker.node_name: Required value", + name: "missing worker_pod_uid", + mutate: func(w *ateapipb.Worker) { w.WorkerPodUid = "" }, + want: field.ErrorList{field.Required(field.NewPath("worker", "worker_pod_uid"), "")}, }, { - name: "invalid node_name", - mutate: func(w *ateapipb.Worker) { w.NodeName = "NODE_NAME" }, - wantMsg: "worker.node_name: Invalid value", + name: "invalid worker_pod_uid", + mutate: 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 metadata", - mutate: func(w *ateapipb.Worker) { w.Metadata = nil }, - wantMsg: "worker.metadata.name: Required value", + name: "missing node_name", + mutate: func(w *ateapipb.Worker) { w.NodeName = "" }, + want: field.ErrorList{field.Required(field.NewPath("worker", "node_name"), "")}, }, { - name: "missing metadata.name", - mutate: func(w *ateapipb.Worker) { w.Metadata = &ateapipb.ResourceMetadata{} }, - wantMsg: "worker.metadata.name: Required value", + name: "invalid node_name", + mutate: 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: "invalid metadata.name", - mutate: func(w *ateapipb.Worker) { w.Metadata.Name = "Not A Name" }, - wantMsg: "worker.metadata.name: Invalid value", + name: "missing metadata", + mutate: func(w *ateapipb.Worker) { w.Metadata = nil }, + want: field.ErrorList{field.Required(field.NewPath("worker", "metadata"), "")}, }, { - 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: "missing metadata.name", + mutate: func(w *ateapipb.Worker) { w.Metadata = &ateapipb.ResourceMetadata{} }, + want: field.ErrorList{field.Required(field.NewPath("worker", "metadata", "name"), "")}, + }, { + name: "invalid metadata.name", + mutate: func(w *ateapipb.Worker) { w.Metadata.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", + mutate: func(w *ateapipb.Worker) { w.Metadata.Atespace = "team-a" }, + want: field.ErrorList{field.Forbidden(field.NewPath("worker", "metadata", "atespace"), "")}, }} for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -720,21 +745,218 @@ func TestValidateWorker(t *testing.T) { if tc.mutate != nil { tc.mutate(worker) } - errs := validateWorker(worker, field.NewPath("worker")) - if tc.wantMsg == "" { - if len(errs) > 0 { - t.Fatalf("validateWorker() = %v, want no errors", errs) - } - return + req := &ateapipb.CreateWorkerRequest{Worker: worker} + assertValidateErr(t, validateCreateWorkerRequest(context.Background(), 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, newAPIWorker(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) } - // 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 - } + got, err := persistence.GetWorker(ctx, apiWorkerName) + if err != nil { + t.Fatalf("GetWorker failed: %v", err) } - t.Errorf("validateWorker() = %v, want an error containing %q", errs, tc.wantMsg) + if got.GetMetadata().GetVersion() != 1 { + t.Errorf("rejected mutation bumped the version to %d, want 1", got.GetMetadata().GetVersion()) + } + }) + } +} + +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 := newAPIWorker(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 := newAPIWorker(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 := newAPIWorker(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()) + } +} diff --git a/cmd/ateapi/internal/controlapi/zz_generated.validation.go b/cmd/ateapi/internal/controlapi/zz_generated.validation.go index e09b0c8463..2f2db9a75d 100644 --- a/cmd/ateapi/internal/controlapi/zz_generated.validation.go +++ b/cmd/ateapi/internal/controlapi/zz_generated.validation.go @@ -547,6 +547,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_DeleteAtespaceRequest validates an instance of DeleteAtespaceRequest according // to declarative validation rules in the API schema. func Validate_DeleteAtespaceRequest( @@ -606,13 +646,84 @@ func Validate_DeleteAtespaceRequest( return errs } -// Validate_GetAtespaceRequest validates an instance of GetAtespaceRequest according +// Validate_DeleteOptions validates an instance of DeleteOptions according // to declarative validation rules in the API schema. -func Validate_GetAtespaceRequest( +func Validate_DeleteOptions( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.GetAtespaceRequest) (errs field.ErrorList) { + obj, oldObj *ateapipb.DeleteOptions) (errs field.ErrorList) { - { // field ateapipb.GetAtespaceRequest.Atespace + { // field ateapipb.DeleteOptions.Version + 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.DeleteOptions) *int64 { + return &oldObj.Version + }) + errs = append(errs, fn(fldPath.Child("version"), &obj.Version, oldVal, oldObj != nil)...) + } + + { // field ateapipb.DeleteOptions.Uid + 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.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.DeleteOptions) *string { + return &oldObj.Uid + }) + errs = append(errs, fn(fldPath.Child("uid"), &obj.Uid, oldVal, oldObj != nil)...) + } + + return errs +} + +// 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, @@ -656,119 +767,397 @@ func Validate_GetAtespaceRequest( return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.GetAtespaceRequest) *ateapipb.ObjectRef { - return oldObj.Atespace + func(oldObj *ateapipb.DeleteWorkerRequest) *ateapipb.ObjectRef { + return oldObj.Worker }) - errs = append(errs, fn(fldPath.Child("atespace"), obj.Atespace, oldVal, oldObj != nil)...) + 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_ListAtespacesRequest validates an instance of ListAtespacesRequest according +// Validate_DrainWorkerRequest validates an instance of DrainWorkerRequest according // to declarative validation rules in the API schema. -func Validate_ListAtespacesRequest( +func Validate_DrainWorkerRequest( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.ListAtespacesRequest) (errs field.ErrorList) { + obj, oldObj *ateapipb.DrainWorkerRequest) (errs field.ErrorList) { - { // field ateapipb.ListAtespacesRequest.PageSize + { // field ateapipb.DrainWorkerRequest.Worker fn := func( fldPath *field.Path, - obj, oldObj *int32, + obj, oldObj *ateapipb.ObjectRef, 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) { + if ateDeepEqual(obj, oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.OptionalValue(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 } - 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.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.ListAtespacesRequest) *int32 { - return &oldObj.PageSize + func(oldObj *ateapipb.DrainWorkerRequest) *ateapipb.ObjectRef { + return oldObj.Worker }) - errs = append(errs, fn(fldPath.Child("page_size"), &obj.PageSize, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("worker"), obj.Worker, oldVal, oldObj != nil)...) } - { // field ateapipb.ListAtespacesRequest.PageToken + return errs +} + +// Validate_GetAtespaceRequest validates an instance of GetAtespaceRequest according +// to declarative validation rules in the API schema. +func Validate_GetAtespaceRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.GetAtespaceRequest) (errs field.ErrorList) { + + { // field ateapipb.GetAtespaceRequest.Atespace 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 { - if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + if ateDeepEqual(obj, oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.OptionalValue(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 } - if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256); 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.ListAtespacesRequest) *string { - return &oldObj.PageToken + func(oldObj *ateapipb.GetAtespaceRequest) *ateapipb.ObjectRef { + return oldObj.Atespace }) - errs = append(errs, fn(fldPath.Child("page_token"), &obj.PageToken, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("atespace"), obj.Atespace, oldVal, oldObj != nil)...) } return errs } -// Validate_ObjectRef validates an instance of ObjectRef according +// Validate_GetWorkerRequest validates an instance of GetWorkerRequest according // to declarative validation rules in the API schema. -func Validate_ObjectRef( +func Validate_GetWorkerRequest( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.ObjectRef) (errs field.ErrorList) { + obj, oldObj *ateapipb.GetWorkerRequest) (errs field.ErrorList) { - { // field ateapipb.ObjectRef.Atespace + { // 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 { - if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + if ateDeepEqual(obj, oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.OptionalValue(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 } - if e := validate.ShortName(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.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.ObjectRef) *string { - return &oldObj.Atespace + func(oldObj *ateapipb.GetWorkerRequest) *ateapipb.ObjectRef { + return oldObj.Worker + }) + errs = append(errs, fn(fldPath.Child("worker"), obj.Worker, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_ListAtespacesRequest validates an instance of ListAtespacesRequest according +// to declarative validation rules in the API schema. +func Validate_ListAtespacesRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.ListAtespacesRequest) (errs field.ErrorList) { + + { // field ateapipb.ListAtespacesRequest.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.ListAtespacesRequest) *int32 { + return &oldObj.PageSize + }) + errs = append(errs, fn(fldPath.Child("page_size"), &obj.PageSize, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ListAtespacesRequest.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.ListAtespacesRequest) *string { + return &oldObj.PageToken + }) + errs = append(errs, fn(fldPath.Child("page_token"), &obj.PageToken, oldVal, oldObj != nil)...) + } + + 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_ObjectRef validates an instance of ObjectRef according +// to declarative validation rules in the API schema. +func Validate_ObjectRef( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.ObjectRef) (errs field.ErrorList) { + + { // field ateapipb.ObjectRef.Atespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ObjectRef) *string { + return &oldObj.Atespace }) errs = append(errs, fn(fldPath.Child("atespace"), &obj.Atespace, oldVal, oldObj != nil)...) } @@ -780,41 +1169,481 @@ func Validate_ObjectRef( 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) { + 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.ObjectRef) *string { + return &oldObj.Name + }) + errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_ResourceMetadata validates an instance of ResourceMetadata according +// to declarative validation rules in the API schema. +func Validate_ResourceMetadata( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.ResourceMetadata) (errs field.ErrorList) { + + { // field ateapipb.ResourceMetadata.Atespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.Immutable(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ResourceMetadata) *string { + return &oldObj.Atespace + }) + errs = append(errs, fn(fldPath.Child("atespace"), &obj.Atespace, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ResourceMetadata.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.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.ResourceMetadata) *string { + return &oldObj.Name + }) + errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ResourceMetadata.Uid + 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.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + 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.ResourceMetadata) *string { + return &oldObj.Uid + }) + errs = append(errs, fn(fldPath.Child("uid"), &obj.Uid, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ResourceMetadata.Version + 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 e := validate.UpdateValue(ctx, op, fldPath, obj, oldObj, + func(a int64, b int64) bool { return a == b }, validate.NoUnset).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Monotonic(ctx, op, fldPath, obj, oldObj).MarkAlpha(); 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.ResourceMetadata) *int64 { + return &oldObj.Version + }) + errs = append(errs, fn(fldPath.Child("version"), &obj.Version, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ResourceMetadata.CreateTime + fn := func( + fldPath *field.Path, + obj, oldObj *timestamppb.Timestamp, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.Immutable(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ResourceMetadata) *timestamppb.Timestamp { + return oldObj.CreateTime + }) + errs = append(errs, fn(fldPath.Child("create_time"), obj.CreateTime, oldVal, oldObj != nil)...) + } + + { // field ateapipb.ResourceMetadata.UpdateTime + fn := func( + fldPath *field.Path, + obj, oldObj *timestamppb.Timestamp, + 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 e := validate.UpdatePointer(ctx, op, fldPath, obj, oldObj, validate.NoUnset).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.ResourceMetadata) *timestamppb.Timestamp { + return oldObj.UpdateTime + }) + errs = append(errs, fn(fldPath.Child("update_time"), obj.UpdateTime, oldVal, oldObj != nil)...) + } + + return errs +} + +var unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_Selector_ = validate.NewUnionMembership(validate.NewUnionMember("match_labels")) + +// Validate_Selector validates an instance of Selector according +// to declarative validation rules in the API schema. +func Validate_Selector( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.Selector) (errs field.ErrorList) { + + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_Selector_, + func(obj *ateapipb.Selector) bool { + if obj == nil { + return false + } + return len(obj.MatchLabels) != 0 + }); len(e) != 0 { + errs = append(errs, e...) + } + + { // field ateapipb.Selector.MatchLabels + fn := func( + fldPath *field.Path, + obj, oldObj map[string]string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.MaxProperties(ctx, op, fldPath, obj, oldObj, 10).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalMap(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.EachMapKey(ctx, op, fldPath, obj, oldObj, validate.LabelKey); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MinProperties(ctx, op, fldPath, obj, oldObj, 1); 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.Selector) map[string]string { + return oldObj.MatchLabels + }) + errs = append(errs, fn(fldPath.Child("match_labels"), obj.MatchLabels, 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_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.RequiredValue(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 } - if e := validate.ShortName(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.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.ObjectRef) *string { - return &oldObj.Name + func(oldObj *ateapipb.Worker) *ateapipb.ResourceMetadata { + return oldObj.Metadata }) - errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("metadata"), obj.Metadata, oldVal, oldObj != nil)...) } - return errs -} - -// Validate_ResourceMetadata validates an instance of ResourceMetadata according -// to declarative validation rules in the API schema. -func Validate_ResourceMetadata( - ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.ResourceMetadata) (errs field.ErrorList) { - - { // field ateapipb.ResourceMetadata.Atespace + { // field ateapipb.Worker.WorkerNamespace fn := func( fldPath *field.Path, obj, oldObj *string, @@ -831,7 +1660,8 @@ func Validate_ResourceMetadata( errs = append(errs, e...) earlyReturn = true } - if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) earlyReturn = true } if earlyReturn { @@ -843,13 +1673,13 @@ func Validate_ResourceMetadata( return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.ResourceMetadata) *string { - return &oldObj.Atespace + func(oldObj *ateapipb.Worker) *string { + return &oldObj.WorkerNamespace }) - errs = append(errs, fn(fldPath.Child("atespace"), &obj.Atespace, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("worker_namespace"), &obj.WorkerNamespace, oldVal, oldObj != nil)...) } - { // field ateapipb.ResourceMetadata.Name + { // field ateapipb.Worker.WorkerPool fn := func( fldPath *field.Path, obj, oldObj *string, @@ -873,19 +1703,19 @@ func Validate_ResourceMetadata( if earlyReturn { return // do not proceed } - if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + if e := validate.LongName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.ResourceMetadata) *string { - return &oldObj.Name + func(oldObj *ateapipb.Worker) *string { + return &oldObj.WorkerPool }) - errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("worker_pool"), &obj.WorkerPool, oldVal, oldObj != nil)...) } - { // field ateapipb.ResourceMetadata.Uid + { // field ateapipb.Worker.WorkerPod fn := func( fldPath *field.Path, obj, oldObj *string, @@ -902,28 +1732,29 @@ func Validate_ResourceMetadata( errs = append(errs, e...) earlyReturn = true } - if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + 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 { + if e := validate.LongName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.ResourceMetadata) *string { - return &oldObj.Uid + func(oldObj *ateapipb.Worker) *string { + return &oldObj.WorkerPod }) - errs = append(errs, fn(fldPath.Child("uid"), &obj.Uid, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("worker_pod"), &obj.WorkerPod, oldVal, oldObj != nil)...) } - { // field ateapipb.ResourceMetadata.Version + { // field ateapipb.Worker.WorkerPodUid fn := func( fldPath *field.Path, - obj, oldObj *int64, + obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -933,40 +1764,37 @@ func Validate_ResourceMetadata( } // call field-attached validations earlyReturn := false - if e := validate.OptionalValue(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.UpdateValue(ctx, op, fldPath, obj, oldObj, - func(a int64, b int64) bool { return a == b }, validate.NoUnset).MarkShortCircuit(); len(e) != 0 { + 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.Monotonic(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 { - errs = append(errs, e...) - } - if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); 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.ResourceMetadata) *int64 { - return &oldObj.Version + func(oldObj *ateapipb.Worker) *string { + return &oldObj.WorkerPodUid }) - errs = append(errs, fn(fldPath.Child("version"), &obj.Version, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("worker_pod_uid"), &obj.WorkerPodUid, oldVal, oldObj != nil)...) } - { // field ateapipb.ResourceMetadata.CreateTime + { // field ateapipb.Worker.NodeName fn := func( fldPath *field.Path, - obj, oldObj *timestamppb.Timestamp, + 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 } } @@ -976,78 +1804,69 @@ func Validate_ResourceMetadata( errs = append(errs, e...) earlyReturn = true } - if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + 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.ResourceMetadata) *timestamppb.Timestamp { - return oldObj.CreateTime + func(oldObj *ateapipb.Worker) *string { + return &oldObj.NodeName }) - errs = append(errs, fn(fldPath.Child("create_time"), obj.CreateTime, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("node_name"), &obj.NodeName, oldVal, oldObj != nil)...) } - { // field ateapipb.ResourceMetadata.UpdateTime + { // field ateapipb.Worker.Ip fn := func( fldPath *field.Path, - obj, oldObj *timestamppb.Timestamp, + 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.OptionalPointer(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.UpdatePointer(ctx, op, fldPath, obj, oldObj, validate.NoUnset).MarkShortCircuit(); len(e) != 0 { + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } if earlyReturn { return // do not proceed } + // custom validation + if e := ValidateCustom_Worker_Ip(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.ResourceMetadata) *timestamppb.Timestamp { - return oldObj.UpdateTime + func(oldObj *ateapipb.Worker) *string { + return &oldObj.Ip }) - errs = append(errs, fn(fldPath.Child("update_time"), obj.UpdateTime, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("ip"), &obj.Ip, oldVal, oldObj != nil)...) } - return errs -} - -var unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_Selector_ = validate.NewUnionMembership(validate.NewUnionMember("match_labels")) - -// Validate_Selector validates an instance of Selector according -// to declarative validation rules in the API schema. -func Validate_Selector( - ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.Selector) (errs field.ErrorList) { - - if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_Selector_, - func(obj *ateapipb.Selector) bool { - if obj == nil { - return false - } - return len(obj.MatchLabels) != 0 - }); len(e) != 0 { - errs = append(errs, e...) - } + // field ateapipb.Worker.SandboxClass has no validation + // field ateapipb.Worker.Labels has no validation - { // field ateapipb.Selector.MatchLabels + { // field ateapipb.Worker.Capacity fn := func( fldPath *field.Path, - obj, oldObj map[string]string, + obj, oldObj *ateapipb.WorkerCapacity, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -1057,47 +1876,29 @@ func Validate_Selector( } // call field-attached validations earlyReturn := false - if e := validate.MaxProperties(ctx, op, fldPath, obj, oldObj, 10).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.OptionalMap(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { earlyReturn = true } if earlyReturn { return // do not proceed } - if e := validate.EachMapKey(ctx, op, fldPath, obj, oldObj, validate.LabelKey); len(e) != 0 { - errs = append(errs, e...) - } - if e := validate.MinProperties(ctx, op, fldPath, obj, oldObj, 1); 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.Selector) map[string]string { - return oldObj.MatchLabels + func(oldObj *ateapipb.Worker) *ateapipb.WorkerCapacity { + return oldObj.Capacity }) - errs = append(errs, fn(fldPath.Child("match_labels"), obj.MatchLabels, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("capacity"), obj.Capacity, 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.Status fn := func( fldPath *field.Path, - obj, oldObj *ateapipb.Actor, + obj, oldObj *ateapipb.WorkerStatus, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -1107,35 +1908,21 @@ func Validate_UpdateActorRequest( } // 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.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_WorkerStatus(ctx, op, fldPath, obj, oldObj)...) return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.UpdateActorRequest) *ateapipb.Actor { - return oldObj.Actor + func(oldObj *ateapipb.Worker) *ateapipb.WorkerStatus { + return oldObj.Status }) - errs = append(errs, fn(fldPath.Child("actor"), obj.Actor, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("status"), obj.Status, oldVal, oldObj != nil)...) } return errs @@ -1361,6 +2148,78 @@ func Validate_WorkerAssignment( 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 + } + 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 a841087053..e570c2a46d 100644 --- a/cmd/ateapi/internal/store/atepg/atepg.go +++ b/cmd/ateapi/internal/store/atepg/atepg.go @@ -1251,18 +1251,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()) + dbWorker.Metadata = newUpdateMetadata(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 194e449de5..109d6b864d 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 ( @@ -272,45 +271,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 b49151bde7..4d201f7eb1 100644 --- a/cmd/ateapi/internal/store/storecontract/contract.go +++ b/cmd/ateapi/internal/store/storecontract/contract.go @@ -1320,58 +1320,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 a37128f43e..16be12c446 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -379,7 +379,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. @@ -4115,9 +4115,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 @@ -4172,9 +4178,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 @@ -4281,7 +4293,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 @@ -4327,6 +4341,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 @@ -4382,6 +4398,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 @@ -4427,9 +4448,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 @@ -4482,7 +4507,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 @@ -4661,24 +4688,52 @@ 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"` + // +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"` // 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"` // 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 @@ -4793,8 +4848,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 diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 5e7b9f6797..156375b171 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -920,10 +920,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; } @@ -931,10 +937,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; } @@ -952,12 +964,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; } @@ -973,21 +989,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; } @@ -1028,15 +1055,36 @@ 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. @@ -1044,12 +1092,19 @@ message Worker { 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; } @@ -1062,9 +1117,14 @@ enum WorkerState { } 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; } From cc7cd533990d27acbfb5a0b346c1570be7657f51 Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Thu, 27 Aug 2026 08:41:05 -0700 Subject: [PATCH 02/11] Add DV for ActorAssignment --- cmd/ateapi/internal/controlapi/worker_test.go | 69 +++++++ .../controlapi/zz_generated.validation.go | 193 ++++++++++++++++++ pkg/proto/ateapipb/ateapi.pb.go | 21 +- pkg/proto/ateapipb/ateapi.proto | 14 +- 4 files changed, 290 insertions(+), 7 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/worker_test.go b/cmd/ateapi/internal/controlapi/worker_test.go index ee1d001127..4abeb5d54f 100644 --- a/cmd/ateapi/internal/controlapi/worker_test.go +++ b/cmd/ateapi/internal/controlapi/worker_test.go @@ -655,6 +655,39 @@ func TestValidateCreateWorkerRequest(t *testing.T) { w.Status = &ateapipb.WorkerStatus{} }, want: field.ErrorList{field.Required(field.NewPath("worker", "status", "state"), "")}, + }, { + name: "valid assignment, when carried, passes", + mutate: func(w *ateapipb.Worker) { + w.Status = &ateapipb.WorkerStatus{ + State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, + Assignment: newAPIAssignment(apiOtherWorkerName), + } + }, + }, { + name: "assignment actor_uid must be a uuid", + mutate: func(w *ateapipb.Worker) { + w.Status = &ateapipb.WorkerStatus{ + State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, + Assignment: newAPIAssignment("not-a-uuid"), + } + }, + want: field.ErrorList{field.Invalid(field.NewPath("worker", "status", "assignment", "actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "assignment actor ref needs an atespace", + mutate: func(w *ateapipb.Worker) { + assignment := newAPIAssignment(apiOtherWorkerName) + assignment.Actor.Atespace = "" + w.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, Assignment: assignment} + }, + want: field.ErrorList{field.Required(field.NewPath("worker", "status", "assignment", "actor", "atespace"), "")}, + }, { + name: "assignment template name must be a long name", + mutate: func(w *ateapipb.Worker) { + assignment := newAPIAssignment(apiOtherWorkerName) + assignment.ActorTemplate.Name = "TMPL_1" + w.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, Assignment: assignment} + }, + want: field.ErrorList{field.Invalid(field.NewPath("worker", "status", "assignment", "actor_template", "name"), nil, "").WithOrigin("format=k8s-long-name")}, }, { name: "absent capacity is allowed", mutate: func(w *ateapipb.Worker) { w.Capacity = nil }, @@ -944,3 +977,39 @@ func TestCreateWorker_IgnoresRequestMetadataServerFields(t *testing.T) { 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, newAPIWorker(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/zz_generated.validation.go b/cmd/ateapi/internal/controlapi/zz_generated.validation.go index d82e0205ef..648bdd648f 100644 --- a/cmd/ateapi/internal/controlapi/zz_generated.validation.go +++ b/cmd/ateapi/internal/controlapi/zz_generated.validation.go @@ -319,6 +319,124 @@ func Validate_Actor( return errs } +// 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) { + + { // 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.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_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)...) + } + + return errs +} + // Validate_ActorMetadataDataSource validates an instance of ActorMetadataDataSource according // to declarative validation rules in the API schema. func Validate_ActorMetadataDataSource( @@ -1208,6 +1326,79 @@ func Validate_GetWorkerRequest( 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( @@ -2692,6 +2883,8 @@ func Validate_WorkerStatus( 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, diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index a416ac1774..a8dfe04554 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -5301,10 +5301,15 @@ type ActorAssignment struct { state protoimpl.MessageState `protogen:"open.v1"` // ActorTemplates are Kubernetes CRDs rather than Substrate API resources, so // this stays a kube reference. + // + // +k8s:required 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: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"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -5361,9 +5366,13 @@ func (x *ActorAssignment) GetActorUid() string { } 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 6b257aae57..79c3b6cd0c 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -1217,14 +1217,26 @@ message WorkerCapacity { message ActorAssignment { // ActorTemplates are Kubernetes CRDs rather than Substrate API resources, so // this stays a kube reference. + // + // +k8s:required 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; } message KubeNamespacedObjectRef { + // +k8s:required + // +k8s:format=k8s-short-name string namespace = 1; + + // +k8s:required + // +k8s:format=k8s-long-name string name = 2; } From 4fff7231517bdfeddf61584a7e5bc49a4719466b Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Thu, 27 Aug 2026 08:52:20 -0700 Subject: [PATCH 03/11] Address comments --- cmd/ateapi/internal/controlapi/validate.go | 22 ------------------ cmd/ateapi/internal/controlapi/worker.go | 27 ++++++++++++++++++---- cmd/ateapi/internal/store/atepg/atepg.go | 8 ++++--- pkg/proto/ateapipb/ateapi.pb.go | 2 +- pkg/proto/ateapipb/ateapi.proto | 1 + 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/validate.go b/cmd/ateapi/internal/controlapi/validate.go index 5351644f8a..68358d20ee 100644 --- a/cmd/ateapi/internal/controlapi/validate.go +++ b/cmd/ateapi/internal/controlapi/validate.go @@ -97,25 +97,3 @@ func ValidateCustom_UpdateActorRequest_Actor(ctx context.Context, op operation.O func ValidateCustom_WorkerAssignment_WorkerPodIp(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { return validation.IsValidIP(fldPath, *value) } - -// 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) -} - -// 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 -} diff --git a/cmd/ateapi/internal/controlapi/worker.go b/cmd/ateapi/internal/controlapi/worker.go index 2e02d10eeb..d130a8a04d 100644 --- a/cmd/ateapi/internal/controlapi/worker.go +++ b/cmd/ateapi/internal/controlapi/worker.go @@ -26,6 +26,7 @@ import ( "google.golang.org/protobuf/proto" "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" ) @@ -45,7 +46,6 @@ 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) } @@ -72,7 +72,6 @@ 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) } @@ -210,7 +209,6 @@ 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) } @@ -313,6 +311,27 @@ func validateWorkerUpdate(ctx context.Context, fldPath *field.Path, newVal, oldV } func (s *ServiceImpl) WatchWorkers(ctx context.Context) (*store.WorkerWatch, error) { - // TODO: implement this return s.store.WatchWorkers(ctx) } + +// 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) +} + +// 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 +} diff --git a/cmd/ateapi/internal/store/atepg/atepg.go b/cmd/ateapi/internal/store/atepg/atepg.go index e570c2a46d..a4b945c559 100644 --- a/cmd/ateapi/internal/store/atepg/atepg.go +++ b/cmd/ateapi/internal/store/atepg/atepg.go @@ -1168,8 +1168,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 { @@ -1260,7 +1262,7 @@ func (p *Persistence) UpdateWorker(ctx context.Context, name string, preconditio } // 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(oldMeta) + setUpdateMetadata(dbWorker.Metadata, oldMeta) protoBytes, err := proto.Marshal(dbWorker) if err != nil { diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index a8dfe04554..97bf1ebff9 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -374,7 +374,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. diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 79c3b6cd0c..0e2d446ebb 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -1187,6 +1187,7 @@ 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 { From 6ca4c1501e27f9dc2499361c1bc0e4c4ad721c76 Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Thu, 27 Aug 2026 17:04:46 -0700 Subject: [PATCH 04/11] Fix DV for ActorAssignment after rebase with main --- cmd/ateapi/internal/controlapi/worker_test.go | 33 ++++++++++ .../controlapi/zz_generated.validation.go | 64 ++++++++++++++++++- pkg/proto/ateapipb/ateapi.pb.go | 7 +- pkg/proto/ateapipb/ateapi.proto | 7 +- 4 files changed, 104 insertions(+), 7 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/worker_test.go b/cmd/ateapi/internal/controlapi/worker_test.go index 4abeb5d54f..128f0009fc 100644 --- a/cmd/ateapi/internal/controlapi/worker_test.go +++ b/cmd/ateapi/internal/controlapi/worker_test.go @@ -680,6 +680,39 @@ func TestValidateCreateWorkerRequest(t *testing.T) { w.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, Assignment: assignment} }, want: field.ErrorList{field.Required(field.NewPath("worker", "status", "assignment", "actor", "atespace"), "")}, + }, { + name: "assignment with a template resource ref passes", + mutate: func(w *ateapipb.Worker) { + assignment := newAPIAssignment(apiOtherWorkerName) + assignment.ActorTemplate = nil + assignment.ActorTemplateRef = &ateapipb.ObjectRef{Atespace: "team-a", Name: "tmpl"} + w.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, Assignment: assignment} + }, + }, { + name: "assignment template ref needs an atespace", + mutate: func(w *ateapipb.Worker) { + assignment := newAPIAssignment(apiOtherWorkerName) + assignment.ActorTemplate = nil + assignment.ActorTemplateRef = &ateapipb.ObjectRef{Name: "tmpl"} + w.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, Assignment: assignment} + }, + want: field.ErrorList{field.Required(field.NewPath("worker", "status", "assignment", "actor_template_ref", "atespace"), "")}, + }, { + name: "assignment must name its template exactly once: both set", + mutate: func(w *ateapipb.Worker) { + assignment := newAPIAssignment(apiOtherWorkerName) + assignment.ActorTemplateRef = &ateapipb.ObjectRef{Atespace: "team-a", Name: "tmpl"} + w.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, Assignment: assignment} + }, + want: field.ErrorList{field.Invalid(field.NewPath("worker", "status", "assignment"), nil, "").WithOrigin("union")}, + }, { + name: "assignment must name its template exactly once: neither set", + mutate: func(w *ateapipb.Worker) { + assignment := newAPIAssignment(apiOtherWorkerName) + assignment.ActorTemplate = nil + w.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, Assignment: assignment} + }, + want: field.ErrorList{field.Invalid(field.NewPath("worker", "status", "assignment"), nil, "").WithOrigin("union")}, }, { name: "assignment template name must be a long name", mutate: func(w *ateapipb.Worker) { diff --git a/cmd/ateapi/internal/controlapi/zz_generated.validation.go b/cmd/ateapi/internal/controlapi/zz_generated.validation.go index 0f5c777876..752c762b0a 100644 --- a/cmd/ateapi/internal/controlapi/zz_generated.validation.go +++ b/cmd/ateapi/internal/controlapi/zz_generated.validation.go @@ -317,12 +317,30 @@ 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, @@ -431,7 +449,51 @@ func Validate_ActorAssignment( errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) } - // field ateapipb.ActorAssignment.ActorTemplateRef has no validation + { // 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 } diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index 8959e57389..bd576422ed 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -5491,6 +5491,7 @@ type ActorAssignment struct { // 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:required // +k8s:subfield(atespace)=+k8s:required @@ -5498,9 +5499,9 @@ type ActorAssignment struct { // +k8s:required // +k8s:format=k8s-uuid ActorUid string `protobuf:"bytes,3,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` - // +k8s:opaqueType - // TODO: Add DV (subfield(atespace)=required) and enforce "exactly one of - // actor_template, actor_template_ref" once union tags are available. + // +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 diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 5c22011006..f98aa6bb11 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -1264,6 +1264,7 @@ message ActorAssignment { // Exactly one is set. // // +k8s:optional + // +k8s:unionMember KubeNamespacedObjectRef actor_template = 1; // +k8s:required @@ -1274,9 +1275,9 @@ message ActorAssignment { // +k8s:format=k8s-uuid string actor_uid = 3; - // +k8s:opaqueType - // TODO: Add DV (subfield(atespace)=required) and enforce "exactly one of - // actor_template, actor_template_ref" once union tags are available. + // +k8s:optional + // +k8s:unionMember + // +k8s:subfield(atespace)=+k8s:required ObjectRef actor_template_ref = 4; } From aff578e70e0687a175646d81490b4469adac81e7 Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Thu, 27 Aug 2026 17:34:41 -0700 Subject: [PATCH 05/11] Fix exmeptions --- tools/apitool/exemptions.json | 70 ----------------------------------- 1 file changed, 70 deletions(-) diff --git a/tools/apitool/exemptions.json b/tools/apitool/exemptions.json index f7938123e6..9865901408 100644 --- a/tools/apitool/exemptions.json +++ b/tools/apitool/exemptions.json @@ -9,11 +9,6 @@ "subject": "ateapi.Actor", "message": "message has no doc comment" }, - { - "rule": "documented", - "subject": "ateapi.ActorAssignment.actor_uid", - "message": "field has no doc comment" - }, { "rule": "documented", "subject": "ateapi.ActorSnapshot.status", @@ -114,16 +109,6 @@ "subject": "ateapi.ArchAssets", "message": "message has no doc comment" }, - { - "rule": "documented", - "subject": "ateapi.Capabilities.add", - "message": "field has no doc comment" - }, - { - "rule": "documented", - "subject": "ateapi.Capabilities.drop", - "message": "field has no doc comment" - }, { "rule": "documented", "subject": "ateapi.Container.env", @@ -274,26 +259,11 @@ "subject": "ateapi.HTTPGetAction.port", "message": "field has no doc comment" }, - { - "rule": "documented", - "subject": "ateapi.ImageVolumeSource.reference", - "message": "field has no doc comment" - }, { "rule": "documented", "subject": "ateapi.KubeNamespacedObjectRef", "message": "message has no doc comment" }, - { - "rule": "documented", - "subject": "ateapi.KubeNamespacedObjectRef.name", - "message": "field has no doc comment" - }, - { - "rule": "documented", - "subject": "ateapi.KubeNamespacedObjectRef.namespace", - "message": "field has no doc comment" - }, { "rule": "documented", "subject": "ateapi.Limits", @@ -429,11 +399,6 @@ "subject": "ateapi.SandboxAssets.sandbox_class", "message": "field has no doc comment" }, - { - "rule": "documented", - "subject": "ateapi.SecurityContext.capabilities", - "message": "field has no doc comment" - }, { "rule": "documented", "subject": "ateapi.SnapshotContentScope", @@ -479,41 +444,11 @@ "subject": "ateapi.Volume.external_volume_template", "message": "field has no doc comment" }, - { - "rule": "documented", - "subject": "ateapi.Volume.image", - "message": "field has no doc comment" - }, - { - "rule": "documented", - "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", - "message": "field has no doc comment" - }, - { - "rule": "documented", - "subject": "ateapi.Worker.worker_pod", - "message": "field has no doc comment" - }, - { - "rule": "documented", - "subject": "ateapi.Worker.worker_pod_uid", - "message": "field has no doc comment" - }, - { - "rule": "documented", - "subject": "ateapi.Worker.worker_pool", - "message": "field has no doc comment" - }, { "rule": "documented", "subject": "ateapi.WorkerCapacity.cpu_milli", @@ -533,10 +468,5 @@ "rule": "documented", "subject": "ateapi.WorkerStatus", "message": "message has no doc comment" - }, - { - "rule": "documented", - "subject": "ateapi.WorkerStatus.state", - "message": "field has no doc comment" } ] From fd73420851c0b41a5bb7f95aea65f31371c427aa Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Fri, 28 Aug 2026 10:40:58 -0700 Subject: [PATCH 06/11] Fix exemptions --- tools/apitool/exemptions.json | 37 ++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/tools/apitool/exemptions.json b/tools/apitool/exemptions.json index 1a2b8c8e61..0341a503df 100644 --- a/tools/apitool/exemptions.json +++ b/tools/apitool/exemptions.json @@ -359,6 +359,16 @@ "subject": "ateapi.KubeNamespacedObjectRef", "message": "message has no doc comment" }, + { + "rule": "documented", + "subject": "ateapi.KubeNamespacedObjectRef.name", + "message": "field has no doc comment" + }, + { + "rule": "documented", + "subject": "ateapi.KubeNamespacedObjectRef.namespace", + "message": "field has no doc comment" + }, { "rule": "documented", "subject": "ateapi.Limits", @@ -584,6 +594,26 @@ "subject": "ateapi.Worker.labels", "message": "field has no doc comment" }, + { + "rule": "documented", + "subject": "ateapi.Worker.node_name", + "message": "field has no doc comment" + }, + { + "rule": "documented", + "subject": "ateapi.Worker.worker_pod", + "message": "field has no doc comment" + }, + { + "rule": "documented", + "subject": "ateapi.Worker.worker_pod_uid", + "message": "field has no doc comment" + }, + { + "rule": "documented", + "subject": "ateapi.Worker.worker_pool", + "message": "field has no doc comment" + }, { "rule": "documented", "subject": "ateapi.WorkerCapacity.cpu_milli", @@ -603,5 +633,10 @@ "rule": "documented", "subject": "ateapi.WorkerStatus", "message": "message has no doc comment" + }, + { + "rule": "documented", + "subject": "ateapi.WorkerStatus.state", + "message": "field has no doc comment" } -] \ No newline at end of file +] From 3a7bf5058b777ba525ef63e18c50f732ce0a543d Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Mon, 31 Aug 2026 05:45:42 -0700 Subject: [PATCH 07/11] Add dv for sandbox_class, labels and workerCapacity --- cmd/ateapi/internal/controlapi/worker_test.go | 24 ++++ .../controlapi/zz_generated.validation.go | 136 +++++++++++++++++- pkg/proto/ateapipb/ateapi.pb.go | 29 +++- pkg/proto/ateapipb/ateapi.proto | 25 +++- 4 files changed, 203 insertions(+), 11 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/worker_test.go b/cmd/ateapi/internal/controlapi/worker_test.go index 128f0009fc..00a84eddb5 100644 --- a/cmd/ateapi/internal/controlapi/worker_test.go +++ b/cmd/ateapi/internal/controlapi/worker_test.go @@ -788,6 +788,30 @@ func TestValidateCreateWorkerRequest(t *testing.T) { name: "metadata.atespace set on a global-scoped Worker", mutate: func(w *ateapipb.Worker) { w.Metadata.Atespace = "team-a" }, want: field.ErrorList{field.Forbidden(field.NewPath("worker", "metadata", "atespace"), "")}, + }, { + name: "valid labels", + mutate: func(w *ateapipb.Worker) { + w.Labels = map[string]string{"tier": "batch", "pool.ate.io/zone": "us-west1-c"} + }, + }, { + name: "invalid label key", + mutate: 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", + mutate: 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: "valid capacity", + mutate: func(w *ateapipb.Worker) { w.Capacity = &ateapipb.WorkerCapacity{CpuMilli: 2000, MemoryBytes: 4 << 30} }, + }, { + name: "negative capacity.cpu_milli", + mutate: 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: "negative capacity.memory_bytes", + mutate: 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")}, }} for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { diff --git a/cmd/ateapi/internal/controlapi/zz_generated.validation.go b/cmd/ateapi/internal/controlapi/zz_generated.validation.go index 26b813b474..fb452cbce8 100644 --- a/cmd/ateapi/internal/controlapi/zz_generated.validation.go +++ b/cmd/ateapi/internal/controlapi/zz_generated.validation.go @@ -3412,8 +3412,67 @@ func Validate_Worker( errs = append(errs, fn(fldPath.Child("ip"), &obj.Ip, oldVal, oldObj != nil)...) } - // field ateapipb.Worker.SandboxClass has no validation - // field ateapipb.Worker.Labels has no validation + { // field ateapipb.Worker.SandboxClass + 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 + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.Worker) *string { + return &oldObj.SandboxClass + }) + errs = append(errs, fn(fldPath.Child("sandbox_class"), &obj.SandboxClass, oldVal, oldObj != nil)...) + } + + { // field ateapipb.Worker.Labels + fn := func( + fldPath *field.Path, + obj, oldObj map[string]string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalMap(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.EachMapKey(ctx, op, fldPath, obj, oldObj, validate.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.Worker) map[string]string { + return oldObj.Labels + }) + errs = append(errs, fn(fldPath.Child("labels"), obj.Labels, oldVal, oldObj != nil)...) + } { // field ateapipb.Worker.Capacity fn := func( @@ -3438,6 +3497,8 @@ func Validate_Worker( 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, @@ -3700,6 +3761,77 @@ 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( diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index 98d0bfd602..0643dd1ab0 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -5690,9 +5690,18 @@ type Worker struct { // +k8s:customValidation # until `format=k8s-ip` is supported // +k8s:immutable 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"` + // sandbox_class mirrors the WorkerPool's sandboxClass; its values are the + // CRD's own vocabulary, so it is not validated beyond that. Mutable. + // + // +k8s:optional + 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. + // + // +k8s:optional + // +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. An update // replaces the worker rather than patching it, so a request that omits @@ -5883,9 +5892,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 } diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 08767e82cd..67ce11a00f 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -1365,8 +1365,18 @@ message Worker { // +k8s:immutable string ip = 7; - // Mutable. + // sandbox_class mirrors the WorkerPool's sandboxClass; its values are the + // CRD's own vocabulary, so it is not validated beyond that. Mutable. + // + // +k8s:optional string sandbox_class = 8; + + // labels mirror the WorkerPool object's Kubernetes labels, which selectors + // match against. + // + // +k8s:optional + // +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 @@ -1414,8 +1424,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 From b2636743b2f5ed203dc714b9b1e6ea748cdf3709 Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Mon, 31 Aug 2026 05:58:30 -0700 Subject: [PATCH 08/11] Add missing tests --- .../internal/controlapi/validation_test.go | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) 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)) + }) + } +} From fe8f2c6c77ac5d89529937e8317519a38b8c5c9a Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Mon, 31 Aug 2026 06:24:48 -0700 Subject: [PATCH 09/11] Restyle worker tests --- cmd/ateapi/internal/controlapi/worker_test.go | 366 ++++++++++-------- .../controlapi/workflow_worker_delete_test.go | 16 +- 2 files changed, 204 insertions(+), 178 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/worker_test.go b/cmd/ateapi/internal/controlapi/worker_test.go index 00a84eddb5..4fcdbd7fbf 100644 --- a/cmd/ateapi/internal/controlapi/worker_test.go +++ b/cmd/ateapi/internal/controlapi/worker_test.go @@ -36,10 +36,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 +50,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 { @@ -157,7 +180,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 { @@ -171,7 +194,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 @@ -199,7 +222,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) } @@ -230,7 +253,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"), @@ -249,9 +272,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) } @@ -279,7 +302,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 } @@ -294,7 +317,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) { @@ -324,7 +347,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) @@ -351,7 +374,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{ @@ -373,7 +396,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{ @@ -425,7 +448,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. @@ -469,7 +492,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) @@ -492,7 +515,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 { @@ -522,7 +545,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{ @@ -565,7 +588,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 { @@ -594,7 +617,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)}) @@ -637,190 +660,193 @@ 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 - want field.ErrorList + name string + req *ateapipb.CreateWorkerRequest + want field.ErrorList }{{ name: "valid unassigned worker", + req: validReq(validWorker(apiWorkerName)), }, { - name: "status, when carried, is validated", - mutate: func(w *ateapipb.Worker) { - w.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState(99)} - }, - want: field.ErrorList{field.Invalid(field.NewPath("worker", "status", "state"), nil, "").WithOrigin("maximum")}, + name: "valid with status", + req: validReq(validWorker(apiWorkerName, withStatus())), }, { - name: "status, when carried, needs a state", - mutate: func(w *ateapipb.Worker) { - w.Status = &ateapipb.WorkerStatus{} - }, - want: field.ErrorList{field.Required(field.NewPath("worker", "status", "state"), "")}, + name: "missing worker", + req: &ateapipb.CreateWorkerRequest{Worker: nil}, + want: field.ErrorList{field.Required(field.NewPath("worker"), "")}, }, { - name: "valid assignment, when carried, passes", - mutate: func(w *ateapipb.Worker) { - w.Status = &ateapipb.WorkerStatus{ - State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, - Assignment: newAPIAssignment(apiOtherWorkerName), - } - }, + name: "missing metadata", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.Metadata = nil })), + want: field.ErrorList{field.Required(field.NewPath("worker", "metadata"), "")}, }, { - name: "assignment actor_uid must be a uuid", - mutate: func(w *ateapipb.Worker) { - w.Status = &ateapipb.WorkerStatus{ - State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, - Assignment: newAPIAssignment("not-a-uuid"), - } - }, - want: field.ErrorList{field.Invalid(field.NewPath("worker", "status", "assignment", "actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + 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: "assignment actor ref needs an atespace", - mutate: func(w *ateapipb.Worker) { - assignment := newAPIAssignment(apiOtherWorkerName) - assignment.Actor.Atespace = "" - w.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, Assignment: assignment} - }, - want: field.ErrorList{field.Required(field.NewPath("worker", "status", "assignment", "actor", "atespace"), "")}, + 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: "assignment with a template resource ref passes", - mutate: func(w *ateapipb.Worker) { - assignment := newAPIAssignment(apiOtherWorkerName) - assignment.ActorTemplate = nil - assignment.ActorTemplateRef = &ateapipb.ObjectRef{Atespace: "team-a", Name: "tmpl"} - w.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, Assignment: assignment} - }, + 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: "assignment template ref needs an atespace", - mutate: func(w *ateapipb.Worker) { - assignment := newAPIAssignment(apiOtherWorkerName) - assignment.ActorTemplate = nil - assignment.ActorTemplateRef = &ateapipb.ObjectRef{Name: "tmpl"} - w.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, Assignment: assignment} - }, - want: field.ErrorList{field.Required(field.NewPath("worker", "status", "assignment", "actor_template_ref", "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: "assignment must name its template exactly once: both set", - mutate: func(w *ateapipb.Worker) { - assignment := newAPIAssignment(apiOtherWorkerName) - assignment.ActorTemplateRef = &ateapipb.ObjectRef{Atespace: "team-a", Name: "tmpl"} - w.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, Assignment: assignment} - }, - want: field.ErrorList{field.Invalid(field.NewPath("worker", "status", "assignment"), nil, "").WithOrigin("union")}, + 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: "assignment must name its template exactly once: neither set", - mutate: func(w *ateapipb.Worker) { - assignment := newAPIAssignment(apiOtherWorkerName) - assignment.ActorTemplate = nil - w.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, Assignment: assignment} - }, - want: field.ErrorList{field.Invalid(field.NewPath("worker", "status", "assignment"), nil, "").WithOrigin("union")}, + 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: "assignment template name must be a long name", - mutate: func(w *ateapipb.Worker) { - assignment := newAPIAssignment(apiOtherWorkerName) - assignment.ActorTemplate.Name = "TMPL_1" - w.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, Assignment: assignment} - }, - want: field.ErrorList{field.Invalid(field.NewPath("worker", "status", "assignment", "actor_template", "name"), nil, "").WithOrigin("format=k8s-long-name")}, + 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: "absent capacity is allowed", - mutate: func(w *ateapipb.Worker) { w.Capacity = nil }, + 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: "missing worker_namespace", - mutate: func(w *ateapipb.Worker) { w.WorkerNamespace = "" }, - want: field.ErrorList{field.Required(field.NewPath("worker", "worker_namespace"), "")}, + 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: "invalid worker_namespace", - mutate: 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_pod_uid", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.WorkerPodUid = "" })), + want: field.ErrorList{field.Required(field.NewPath("worker", "worker_pod_uid"), "")}, }, { - name: "missing worker_pool", - mutate: func(w *ateapipb.Worker) { w.WorkerPool = "" }, - want: field.ErrorList{field.Required(field.NewPath("worker", "worker_pool"), "")}, + 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: "invalid worker_pool", - mutate: 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 node_name", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.NodeName = "" })), + want: field.ErrorList{field.Required(field.NewPath("worker", "node_name"), "")}, }, { - name: "missing worker_pod", - mutate: func(w *ateapipb.Worker) { w.WorkerPod = "" }, - want: field.ErrorList{field.Required(field.NewPath("worker", "worker_pod"), "")}, + 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: "invalid worker_pod", - mutate: 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 ip", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.Ip = "" })), + want: field.ErrorList{field.Required(field.NewPath("worker", "ip"), "")}, }, { - name: "missing ip", - mutate: 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: "invalid ip", - mutate: func(w *ateapipb.Worker) { w.Ip = "not-an-ip" }, - want: field.ErrorList{field.Invalid(field.NewPath("worker", "ip"), nil, "").WithOrigin("format=ip-strict")}, + 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: "missing worker_pod_uid", - mutate: func(w *ateapipb.Worker) { w.WorkerPodUid = "" }, - want: field.ErrorList{field.Required(field.NewPath("worker", "worker_pod_uid"), "")}, + 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 worker_pod_uid", - mutate: 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: "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: "missing node_name", - mutate: func(w *ateapipb.Worker) { w.NodeName = "" }, - want: field.ErrorList{field.Required(field.NewPath("worker", "node_name"), "")}, + name: "absent capacity is allowed", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.Capacity = nil })), }, { - name: "invalid node_name", - mutate: 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: "valid capacity", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.Capacity = &ateapipb.WorkerCapacity{CpuMilli: 2000, MemoryBytes: 4 << 30} })), }, { - name: "missing metadata", - mutate: func(w *ateapipb.Worker) { w.Metadata = nil }, - want: field.ErrorList{field.Required(field.NewPath("worker", "metadata"), "")}, + 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 metadata.name", - mutate: func(w *ateapipb.Worker) { w.Metadata = &ateapipb.ResourceMetadata{} }, - want: field.ErrorList{field.Required(field.NewPath("worker", "metadata", "name"), "")}, + 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: "invalid metadata.name", - mutate: func(w *ateapipb.Worker) { w.Metadata.Name = "Not A Name" }, - want: field.ErrorList{field.Invalid(field.NewPath("worker", "metadata", "name"), nil, "").WithOrigin("format=k8s-short-name")}, + 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: "metadata.atespace set on a global-scoped Worker", - mutate: func(w *ateapipb.Worker) { w.Metadata.Atespace = "team-a" }, - want: field.ErrorList{field.Forbidden(field.NewPath("worker", "metadata", "atespace"), "")}, + 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: "valid labels", - mutate: func(w *ateapipb.Worker) { - w.Labels = map[string]string{"tier": "batch", "pool.ate.io/zone": "us-west1-c"} - }, + 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: "valid assignment, when carried, passes", + req: validReq(validWorker(apiWorkerName, withStatus(func(s *ateapipb.WorkerStatus) { + s.Assignment = newAPIAssignment(apiOtherWorkerName) + }))), }, { - name: "invalid label key", - mutate: 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: "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: "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 label value", - mutate: 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: "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: "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: "valid capacity", - mutate: func(w *ateapipb.Worker) { w.Capacity = &ateapipb.WorkerCapacity{CpuMilli: 2000, MemoryBytes: 4 << 30} }, + 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: "negative capacity.cpu_milli", - mutate: 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: "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: "negative capacity.memory_bytes", - mutate: 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: "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) - } - req := &ateapipb.CreateWorkerRequest{Worker: worker} - assertValidateErr(t, validateCreateWorkerRequest(context.Background(), req), tc.want) + assertValidateErr(t, validateCreateWorkerRequest(context.Background(), tc.req), tc.want) }) } } @@ -837,7 +863,7 @@ func TestServiceImplUpdateWorker_ImmutableFields(t *testing.T) { // Every case below is rejected, so nothing writes and this stays the // current incarnation for all of them. - created := seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + created := seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) for _, tc := range []struct { name string @@ -940,7 +966,7 @@ func TestValidateUpdateWorkerRequest(t *testing.T) { // 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 := newAPIWorker(apiWorkerName) + worker := validWorker(apiWorkerName) worker.Metadata.Uid = apiOtherWorkerName worker.Metadata.Version = 3 for _, m := range mods { @@ -999,7 +1025,7 @@ func TestValidateUpdateWorkerRequest(t *testing.T) { // 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 := newAPIWorker(apiWorkerName) + oldVal := validWorker(apiWorkerName) oldVal.Status = &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE} newVal := proto.Clone(oldVal).(*ateapipb.Worker) newVal.Status = nil @@ -1019,7 +1045,7 @@ func TestCreateWorker_IgnoresRequestMetadataServerFields(t *testing.T) { ctx := context.Background() svc, _ := newWorkerAPIService(t) - in := newAPIWorker(apiWorkerName) + in := validWorker(apiWorkerName) in.Metadata.Uid = "not-a-uuid" in.Metadata.Version = -5 @@ -1043,7 +1069,7 @@ func TestServiceImplUpdateWorker_ValidatesAssignment(t *testing.T) { persistence, cleanup := storetest.SetupTestStore(t) t.Cleanup(cleanup) impl := newServiceImpl(persistence, nil, nil) - created := seedAPIWorker(t, ctx, persistence, newAPIWorker(apiWorkerName)) + 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 { diff --git a/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go b/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go index 172cb662c3..4fc433f19a 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() @@ -80,7 +80,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. @@ -143,7 +143,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()) @@ -176,7 +176,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") @@ -198,7 +198,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) }) @@ -226,7 +226,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{}) @@ -272,7 +272,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()) @@ -305,7 +305,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()) From d3ced8314faa8f6e5404ff6b2b4950c7a483fb18 Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Mon, 31 Aug 2026 06:29:01 -0700 Subject: [PATCH 10/11] Update apitool exemptions --- tools/apitool/exemptions.json | 15 --------------- 1 file changed, 15 deletions(-) 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", From 96df21a720a28edff0cb8d0d0317cf30a20d5b2f Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Mon, 31 Aug 2026 11:23:50 -0700 Subject: [PATCH 11/11] Add bound for labels and sandboxclass --- cmd/ateapi/internal/controlapi/worker_test.go | 15 +++++++++++++++ .../controlapi/zz_generated.validation.go | 7 +++++++ pkg/proto/ateapipb/ateapi.pb.go | 6 +++++- pkg/proto/ateapipb/ateapi.proto | 6 +++++- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/worker_test.go b/cmd/ateapi/internal/controlapi/worker_test.go index 4fcdbd7fbf..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" @@ -750,11 +751,25 @@ func TestValidateCreateWorkerRequest(t *testing.T) { 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")}, }, { 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"} })), diff --git a/cmd/ateapi/internal/controlapi/zz_generated.validation.go b/cmd/ateapi/internal/controlapi/zz_generated.validation.go index fb452cbce8..7891d35199 100644 --- a/cmd/ateapi/internal/controlapi/zz_generated.validation.go +++ b/cmd/ateapi/internal/controlapi/zz_generated.validation.go @@ -3431,6 +3431,9 @@ func Validate_Worker( if earlyReturn { return // do not proceed } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 63); len(e) != 0 { + errs = append(errs, e...) + } return } oldVal := safe.Field(oldObj, @@ -3453,6 +3456,10 @@ func Validate_Worker( } // call field-attached validations earlyReturn := false + 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 } diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index 0643dd1ab0..4f1af26256 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -5691,14 +5691,18 @@ type Worker struct { // +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 not validated beyond that. Mutable. + // 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"` diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 67ce11a00f..9ff11cde9f 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -1366,15 +1366,19 @@ message Worker { string ip = 7; // sandbox_class mirrors the WorkerPool's sandboxClass; its values are the - // CRD's own vocabulary, so it is not validated beyond that. Mutable. + // 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;