Fix Property Override Services parsing (#17584)

Ensure that the embedded api struct is properly parsed when
deserializing config containing a set ResourceFilter.Services field.

Also enhance existing integration test to guard against bugs and
exercise this field.
This commit is contained in:
Michael Zalimeni 2023-06-06 15:40:37 -04:00 committed by GitHub
parent cb2999e43c
commit 378a15af32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 97 additions and 7 deletions

View File

@ -7,6 +7,7 @@ import (
envoy_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/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_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" envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
"github.com/hashicorp/consul/lib/decode"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@ -73,7 +74,7 @@ func matchesResourceFilter[K proto.Message](rf ResourceFilter, resourceType Reso
} }
type ServiceName struct { type ServiceName struct {
api.CompoundServiceName api.CompoundServiceName `mapstructure:",squash"`
} }
// ResourceType is the type of Envoy resource being patched. // ResourceType is the type of Envoy resource being patched.
@ -275,7 +276,21 @@ func Constructor(ext api.EnvoyExtension) (extensioncommon.EnvoyExtender, error)
if name := ext.Name; name != api.BuiltinPropertyOverrideExtension { if name := ext.Name; name != api.BuiltinPropertyOverrideExtension {
return nil, fmt.Errorf("expected extension name %q but got %q", api.BuiltinPropertyOverrideExtension, name) return nil, fmt.Errorf("expected extension name %q but got %q", api.BuiltinPropertyOverrideExtension, name)
} }
if err := mapstructure.WeakDecode(ext.Arguments, &p); err != nil { // This avoids issues with decoding nested slices, which are error-prone
// due to slice<->map coercion by mapstructure. See HookWeakDecodeFromSlice
// and WeaklyTypedInput docs for more details.
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.ComposeDecodeHookFunc(
decode.HookWeakDecodeFromSlice,
decode.HookTranslateKeys,
),
WeaklyTypedInput: true,
Result: &p,
})
if err != nil {
return nil, fmt.Errorf("error configuring decoder: %v", err)
}
if err := d.Decode(ext.Arguments); err != nil {
return nil, fmt.Errorf("error decoding extension arguments: %v", err) return nil, fmt.Errorf("error decoding extension arguments: %v", err)
} }
if err := p.validate(); err != nil { if err := p.validate(); err != nil {

View File

@ -220,8 +220,8 @@ func TestConstructor(t *testing.T) {
arguments: makeArguments(map[string]any{"Patches": []map[string]any{ arguments: makeArguments(map[string]any{"Patches": []map[string]any{
makePatch(map[string]any{ makePatch(map[string]any{
"ResourceFilter": makeResourceFilter(map[string]any{ "ResourceFilter": makeResourceFilter(map[string]any{
"Services": []ServiceName{ "Services": []map[string]any{
{CompoundServiceName: api.CompoundServiceName{}}, {},
}, },
}), }),
}), }),
@ -240,6 +240,42 @@ func TestConstructor(t *testing.T) {
expected: validTestCase(OpAdd, extensioncommon.TrafficDirectionOutbound, ResourceTypeRoute).expected, expected: validTestCase(OpAdd, extensioncommon.TrafficDirectionOutbound, ResourceTypeRoute).expected,
ok: true, ok: true,
}, },
// Ensure that embedded api struct used for Services is parsed correctly.
// See also above comment on decode.HookWeakDecodeFromSlice.
"single value Services decoded as map construction succeeds": {
arguments: makeArguments(map[string]any{"Patches": []map[string]any{
makePatch(map[string]any{
"ResourceFilter": makeResourceFilter(map[string]any{
"Services": []map[string]any{
{"Name": "foo"},
},
}),
}),
}}),
expected: propertyOverride{
Patches: []Patch{
{
ResourceFilter: ResourceFilter{
ResourceType: ResourceTypeRoute,
TrafficDirection: extensioncommon.TrafficDirectionOutbound,
Services: []*ServiceName{
{CompoundServiceName: api.CompoundServiceName{
Name: "foo",
Namespace: "default",
Partition: "default",
}},
},
},
Op: OpAdd,
Path: "/name",
Value: "foo",
},
},
Debug: true,
ProxyType: api.ServiceKindConnectProxy,
},
ok: true,
},
"invalid ProxyType": { "invalid ProxyType": {
arguments: makeArguments(map[string]any{ arguments: makeArguments(map[string]any{
"Patches": []map[string]any{ "Patches": []map[string]any{

View File

@ -5,3 +5,4 @@
snapshot_envoy_admin localhost:19000 s1 primary || true snapshot_envoy_admin localhost:19000 s1 primary || true
snapshot_envoy_admin localhost:19001 s2 primary || true snapshot_envoy_admin localhost:19001 s2 primary || true
snapshot_envoy_admin localhost:19002 s3 primary || true

View File

@ -11,6 +11,10 @@ services {
{ {
destination_name = "s2" destination_name = "s2"
local_bind_port = 5000 local_bind_port = 5000
},
{
destination_name = "s3"
local_bind_port = 5001
} }
] ]
} }

View File

@ -0,0 +1,8 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
services {
name = "s3"
port = 8181
connect { sidecar_service {} }
}

View File

@ -28,6 +28,12 @@ EnvoyExtensions = [
] ]
' '
upsert_config_entry primary '
Kind = "service-defaults"
Name = "s3"
Protocol = "http"
'
upsert_config_entry primary ' upsert_config_entry primary '
Kind = "service-defaults" Kind = "service-defaults"
Name = "s1" Name = "s1"
@ -37,7 +43,8 @@ EnvoyExtensions = [
Name = "builtin/property-override" Name = "builtin/property-override"
Arguments = { Arguments = {
ProxyType = "connect-proxy" ProxyType = "connect-proxy"
Patches = [{ Patches = [
{
ResourceFilter = { ResourceFilter = {
ResourceType = "cluster" ResourceType = "cluster"
TrafficDirection = "outbound" TrafficDirection = "outbound"
@ -45,7 +52,19 @@ EnvoyExtensions = [
Op = "add" Op = "add"
Path = "/upstream_connection_options/tcp_keepalive/keepalive_probes" Path = "/upstream_connection_options/tcp_keepalive/keepalive_probes"
Value = 1234 Value = 1234
}] },
{
ResourceFilter = {
ResourceType = "cluster"
TrafficDirection = "outbound"
Services = [{
Name = "s2"
}]
}
Op = "remove"
Path = "/outlier_detection"
}
]
} }
} }
] ]

View File

@ -3,4 +3,4 @@
# SPDX-License-Identifier: MPL-2.0 # SPDX-License-Identifier: MPL-2.0
export REQUIRED_SERVICES="s1 s1-sidecar-proxy s2 s2-sidecar-proxy" export REQUIRED_SERVICES="s1 s1-sidecar-proxy s2 s2-sidecar-proxy s3 s3-sidecar-proxy"

View File

@ -19,6 +19,13 @@ load helpers
[ "$status" == 0 ] [ "$status" == 0 ]
[ "$(echo "$output" | jq -r '.upstream_connection_options.tcp_keepalive.keepalive_probes')" == "1234" ] [ "$(echo "$output" | jq -r '.upstream_connection_options.tcp_keepalive.keepalive_probes')" == "1234" ]
[ "$(echo "$output" | jq -r '.outlier_detection')" == "null" ]
run get_envoy_cluster_config localhost:19000 s3
[ "$status" == 0 ]
[ "$(echo "$output" | jq -r '.upstream_connection_options.tcp_keepalive.keepalive_probes')" == "1234" ]
[ "$(echo "$output" | jq -r '.outlier_detection')" == "{}" ]
} }
@test "s2 proxy is configured with the expected envoy patches" { @test "s2 proxy is configured with the expected envoy patches" {