extproc: register filter and parse base and override config#9073
extproc: register filter and parse base and override config#9073eshitachandwani wants to merge 10 commits intogrpc:masterfrom
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #9073 +/- ##
==========================================
+ Coverage 80.52% 83.03% +2.50%
==========================================
Files 413 416 +3
Lines 33543 33624 +81
==========================================
+ Hits 27012 27921 +909
+ Misses 4316 4268 -48
+ Partials 2215 1435 -780
🚀 New features to boost your workflow:
|
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request implements the Envoy external processing (ext_proc) HTTP filter for xDS, including configuration parsing for base and override settings. The reviewer noted that the implementation incorrectly uses a non-existent GRPC body processing mode instead of STREAMED and lacks support for TypedStruct types in the configuration parser. As a result, the validation logic and unit tests require updates to correctly handle supported processing modes and additional configuration formats.
|
LGTM, barring the udpa.type.v1.TypedStruct and xds.type.v3.TypedStruct support comment from gemini. Can go ahead if we get an answer on those. |
It has been discussed that we only have to support |
| return nil, fmt.Errorf("ext_proc: %v", err) | ||
| } | ||
| } | ||
| return baseConfig{config: msg}, nil |
There was a problem hiding this comment.
Why are we storing it in proto form in the internal representation of the config? We should be storing it as a native Go struct that we own.
There was a problem hiding this comment.
Basically if we parse and store it in say interceptorConfig we will have difficulty understanding if the override had a nil value and we do not have to override, or we have to override with say false. For example for the FailureModeAllow here . We could change this to constructing a go interceptorConfig struct for base config and using the proto for override to differentiate between no override and override with false and then use the override proto to override the necessary fields. But I think keeping the parsing logic same for both makes more sense to me. WDYT?
There was a problem hiding this comment.
I would like to simplify this by saying that if an override config exists, it will have to explicitly set this value to true to allow RPCs to continue upon failure. If this field is not set (or false), we will fail RPCs. If we go with that approach, we don't have to store the proto form. Please do check with other language implementations. I don't exactly remember if their proto API allows for presence checks, and if they plan on using them.
changes to be introduced as per grpc service design warrant changes in this code
| "google.golang.org/grpc/internal/xds/matcher" | ||
| "google.golang.org/grpc/metadata" | ||
|
|
||
| mutationpb "github.com/envoyproxy/go-control-plane/envoy/config/common/mutation_rules/v3" |
There was a problem hiding this comment.
Nit: a v3 on this import rename as well to be consistent?
| return nil, fmt.Errorf("ext_proc: %v", err) | ||
| } | ||
| } | ||
| return baseConfig{config: msg}, nil |
There was a problem hiding this comment.
I would like to simplify this by saying that if an override config exists, it will have to explicitly set this value to true to allow RPCs to continue upon failure. If this field is not set (or false), we will fail RPCs. If we go with that approach, we don't have to store the proto form. Please do check with other language implementations. I don't exactly remember if their proto API allows for presence checks, and if they plan on using them.
| type serverConfig struct { | ||
| // targetURI is the name of the external server. | ||
| targetURI string | ||
| // channelCredentials specifies the transport credentials to use to connect to | ||
| // the external server. Must not be nil. | ||
| channelCredentials credentials.TransportCredentials | ||
| // callCredentials specifies the per-RPC credentials to use when making calls | ||
| // to the external server. | ||
| callCredentials []credentials.PerRPCCredentials | ||
| // timeout is the RPC timeout for the call to the external server. If unset, | ||
| // the timeout depends on the usage of this external server. For example, | ||
| // cases like ext_authz and ext_proc, where there is a 1:1 mapping between the | ||
| // data plane RPC and the external server call, the timeout will be capped by | ||
| // the timeout on the data plane RPC. For cases like RLQS where there is a | ||
| // side channel to the external server, an unset timeout will result in no | ||
| // timeout being applied to the external server call. | ||
| timeout time.Duration | ||
| // initialMetadata is the additional metadata to include in all RPCs sent to | ||
| // the external server. | ||
| initialMetadata metadata.MD | ||
| } |
There was a problem hiding this comment.
Based on some of our offline discussions, this stuct must be comparable. So, this cannot store the credentials as interfaces, but instead should store them as JSON strings.
| // failureModeAllow specifies the behavior when the RPC to the external | ||
| // processing server fails. If true, the dataplane RPC will be allowed to | ||
| // continue. If false, the data plane RPC will be failed with a grpc status | ||
| // code of UNAVAILABLE. | ||
| failureModeAllow *bool | ||
| // processingModes specifies the processing mode for each dataplane event. | ||
| processingModes *processingModes |
There was a problem hiding this comment.
Why do these fields have to pointers? I understand they can come either from the base config or the override config. But I don't see the reason for them being pointers.
There was a problem hiding this comment.
The reason for them being pointers is that even in override proto , the override needs to happen per field. The fileds of override proto can either be present and need to be overwritten or not be present. And since we construct the interceptorConfig while parsing and merge the interceptorConfigs together, we need a way to have the differentiation between no override vs false override in the override interceptorConfig.
THe other way is what I had earlier, i.e. do the required validation and store it as a proto when parsing and then create the interceptorCOnfig only once when building the interceptor using both the protos.
THe pro of current way is that we discussed that parsing functions will have bootstrap and server info and that is where we will need to call the grpcServiceProtoToInternalRep utility from. WDYT>
| // | ||
| // mutationRules specifies the rules for what modifications an external | ||
| // processing server may make to headers/trailers sent to it. | ||
| mutationRules headerMutationRules |
There was a problem hiding this comment.
Since this will be used by ext_authz as well, can you please find a common location for this?
There was a problem hiding this comment.
Moved this and serverConfig and related functions to a extconfig.go file in httpfilter package so that it can be shared across both ext_proc and ext_authz filters or any other filters in future.
| if pm == nil { | ||
| return nil | ||
| } |
There was a problem hiding this comment.
What is the expected behavior in this case? For the base config, this is a required field.
And in the override config section of the gRFC, it says "Same as in top-level filter config". Does that mean that if the override is specified, this field must be set?
There was a problem hiding this comment.
I dont think that should be the case , but let me confirm.
| func validateServerConfig(cfg *serverConfig) error { | ||
| // TODO(https://github.com/grpc/grpc-go/issues/8747): Once we have a common | ||
| // way to validate a target URI, switch to that. | ||
| if cfg.targetURI == "" { | ||
| return fmt.Errorf("extproc: targetURI must be a non-empty string") | ||
| } | ||
| if cfg.channelCredentials == nil { | ||
| return fmt.Errorf("extproc: channelCredentials must be non-nil") | ||
| } | ||
| if cfg.timeout < 0 { | ||
| return fmt.Errorf("extproc: timeout must be non-negative") | ||
| } | ||
| for k, vals := range cfg.initialMetadata { | ||
| if len(k) == 0 || len(k) >= 16384 { | ||
| return fmt.Errorf("extproc: initialMetadata key %q has invalid length %d; must be in range [1, 16384)", k, len(k)) | ||
| } | ||
| for _, v := range vals { | ||
| if len(v) >= 16384 { | ||
| return fmt.Errorf("extproc: initialMetadata value for key %q has invalid length %d; must be less than 16384", k, len(v)) | ||
| } | ||
| } | ||
| } | ||
| return nil | ||
| } |
There was a problem hiding this comment.
The common function that takes care of converting the GrpcService proto to its internal representation should take care of these validation checks.
There was a problem hiding this comment.
Okay! removing it. I was not sure if that utility would include validation or not and so added it to be safe.
| iCfg := interceptorConfig{ | ||
| processingModes: processingModesFromProto(msg.GetProcessingMode()), | ||
| requestAttributes: msg.GetRequestAttributes(), | ||
| responseAttributes: msg.GetResponseAttributes(), | ||
| disableImmediateResponse: msg.GetDisableImmediateResponse(), | ||
| observabilityMode: msg.GetObservabilityMode(), | ||
| } |
There was a problem hiding this comment.
Can this literal struct initialization be done later, after all checks that can result in early returns are complete.
| if msg.GetGrpcService() == nil { | ||
| return nil, fmt.Errorf("extproc: empty grpc_service provided in config %v", cfg) | ||
| } | ||
| if msg.GetGrpcService().GetGoogleGrpc() == nil { |
There was a problem hiding this comment.
This should also be taken care of by the common function, right?
There was a problem hiding this comment.
Changed. I was not sure if these validations would be part of the utiltiy or not.
| if allowed := msg.GetForwardRules().GetAllowedHeaders(); allowed != nil { | ||
| allowedHeaders, err := convertStringMatchers(allowed.GetPatterns()) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| iCfg.allowedHeaders = allowedHeaders | ||
| } | ||
|
|
||
| if disallowed := msg.GetForwardRules().GetDisallowedHeaders(); disallowed != nil { | ||
| disallowedHeaders, err := convertStringMatchers(disallowed.GetPatterns()) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| iCfg.disallowedHeaders = disallowedHeaders | ||
| } |
There was a problem hiding this comment.
Would it make sense to have a type to represent the "forwarding rules", such that it has a method that accepts a list of headers, and returns a list of headers based on the rules. That way, the filter will just have a single line of code, and all the logic will be inside this type.
There was a problem hiding this comment.
I am not sure if I understand your point completely but I planned to have a helper function constructHeaderMap that will take the allowedHeaders, disallowedHeaders and a list of headers and do the computation. Same as we have for ext_authz here
This PR is part of implementing A93: xds-ext-proc
This PR adds the ext_proc filter and the builder that implements the builder interface. That includes parsing and validating the base and the override config. The registration of the filter is under the
GRPC_EXPERIMENTAL_XDS_EXT_PROC_ON_CLIENTflag.#ext-proc-a93
RELEASE NOTES: None