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
147 lines
5.1 KiB
Go
147 lines
5.1 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"
|
|
"reflect"
|
|
|
|
"github.com/agent-substrate/substrate/pkg/proto/ateapipb"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/reflect/protopath"
|
|
"google.golang.org/protobuf/reflect/protorange"
|
|
"google.golang.org/protobuf/reflect/protoreflect"
|
|
"k8s.io/apimachinery/pkg/api/operation"
|
|
"k8s.io/apimachinery/pkg/api/validate"
|
|
"k8s.io/apimachinery/pkg/util/validation"
|
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
|
)
|
|
|
|
func toGRPCStatusError(errs field.ErrorList) error {
|
|
return status.Error(codes.InvalidArgument, errs.ToAggregate().Error())
|
|
}
|
|
|
|
// validateNoUnknownFields reports an error for every unknown field in m, at
|
|
// every level of nesting.
|
|
//
|
|
// A client newer than this binary can set fields this binary has no descriptor
|
|
// for. protobuf keeps those bytes on the parsed message, so an Update — which
|
|
// replaces the whole object — would persist them. The server cannot validate
|
|
// such a field.
|
|
func validateNoUnknownFields(m proto.Message, fldPath *field.Path) field.ErrorList {
|
|
r := m.ProtoReflect()
|
|
if !r.IsValid() {
|
|
return nil
|
|
}
|
|
|
|
var errs field.ErrorList
|
|
if err := protorange.Range(r, func(p protopath.Values) error {
|
|
msg, ok := p.Index(-1).Value.Interface().(protoreflect.Message)
|
|
if !ok || len(msg.GetUnknown()) == 0 {
|
|
return nil
|
|
}
|
|
// Report the path of the unknwown field
|
|
errs = append(errs, field.Invalid(toFieldPath(p.Path, fldPath), field.OmitValueType{}, ""))
|
|
return nil
|
|
}); err != nil {
|
|
errs = append(errs, field.InternalError(fldPath, err))
|
|
}
|
|
return errs
|
|
}
|
|
|
|
// toFieldPath renders a protopath as a field.Path rooted at root.
|
|
func toFieldPath(p protopath.Path, root *field.Path) *field.Path {
|
|
out := root
|
|
for _, step := range p {
|
|
switch step.Kind() {
|
|
case protopath.FieldAccessStep:
|
|
out = out.Child(step.FieldDescriptor().TextName())
|
|
case protopath.ListIndexStep:
|
|
out = out.Index(step.ListIndex())
|
|
case protopath.MapIndexStep:
|
|
out = out.Key(step.MapIndex().String())
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func toGRPCInternalError(errs field.ErrorList) error {
|
|
return status.Error(codes.Internal, errs.ToAggregate().Error())
|
|
}
|
|
|
|
// scrubResourceMetadataForCreate removes fields that should not be set by the
|
|
// user when creating a resource.
|
|
func scrubResourceMetadataForCreate(in *ateapipb.ResourceMetadata) {
|
|
if in == nil {
|
|
return // validation will flag it
|
|
}
|
|
in.Uid = "" // will be set later
|
|
in.Version = 0 // will be set later
|
|
in.CreateTime = nil // will be set later
|
|
in.UpdateTime = nil // will be set later
|
|
}
|
|
|
|
// scrubResourceMetadataForUpdate removes fields that should not be set by the
|
|
// user when updating a resource.
|
|
func scrubResourceMetadataForUpdate(in *ateapipb.ResourceMetadata) {
|
|
if in == nil {
|
|
return // validation will flag it
|
|
}
|
|
// in.Uid and in.Version are preconditions, so we don't scrub them.
|
|
in.CreateTime = nil // will be set later
|
|
in.UpdateTime = nil // will be set later
|
|
}
|
|
|
|
// ateDeepEqual compares two values of any type, using proto.Equal if both are
|
|
// proto messages, and reflect.DeepEqual otherwise. This is called by
|
|
// declarative validation's generated code.
|
|
func ateDeepEqual[T any](a, b T) bool {
|
|
asProto := func(x any) proto.Message {
|
|
pm, ok := x.(proto.Message)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return pm
|
|
}
|
|
|
|
if pa, pb := asProto(a), asProto(b); pa != nil && pb != nil {
|
|
return proto.Equal(pa, pb)
|
|
}
|
|
return reflect.DeepEqual(a, b)
|
|
}
|
|
|
|
// This exists only because nested subfield tags are not supported yet.
|
|
func ValidateCustom_UpdateActorRequest_Actor(ctx context.Context, op operation.Operation, fldPath *field.Path, actor, _ *ateapipb.Actor) field.ErrorList {
|
|
if actor == nil || actor.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:required
|
|
errs := Validate_ResourceMetadata(ctx, op, fldPath.Child("metadata"), actor.Metadata, nil)
|
|
errs = append(errs, validate.RequiredValue(ctx, op, fldPath.Child("metadata", "atespace"), &actor.Metadata.Atespace, nil)...)
|
|
return errs
|
|
}
|
|
|
|
// This is needed because DV doesn't have a standard format for IP addresses yet.
|
|
func ValidateCustom_WorkerAssignment_WorkerPodIp(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList {
|
|
return validation.IsValidIP(fldPath, *value)
|
|
}
|