Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 1 addition & 38 deletions cmd/ateapi/internal/controlapi/create_actor.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ 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"
Expand All @@ -30,7 +29,7 @@ import (
)

func (s *Service) CreateActor(ctx context.Context, req *ateapipb.CreateActorRequest) (*ateapipb.CreateActorResponse, error) {
if err := validateCreateActorRequest(req); err != nil {
if err := req.Validate(ctx); err != nil {
return nil, err
}
_, err := s.actorTemplateLister.ActorTemplates(req.GetActorTemplateNamespace()).Get(req.GetActorTemplateName())
Expand Down Expand Up @@ -78,42 +77,6 @@ func (s *Service) CreateActor(ctx context.Context, req *ateapipb.CreateActorRequ
}, nil
}

func validateCreateActorRequest(req *ateapipb.CreateActorRequest) error {
var fldPath *field.Path
var errs field.ErrorList

if val, fldPath := req.ActorTemplateNamespace, fldPath.Child("actor_template_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 := req.ActorTemplateName, fldPath.Child("actor_template_name"); 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 := req.ActorRef, fldPath.Child("actor_ref"); val == nil {
errs = append(errs, field.Required(fldPath, ""))
} else {
errs = append(errs, resources.ValidateActorRef(val, fldPath)...)
}

if val := req.WorkerSelector; val != nil {
errs = append(errs, validateSelector(val, fldPath.Child("worker_selector"))...)
}

if len(errs) > 0 {
return status.Error(codes.InvalidArgument, errs.ToAggregate().Error())
}
return nil
}

func validateSelector(sel *ateapipb.Selector, fldPath *field.Path) field.ErrorList {
var errs field.ErrorList

Expand Down
3 changes: 2 additions & 1 deletion cmd/ateapi/internal/controlapi/functional_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1891,6 +1891,7 @@ func TestValidation(t *testing.T) {
tc := setupTest(t, ns)
defer tc.cleanup()

// FIXME: this should move next to the proto files and the validate method

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually like having a test here for validation as it not only asserts that the validator works, but also that the gRPC server (as we whole) is propagating the Go error to the right gRPC status response.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ACK the value of complete testing, but also point out that doing rigorous-but-not-brittle testing requires things like something like "origin" which really shouldn't be part of the user-facing API but is fine for internal API.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we move all the exhaustive test cases next to the proto / validate methods, and make it assert on the full (type, field, detail, origin) tuple, but then scope down ValidationError (or the AIP equivalent FieldViolation1) to what we actually want to put on the write (I assume origin, for example, is not meant to be part of the API contract). Then we have a thin layer of tests here just to make sure we properly map validation errors to gRPC status.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems plausible. I added ValidationError because FieldValidation could not carry origin.

t.Run("CreateActor", func(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -1964,7 +1965,7 @@ func TestValidation(t *testing.T) {
ActorTemplateName: "tmpl1",
WorkerSelector: &ateapipb.Selector{MatchLabels: map[string]string{"bad key!": "x"}},
},
`worker_selector.match_labels\[bad key!\]: Invalid value`,
`worker_selector.match_labels: Invalid value: \"bad key!\"`,
}, {
"invalid worker_selector label value",
&ateapipb.CreateActorRequest{
Expand Down
19 changes: 18 additions & 1 deletion internal/ateinterceptors/ateinterceptors.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/protoadapt"
"google.golang.org/protobuf/reflect/protoreflect"
)

Expand Down Expand Up @@ -61,7 +62,23 @@ func ServerUnaryInterceptor(ctx context.Context, req any, info *grpc.UnaryServer

if errors.As(err, &statusErr) {
st := statusErr.GRPCStatus()
return nil, status.Error(st.Code(), st.Message())
//FIXME: investigate why we are doing this error deconstruction

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not 100% sure why this is here. It looks like it has the effect of sanitizing internal details from the response by unwrapping the error? But then it could just return the unwrapped error directly and avoid copying details.

Also, if that's the goal, then returning the raw error at the bottom still can leak internals.

Git blame shows Taahir Ahmed (@ahmedtd) as committer, adding him in case there is a Chesterton's Fence issue.

result := status.New(st.Code(), st.Message())

origDetails := st.Details()
v1Details := make([]protoadapt.MessageV1, 0, len(origDetails))
for _, d := range origDetails {
if msg, ok := d.(protoadapt.MessageV2); ok {
v1Details = append(v1Details, protoadapt.MessageV1Of(msg))
} else {
return nil, status.Errorf(codes.Internal, "internal error saving error details: not a protoadapt.MessageV2")
}
}
result, err := result.WithDetails(v1Details...)
if err != nil {
return nil, status.Errorf(codes.Internal, "internal error saving error details: %v", err)
}
return nil, result.Err()
}

// No status error found in chain.
Expand Down
Loading
Loading