You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
consul/agent/envoyextensions/builtin/property-override/structpatcher_test.go

1157 lines
35 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
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)
}
}
})
}
}