package propertyoverride import ( "fmt" "google.golang.org/protobuf/types/known/anypb" "testing" envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" _struct "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/wrapperspb" ) func TestPatchStruct(t *testing.T) { makePatch := func(o Op, p string, v any) Patch { return Patch{ Op: o, Path: p, Value: v, } } makeAddPatch := func(p string, v any) Patch { return makePatch(OpAdd, p, v) } makeRemovePatch := func(p string) Patch { return makePatch(OpRemove, p, nil) } expectFieldsListErr := func(resourceName string, truncatedFieldsWarning bool) func(t *testing.T, err error) { return func(t *testing.T, err error) { require.Contains(t, err.Error(), fmt.Sprintf("available %s fields:", resourceName)) if truncatedFieldsWarning { require.Contains(t, err.Error(), "First 10 fields for this message included, configure with `Debug = true` to print all.") } else { require.NotContains(t, err.Error(), "First 10 fields for this message included, configure with `Debug = true` to print all.") } } } type args struct { k proto.Message patches []Patch debug bool } type testCase struct { args args expected proto.Message ok bool errMsg string errFunc func(*testing.T, error) } // Simplify test case construction for variants of potential input types uint32VariantTestCase := func(i any) testCase { return testCase{ args: args{ k: &envoy_endpoint_v3.Endpoint{ HealthCheckConfig: &envoy_endpoint_v3.Endpoint_HealthCheckConfig{ PortValue: 3000, }, }, patches: []Patch{makeAddPatch( "/health_check_config/port_value", i, )}, }, expected: &envoy_endpoint_v3.Endpoint{ HealthCheckConfig: &envoy_endpoint_v3.Endpoint_HealthCheckConfig{ PortValue: 1234, }, }, ok: true, } } uint32WrapperVariantTestCase := func(i any) testCase { return testCase{ args: args{ k: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999), }, }, patches: []Patch{makeAddPatch( "/outlier_detection/enforcing_consecutive_5xx", i, )}, }, expected: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ EnforcingConsecutive_5Xx: wrapperspb.UInt32(1234), }, }, ok: true, } } uint64WrapperVariantTestCase := func(i any) testCase { return testCase{ args: args{ k: &envoy_cluster_v3.Cluster{ LbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig_{ RingHashLbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig{ MaximumRingSize: wrapperspb.UInt64(999999999), }, }, }, patches: []Patch{makeAddPatch( "/ring_hash_lb_config/maximum_ring_size", i, )}, }, expected: &envoy_cluster_v3.Cluster{ LbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig_{ RingHashLbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig{ MaximumRingSize: wrapperspb.UInt64(12345678), }, }, }, ok: true, } } doubleVariantTestCase := func(i any) testCase { return testCase{ args: args{ k: &envoy_cluster_v3.Cluster{ LbConfig: &envoy_cluster_v3.Cluster_LeastRequestLbConfig_{ LeastRequestLbConfig: &envoy_cluster_v3.Cluster_LeastRequestLbConfig{ ActiveRequestBias: &corev3.RuntimeDouble{ DefaultValue: 1.0, }, }, }, }, patches: []Patch{makeAddPatch( "/least_request_lb_config/active_request_bias/default_value", i, )}, }, expected: &envoy_cluster_v3.Cluster{ LbConfig: &envoy_cluster_v3.Cluster_LeastRequestLbConfig_{ LeastRequestLbConfig: &envoy_cluster_v3.Cluster_LeastRequestLbConfig{ ActiveRequestBias: &corev3.RuntimeDouble{ DefaultValue: 1.5, }, }, }, }, ok: true, } } doubleWrapperVariantTestCase := func(i any) testCase { return testCase{ args: args{ k: &envoy_cluster_v3.Cluster{ PreconnectPolicy: &envoy_cluster_v3.Cluster_PreconnectPolicy{ PerUpstreamPreconnectRatio: wrapperspb.Double(1.0), }, }, patches: []Patch{makeAddPatch( "/preconnect_policy/per_upstream_preconnect_ratio", i, )}, }, expected: &envoy_cluster_v3.Cluster{ PreconnectPolicy: &envoy_cluster_v3.Cluster_PreconnectPolicy{ PerUpstreamPreconnectRatio: wrapperspb.Double(1.5), }, }, ok: true, } } enumByNumberVariantTestCase := func(i any) testCase { return testCase{ args: args{ k: &envoy_cluster_v3.Cluster{ LbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig_{ RingHashLbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig{ HashFunction: envoy_cluster_v3.Cluster_RingHashLbConfig_XX_HASH, }, }, }, patches: []Patch{makeAddPatch( "/ring_hash_lb_config/hash_function", i, )}, }, expected: &envoy_cluster_v3.Cluster{ LbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig_{ RingHashLbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig{ HashFunction: envoy_cluster_v3.Cluster_RingHashLbConfig_MURMUR_HASH_2, }, }, }, ok: true, } } repeatedIntVariantTestCase := func(i any) testCase { return testCase{ args: args{ k: &envoy_route_v3.RetryPolicy{ RetriableStatusCodes: []uint32{429, 502}, }, patches: []Patch{makeAddPatch( "/retriable_status_codes", i, )}, }, expected: &envoy_route_v3.RetryPolicy{ RetriableStatusCodes: []uint32{503, 504}, }, ok: true, } } cases := map[string]testCase{ // Some variants of target types are covered in conversion code but missing // from this table due to lacking available examples in Envoy v3 protos. An // improvement could be a home-rolled proto with every possible target type. "add single field: int->uint32": uint32VariantTestCase(int(1234)), "add single field: int32->uint32": uint32VariantTestCase(int32(1234)), "add single field: int64->uint32": uint32VariantTestCase(int64(1234)), "add single field: uint->uint32": uint32VariantTestCase(uint(1234)), "add single field: uint32->uint32": uint32VariantTestCase(uint32(1234)), "add single field: uint64->uint32": uint32VariantTestCase(uint64(1234)), "add single field: float32->uint32": uint32VariantTestCase(float32(1234.0)), "add single field: float64->uint32": uint32VariantTestCase(float64(1234.0)), "add single field: int->uint32 wrapper": uint32WrapperVariantTestCase(int(1234)), "add single field: int32->uint32 wrapper": uint32WrapperVariantTestCase(int32(1234)), "add single field: int64->uint32 wrapper": uint32WrapperVariantTestCase(int64(1234)), "add single field: uint->uint32 wrapper": uint32WrapperVariantTestCase(uint(1234)), "add single field: uint32->uint32 wrapper": uint32WrapperVariantTestCase(uint32(1234)), "add single field: uint64->uint32 wrapper": uint32WrapperVariantTestCase(uint64(1234)), "add single field: float32->uint32 wrapper": uint32WrapperVariantTestCase(float32(1234.0)), "add single field: float64->uint32 wrapper": uint32WrapperVariantTestCase(float64(1234.0)), "add single field: int->uint64 wrapper": uint64WrapperVariantTestCase(int(12345678)), "add single field: int32->uint64 wrapper": uint64WrapperVariantTestCase(int32(12345678)), "add single field: int64->uint64 wrapper": uint64WrapperVariantTestCase(int64(12345678)), "add single field: uint->uint64 wrapper": uint64WrapperVariantTestCase(uint(12345678)), "add single field: uint32->uint64 wrapper": uint64WrapperVariantTestCase(uint32(12345678)), "add single field: uint64->uint64 wrapper": uint64WrapperVariantTestCase(uint64(12345678)), "add single field: float32->uint64 wrapper": uint64WrapperVariantTestCase(float32(12345678.0)), "add single field: float64->uint64 wrapper": uint64WrapperVariantTestCase(float64(12345678.0)), "add single field: float32->double": doubleVariantTestCase(float32(1.5)), "add single field: float64->double": doubleVariantTestCase(float64(1.5)), "add single field: float32->double wrapper": doubleWrapperVariantTestCase(float32(1.5)), "add single field: float64->double wrapper": doubleWrapperVariantTestCase(float64(1.5)), "add single field: bool": { args: args{ k: &envoy_cluster_v3.Cluster{ RespectDnsTtl: false, }, patches: []Patch{makeAddPatch( "/respect_dns_ttl", true, )}, }, expected: &envoy_cluster_v3.Cluster{ RespectDnsTtl: true, }, ok: true, }, "add single field: bool wrapper": { args: args{ k: &envoy_listener_v3.Listener{ UseOriginalDst: wrapperspb.Bool(false), }, patches: []Patch{makeAddPatch( "/use_original_dst", true, )}, }, expected: &envoy_listener_v3.Listener{ UseOriginalDst: wrapperspb.Bool(true), }, ok: true, }, "add single field: string": { args: args{ k: &envoy_cluster_v3.Cluster{ AltStatName: "foo", }, patches: []Patch{makeAddPatch( "/alt_stat_name", "bar", )}, }, expected: &envoy_cluster_v3.Cluster{ AltStatName: "bar", }, ok: true, }, "add single field: enum by name": { args: args{ k: &envoy_cluster_v3.Cluster{ LbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig_{ RingHashLbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig{ HashFunction: envoy_cluster_v3.Cluster_RingHashLbConfig_XX_HASH, }, }, }, patches: []Patch{makeAddPatch( "/ring_hash_lb_config/hash_function", "MURMUR_HASH_2", )}, }, expected: &envoy_cluster_v3.Cluster{ LbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig_{ RingHashLbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig{ HashFunction: envoy_cluster_v3.Cluster_RingHashLbConfig_MURMUR_HASH_2, }, }, }, ok: true, }, "add single field: enum by number int": enumByNumberVariantTestCase(int(1)), "add single field: enum by number int32": enumByNumberVariantTestCase(int32(1)), "add single field: enum by number int64": enumByNumberVariantTestCase(int64(1)), "add single field: enum by number uint": enumByNumberVariantTestCase(uint(1)), "add single field: enum by number uint32": enumByNumberVariantTestCase(uint32(1)), "add single field: enum by number uint64": enumByNumberVariantTestCase(uint64(1)), "add single field previously unmodified": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{makeAddPatch( "/alt_stat_name", "bar", )}, }, expected: &envoy_cluster_v3.Cluster{ AltStatName: "bar", }, ok: true, }, "add single field deeply nested": { args: args{ k: &envoy_cluster_v3.Cluster{ UpstreamConnectionOptions: &envoy_cluster_v3.UpstreamConnectionOptions{ TcpKeepalive: &corev3.TcpKeepalive{ KeepaliveProbes: wrapperspb.UInt32(2), }, }, }, patches: []Patch{makeAddPatch( "/upstream_connection_options/tcp_keepalive/keepalive_probes", 5, )}, }, expected: &envoy_cluster_v3.Cluster{ UpstreamConnectionOptions: &envoy_cluster_v3.UpstreamConnectionOptions{ TcpKeepalive: &corev3.TcpKeepalive{ KeepaliveProbes: wrapperspb.UInt32(5), }, }, }, ok: true, }, "add single field deeply nested with intermediate unset field": { args: args{ k: &envoy_cluster_v3.Cluster{ // Explicitly set to nil just in case defaults change. UpstreamConnectionOptions: nil, }, patches: []Patch{makeAddPatch( "/upstream_connection_options/tcp_keepalive/keepalive_probes", 1234, )}, }, expected: &envoy_cluster_v3.Cluster{ UpstreamConnectionOptions: &envoy_cluster_v3.UpstreamConnectionOptions{ TcpKeepalive: &corev3.TcpKeepalive{ KeepaliveProbes: wrapperspb.UInt32(1234), }, }, }, ok: true, }, "add repeated field: int->uint32": repeatedIntVariantTestCase([]int{503, 504}), "add repeated field: int32->uint32": repeatedIntVariantTestCase([]int32{503, 504}), "add repeated field: int64->uint32": repeatedIntVariantTestCase([]int64{503, 504}), "add repeated field: uint->uint32": repeatedIntVariantTestCase([]uint{503, 504}), "add repeated field: uint32->uint32": repeatedIntVariantTestCase([]uint32{503, 504}), "add repeated field: uint64->uint32": repeatedIntVariantTestCase([]uint64{503, 504}), "add repeated field: float32->uint32": repeatedIntVariantTestCase([]float32{503.0, 504.0}), "add repeated field: float64->uint32": repeatedIntVariantTestCase([]float64{503.0, 504.0}), "add repeated field: string": { args: args{ k: &envoy_route_v3.RouteConfiguration{}, patches: []Patch{makeAddPatch( "/internal_only_headers", []string{"X-Custom-Header1", "X-Custom-Header-2"}, )}, }, expected: &envoy_route_v3.RouteConfiguration{ InternalOnlyHeaders: []string{"X-Custom-Header1", "X-Custom-Header-2"}, }, ok: true, }, "add repeated field: enum by name": { args: args{ k: &corev3.HealthStatusSet{ Statuses: []corev3.HealthStatus{corev3.HealthStatus_DRAINING}, }, patches: []Patch{makeAddPatch( "/statuses", []string{"HEALTHY", "UNHEALTHY"}, )}, }, expected: &corev3.HealthStatusSet{ Statuses: []corev3.HealthStatus{corev3.HealthStatus_HEALTHY, corev3.HealthStatus_UNHEALTHY}, }, ok: true, }, "add repeated field: enum by number": { args: args{ k: &corev3.HealthStatusSet{ Statuses: []corev3.HealthStatus{corev3.HealthStatus_DRAINING}, }, patches: []Patch{makeAddPatch( "/statuses", []int{1, 2}, )}, }, expected: &corev3.HealthStatusSet{ Statuses: []corev3.HealthStatus{corev3.HealthStatus_HEALTHY, corev3.HealthStatus_UNHEALTHY}, }, ok: true, }, "add message field: empty": { args: args{ k: &envoy_listener_v3.Listener{}, patches: []Patch{makeAddPatch( "/connection_balance_config/exact_balance", map[string]any{}, )}, }, expected: &envoy_listener_v3.Listener{ ConnectionBalanceConfig: &envoy_listener_v3.Listener_ConnectionBalanceConfig{ BalanceType: &envoy_listener_v3.Listener_ConnectionBalanceConfig_ExactBalance_{}, }, }, ok: true, }, "add message field: multiple fields": { args: args{ k: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999), FailurePercentageThreshold: wrapperspb.UInt32(9999), }, }, patches: []Patch{makeAddPatch( "/outlier_detection", map[string]any{ "enforcing_consecutive_5xx": 1234, "failure_percentage_request_volume": 2345, }, )}, }, expected: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ EnforcingConsecutive_5Xx: wrapperspb.UInt32(1234), FailurePercentageRequestVolume: wrapperspb.UInt32(2345), }, }, ok: true, }, "add multiple single field patches merge with existing object": { args: args{ k: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999), FailurePercentageThreshold: wrapperspb.UInt32(9999), }, }, patches: []Patch{ makeAddPatch( "/outlier_detection/enforcing_consecutive_5xx", 1234, ), makeAddPatch( "/outlier_detection/failure_percentage_request_volume", 2345, ), }, }, expected: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ EnforcingConsecutive_5Xx: wrapperspb.UInt32(1234), FailurePercentageRequestVolume: wrapperspb.UInt32(2345), // Previously unspecified field set FailurePercentageThreshold: wrapperspb.UInt32(9999), // Existing unmodified field retained }, }, ok: true, }, "remove single field: scalar wrapper": { args: args{ k: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999), }, }, patches: []Patch{makeRemovePatch( "/outlier_detection/enforcing_consecutive_5xx", )}, }, expected: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{}, }, ok: true, }, "remove single field: string (reset to empty)": { args: args{ k: &envoy_cluster_v3.Cluster{ AltStatName: "foo", }, patches: []Patch{makeRemovePatch( "/alt_stat_name", )}, }, expected: &envoy_cluster_v3.Cluster{}, ok: true, }, "remove single field: bool (reset to false)": { args: args{ k: &envoy_cluster_v3.Cluster{ RespectDnsTtl: true, }, patches: []Patch{makeRemovePatch( "/respect_dns_ttl", )}, }, expected: &envoy_cluster_v3.Cluster{ RespectDnsTtl: false, }, ok: true, }, "remove single field: enum (reset to default)": { args: args{ k: &envoy_cluster_v3.Cluster{ LbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig_{ RingHashLbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig{ HashFunction: envoy_cluster_v3.Cluster_RingHashLbConfig_MURMUR_HASH_2, }, }, }, patches: []Patch{makeRemovePatch( "/ring_hash_lb_config/hash_function", )}, }, expected: &envoy_cluster_v3.Cluster{ LbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig_{ RingHashLbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig{ HashFunction: envoy_cluster_v3.Cluster_RingHashLbConfig_XX_HASH, }, }, }, ok: true, }, "remove single field: message": { args: args{ k: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999), FailurePercentageThreshold: wrapperspb.UInt32(9999), }, }, patches: []Patch{makeRemovePatch( "/outlier_detection", )}, }, expected: &envoy_cluster_v3.Cluster{}, ok: true, }, "remove single field: map": { args: args{ k: &envoy_cluster_v3.Cluster{ Metadata: &corev3.Metadata{ FilterMetadata: map[string]*_struct.Struct{ "foo": nil, }, }, }, patches: []Patch{makeRemovePatch( "/metadata/filter_metadata", )}, }, expected: &envoy_cluster_v3.Cluster{ Metadata: &corev3.Metadata{}, }, ok: true, }, "remove single field: Any": { args: args{ k: &envoy_cluster_v3.Cluster{ ClusterDiscoveryType: &envoy_cluster_v3.Cluster_ClusterType{ ClusterType: &envoy_cluster_v3.Cluster_CustomClusterType{ TypedConfig: &anypb.Any{ TypeUrl: "foo", }, }, }, }, patches: []Patch{ makeRemovePatch( "/cluster_type/typed_config", ), }, }, // Invalid actual config, but used as an example of removing Any field directly expected: &envoy_cluster_v3.Cluster{ ClusterDiscoveryType: &envoy_cluster_v3.Cluster_ClusterType{ ClusterType: &envoy_cluster_v3.Cluster_CustomClusterType{}, }, }, ok: true, }, "remove single field deeply nested": { args: args{ k: &envoy_cluster_v3.Cluster{ UpstreamConnectionOptions: &envoy_cluster_v3.UpstreamConnectionOptions{ TcpKeepalive: &corev3.TcpKeepalive{ KeepaliveProbes: wrapperspb.UInt32(9999), }, }, }, patches: []Patch{makeRemovePatch( "/upstream_connection_options/tcp_keepalive/keepalive_probes", )}, }, expected: &envoy_cluster_v3.Cluster{ UpstreamConnectionOptions: &envoy_cluster_v3.UpstreamConnectionOptions{ TcpKeepalive: &corev3.TcpKeepalive{}, }, }, ok: true, }, "remove repeated field: message": { args: args{ k: &envoy_cluster_v3.Cluster{ Filters: []*envoy_cluster_v3.Filter{ { Name: "foo", }, }, }, patches: []Patch{makeRemovePatch( "/filters", )}, }, expected: &envoy_cluster_v3.Cluster{}, ok: true, }, "remove multiple single field patches merge with existing object": { args: args{ k: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999), FailurePercentageThreshold: wrapperspb.UInt32(9999), }, }, patches: []Patch{ makeRemovePatch( "/outlier_detection/enforcing_consecutive_5xx", ), makeRemovePatch( "/outlier_detection/failure_percentage_request_volume", // No-op removal ), }, }, expected: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ FailurePercentageThreshold: wrapperspb.UInt32(9999), // Existing unmodified field retained }, }, ok: true, }, "remove does not instantiate intermediate fields that are unset": { args: args{ k: &envoy_cluster_v3.Cluster{ // Explicitly set to nil just in case defaults change. UpstreamConnectionOptions: nil, }, patches: []Patch{makeRemovePatch( "/upstream_connection_options/tcp_keepalive/keepalive_probes", )}, }, expected: &envoy_cluster_v3.Cluster{}, ok: true, }, "add and remove multiple single field patches merge with existing object": { args: args{ k: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ FailurePercentageRequestVolume: wrapperspb.UInt32(9999), FailurePercentageThreshold: wrapperspb.UInt32(9999), }, }, patches: []Patch{ makeAddPatch( "/outlier_detection/enforcing_consecutive_5xx", 1234, ), makeRemovePatch( "/outlier_detection/failure_percentage_request_volume", ), }, }, expected: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ EnforcingConsecutive_5Xx: wrapperspb.UInt32(1234), // New field added FailurePercentageThreshold: wrapperspb.UInt32(9999), // Existing unmodified field retained }, }, ok: true, }, "add then remove respects order of operations": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{ makeAddPatch( "/outlier_detection/enforcing_consecutive_5xx", 1234, ), makeRemovePatch( "/outlier_detection", ), }, }, expected: &envoy_cluster_v3.Cluster{}, ok: true, }, "remove then add respects order of operations": { args: args{ k: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ FailurePercentageRequestVolume: wrapperspb.UInt32(9999), }, }, patches: []Patch{ makeRemovePatch( "/outlier_detection", ), makeAddPatch( "/outlier_detection/enforcing_consecutive_5xx", 1234, ), }, }, expected: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ EnforcingConsecutive_5Xx: wrapperspb.UInt32(1234), // New field added // Previous field removed by remove op }, }, ok: true, }, "add invalid value: scalar->scalar type mismatch": { args: args{ k: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999), }, }, patches: []Patch{makeAddPatch( "/outlier_detection/enforcing_consecutive_5xx", "NaN", )}, }, ok: false, errMsg: "patch value type string could not be applied to target field type 'google.protobuf.UInt32Value'", }, "add invalid value: non-scalar->scalar type mismatch": { args: args{ k: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999), }, }, patches: []Patch{makeAddPatch( "/respect_dns_ttl", []string{"bad", "value"}, )}, }, ok: false, errMsg: "patch value type []string could not be applied to target field type 'bool'", }, "add invalid value: scalar->enum type mismatch": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{makeAddPatch( "/ring_hash_lb_config/hash_function", 1.5, )}, }, ok: false, errMsg: "patch value type float64 could not be applied to target field type 'enum'", }, "add invalid value: nil scalar": { args: args{ k: &envoy_cluster_v3.Cluster{ RespectDnsTtl: false, }, patches: []Patch{makeAddPatch( "/respect_dns_ttl", nil, )}, }, ok: false, errMsg: "non-nil Value is required; use an empty map to reset all fields on a message or the 'remove' op to unset fields", }, "add invalid value: nil wrapper": { args: args{ k: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999), }, }, patches: []Patch{makeAddPatch( "/outlier_detection/enforcing_consecutive_5xx", nil, )}, }, ok: false, errMsg: "non-nil Value is required; use an empty map to reset all fields on a message or the 'remove' op to unset fields", }, "add invalid value: nil message": { args: args{ k: &envoy_cluster_v3.Cluster{ OutlierDetection: &envoy_cluster_v3.OutlierDetection{ EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999), FailurePercentageThreshold: wrapperspb.UInt32(9999), }, }, patches: []Patch{makeAddPatch( "/outlier_detection", nil, )}, }, ok: false, errMsg: "non-nil Value is required; use an empty map to reset all fields on a message or the 'remove' op to unset fields", }, "add invalid value: mixed type scalar": { args: args{ k: &envoy_route_v3.RouteConfiguration{}, patches: []Patch{makeAddPatch( "/internal_only_headers", []any{"X-Custom-Header1", 123}, )}, }, expected: &envoy_route_v3.RouteConfiguration{ InternalOnlyHeaders: []string{"X-Custom-Header1", "X-Custom-Header-2"}, }, ok: false, errMsg: "patch value type []interface {} could not be applied to target field type 'repeated string'", }, "add unsupported target: message with non-scalar fields": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{makeAddPatch( "/dns_failure_refresh_rate", map[string]any{ "base_interval": map[string]any{}, }, )}, }, ok: false, errMsg: "unsupported target field type 'google.protobuf.Duration'", }, "add unsupported target: map field": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{makeAddPatch( "/metadata/filter_metadata", map[string]any{ "foo": "bar", }, )}, }, ok: false, errMsg: "unsupported target field type 'map'", }, "add unsupported target: non-message field via map": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{ makeAddPatch( "/name", map[string]any{ "cluster_refresh_rate": "5s", "cluster_refresh_timeout": "3s", "redirect_refresh_interval": "5s", "redirect_refresh_threshold": 5, }, ), }, }, ok: false, errMsg: "non-message field type 'string' cannot be set via a map", }, "add unsupported target: non-message parent field via single value": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{ makeAddPatch( "/name/foo", "bar", ), }, }, ok: false, errMsg: "path contains member of non-message field 'name' (type 'string'); this type does not support child fields", }, "add unsupported target: non-message parent field via map": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{ makeAddPatch( "/name/foo", map[string]any{ "cluster_refresh_rate": "5s", "cluster_refresh_timeout": "3s", "redirect_refresh_interval": "5s", "redirect_refresh_threshold": 5, }, ), }, }, ok: false, errMsg: "path contains member of non-message field 'name' (type 'string'); this type does not support child fields", }, "add unsupported target: Any field": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{ makeAddPatch( // Purposefully use a wrong-but-reasonable field name to ensure special error is returned "/cluster_type/typed_config/@type", "foo", ), }, }, ok: false, errMsg: "variant-type message fields (google.protobuf.Any) are not supported", }, "add unsupported target: repeated message": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{makeAddPatch( "/filters", []any{}, // We don't need a value in this slice to test behavior )}, }, ok: false, errMsg: "unsupported target field type 'repeated envoy.config.cluster.v3.Filter'", }, "empty path": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{makeAddPatch( "", "ignored", )}, }, ok: false, errMsg: "non-empty, non-root Path is required", errFunc: expectFieldsListErr("envoy.config.cluster.v3.Cluster", true), }, "empty path debug mode": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{makeAddPatch( "", "ignored", )}, debug: true, }, ok: false, errMsg: "non-empty, non-root Path is required", errFunc: expectFieldsListErr("envoy.config.cluster.v3.Cluster", false), }, "root path": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{makeAddPatch( "/", "ignored", )}, }, ok: false, errMsg: "non-empty, non-root Path is required", errFunc: expectFieldsListErr("envoy.config.cluster.v3.Cluster", true), }, "root path debug mode": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{makeAddPatch( "/", "ignored", )}, debug: true, }, ok: false, errMsg: "non-empty, non-root Path is required", errFunc: expectFieldsListErr("envoy.config.cluster.v3.Cluster", false), }, "invalid path: add unknown field": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{makeAddPatch( "/outlier_detection/foo", "ignored", )}, }, ok: false, errMsg: "no match for field 'foo'!", errFunc: expectFieldsListErr("envoy.config.cluster.v3.OutlierDetection", true), }, "invalid path: remove unknown field": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{makeRemovePatch( "/outlier_detection/foo", )}, }, ok: false, errMsg: "no match for field 'foo'!", errFunc: expectFieldsListErr("envoy.config.cluster.v3.OutlierDetection", true), }, "invalid path: unknown field debug mode": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{makeAddPatch( "/outlier_detection/foo", "ignored", )}, debug: true, }, ok: false, errMsg: "no match for field 'foo'!", errFunc: expectFieldsListErr("envoy.config.cluster.v3.OutlierDetection", false), }, "error field list includes first 10 fields when not in debug mode": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{makeAddPatch( "", "ignored", )}, }, ok: false, errMsg: "transport_socket_matches\nname\nalt_stat_name\ntype\ncluster_type\neds_cluster_config\nconnect_timeout\nper_connection_buffer_limit_bytes\nlb_policy\nload_assignment", errFunc: expectFieldsListErr("envoy.config.cluster.v3.Cluster", true), }, "error field list includes all fields when in debug mode": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{makeAddPatch( "", "ignored", )}, debug: true, }, ok: false, errMsg: "transport_socket_matches\nname\nalt_stat_name\ntype\ncluster_type\neds_cluster_config\nconnect_timeout\nper_connection_buffer_limit_bytes\nlb_policy\nload_assignment\nhealth_checks\nmax_requests_per_connection\ncircuit_breakers\nupstream_http_protocol_options\ncommon_http_protocol_options\nhttp_protocol_options\nhttp2_protocol_options\ntyped_extension_protocol_options\ndns_refresh_rate\ndns_failure_refresh_rate\nrespect_dns_ttl\ndns_lookup_family\ndns_resolvers\nuse_tcp_for_dns_lookups\ndns_resolution_config\ntyped_dns_resolver_config\nwait_for_warm_on_init\noutlier_detection\ncleanup_interval\nupstream_bind_config\nlb_subset_config\nring_hash_lb_config\nmaglev_lb_config\noriginal_dst_lb_config\nleast_request_lb_config\nround_robin_lb_config\ncommon_lb_config\ntransport_socket\nmetadata\nprotocol_selection\nupstream_connection_options\nclose_connections_on_host_health_failure\nignore_health_on_host_removal\nfilters\nload_balancing_policy\nlrs_server\ntrack_timeout_budgets\nupstream_config\ntrack_cluster_stats\npreconnect_policy\nconnection_pool_per_downstream_connection", errFunc: expectFieldsListErr("envoy.config.cluster.v3.Cluster", false), }, "error field list warns about first 10 fields only when > 10 available when not in debug mode": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{makeAddPatch( "/upstream_connection_options/tcp_keepalive/foo", "ignored", )}, debug: false, }, ok: false, errMsg: "keepalive_probes\nkeepalive_time\nkeepalive_interval", errFunc: expectFieldsListErr("envoy.config.core.v3.TcpKeepalive", false), }, "invalid path: empty path element": { args: args{ k: &envoy_cluster_v3.Cluster{}, patches: []Patch{makeAddPatch( "/outlier_detection//", "ignored", )}, }, ok: false, errMsg: "empty field name in path", }, "invalid path: repeated field member": { args: args{ k: &envoy_listener_v3.Listener{}, patches: []Patch{makeRemovePatch( "/filter_chains/0/transport_socket_connect_timeout", )}, }, ok: false, errMsg: "path contains member of repeated field 'filter_chains'; repeated field member access is not supported", }, "invalid path: map field member": { args: args{ k: &envoy_cluster_v3.Cluster{ Metadata: &corev3.Metadata{ FilterMetadata: map[string]*_struct.Struct{ "foo": nil, }, }, }, patches: []Patch{makeRemovePatch( "/metadata/filter_metadata/foo", )}, }, ok: false, errMsg: "path contains member of map field 'filter_metadata'; map field member access is not supported", }, } copyMessage := func(m proto.Message) proto.Message { return m } for n, tc := range cases { t.Run(n, func(t *testing.T) { // Copy k so that we can compare before and after copyOfK := copyMessage(tc.args.k) var err error for _, p := range tc.args.patches { // Repeatedly patch value, replacing with the new version each time copyOfK, err = PatchStruct(copyOfK, p, tc.args.debug) if err != nil { break // Break on the first error } } if tc.ok { require.NoError(t, err) if diff := cmp.Diff(tc.expected, copyOfK, protocmp.Transform()); diff != "" { t.Errorf("unexpected difference:\n%v", diff) } } else { require.Error(t, err) if tc.errMsg != "" { require.ErrorContains(t, err, tc.errMsg) } if tc.errFunc != nil { tc.errFunc(t, err) } } }) } }