mirror of
https://github.com/agent-substrate/substrate.git
synced 2026-11-10 10:13:50 +08:00
This mostly eliminates the need to hand-write validation code. This PR is a long series of commits which add DV for most of Actor and all of Atespace. Here is a map to the commits: * The first few take a dep on a new Kubernetes tag, import the code into third_party, and apply a single patch. Because we have different Go modules for tools, I had to do it twice. When that patch lands, we can revert these commits, but that won't be until the 1.38 cycle in a few months. * The next commits slowly add DV support, so a human can review each of them in a reasonable amount of time. The emphasis is on great test cases. * I made a bad choice early on as to where to generate the code into, so I moved it. Rebasing on that was exceedingly hard, so I left it as a move. * This required changing update/go-generate -> update/codegen -- we need to get the ordering of tools right, which `go generate` does not guarantee. * Then I added a "middle" layer called "ServiceImpl" between the RPC and storage layers. This allows things like workflow to call the same business logic as the RPC layer, including validation. Lots of test fixes. * Then I finished the Create() and Update() paths for Actor. Those represent the "right" (or closest to) way to implement resources, and tests for validation. I strongly encourage reviewers to read it commit-by-commit. Rebasing this is VERY tedious, so the sooner it lands or dies completely, the better. Then we can start converting the rest. `./hack/run-tool.sh validation-gen --docs` will produce some docs on the tool and the available tags. @laoj2 @juli4n @EItanya @HavenXia @lalitc375 @yongruilin @jpbetz FYI
152 lines
5.5 KiB
Go
152 lines
5.5 KiB
Go
// Copyright 2026 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package controlapi
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/agent-substrate/substrate/cmd/ateapi/internal/store"
|
|
"github.com/agent-substrate/substrate/pkg/proto/ateapipb"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"k8s.io/apimachinery/pkg/api/operation"
|
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
|
)
|
|
|
|
func (s *RPCService) CreateAtespace(ctx context.Context, req *ateapipb.CreateAtespaceRequest) (*ateapipb.Atespace, error) {
|
|
inAtespace := req.GetAtespace()
|
|
if inAtespace != nil { // otherwise validation will flag it
|
|
scrubResourceMetadataForCreate(inAtespace.Metadata)
|
|
// no status field, but if there were, we would scrub it here
|
|
}
|
|
|
|
// Validate the request, including the object within it.
|
|
if errs := validateCreateAtespaceRequest(ctx, req); len(errs) > 0 {
|
|
return nil, toGRPCStatusError(errs)
|
|
}
|
|
|
|
// Handle the creation, including validation of the final stored object.
|
|
return s.impl.CreateAtespace(ctx, inAtespace)
|
|
}
|
|
|
|
func (s *ServiceImpl) CreateAtespace(ctx context.Context, inAtespace *ateapipb.Atespace) (*ateapipb.Atespace, error) {
|
|
// no further processing or status, but if there were, we would do it here
|
|
|
|
// Save the data in the storage layer.
|
|
stored, err := s.store.CreateAtespace(ctx, inAtespace)
|
|
if err != nil {
|
|
if errors.Is(err, store.ErrAlreadyExists) {
|
|
return nil, status.Errorf(codes.AlreadyExists, "Atespace %s already exists", inAtespace.Metadata.Name)
|
|
}
|
|
return nil, fmt.Errorf("while creating actor: %w", err)
|
|
}
|
|
|
|
return stored, nil
|
|
}
|
|
|
|
func validateCreateAtespaceRequest(ctx context.Context, req *ateapipb.CreateAtespaceRequest) field.ErrorList {
|
|
// Call the generated validation.
|
|
op := operation.Operation{Type: operation.Create}
|
|
errs := Validate_CreateAtespaceRequest(ctx, op, nil, req, nil)
|
|
errs = append(errs, validateNoUnknownFields(req.Atespace, field.NewPath("atespace"))...)
|
|
return errs
|
|
}
|
|
|
|
func (s *RPCService) GetAtespace(ctx context.Context, req *ateapipb.GetAtespaceRequest) (*ateapipb.Atespace, error) {
|
|
if errs := validateGetAtespaceRequest(ctx, req); len(errs) > 0 {
|
|
return nil, toGRPCStatusError(errs)
|
|
}
|
|
|
|
return s.impl.GetAtespace(ctx, req.Atespace.Name)
|
|
}
|
|
|
|
func (s *ServiceImpl) GetAtespace(ctx context.Context, name string) (*ateapipb.Atespace, error) {
|
|
atespace, err := s.store.GetAtespace(ctx, name)
|
|
if errors.Is(err, store.ErrNotFound) {
|
|
return nil, status.Errorf(codes.NotFound, "Atespace %s not found", name)
|
|
} else if err != nil {
|
|
return nil, fmt.Errorf("while getting atespace from DB: %w", err)
|
|
}
|
|
|
|
return atespace, nil
|
|
}
|
|
|
|
func validateGetAtespaceRequest(ctx context.Context, req *ateapipb.GetAtespaceRequest) field.ErrorList {
|
|
// Call the generated validation.
|
|
op := operation.Operation{Type: operation.Create}
|
|
return Validate_GetAtespaceRequest(ctx, op, nil, req, nil)
|
|
}
|
|
|
|
func (s *RPCService) ListAtespaces(ctx context.Context, req *ateapipb.ListAtespacesRequest) (*ateapipb.ListAtespacesResponse, error) {
|
|
if errs := validateListAtespacesRequest(ctx, req); len(errs) > 0 {
|
|
return nil, toGRPCStatusError(errs)
|
|
}
|
|
|
|
page, err := s.impl.ListAtespaces(ctx, store.ListOptions{PageSize: req.PageSize, PageToken: req.PageToken})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ateapipb.ListAtespacesResponse{
|
|
Atespaces: page.Items,
|
|
NextPageToken: page.NextPageToken,
|
|
}, nil
|
|
}
|
|
|
|
func (s *ServiceImpl) ListAtespaces(ctx context.Context, opts store.ListOptions) (store.ListResponse[*ateapipb.Atespace], error) {
|
|
opts.PageSize = effectivePageSize(opts.PageSize)
|
|
page, err := s.store.ListAtespaces(ctx, opts)
|
|
if err != nil {
|
|
return page, mapListError(fmt.Errorf("while listing atespaces in db: %w", err))
|
|
}
|
|
return page, nil
|
|
}
|
|
|
|
func validateListAtespacesRequest(ctx context.Context, req *ateapipb.ListAtespacesRequest) field.ErrorList {
|
|
// Call the generated validation.
|
|
op := operation.Operation{Type: operation.Create}
|
|
return Validate_ListAtespacesRequest(ctx, op, nil, req, nil)
|
|
}
|
|
|
|
func (s *RPCService) DeleteAtespace(ctx context.Context, req *ateapipb.DeleteAtespaceRequest) (*ateapipb.Atespace, error) {
|
|
if errs := validateDeleteAtespaceRequest(ctx, req); len(errs) > 0 {
|
|
return nil, toGRPCStatusError(errs)
|
|
}
|
|
|
|
return s.impl.DeleteAtespace(ctx, req.Atespace.Name)
|
|
}
|
|
|
|
func (s *ServiceImpl) DeleteAtespace(ctx context.Context, name string) (*ateapipb.Atespace, error) {
|
|
deleted, err := s.store.DeleteAtespace(ctx, name)
|
|
if err != nil {
|
|
if errors.Is(err, store.ErrNotFound) {
|
|
return nil, status.Errorf(codes.NotFound, "Atespace %s not found", name)
|
|
}
|
|
if errors.Is(err, store.ErrFailedPrecondition) {
|
|
return nil, status.Errorf(codes.FailedPrecondition, "Atespace %s is not empty", name)
|
|
}
|
|
return nil, fmt.Errorf("while deleting atespace from DB: %w", err)
|
|
}
|
|
|
|
return deleted, nil
|
|
}
|
|
|
|
func validateDeleteAtespaceRequest(ctx context.Context, req *ateapipb.DeleteAtespaceRequest) field.ErrorList {
|
|
// Call the generated validation.
|
|
op := operation.Operation{Type: operation.Create}
|
|
return Validate_DeleteAtespaceRequest(ctx, op, nil, req, nil)
|
|
}
|