diff --git a/experimental/optional/optional.go b/experimental/optional/optional.go new file mode 100644 index 000000000000..b958f342c39f --- /dev/null +++ b/experimental/optional/optional.go @@ -0,0 +1,61 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * 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 optional adds generic optional types. +// +// All APIs in this package are experimental. +package optional + +// Option represents an optional value of type T. +type Option[T any] struct { + val T + isSet bool +} + +// New creates a new Option that does not have a value set. This can also be +// done implicitly using a zero-value declaration: `var opt optional.Option[T]“ +func New[T any]() Option[T] { + return Option[T]{} +} + +// NewValue creates a new Option with the provided value. +func NewValue[T any](value T) Option[T] { + return Option[T]{ + val: value, + isSet: true, + } +} + +// Value returns the underlying value and a boolean indicating if the value is +// set. If the value is not set, it returns the zero value of T and false. +func (o Option[T]) Value() (T, bool) { + return o.val, o.isSet +} + +// WithValue returns a new Option containing the provided value. +func (o Option[T]) WithValue(value T) Option[T] { + return Option[T]{ + val: value, + isSet: true, + } +} + +// Clear returns an empty Option. +func (o Option[T]) Clear() Option[T] { + return Option[T]{} +} diff --git a/experimental/optional/optional_test.go b/experimental/optional/optional_test.go new file mode 100644 index 000000000000..dda76d002d68 --- /dev/null +++ b/experimental/optional/optional_test.go @@ -0,0 +1,161 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * 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 optional_test + +import ( + "testing" + + "google.golang.org/grpc/experimental/optional" + "google.golang.org/grpc/internal/grpctest" +) + +type s struct { + grpctest.Tester +} + +func Test(t *testing.T) { + grpctest.RunSubTests(t, s{}) +} + +type testStruct struct { + Name string + Age int +} + +// TestOption_Int tests the scenario of using integer optional values and +// verifies that default value, constructors, and mutation methods work as +// expected for primitive integers. +func (s) TestOption_Int(t *testing.T) { + var opt optional.Option[int] + // Test unset value. + if v, set := opt.Value(); set || v != 0 { + t.Fatalf("Zero-value Option[int] = (%v, %v); want (0, false)", v, set) + } + + // Test that New() function also returns an unset optional value. + optNew := optional.New[int]() + if v, set := optNew.Value(); set || v != 0 { + t.Fatalf("New[int]() = (%v, %v); want (0, false)", v, set) + } + + optVal := optional.NewValue(42) + if v, set := optVal.Value(); !set || v != 42 { + t.Fatalf("NewValue(42) = (%v, %v); want (42, true)", v, set) + } + + opt = opt.WithValue(100) + if v, set := opt.Value(); !set || v != 100 { + t.Fatalf("WithValue(100) = (%v, %v); want (100, true)", v, set) + } + + opt = opt.Clear() + if v, set := opt.Value(); set || v != 0 { + t.Fatalf("Clear() = (%v, %v); want (0, false)", v, set) + } +} + +// TestOption_String tests the scenario of using string optional values and +// verifies that default value, constructors, and mutation methods work as +// expected for text strings. +func (s) TestOption_String(t *testing.T) { + var opt optional.Option[string] + // Test unset value. + if v, set := opt.Value(); set || v != "" { + t.Fatalf("Zero-value Option[string] = (%q, %v); want (%q, false)", v, set, "") + } + + // Test that New() function also returns an unset optional value. + optNew := optional.New[string]() + if v, set := optNew.Value(); set || v != "" { + t.Fatalf("New Option[string] = (%q, %v); want (%q, false)", v, set, "") + } + + wantString := "test-string" + optVal := optional.NewValue(wantString) + if v, set := optVal.Value(); !set || v != wantString { + t.Fatalf("NewValue(%q) = (%q, %v); want (%q, true)", wantString, v, set, wantString) + } + + wantStringNew := "world" + opt = opt.WithValue(wantStringNew) + if v, set := opt.Value(); !set || v != wantStringNew { + t.Fatalf("WithValue(%q) = (%q, %v); want (%q, true)", wantStringNew, v, set, wantStringNew) + } + + opt = opt.Clear() + if v, set := opt.Value(); set || v != "" { + t.Fatalf("Clear() = (%q, %v); want (%q, false)", v, set, "") + } +} + +// TestOption_Struct tests the scenario of using a custom struct type inside an +// option type and verifies that custom struct field values are preserved, +// modified, and cleared correctly. +func (s) TestOption_Struct(t *testing.T) { + val1 := testStruct{Name: "Alice", Age: 30} + val2 := testStruct{Name: "Bob", Age: 40} + + var opt optional.Option[testStruct] + if v, set := opt.Value(); set || v != (testStruct{}) { + t.Fatalf("Zero-value Option[struct] = (%v, %v); want (empty, false)", v, set) + } + + optVal := optional.NewValue(val1) + if v, set := optVal.Value(); !set || v != val1 { + t.Fatalf("NewValue(val1) = (%v, %v); want (%v, true)", v, set, val1) + } + + opt = opt.WithValue(val2) + if v, set := opt.Value(); !set || v != val2 { + t.Fatalf("WithValue(val2) = (%v, %v); want (%v, true)", v, set, val2) + } + + opt = opt.Clear() + if v, set := opt.Value(); set || v != (testStruct{}) { + t.Fatalf("Clear() = (%v, %v); want (empty, false)", v, set) + } +} + +// TestOption_Pointer tests the scenario of using a pointer type inside an +// option type and verifies that nil status, address preservation, and +// underlying value dereferencing work as expected. +func (s) TestOption_Pointer(t *testing.T) { + val1 := 42 + val2 := 100 + + var opt optional.Option[*int] + if v, set := opt.Value(); set || v != nil { + t.Fatalf("Zero-value Option[*int] = (%v, %v); want (nil, false)", v, set) + } + + optVal := optional.NewValue(&val1) + if v, set := optVal.Value(); !set || v != &val1 || *v != val1 { + t.Fatalf("NewValue(%v) = (%v, %v); want (%v, true)", &val1, v, set, &val1) + } + + opt = opt.WithValue(&val2) + if v, set := opt.Value(); !set || v != &val2 || *v != val2 { + t.Fatalf("WithValue(%v) = (%v, %v); want (%v, true)", &val2, v, set, &val2) + } + + opt = opt.Clear() + if v, set := opt.Value(); set || v != nil { + t.Fatalf("Clear() = (%v, %v); want (nil, false)", v, set) + } +} diff --git a/internal/xds/httpfilter/extconfig.go b/internal/xds/httpfilter/extconfig.go new file mode 100644 index 000000000000..ac40fccb7d8f --- /dev/null +++ b/internal/xds/httpfilter/extconfig.go @@ -0,0 +1,116 @@ +/* + * + * Copyright 2021 gRPC authors. + * + * 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 httpfilter contains interface definitions for xDS-based HTTP filters +// and a registry for filter builders. +package httpfilter + +import ( + "encoding/json" + "fmt" + "regexp" + "time" + + "google.golang.org/grpc/internal/xds/matcher" + "google.golang.org/grpc/metadata" + + v3mutationpb "github.com/envoyproxy/go-control-plane/envoy/config/common/mutation_rules/v3" + v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" +) + +// HeaderMutationRules specifies the rules for what modifications an external +// processing server may make to headers sent on the data plane RPC. +type HeaderMutationRules struct { + // AllowExpr specifies a regular expression that matches the headers that can + // be mutated. + AllowExpr *regexp.Regexp + // DisallowExpr specifies a regular expression that matches the headers that + // cannot be mutated. This overrides the above allowExpr if a header matches + // both. + DisallowExpr *regexp.Regexp + // DisallowAll specifies that no header mutations are allowed. This overrides + // all other settings. + DisallowAll bool + // DisallowIsError specifies whether to return an error if a header mutation + // is disallowed. If true, the data plane RPC will be failed with a grpc + // status code of Unknown. + DisallowIsError bool +} + +// ServerConfig contains the configuration for an external server. +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 json.RawMessage + // CallCredentials specifies the per-RPC credentials to use when making calls + // to the external server. + CallCredentials []json.RawMessage + // 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 +} + +// ConvertStringMatchers converts a slice of protobuf StringMatcher messages to +// a slice of matcher.StringMatcher. +func ConvertStringMatchers(patterns []*v3matcherpb.StringMatcher) ([]matcher.StringMatcher, error) { + matchers := make([]matcher.StringMatcher, 0, len(patterns)) + for _, p := range patterns { + sm, err := matcher.StringMatcherFromProto(p) + if err != nil { + return nil, err + } + matchers = append(matchers, sm) + } + return matchers, nil +} + +// HeaderMutationRulesFromProto converts a protobuf HeaderMutationRules message +// to a headerMutationRules struct. +func HeaderMutationRulesFromProto(mr *v3mutationpb.HeaderMutationRules) (HeaderMutationRules, error) { + var rules HeaderMutationRules + if mr == nil { + return rules, nil + } + if allowExpr := mr.GetAllowExpression(); allowExpr != nil { + re, err := regexp.Compile(allowExpr.GetRegex()) + if err != nil { + return rules, fmt.Errorf("extproc: %v", err) + } + rules.AllowExpr = re + } + if disallowExpr := mr.GetDisallowExpression(); disallowExpr != nil { + re, err := regexp.Compile(disallowExpr.GetRegex()) + if err != nil { + return rules, fmt.Errorf("extproc: %v", err) + } + rules.DisallowExpr = re + } + rules.DisallowAll = mr.GetDisallowAll().GetValue() + rules.DisallowIsError = mr.GetDisallowIsError().GetValue() + return rules, nil +} diff --git a/internal/xds/httpfilter/extproc/config.go b/internal/xds/httpfilter/extproc/config.go new file mode 100644 index 000000000000..594977427bce --- /dev/null +++ b/internal/xds/httpfilter/extproc/config.go @@ -0,0 +1,155 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * 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 extproc + +import ( + "time" + + "google.golang.org/grpc/experimental/optional" + "google.golang.org/grpc/internal/xds/httpfilter" + "google.golang.org/grpc/internal/xds/matcher" +) + +type baseConfig struct { + httpfilter.FilterConfig + config interceptorConfig +} + +type overrideConfig struct { + httpfilter.FilterConfig + config interceptorOverrideConfig +} + +// interceptorOverrideConfig contains the configuration for the external +// processing client interceptor override. This is used for overriding the base +// config. If a particular field is set , that will be used instead of the base +// config. +type interceptorOverrideConfig struct { + // server is the configuration for the external processing server. + server optional.Option[httpfilter.ServerConfig] + // processingModes specifies the processing mode for each dataplane event. + + processingModes optional.Option[processingModes] + // 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 optional.Option[bool] + // Attributes to be sent to the external processing server along with the + // request and response dataplane events. + requestAttributes []string + responseAttributes []string +} + +// interceptorConfig contains the configuration for the external processing +// client interceptor. +type interceptorConfig struct { + // The following fields can be set either in the filter config or the override + // config. If both are set, the override config will be used. + // + // server is the configuration for the external processing server. + server httpfilter.ServerConfig + // 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 + // Attributes to be sent to the external processing server along with the + // request and response dataplane events. + requestAttributes []string + responseAttributes []string + + // The following fields can only be set in the base config. + // + // mutationRules specifies the rules for what modifications an external + // processing server may make to headers/trailers sent to it. + mutationRules httpfilter.HeaderMutationRules + // allowedHeaders specifies the headers that are allowed to be sent to the + // external processing server. If unset, all headers are allowed. + allowedHeaders []matcher.StringMatcher + // disallowedHeaders specifies the headers that will not be sent to the + // external processing server. This overrides the above allowedHeaders if a + // header matches both. + disallowedHeaders []matcher.StringMatcher + // disableImmediateResponse specifies whether to disable immediate response + // from the external processing server. When true, if the response from + // external processing server has the `immediate_response` field set, the + // dataplane RPC will be failed with `UNAVAILABLE` status code. When false, + // the `immediate_response` field in the response from external processing + // server will be ignored. + disableImmediateResponse bool + // observabilityMode determines if the filter waits for the external + // processing server. If true, events are sent to the server in + // "observation-only" mode; the filter does not wait for a response. If false, + // the filter waits for a response, allowing the server to modify events + // before they reach the dataplane. + observabilityMode bool + // deferredCloseTimeout is the duration the filter waits before closing the + // external processing stream after the dataplane RPC completes. This is only + // applicable when observabilityMode is true; otherwise, it is ignored. The + // default value is 5 seconds. + deferredCloseTimeout time.Duration +} + +// processingMode defines how headers, trailers, and bodies are handled in +// relation to the external processing server. +type processingMode int + +const ( + // modeSkip indicates that the header/trailer/body should not be sent. + modeSkip processingMode = iota + // modeSend indicates that the header/trailer/body should be sent. + modeSend +) + +type processingModes struct { + requestHeaderMode processingMode + responseHeaderMode processingMode + responseTrailerMode processingMode + requestBodyMode processingMode + responseBodyMode processingMode +} + +// newInterceptorConfig creates the interceptor config from the base and +// override filter configs. The base config is required and the override config +// is optional. If a field is set in both the base and override configs, the +// value from the override config will be used. +func newInterceptorConfig(base interceptorConfig, override interceptorOverrideConfig) interceptorConfig { + ic := base + + // Apply overrides if present. + if val, ok := override.server.Value(); ok { + ic.server = val + } + if val, ok := override.failureModeAllow.Value(); ok { + ic.failureModeAllow = val + } + if override.requestAttributes != nil { + ic.requestAttributes = override.requestAttributes + } + if override.responseAttributes != nil { + ic.responseAttributes = override.responseAttributes + } + if val, ok := override.processingModes.Value(); ok { + ic.processingModes = val + } + return ic +} diff --git a/internal/xds/httpfilter/extproc/ext_proc.go b/internal/xds/httpfilter/extproc/ext_proc.go new file mode 100644 index 000000000000..42e05babb396 --- /dev/null +++ b/internal/xds/httpfilter/extproc/ext_proc.go @@ -0,0 +1,93 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * 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 extproc implements the Envoy external processing filter. +package extproc + +import ( + "fmt" + + "google.golang.org/grpc" + "google.golang.org/grpc/internal/resolver" + "google.golang.org/grpc/internal/xds/httpfilter" + + v3procservicepb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" +) + +type builder struct{} + +func (builder) BuildClientFilter() httpfilter.ClientFilter { + return clientFilter{} +} + +var _ httpfilter.ClientFilterBuilder = builder{} + +type clientFilter struct{} + +func (clientFilter) Close() {} + +var createExtProcChannel = func(httpfilter.ServerConfig) (*grpc.ClientConn, error) { + return nil, fmt.Errorf("dialing external processing server with raw JSON credentials is not yet supported") +} + +func (clientFilter) BuildClientInterceptor(cfg, override httpfilter.FilterConfig) (resolver.ClientInterceptor, error) { + if cfg == nil { + return nil, fmt.Errorf("extproc: nil config provided") + } + + c, ok := cfg.(baseConfig) + if !ok { + return nil, fmt.Errorf("extproc: incorrect config type provided (%T): %v", cfg, cfg) + } + + var ov overrideConfig + if override != nil { + ov, ok = override.(overrideConfig) + if !ok { + return nil, fmt.Errorf("extproc: incorrect override config type provided (%T): %v", override, override) + } + } + + config := newInterceptorConfig(c.config, ov.config) + + // Create a channel to the external processing server. + cc, err := createExtProcChannel(config.server) + if err != nil { + return nil, fmt.Errorf("extproc: failed to create client: %v", err) + } + extClient := v3procservicepb.NewExternalProcessorClient(cc) + + return &interceptor{ + config: config, + extClient: extClient, + cc: cc, + }, nil +} + +type interceptor struct { + resolver.ClientInterceptor + config interceptorConfig + extClient v3procservicepb.ExternalProcessorClient + cc *grpc.ClientConn +} + +func (i *interceptor) Close() { + if i.cc != nil { + i.cc.Close() + } +} diff --git a/internal/xds/httpfilter/extproc/ext_proc_test.go b/internal/xds/httpfilter/extproc/ext_proc_test.go new file mode 100644 index 000000000000..13b33c1418f1 --- /dev/null +++ b/internal/xds/httpfilter/extproc/ext_proc_test.go @@ -0,0 +1,330 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * 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 extproc + +import ( + "regexp" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/experimental/optional" + "google.golang.org/grpc/internal/grpctest" + "google.golang.org/grpc/internal/xds/httpfilter" + "google.golang.org/grpc/internal/xds/matcher" + "google.golang.org/grpc/metadata" +) + +const testBaseURI = "base-uri" + +// incorrectFilterConfig embeds httpfilter.FilterConfig but is not of type +// baseConfig/overrideConfig, and is used to test incorrect config types being +// passed to BuildClientInterceptor. +type incorrectFilterConfig struct { + httpfilter.FilterConfig +} + +type s struct { + grpctest.Tester +} + +func Test(t *testing.T) { + grpctest.RunSubTests(t, s{}) +} + +func (s) TestBuildClientInterceptor(t *testing.T) { + origCreateExtProcChannel := createExtProcChannel + defer func() { createExtProcChannel = origCreateExtProcChannel }() + createExtProcChannel = func(cfg httpfilter.ServerConfig) (*grpc.ClientConn, error) { + return grpc.NewClient(cfg.TargetURI, grpc.WithTransportCredentials(insecure.NewCredentials())) + } + + b := builder{} + f := b.BuildClientFilter() + defer f.Close() + + tests := []struct { + name string + cfg httpfilter.FilterConfig + override httpfilter.FilterConfig + wantConfig *interceptorConfig + wantErr string + }{ + { + name: "NilConfig", + cfg: nil, + wantErr: "extproc: nil config provided", + }, + { + name: "IncorrectConfigType", + cfg: incorrectFilterConfig{}, + wantErr: "extproc: incorrect config type provided", + }, + { + name: "IncorrectOverrideType", + cfg: baseConfig{config: interceptorConfig{}}, + override: incorrectFilterConfig{}, + wantErr: "extproc: incorrect override config type provided", + }, + { + name: "ConfigUsingOnlyBase", + cfg: baseConfig{ + config: interceptorConfig{ + failureModeAllow: true, + requestAttributes: []string{"attr1"}, + responseAttributes: []string{"attr2"}, + observabilityMode: true, + disableImmediateResponse: true, + deferredCloseTimeout: 10 * time.Second, + processingModes: processingModes{ + requestHeaderMode: modeSend, + responseHeaderMode: modeSkip, + responseTrailerMode: modeSend, + requestBodyMode: modeSend, + responseBodyMode: modeSkip, + }, + server: httpfilter.ServerConfig{ + TargetURI: testBaseURI, + ChannelCredentials: []byte("{}"), + }, + mutationRules: httpfilter.HeaderMutationRules{ + AllowExpr: regexp.MustCompile("allow-.*"), + DisallowExpr: regexp.MustCompile("disallow-.*"), + DisallowAll: true, + DisallowIsError: true, + }, + allowedHeaders: []matcher.StringMatcher{matcher.NewExactStringMatcher("allow-header", false)}, + }, + }, + wantConfig: &interceptorConfig{ + failureModeAllow: true, + requestAttributes: []string{"attr1"}, + responseAttributes: []string{"attr2"}, + mutationRules: httpfilter.HeaderMutationRules{ + AllowExpr: regexp.MustCompile("allow-.*"), + DisallowExpr: regexp.MustCompile("disallow-.*"), + DisallowAll: true, + DisallowIsError: true, + }, + observabilityMode: true, + disableImmediateResponse: true, + deferredCloseTimeout: 10 * time.Second, + processingModes: processingModes{ + requestHeaderMode: modeSend, + responseHeaderMode: modeSkip, + responseTrailerMode: modeSend, + requestBodyMode: modeSend, + responseBodyMode: modeSkip, + }, + server: httpfilter.ServerConfig{ + TargetURI: testBaseURI, + ChannelCredentials: []byte("{}"), + }, + allowedHeaders: []matcher.StringMatcher{matcher.NewExactStringMatcher("allow-header", false)}, + }, + }, + { + name: "ConfigUsingBaseAndOverride", + cfg: baseConfig{ + config: interceptorConfig{ + failureModeAllow: false, + requestAttributes: []string{"base-attr1"}, + responseAttributes: []string{"base-attr2"}, + observabilityMode: true, + disableImmediateResponse: true, + deferredCloseTimeout: 10 * time.Second, + processingModes: processingModes{ + requestHeaderMode: modeSend, + responseHeaderMode: modeSkip, + responseTrailerMode: modeSend, + requestBodyMode: modeSend, + responseBodyMode: modeSkip, + }, + server: httpfilter.ServerConfig{ + TargetURI: testBaseURI, + ChannelCredentials: []byte("{}"), + Timeout: time.Second, + InitialMetadata: metadata.MD(metadata.Pairs("key1", "value1")), + }, + mutationRules: httpfilter.HeaderMutationRules{ + AllowExpr: regexp.MustCompile("allow-.*"), + DisallowExpr: regexp.MustCompile("disallow-.*"), + DisallowAll: true, + DisallowIsError: true, + }, + allowedHeaders: []matcher.StringMatcher{matcher.NewExactStringMatcher("allow-header", false)}, + disallowedHeaders: []matcher.StringMatcher{matcher.NewExactStringMatcher("disallow-header", false)}, + }, + }, + override: overrideConfig{ + config: interceptorOverrideConfig{ + failureModeAllow: optional.NewValue(true), + requestAttributes: []string{"override-attr1"}, + responseAttributes: []string{"override-attr2"}, + processingModes: optional.NewValue(processingModes{ + requestHeaderMode: modeSkip, + responseHeaderMode: modeSend, + responseTrailerMode: modeSkip, + requestBodyMode: modeSkip, + responseBodyMode: modeSend, + }), + server: optional.NewValue(httpfilter.ServerConfig{ + TargetURI: "override-uri", + ChannelCredentials: []byte("{}"), + }), + }, + }, + wantConfig: &interceptorConfig{ + failureModeAllow: true, + requestAttributes: []string{"override-attr1"}, + responseAttributes: []string{"override-attr2"}, + mutationRules: httpfilter.HeaderMutationRules{ + AllowExpr: regexp.MustCompile("allow-.*"), + DisallowExpr: regexp.MustCompile("disallow-.*"), + DisallowAll: true, + DisallowIsError: true, + }, + observabilityMode: true, + disableImmediateResponse: true, + deferredCloseTimeout: 10 * time.Second, + processingModes: processingModes{ + requestHeaderMode: modeSkip, + responseHeaderMode: modeSend, + responseTrailerMode: modeSkip, + requestBodyMode: modeSkip, + responseBodyMode: modeSend, + }, + server: httpfilter.ServerConfig{ + TargetURI: "override-uri", + ChannelCredentials: []byte("{}"), + }, + allowedHeaders: []matcher.StringMatcher{matcher.NewExactStringMatcher("allow-header", false)}, + disallowedHeaders: []matcher.StringMatcher{matcher.NewExactStringMatcher("disallow-header", false)}, + }, + }, + { + name: "ConfigUsingBaseAndPartialOverride", + cfg: baseConfig{ + config: interceptorConfig{ + failureModeAllow: false, + requestAttributes: []string{"base-attr1"}, + responseAttributes: []string{"base-attr2"}, + observabilityMode: true, + disableImmediateResponse: true, + deferredCloseTimeout: 10 * time.Second, + processingModes: processingModes{ + requestHeaderMode: modeSend, + responseHeaderMode: modeSkip, + responseTrailerMode: modeSend, + requestBodyMode: modeSend, + responseBodyMode: modeSkip, + }, + server: httpfilter.ServerConfig{ + TargetURI: testBaseURI, + ChannelCredentials: []byte("{}"), + Timeout: time.Second, + InitialMetadata: metadata.MD(metadata.Pairs("key1", "value1")), + }, + mutationRules: httpfilter.HeaderMutationRules{ + AllowExpr: regexp.MustCompile("allow-.*"), + DisallowExpr: regexp.MustCompile("disallow-.*"), + DisallowAll: true, + DisallowIsError: true, + }, + allowedHeaders: []matcher.StringMatcher{matcher.NewExactStringMatcher("allow-header", false)}, + disallowedHeaders: []matcher.StringMatcher{matcher.NewExactStringMatcher("disallow-header", false)}, + }, + }, + override: overrideConfig{ + config: interceptorOverrideConfig{ + failureModeAllow: optional.NewValue(true), + }, + }, + wantConfig: &interceptorConfig{ + failureModeAllow: true, + requestAttributes: []string{"base-attr1"}, + responseAttributes: []string{"base-attr2"}, + mutationRules: httpfilter.HeaderMutationRules{ + AllowExpr: regexp.MustCompile("allow-.*"), + DisallowExpr: regexp.MustCompile("disallow-.*"), + DisallowAll: true, + DisallowIsError: true, + }, + observabilityMode: true, + disableImmediateResponse: true, + deferredCloseTimeout: 10 * time.Second, + processingModes: processingModes{ + requestHeaderMode: modeSend, + responseHeaderMode: modeSkip, + responseTrailerMode: modeSend, + requestBodyMode: modeSend, + responseBodyMode: modeSkip, + }, + server: httpfilter.ServerConfig{ + TargetURI: testBaseURI, + ChannelCredentials: []byte("{}"), + Timeout: time.Second, + InitialMetadata: metadata.MD(metadata.Pairs("key1", "value1")), + }, + allowedHeaders: []matcher.StringMatcher{matcher.NewExactStringMatcher("allow-header", false)}, + disallowedHeaders: []matcher.StringMatcher{matcher.NewExactStringMatcher("disallow-header", false)}, + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + intptr, err := f.BuildClientInterceptor(tc.cfg, tc.override) + if tc.wantErr == "" { + if err != nil { + t.Fatalf("BuildClientInterceptor() returned unexpected error: %v", err) + } + ic, ok := intptr.(*interceptor) + if !ok { + t.Fatalf("BuildClientInterceptor() returned %T, want *interceptor", intptr) + } + cmpOpts := []cmp.Option{ + cmp.AllowUnexported(interceptorConfig{}, processingModes{}), + cmp.Transformer("RegexpToString", func(r *regexp.Regexp) string { + if r == nil { + return "" + } + return r.String() + }), + cmp.Comparer(func(x, y matcher.StringMatcher) bool { + return x.Equal(y) + }), + } + if diff := cmp.Diff(ic.config, *tc.wantConfig, cmpOpts...); diff != "" { + t.Fatalf("Interceptor config returned unexpected diff (-got +want):\n%s", diff) + } + intptr.Close() + return + } + if err == nil { + t.Fatalf("BuildClientInterceptor() returned nil error, want error containing %q", tc.wantErr) + } + if !strings.Contains(err.Error(), tc.wantErr) { + t.Fatalf("BuildClientInterceptor() error = %v, want error containing %q", err, tc.wantErr) + } + }) + } +}