mirror of https://github.com/hashicorp/consul
450 lines
12 KiB
Go
450 lines
12 KiB
Go
package propertyoverride
|
|
|
|
import (
|
|
"fmt"
|
|
routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
|
"strings"
|
|
"testing"
|
|
|
|
clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
|
"github.com/hashicorp/consul/api"
|
|
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// TestConstructor tests raw input to the Constructor function called to initialize
|
|
// the property-override extension. This includes implicit validation of the deserialized
|
|
// input to the extension.
|
|
func TestConstructor(t *testing.T) {
|
|
// These helpers aid in constructing valid raw example input (map[string]any)
|
|
// for the Constructor, with optional overrides for fields under test.
|
|
applyOverrides := func(m map[string]any, overrides map[string]any) map[string]any {
|
|
for k, v := range overrides {
|
|
if v == nil {
|
|
delete(m, k)
|
|
} else {
|
|
m[k] = v
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
makeResourceFilter := func(overrides map[string]any) map[string]any {
|
|
f := map[string]any{
|
|
"ResourceType": ResourceTypeRoute,
|
|
"TrafficDirection": TrafficDirectionOutbound,
|
|
}
|
|
return applyOverrides(f, overrides)
|
|
}
|
|
makePatch := func(overrides map[string]any) map[string]any {
|
|
p := map[string]any{
|
|
"ResourceFilter": makeResourceFilter(map[string]any{}),
|
|
"Op": OpAdd,
|
|
"Path": "/name",
|
|
"Value": "foo",
|
|
}
|
|
return applyOverrides(p, overrides)
|
|
}
|
|
makeArguments := func(overrides map[string]any) map[string]any {
|
|
a := map[string]any{
|
|
"Patches": []map[string]any{
|
|
makePatch(map[string]any{}),
|
|
},
|
|
"Debug": true,
|
|
"ProxyType": api.ServiceKindConnectProxy,
|
|
}
|
|
return applyOverrides(a, overrides)
|
|
}
|
|
type testCase struct {
|
|
extensionName string
|
|
arguments map[string]any
|
|
expected propertyOverride
|
|
ok bool
|
|
errMsg string
|
|
}
|
|
|
|
validTestCase := func(o Op, d TrafficDirection, t ResourceType) testCase {
|
|
var v any = "foo"
|
|
if o != OpAdd {
|
|
v = nil
|
|
}
|
|
|
|
// Use a valid field (name) for all resource types.
|
|
path := "/name"
|
|
|
|
return testCase{
|
|
arguments: makeArguments(map[string]any{
|
|
"Patches": []map[string]any{
|
|
makePatch(map[string]any{
|
|
"ResourceFilter": makeResourceFilter(map[string]any{
|
|
"ResourceType": t,
|
|
"TrafficDirection": d,
|
|
}),
|
|
"Op": o,
|
|
"Path": path,
|
|
"Value": v,
|
|
}),
|
|
},
|
|
}),
|
|
expected: propertyOverride{
|
|
Patches: []Patch{
|
|
{
|
|
ResourceFilter: ResourceFilter{
|
|
ResourceType: t,
|
|
TrafficDirection: d,
|
|
},
|
|
Op: o,
|
|
Path: path,
|
|
Value: v,
|
|
},
|
|
},
|
|
Debug: true,
|
|
ProxyType: api.ServiceKindConnectProxy,
|
|
},
|
|
ok: true,
|
|
}
|
|
}
|
|
cases := map[string]testCase{
|
|
"with no arguments": {
|
|
arguments: nil,
|
|
ok: false,
|
|
errMsg: "at least one patch is required",
|
|
},
|
|
"with an invalid name": {
|
|
arguments: makeArguments(map[string]any{}),
|
|
extensionName: "bad",
|
|
ok: false,
|
|
errMsg: "expected extension name \"builtin/property-override\" but got \"bad\"",
|
|
},
|
|
"empty Patches": {
|
|
arguments: makeArguments(map[string]any{"Patches": []map[string]any{}}),
|
|
ok: false,
|
|
errMsg: "at least one patch is required",
|
|
},
|
|
"patch with no ResourceFilter": {
|
|
arguments: makeArguments(map[string]any{"Patches": []map[string]any{
|
|
makePatch(map[string]any{
|
|
"ResourceFilter": nil,
|
|
}),
|
|
}}),
|
|
ok: false,
|
|
errMsg: "field ResourceFilter is required",
|
|
},
|
|
"patch with no ResourceType": {
|
|
arguments: makeArguments(map[string]any{"Patches": []map[string]any{
|
|
makePatch(map[string]any{
|
|
"ResourceFilter": makeResourceFilter(map[string]any{
|
|
"ResourceType": nil,
|
|
}),
|
|
}),
|
|
}}),
|
|
ok: false,
|
|
errMsg: "field ResourceType is required",
|
|
},
|
|
"patch with invalid ResourceType": {
|
|
arguments: makeArguments(map[string]any{"Patches": []map[string]any{
|
|
makePatch(map[string]any{
|
|
"ResourceFilter": makeResourceFilter(map[string]any{
|
|
"ResourceType": "foo",
|
|
}),
|
|
}),
|
|
}}),
|
|
ok: false,
|
|
errMsg: "invalid ResourceType",
|
|
},
|
|
"patch with no TrafficDirection": {
|
|
arguments: makeArguments(map[string]any{"Patches": []map[string]any{
|
|
makePatch(map[string]any{
|
|
"ResourceFilter": makeResourceFilter(map[string]any{
|
|
"TrafficDirection": nil,
|
|
}),
|
|
}),
|
|
}}),
|
|
ok: false,
|
|
errMsg: "field TrafficDirection is required",
|
|
},
|
|
"patch with invalid TrafficDirection": {
|
|
arguments: makeArguments(map[string]any{"Patches": []map[string]any{
|
|
makePatch(map[string]any{
|
|
"ResourceFilter": makeResourceFilter(map[string]any{
|
|
"TrafficDirection": "foo",
|
|
}),
|
|
}),
|
|
}}),
|
|
ok: false,
|
|
errMsg: "invalid TrafficDirection",
|
|
},
|
|
"patch with no Op": {
|
|
arguments: makeArguments(map[string]any{"Patches": []map[string]any{
|
|
makePatch(map[string]any{
|
|
"Op": nil,
|
|
}),
|
|
}}),
|
|
ok: false,
|
|
errMsg: "field Op is required",
|
|
},
|
|
"patch with invalid Op": {
|
|
arguments: makeArguments(map[string]any{"Patches": []map[string]any{
|
|
makePatch(map[string]any{
|
|
"Op": "foo",
|
|
}),
|
|
}}),
|
|
ok: false,
|
|
errMsg: "invalid Op",
|
|
},
|
|
"patch with invalid Envoy resource Path": {
|
|
arguments: makeArguments(map[string]any{"Patches": []map[string]any{
|
|
makePatch(map[string]any{
|
|
"Path": "/invalid",
|
|
}),
|
|
}}),
|
|
ok: false,
|
|
errMsg: "no match for field", // this error comes from the patcher dry-run attempt
|
|
},
|
|
"non-Add patch with Value": {
|
|
arguments: makeArguments(map[string]any{"Patches": []map[string]any{
|
|
makePatch(map[string]any{
|
|
"Op": OpRemove,
|
|
"Value": 1,
|
|
}),
|
|
}}),
|
|
ok: false,
|
|
errMsg: fmt.Sprintf("field Value is not supported for %s operation", OpRemove),
|
|
},
|
|
// See decode.HookWeakDecodeFromSlice for more details. In practice, we can end up
|
|
// with a "Patches" field decoded to the single "Patch" value contained in the
|
|
// serialized slice (raised from the containing slice). Using WeakDecode solves
|
|
// for this. Ideally, we would kill that decoding hook entirely, but this test
|
|
// enforces expected behavior until we do. Multi-member slices should be unaffected
|
|
// by WeakDecode as it is a more-permissive version of the default behavior.
|
|
"single value Patches decoded as map construction succeeds": {
|
|
arguments: makeArguments(map[string]any{"Patches": makePatch(map[string]any{})}),
|
|
expected: validTestCase(OpAdd, TrafficDirectionOutbound, ResourceTypeRoute).expected,
|
|
ok: true,
|
|
},
|
|
"invalid ProxyType": {
|
|
arguments: makeArguments(map[string]any{
|
|
"Patches": []map[string]any{
|
|
makePatch(map[string]any{}),
|
|
},
|
|
"ProxyType": "invalid",
|
|
}),
|
|
ok: false,
|
|
errMsg: "invalid ProxyType",
|
|
},
|
|
"unsupported ProxyType": {
|
|
arguments: makeArguments(map[string]any{
|
|
"Patches": []map[string]any{
|
|
makePatch(map[string]any{}),
|
|
},
|
|
"ProxyType": api.ServiceKindMeshGateway,
|
|
}),
|
|
ok: false,
|
|
errMsg: "invalid ProxyType",
|
|
},
|
|
}
|
|
|
|
for o := range Ops {
|
|
for d := range TrafficDirections {
|
|
for t := range ResourceTypes {
|
|
cases["valid everything: "+strings.Join([]string{o, d, t}, ",")] =
|
|
validTestCase(Op(o), TrafficDirection(d), ResourceType(t))
|
|
}
|
|
}
|
|
}
|
|
|
|
for n, tc := range cases {
|
|
t.Run(n, func(t *testing.T) {
|
|
|
|
extensionName := api.BuiltinPropertyOverrideExtension
|
|
if tc.extensionName != "" {
|
|
extensionName = tc.extensionName
|
|
}
|
|
|
|
// Build the wrapping RuntimeConfig struct, which contains the serialized
|
|
// arguments for constructing the property-override extension.
|
|
svc := api.CompoundServiceName{Name: "svc"}
|
|
ext := extensioncommon.RuntimeConfig{
|
|
ServiceName: svc,
|
|
EnvoyExtension: api.EnvoyExtension{
|
|
Name: extensionName,
|
|
Arguments: tc.arguments,
|
|
},
|
|
}
|
|
|
|
// Construct the actual extension
|
|
e, err := Constructor(ext.EnvoyExtension)
|
|
|
|
if tc.ok {
|
|
require.NoError(t, err)
|
|
require.Equal(t, &extensioncommon.BasicEnvoyExtender{Extension: &tc.expected}, e)
|
|
} else {
|
|
require.ErrorContains(t, err, tc.errMsg)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_patchResourceType(t *testing.T) {
|
|
makeExtension := func(patches ...Patch) *propertyOverride {
|
|
return &propertyOverride{
|
|
Patches: patches,
|
|
}
|
|
}
|
|
makePatchWithPath := func(t ResourceType, d TrafficDirection, p string) Patch {
|
|
return Patch{
|
|
ResourceFilter: ResourceFilter{
|
|
ResourceType: t,
|
|
TrafficDirection: d,
|
|
},
|
|
Op: OpAdd,
|
|
Path: p,
|
|
Value: 1,
|
|
}
|
|
}
|
|
makePatch := func(t ResourceType, d TrafficDirection) Patch {
|
|
return makePatchWithPath(t, d, "/foo")
|
|
}
|
|
|
|
clusterOutbound := makePatch(ResourceTypeCluster, TrafficDirectionOutbound)
|
|
clusterInbound := makePatch(ResourceTypeCluster, TrafficDirectionInbound)
|
|
routeOutbound := makePatch(ResourceTypeRoute, TrafficDirectionOutbound)
|
|
routeOutbound2 := makePatchWithPath(ResourceTypeRoute, TrafficDirectionOutbound, "/bar")
|
|
routeInbound := makePatch(ResourceTypeRoute, TrafficDirectionInbound)
|
|
|
|
type args struct {
|
|
d TrafficDirection
|
|
k proto.Message
|
|
p *propertyOverride
|
|
t ResourceType
|
|
}
|
|
type testCase struct {
|
|
args args
|
|
expectPatched bool
|
|
wantApplied []Patch
|
|
}
|
|
cases := map[string]testCase{
|
|
"outbound gets matching patch": {
|
|
args: args{
|
|
d: TrafficDirectionOutbound,
|
|
k: &clusterv3.Cluster{},
|
|
p: makeExtension(clusterOutbound),
|
|
t: ResourceTypeCluster,
|
|
},
|
|
expectPatched: true,
|
|
wantApplied: []Patch{clusterOutbound},
|
|
},
|
|
"inbound gets matching patch": {
|
|
args: args{
|
|
d: TrafficDirectionInbound,
|
|
k: &clusterv3.Cluster{},
|
|
p: makeExtension(clusterInbound),
|
|
t: ResourceTypeCluster,
|
|
},
|
|
expectPatched: true,
|
|
wantApplied: []Patch{clusterInbound},
|
|
},
|
|
"multiple resources same direction only gets matching resource": {
|
|
args: args{
|
|
d: TrafficDirectionOutbound,
|
|
k: &clusterv3.Cluster{},
|
|
p: makeExtension(clusterOutbound, routeOutbound),
|
|
t: ResourceTypeCluster,
|
|
},
|
|
expectPatched: true,
|
|
wantApplied: []Patch{clusterOutbound},
|
|
},
|
|
"multiple directions same resource only gets matching direction": {
|
|
args: args{
|
|
d: TrafficDirectionOutbound,
|
|
k: &clusterv3.Cluster{},
|
|
p: makeExtension(clusterOutbound, clusterInbound),
|
|
t: ResourceTypeCluster,
|
|
},
|
|
expectPatched: true,
|
|
wantApplied: []Patch{clusterOutbound},
|
|
},
|
|
"multiple directions and resources only gets matching patch": {
|
|
args: args{
|
|
d: TrafficDirectionInbound,
|
|
k: &routev3.RouteConfiguration{},
|
|
p: makeExtension(clusterOutbound, clusterInbound, routeOutbound, routeInbound),
|
|
t: ResourceTypeRoute,
|
|
},
|
|
expectPatched: true,
|
|
wantApplied: []Patch{routeInbound},
|
|
},
|
|
"multiple directions and resources multiple matches gets all matching patches": {
|
|
args: args{
|
|
d: TrafficDirectionOutbound,
|
|
k: &routev3.RouteConfiguration{},
|
|
p: makeExtension(clusterOutbound, clusterInbound, routeOutbound, routeInbound, routeOutbound2),
|
|
t: ResourceTypeRoute,
|
|
},
|
|
expectPatched: true,
|
|
wantApplied: []Patch{routeOutbound, routeOutbound2},
|
|
},
|
|
"multiple directions and resources no matches gets no patches": {
|
|
args: args{
|
|
d: TrafficDirectionOutbound,
|
|
k: &routev3.RouteConfiguration{},
|
|
p: makeExtension(clusterInbound, routeOutbound, routeInbound, routeOutbound2),
|
|
t: ResourceTypeCluster,
|
|
},
|
|
expectPatched: false,
|
|
wantApplied: nil,
|
|
},
|
|
}
|
|
for n, tc := range cases {
|
|
t.Run(n, func(t *testing.T) {
|
|
mockPatcher := MockPatcher[proto.Message]{}
|
|
_, patched, err := patchResourceType[proto.Message](tc.args.k, tc.args.p, tc.args.t, tc.args.d, &mockPatcher)
|
|
|
|
require.NoError(t, err, "unexpected error from mock")
|
|
require.Equal(t, tc.expectPatched, patched)
|
|
require.Equal(t, tc.wantApplied, mockPatcher.appliedPatches)
|
|
})
|
|
}
|
|
}
|
|
|
|
type MockPatcher[K proto.Message] struct {
|
|
appliedPatches []Patch
|
|
}
|
|
|
|
func (m *MockPatcher[K]) applyPatch(k K, p Patch, _ bool) (result K, e error) {
|
|
m.appliedPatches = append(m.appliedPatches, p)
|
|
return k, nil
|
|
}
|
|
|
|
func TestCanApply(t *testing.T) {
|
|
cases := map[string]struct {
|
|
ext *propertyOverride
|
|
conf *extensioncommon.RuntimeConfig
|
|
canApply bool
|
|
}{
|
|
"valid proxy type": {
|
|
ext: &propertyOverride{
|
|
ProxyType: api.ServiceKindConnectProxy,
|
|
},
|
|
conf: &extensioncommon.RuntimeConfig{
|
|
Kind: api.ServiceKindConnectProxy,
|
|
},
|
|
canApply: true,
|
|
},
|
|
"invalid proxy type": {
|
|
ext: &propertyOverride{
|
|
ProxyType: api.ServiceKindTerminatingGateway,
|
|
},
|
|
conf: &extensioncommon.RuntimeConfig{
|
|
Kind: api.ServiceKindMeshGateway,
|
|
},
|
|
canApply: false,
|
|
},
|
|
}
|
|
for n, tc := range cases {
|
|
t.Run(n, func(t *testing.T) {
|
|
require.Equal(t, tc.canApply, tc.ext.CanApply(tc.conf))
|
|
})
|
|
}
|
|
}
|