consul/agent/envoyextensions/builtin/property-override/structpatcher_test.go

1157 lines
35 KiB
Go
Raw Normal View History

[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2023-08-11 13:12:13 +00:00
// 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)
}
}
})
}
}