mirror of https://github.com/status-im/consul.git
3807 lines
85 KiB
Go
3807 lines
85 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package structs
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/hashicorp/hcl"
|
|
"github.com/mitchellh/copystructure"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/hashicorp/consul-net-rpc/go-msgpack/codec"
|
|
|
|
"github.com/hashicorp/consul/acl"
|
|
"github.com/hashicorp/consul/agent/cache"
|
|
"github.com/hashicorp/consul/api"
|
|
"github.com/hashicorp/consul/sdk/testutil"
|
|
"github.com/hashicorp/consul/types"
|
|
)
|
|
|
|
func TestNormalizeGenerateHash(t *testing.T) {
|
|
for _, cType := range AllConfigEntryKinds {
|
|
//this is an enterprise only config entry
|
|
if cType == RateLimitIPConfig {
|
|
continue
|
|
}
|
|
entry, err := MakeConfigEntry(cType, "global")
|
|
require.NoError(t, err)
|
|
require.NoError(t, entry.Normalize())
|
|
require.NotEmpty(t, entry.GetHash(), entry.GetKind())
|
|
}
|
|
|
|
}
|
|
|
|
func TestConfigEntries_ACLs(t *testing.T) {
|
|
type testACL = configEntryTestACL
|
|
type testcase = configEntryACLTestCase
|
|
|
|
newAuthz := func(t *testing.T, src string) acl.Authorizer {
|
|
policy, err := acl.NewPolicyFromSource(src, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
authorizer, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
|
|
require.NoError(t, err)
|
|
return authorizer
|
|
}
|
|
|
|
cases := []testcase{
|
|
// =================== proxy-defaults ===================
|
|
{
|
|
name: "proxy-defaults",
|
|
entry: &ProxyConfigEntry{},
|
|
expectACLs: []testACL{
|
|
{
|
|
name: "no-authz",
|
|
authorizer: newAuthz(t, ``),
|
|
canRead: true, // unauthenticated
|
|
canWrite: false,
|
|
},
|
|
{
|
|
name: "proxy-defaults: operator deny",
|
|
authorizer: newAuthz(t, `operator = "deny"`),
|
|
canRead: true, // unauthenticated
|
|
canWrite: false,
|
|
},
|
|
{
|
|
name: "proxy-defaults: operator read",
|
|
authorizer: newAuthz(t, `operator = "read"`),
|
|
canRead: true, // unauthenticated
|
|
canWrite: false,
|
|
},
|
|
{
|
|
name: "proxy-defaults: operator write",
|
|
authorizer: newAuthz(t, `operator = "write"`),
|
|
canRead: true, // unauthenticated
|
|
canWrite: true,
|
|
},
|
|
{
|
|
name: "proxy-defaults: mesh deny",
|
|
authorizer: newAuthz(t, `mesh = "deny"`),
|
|
canRead: true, // unauthenticated
|
|
canWrite: false,
|
|
},
|
|
{
|
|
name: "proxy-defaults: mesh read",
|
|
authorizer: newAuthz(t, `mesh = "read"`),
|
|
canRead: true, // unauthenticated
|
|
canWrite: false,
|
|
},
|
|
{
|
|
name: "proxy-defaults: mesh write",
|
|
authorizer: newAuthz(t, `mesh = "write"`),
|
|
canRead: true, // unauthenticated
|
|
canWrite: true,
|
|
},
|
|
{
|
|
name: "proxy-defaults: operator deny and mesh read",
|
|
authorizer: newAuthz(t, `operator = "deny" mesh = "read"`),
|
|
canRead: true, // unauthenticated
|
|
canWrite: false,
|
|
},
|
|
{
|
|
name: "proxy-defaults: operator deny and mesh write",
|
|
authorizer: newAuthz(t, `operator = "deny" mesh = "write"`),
|
|
canRead: true, // unauthenticated
|
|
canWrite: true,
|
|
},
|
|
},
|
|
},
|
|
// =================== mesh ===================
|
|
{
|
|
name: "mesh",
|
|
entry: &MeshConfigEntry{},
|
|
expectACLs: []testACL{
|
|
{
|
|
name: "no-authz",
|
|
authorizer: newAuthz(t, ``),
|
|
canRead: true, // unauthenticated
|
|
canWrite: false,
|
|
},
|
|
{
|
|
name: "mesh: operator deny",
|
|
authorizer: newAuthz(t, `operator = "deny"`),
|
|
canRead: true, // unauthenticated
|
|
canWrite: false,
|
|
},
|
|
{
|
|
name: "mesh: operator read",
|
|
authorizer: newAuthz(t, `operator = "read"`),
|
|
canRead: true, // unauthenticated
|
|
canWrite: false,
|
|
},
|
|
{
|
|
name: "mesh: operator write",
|
|
authorizer: newAuthz(t, `operator = "write"`),
|
|
canRead: true, // unauthenticated
|
|
canWrite: true,
|
|
},
|
|
{
|
|
name: "mesh: mesh deny",
|
|
authorizer: newAuthz(t, `mesh = "deny"`),
|
|
canRead: true, // unauthenticated
|
|
canWrite: false,
|
|
},
|
|
{
|
|
name: "mesh: mesh read",
|
|
authorizer: newAuthz(t, `mesh = "read"`),
|
|
canRead: true, // unauthenticated
|
|
canWrite: false,
|
|
},
|
|
{
|
|
name: "mesh: mesh write",
|
|
authorizer: newAuthz(t, `mesh = "write"`),
|
|
canRead: true, // unauthenticated
|
|
canWrite: true,
|
|
},
|
|
{
|
|
name: "mesh: operator deny and mesh read",
|
|
authorizer: newAuthz(t, `operator = "deny" mesh = "read"`),
|
|
canRead: true, // unauthenticated
|
|
canWrite: false,
|
|
},
|
|
{
|
|
name: "mesh: operator deny and mesh write",
|
|
authorizer: newAuthz(t, `operator = "deny" mesh = "write"`),
|
|
canRead: true, // unauthenticated
|
|
canWrite: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
testConfigEntries_ListRelatedServices_AndACLs(t, cases)
|
|
}
|
|
|
|
type configEntryTestACL struct {
|
|
name string
|
|
authorizer acl.Authorizer
|
|
canRead bool
|
|
canWrite bool
|
|
}
|
|
|
|
type configEntryACLTestCase struct {
|
|
name string
|
|
entry ConfigEntry
|
|
expectServices []ServiceID // optional
|
|
expectACLs []configEntryTestACL
|
|
}
|
|
|
|
func testConfigEntries_ListRelatedServices_AndACLs(t *testing.T, cases []configEntryACLTestCase) {
|
|
|
|
// This test tests both of these because they are related functions.
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// verify test inputs
|
|
require.NoError(t, tc.entry.Normalize())
|
|
require.NoError(t, tc.entry.Validate())
|
|
|
|
if dce, ok := tc.entry.(discoveryChainConfigEntry); ok {
|
|
got := dce.ListRelatedServices()
|
|
require.Equal(t, tc.expectServices, got)
|
|
}
|
|
|
|
if len(tc.expectACLs) == 1 {
|
|
a := tc.expectACLs[0]
|
|
require.Empty(t, a.name)
|
|
} else {
|
|
for _, a := range tc.expectACLs {
|
|
require.NotEmpty(t, a.name)
|
|
t.Run(a.name, func(t *testing.T) {
|
|
canRead := tc.entry.CanRead(a.authorizer)
|
|
if a.canRead {
|
|
require.Nil(t, canRead)
|
|
} else {
|
|
require.Error(t, canRead)
|
|
require.True(t, acl.IsErrPermissionDenied(canRead))
|
|
}
|
|
canWrite := tc.entry.CanWrite(a.authorizer)
|
|
if a.canWrite {
|
|
require.Nil(t, canWrite)
|
|
} else {
|
|
require.Error(t, canWrite)
|
|
require.True(t, acl.IsErrPermissionDenied(canWrite))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecodeConfigEntry_ServiceDefaults(t *testing.T) {
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
camel string
|
|
snake string
|
|
expect ConfigEntry
|
|
expectErr string
|
|
}{
|
|
{
|
|
name: "service-defaults-with-MaxInboundConnections",
|
|
snake: `
|
|
kind = "service-defaults"
|
|
name = "external"
|
|
protocol = "tcp"
|
|
destination {
|
|
addresses = [
|
|
"api.google.com",
|
|
"web.google.com"
|
|
]
|
|
port = 8080
|
|
}
|
|
max_inbound_connections = 14
|
|
`,
|
|
camel: `
|
|
Kind = "service-defaults"
|
|
Name = "external"
|
|
Protocol = "tcp"
|
|
Destination {
|
|
Addresses = [
|
|
"api.google.com",
|
|
"web.google.com"
|
|
]
|
|
Port = 8080
|
|
}
|
|
MaxInboundConnections = 14
|
|
`,
|
|
expect: &ServiceConfigEntry{
|
|
Kind: "service-defaults",
|
|
Name: "external",
|
|
Protocol: "tcp",
|
|
Destination: &DestinationConfig{
|
|
Addresses: []string{
|
|
"api.google.com",
|
|
"web.google.com",
|
|
},
|
|
Port: 8080,
|
|
},
|
|
MaxInboundConnections: 14,
|
|
},
|
|
},
|
|
} {
|
|
tc := tc
|
|
|
|
testbody := func(t *testing.T, body string) {
|
|
var raw map[string]interface{}
|
|
err := hcl.Decode(&raw, body)
|
|
require.NoError(t, err)
|
|
|
|
got, err := DecodeConfigEntry(raw)
|
|
if tc.expectErr != "" {
|
|
require.Nil(t, got)
|
|
require.Error(t, err)
|
|
requireContainsLower(t, err.Error(), tc.expectErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expect, got)
|
|
}
|
|
}
|
|
|
|
t.Run(tc.name+" (snake case)", func(t *testing.T) {
|
|
testbody(t, tc.snake)
|
|
})
|
|
t.Run(tc.name+" (camel case)", func(t *testing.T) {
|
|
testbody(t, tc.camel)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDecodeConfigEntry is the 'structs' mirror image of
|
|
// command/helpers/helpers_test.go:TestParseConfigEntry
|
|
func TestDecodeConfigEntry(t *testing.T) {
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
camel string
|
|
snake string
|
|
expect ConfigEntry
|
|
expectErr string
|
|
}{
|
|
// TODO(rb): test json?
|
|
{
|
|
name: "proxy-defaults: extra fields or typo",
|
|
snake: `
|
|
kind = "proxy-defaults"
|
|
name = "main"
|
|
cornfig {
|
|
"foo" = 19
|
|
}
|
|
`,
|
|
camel: `
|
|
Kind = "proxy-defaults"
|
|
Name = "main"
|
|
Cornfig {
|
|
"foo" = 19
|
|
}
|
|
`,
|
|
expectErr: `invalid config key "cornfig"`,
|
|
},
|
|
{
|
|
name: "proxy-defaults",
|
|
snake: `
|
|
kind = "proxy-defaults"
|
|
name = "main"
|
|
meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
config {
|
|
"foo" = 19
|
|
"bar" = "abc"
|
|
"moreconfig" {
|
|
"moar" = "config"
|
|
}
|
|
"balance_inbound_connections" = "exact_balance"
|
|
}
|
|
mesh_gateway {
|
|
mode = "remote"
|
|
}
|
|
mutual_tls_mode = "permissive"
|
|
`,
|
|
camel: `
|
|
Kind = "proxy-defaults"
|
|
Name = "main"
|
|
Meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
Config {
|
|
"foo" = 19
|
|
"bar" = "abc"
|
|
"moreconfig" {
|
|
"moar" = "config"
|
|
}
|
|
"balance_inbound_connections" = "exact_balance"
|
|
}
|
|
MeshGateway {
|
|
Mode = "remote"
|
|
}
|
|
MutualTLSMode = "permissive"
|
|
`,
|
|
expect: &ProxyConfigEntry{
|
|
Kind: "proxy-defaults",
|
|
Name: "main",
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
Config: map[string]interface{}{
|
|
"foo": 19,
|
|
"bar": "abc",
|
|
"moreconfig": map[string]interface{}{
|
|
"moar": "config",
|
|
},
|
|
"balance_inbound_connections": "exact_balance",
|
|
},
|
|
MeshGateway: MeshGatewayConfig{
|
|
Mode: MeshGatewayModeRemote,
|
|
},
|
|
MutualTLSMode: MutualTLSModePermissive,
|
|
},
|
|
},
|
|
{
|
|
name: "service-defaults",
|
|
snake: `
|
|
kind = "service-defaults"
|
|
name = "main"
|
|
meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
protocol = "http"
|
|
external_sni = "abc-123"
|
|
mesh_gateway {
|
|
mode = "remote"
|
|
}
|
|
mutual_tls_mode = "permissive"
|
|
balance_inbound_connections = "exact_balance"
|
|
upstream_config {
|
|
overrides = [
|
|
{
|
|
name = "redis"
|
|
passive_health_check {
|
|
interval = "2s"
|
|
max_failures = 3
|
|
enforcing_consecutive_5xx = 4
|
|
max_ejection_percent = 5
|
|
base_ejection_time = "6s"
|
|
}
|
|
},
|
|
{
|
|
name = "finance--billing"
|
|
mesh_gateway {
|
|
mode = "remote"
|
|
}
|
|
},
|
|
]
|
|
defaults {
|
|
connect_timeout_ms = 5
|
|
protocol = "http"
|
|
balance_outbound_connections = "exact_balance"
|
|
envoy_listener_json = "foo"
|
|
envoy_cluster_json = "bar"
|
|
limits {
|
|
max_connections = 3
|
|
max_pending_requests = 4
|
|
max_concurrent_requests = 5
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
camel: `
|
|
Kind = "service-defaults"
|
|
Name = "main"
|
|
Meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
Protocol = "http"
|
|
ExternalSNI = "abc-123"
|
|
MeshGateway {
|
|
Mode = "remote"
|
|
}
|
|
MutualTLSMode = "permissive"
|
|
BalanceInboundConnections = "exact_balance"
|
|
UpstreamConfig {
|
|
Overrides = [
|
|
{
|
|
Name = "redis"
|
|
PassiveHealthCheck {
|
|
MaxFailures = 3
|
|
Interval = "2s"
|
|
EnforcingConsecutive5xx = 4
|
|
MaxEjectionPercent = 5
|
|
BaseEjectionTime = "6s"
|
|
}
|
|
},
|
|
{
|
|
Name = "finance--billing"
|
|
MeshGateway {
|
|
Mode = "remote"
|
|
}
|
|
},
|
|
]
|
|
Defaults {
|
|
EnvoyListenerJSON = "foo"
|
|
EnvoyClusterJSON = "bar"
|
|
ConnectTimeoutMs = 5
|
|
Protocol = "http"
|
|
Limits {
|
|
MaxConnections = 3
|
|
MaxPendingRequests = 4
|
|
MaxConcurrentRequests = 5
|
|
}
|
|
BalanceOutboundConnections = "exact_balance"
|
|
}
|
|
}
|
|
`,
|
|
expect: &ServiceConfigEntry{
|
|
Kind: "service-defaults",
|
|
Name: "main",
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
Protocol: "http",
|
|
ExternalSNI: "abc-123",
|
|
MeshGateway: MeshGatewayConfig{
|
|
Mode: MeshGatewayModeRemote,
|
|
},
|
|
MutualTLSMode: MutualTLSModePermissive,
|
|
BalanceInboundConnections: "exact_balance",
|
|
UpstreamConfig: &UpstreamConfiguration{
|
|
Overrides: []*UpstreamConfig{
|
|
{
|
|
Name: "redis",
|
|
PassiveHealthCheck: &PassiveHealthCheck{
|
|
MaxFailures: 3,
|
|
Interval: 2 * time.Second,
|
|
EnforcingConsecutive5xx: uintPointer(4),
|
|
MaxEjectionPercent: uintPointer(5),
|
|
BaseEjectionTime: durationPointer(6 * time.Second),
|
|
},
|
|
},
|
|
{
|
|
Name: "finance--billing",
|
|
MeshGateway: MeshGatewayConfig{Mode: MeshGatewayModeRemote},
|
|
},
|
|
},
|
|
Defaults: &UpstreamConfig{
|
|
EnvoyListenerJSON: "foo",
|
|
EnvoyClusterJSON: "bar",
|
|
ConnectTimeoutMs: 5,
|
|
Protocol: "http",
|
|
Limits: &UpstreamLimits{
|
|
MaxConnections: intPointer(3),
|
|
MaxPendingRequests: intPointer(4),
|
|
MaxConcurrentRequests: intPointer(5),
|
|
},
|
|
BalanceOutboundConnections: "exact_balance",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "service-defaults-with-destination",
|
|
snake: `
|
|
kind = "service-defaults"
|
|
name = "external"
|
|
protocol = "tcp"
|
|
destination {
|
|
addresses = [
|
|
"api.google.com",
|
|
"web.google.com"
|
|
]
|
|
port = 8080
|
|
}
|
|
`,
|
|
camel: `
|
|
Kind = "service-defaults"
|
|
Name = "external"
|
|
Protocol = "tcp"
|
|
Destination {
|
|
Addresses = [
|
|
"api.google.com",
|
|
"web.google.com"
|
|
]
|
|
Port = 8080
|
|
}
|
|
`,
|
|
expect: &ServiceConfigEntry{
|
|
Kind: "service-defaults",
|
|
Name: "external",
|
|
Protocol: "tcp",
|
|
Destination: &DestinationConfig{
|
|
Addresses: []string{
|
|
"api.google.com",
|
|
"web.google.com",
|
|
},
|
|
Port: 8080,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "service-router: kitchen sink",
|
|
snake: `
|
|
kind = "service-router"
|
|
name = "main"
|
|
meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
routes = [
|
|
{
|
|
match {
|
|
http {
|
|
path_exact = "/foo"
|
|
header = [
|
|
{
|
|
name = "debug1"
|
|
present = true
|
|
},
|
|
{
|
|
name = "debug2"
|
|
present = false
|
|
invert = true
|
|
},
|
|
{
|
|
name = "debug3"
|
|
exact = "1"
|
|
},
|
|
{
|
|
name = "debug4"
|
|
prefix = "aaa"
|
|
},
|
|
{
|
|
name = "debug5"
|
|
suffix = "bbb"
|
|
},
|
|
{
|
|
name = "debug6"
|
|
regex = "a.*z"
|
|
},
|
|
]
|
|
}
|
|
}
|
|
destination {
|
|
service = "carrot"
|
|
service_subset = "kale"
|
|
namespace = "leek"
|
|
prefix_rewrite = "/alternate"
|
|
request_timeout = "99s"
|
|
idle_timeout = "99s"
|
|
num_retries = 12345
|
|
retry_on_connect_failure = true
|
|
retry_on_status_codes = [401, 209]
|
|
request_headers {
|
|
add {
|
|
x-foo = "bar"
|
|
}
|
|
set {
|
|
bar = "baz"
|
|
}
|
|
remove = ["qux"]
|
|
}
|
|
response_headers {
|
|
add {
|
|
x-foo = "bar"
|
|
}
|
|
set {
|
|
bar = "baz"
|
|
}
|
|
remove = ["qux"]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
match {
|
|
http {
|
|
path_prefix = "/foo"
|
|
methods = [ "GET", "DELETE" ]
|
|
query_param = [
|
|
{
|
|
name = "hack1"
|
|
present = true
|
|
},
|
|
{
|
|
name = "hack2"
|
|
exact = "1"
|
|
},
|
|
{
|
|
name = "hack3"
|
|
regex = "a.*z"
|
|
},
|
|
]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
match {
|
|
http {
|
|
path_regex = "/foo"
|
|
}
|
|
}
|
|
},
|
|
]
|
|
`,
|
|
camel: `
|
|
Kind = "service-router"
|
|
Name = "main"
|
|
Meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
Routes = [
|
|
{
|
|
Match {
|
|
HTTP {
|
|
PathExact = "/foo"
|
|
Header = [
|
|
{
|
|
Name = "debug1"
|
|
Present = true
|
|
},
|
|
{
|
|
Name = "debug2"
|
|
Present = false
|
|
Invert = true
|
|
},
|
|
{
|
|
Name = "debug3"
|
|
Exact = "1"
|
|
},
|
|
{
|
|
Name = "debug4"
|
|
Prefix = "aaa"
|
|
},
|
|
{
|
|
Name = "debug5"
|
|
Suffix = "bbb"
|
|
},
|
|
{
|
|
Name = "debug6"
|
|
Regex = "a.*z"
|
|
},
|
|
]
|
|
}
|
|
}
|
|
Destination {
|
|
Service = "carrot"
|
|
ServiceSubset = "kale"
|
|
Namespace = "leek"
|
|
PrefixRewrite = "/alternate"
|
|
RequestTimeout = "99s"
|
|
IdleTimeout = "99s"
|
|
NumRetries = 12345
|
|
RetryOnConnectFailure = true
|
|
RetryOnStatusCodes = [401, 209]
|
|
RequestHeaders {
|
|
Add {
|
|
x-foo = "bar"
|
|
}
|
|
Set {
|
|
bar = "baz"
|
|
}
|
|
Remove = ["qux"]
|
|
}
|
|
ResponseHeaders {
|
|
Add {
|
|
x-foo = "bar"
|
|
}
|
|
Set {
|
|
bar = "baz"
|
|
}
|
|
Remove = ["qux"]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
Match {
|
|
HTTP {
|
|
PathPrefix = "/foo"
|
|
Methods = [ "GET", "DELETE" ]
|
|
QueryParam = [
|
|
{
|
|
Name = "hack1"
|
|
Present = true
|
|
},
|
|
{
|
|
Name = "hack2"
|
|
Exact = "1"
|
|
},
|
|
{
|
|
Name = "hack3"
|
|
Regex = "a.*z"
|
|
},
|
|
]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
Match {
|
|
HTTP {
|
|
PathRegex = "/foo"
|
|
}
|
|
}
|
|
},
|
|
]
|
|
`,
|
|
expect: &ServiceRouterConfigEntry{
|
|
Kind: "service-router",
|
|
Name: "main",
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
Routes: []ServiceRoute{
|
|
{
|
|
Match: &ServiceRouteMatch{
|
|
HTTP: &ServiceRouteHTTPMatch{
|
|
PathExact: "/foo",
|
|
Header: []ServiceRouteHTTPMatchHeader{
|
|
{
|
|
Name: "debug1",
|
|
Present: true,
|
|
},
|
|
{
|
|
Name: "debug2",
|
|
Present: false,
|
|
Invert: true,
|
|
},
|
|
{
|
|
Name: "debug3",
|
|
Exact: "1",
|
|
},
|
|
{
|
|
Name: "debug4",
|
|
Prefix: "aaa",
|
|
},
|
|
{
|
|
Name: "debug5",
|
|
Suffix: "bbb",
|
|
},
|
|
{
|
|
Name: "debug6",
|
|
Regex: "a.*z",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Destination: &ServiceRouteDestination{
|
|
Service: "carrot",
|
|
ServiceSubset: "kale",
|
|
Namespace: "leek",
|
|
PrefixRewrite: "/alternate",
|
|
RequestTimeout: 99 * time.Second,
|
|
IdleTimeout: 99 * time.Second,
|
|
NumRetries: 12345,
|
|
RetryOnConnectFailure: true,
|
|
RetryOnStatusCodes: []uint32{401, 209},
|
|
RequestHeaders: &HTTPHeaderModifiers{
|
|
Add: map[string]string{"x-foo": "bar"},
|
|
Set: map[string]string{"bar": "baz"},
|
|
Remove: []string{"qux"},
|
|
},
|
|
ResponseHeaders: &HTTPHeaderModifiers{
|
|
Add: map[string]string{"x-foo": "bar"},
|
|
Set: map[string]string{"bar": "baz"},
|
|
Remove: []string{"qux"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Match: &ServiceRouteMatch{
|
|
HTTP: &ServiceRouteHTTPMatch{
|
|
PathPrefix: "/foo",
|
|
Methods: []string{"GET", "DELETE"},
|
|
QueryParam: []ServiceRouteHTTPMatchQueryParam{
|
|
{
|
|
Name: "hack1",
|
|
Present: true,
|
|
},
|
|
{
|
|
Name: "hack2",
|
|
Exact: "1",
|
|
},
|
|
{
|
|
Name: "hack3",
|
|
Regex: "a.*z",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Match: &ServiceRouteMatch{
|
|
HTTP: &ServiceRouteHTTPMatch{
|
|
PathRegex: "/foo",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "service-router: kitchen sink case insensitive",
|
|
snake: `
|
|
kind = "service-router"
|
|
name = "main"
|
|
meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
routes = [
|
|
{
|
|
match {
|
|
http {
|
|
path_exact = "/foo"
|
|
case_insensitive = true
|
|
header = [
|
|
{
|
|
name = "debug1"
|
|
present = true
|
|
},
|
|
{
|
|
name = "debug2"
|
|
present = false
|
|
invert = true
|
|
},
|
|
{
|
|
name = "debug3"
|
|
exact = "1"
|
|
},
|
|
{
|
|
name = "debug4"
|
|
prefix = "aaa"
|
|
},
|
|
{
|
|
name = "debug5"
|
|
suffix = "bbb"
|
|
},
|
|
{
|
|
name = "debug6"
|
|
regex = "a.*z"
|
|
},
|
|
]
|
|
}
|
|
}
|
|
destination {
|
|
service = "carrot"
|
|
service_subset = "kale"
|
|
namespace = "leek"
|
|
prefix_rewrite = "/alternate"
|
|
request_timeout = "99s"
|
|
idle_timeout = "99s"
|
|
num_retries = 12345
|
|
retry_on_connect_failure = true
|
|
retry_on_status_codes = [401, 209]
|
|
request_headers {
|
|
add {
|
|
x-foo = "bar"
|
|
}
|
|
set {
|
|
bar = "baz"
|
|
}
|
|
remove = ["qux"]
|
|
}
|
|
response_headers {
|
|
add {
|
|
x-foo = "bar"
|
|
}
|
|
set {
|
|
bar = "baz"
|
|
}
|
|
remove = ["qux"]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
match {
|
|
http {
|
|
path_prefix = "/foo"
|
|
methods = [ "GET", "DELETE" ]
|
|
query_param = [
|
|
{
|
|
name = "hack1"
|
|
present = true
|
|
},
|
|
{
|
|
name = "hack2"
|
|
exact = "1"
|
|
},
|
|
{
|
|
name = "hack3"
|
|
regex = "a.*z"
|
|
},
|
|
]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
match {
|
|
http {
|
|
path_regex = "/foo"
|
|
}
|
|
}
|
|
},
|
|
]
|
|
`,
|
|
camel: `
|
|
Kind = "service-router"
|
|
Name = "main"
|
|
Meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
Routes = [
|
|
{
|
|
Match {
|
|
HTTP {
|
|
PathExact = "/foo"
|
|
CaseInsensitive = true
|
|
Header = [
|
|
{
|
|
Name = "debug1"
|
|
Present = true
|
|
},
|
|
{
|
|
Name = "debug2"
|
|
Present = false
|
|
Invert = true
|
|
},
|
|
{
|
|
Name = "debug3"
|
|
Exact = "1"
|
|
},
|
|
{
|
|
Name = "debug4"
|
|
Prefix = "aaa"
|
|
},
|
|
{
|
|
Name = "debug5"
|
|
Suffix = "bbb"
|
|
},
|
|
{
|
|
Name = "debug6"
|
|
Regex = "a.*z"
|
|
},
|
|
]
|
|
}
|
|
}
|
|
Destination {
|
|
Service = "carrot"
|
|
ServiceSubset = "kale"
|
|
Namespace = "leek"
|
|
PrefixRewrite = "/alternate"
|
|
RequestTimeout = "99s"
|
|
IdleTimeout = "99s"
|
|
NumRetries = 12345
|
|
RetryOnConnectFailure = true
|
|
RetryOnStatusCodes = [401, 209]
|
|
RequestHeaders {
|
|
Add {
|
|
x-foo = "bar"
|
|
}
|
|
Set {
|
|
bar = "baz"
|
|
}
|
|
Remove = ["qux"]
|
|
}
|
|
ResponseHeaders {
|
|
Add {
|
|
x-foo = "bar"
|
|
}
|
|
Set {
|
|
bar = "baz"
|
|
}
|
|
Remove = ["qux"]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
Match {
|
|
HTTP {
|
|
PathPrefix = "/foo"
|
|
Methods = [ "GET", "DELETE" ]
|
|
QueryParam = [
|
|
{
|
|
Name = "hack1"
|
|
Present = true
|
|
},
|
|
{
|
|
Name = "hack2"
|
|
Exact = "1"
|
|
},
|
|
{
|
|
Name = "hack3"
|
|
Regex = "a.*z"
|
|
},
|
|
]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
Match {
|
|
HTTP {
|
|
PathRegex = "/foo"
|
|
}
|
|
}
|
|
},
|
|
]
|
|
`,
|
|
expect: &ServiceRouterConfigEntry{
|
|
Kind: "service-router",
|
|
Name: "main",
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
Routes: []ServiceRoute{
|
|
{
|
|
Match: &ServiceRouteMatch{
|
|
HTTP: &ServiceRouteHTTPMatch{
|
|
PathExact: "/foo",
|
|
CaseInsensitive: true,
|
|
Header: []ServiceRouteHTTPMatchHeader{
|
|
{
|
|
Name: "debug1",
|
|
Present: true,
|
|
},
|
|
{
|
|
Name: "debug2",
|
|
Present: false,
|
|
Invert: true,
|
|
},
|
|
{
|
|
Name: "debug3",
|
|
Exact: "1",
|
|
},
|
|
{
|
|
Name: "debug4",
|
|
Prefix: "aaa",
|
|
},
|
|
{
|
|
Name: "debug5",
|
|
Suffix: "bbb",
|
|
},
|
|
{
|
|
Name: "debug6",
|
|
Regex: "a.*z",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Destination: &ServiceRouteDestination{
|
|
Service: "carrot",
|
|
ServiceSubset: "kale",
|
|
Namespace: "leek",
|
|
PrefixRewrite: "/alternate",
|
|
RequestTimeout: 99 * time.Second,
|
|
IdleTimeout: 99 * time.Second,
|
|
NumRetries: 12345,
|
|
RetryOnConnectFailure: true,
|
|
RetryOnStatusCodes: []uint32{401, 209},
|
|
RequestHeaders: &HTTPHeaderModifiers{
|
|
Add: map[string]string{"x-foo": "bar"},
|
|
Set: map[string]string{"bar": "baz"},
|
|
Remove: []string{"qux"},
|
|
},
|
|
ResponseHeaders: &HTTPHeaderModifiers{
|
|
Add: map[string]string{"x-foo": "bar"},
|
|
Set: map[string]string{"bar": "baz"},
|
|
Remove: []string{"qux"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Match: &ServiceRouteMatch{
|
|
HTTP: &ServiceRouteHTTPMatch{
|
|
PathPrefix: "/foo",
|
|
Methods: []string{"GET", "DELETE"},
|
|
QueryParam: []ServiceRouteHTTPMatchQueryParam{
|
|
{
|
|
Name: "hack1",
|
|
Present: true,
|
|
},
|
|
{
|
|
Name: "hack2",
|
|
Exact: "1",
|
|
},
|
|
{
|
|
Name: "hack3",
|
|
Regex: "a.*z",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Match: &ServiceRouteMatch{
|
|
HTTP: &ServiceRouteHTTPMatch{
|
|
PathRegex: "/foo",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "service-splitter: kitchen sink",
|
|
snake: `
|
|
kind = "service-splitter"
|
|
name = "main"
|
|
meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
splits = [
|
|
{
|
|
weight = 99.1
|
|
service_subset = "v1"
|
|
request_headers {
|
|
add {
|
|
foo = "bar"
|
|
}
|
|
set {
|
|
bar = "baz"
|
|
}
|
|
remove = ["qux"]
|
|
}
|
|
response_headers {
|
|
add {
|
|
foo = "bar"
|
|
}
|
|
set {
|
|
bar = "baz"
|
|
}
|
|
remove = ["qux"]
|
|
}
|
|
},
|
|
{
|
|
weight = 0.9
|
|
service = "other"
|
|
namespace = "alt"
|
|
},
|
|
]
|
|
`,
|
|
camel: `
|
|
Kind = "service-splitter"
|
|
Name = "main"
|
|
Meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
Splits = [
|
|
{
|
|
Weight = 99.1
|
|
ServiceSubset = "v1"
|
|
RequestHeaders {
|
|
Add {
|
|
foo = "bar"
|
|
}
|
|
Set {
|
|
bar = "baz"
|
|
}
|
|
Remove = ["qux"]
|
|
}
|
|
ResponseHeaders {
|
|
Add {
|
|
foo = "bar"
|
|
}
|
|
Set {
|
|
bar = "baz"
|
|
}
|
|
Remove = ["qux"]
|
|
}
|
|
},
|
|
{
|
|
Weight = 0.9
|
|
Service = "other"
|
|
Namespace = "alt"
|
|
},
|
|
]
|
|
`,
|
|
expect: &ServiceSplitterConfigEntry{
|
|
Kind: ServiceSplitter,
|
|
Name: "main",
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
Splits: []ServiceSplit{
|
|
{
|
|
Weight: 99.1,
|
|
ServiceSubset: "v1",
|
|
RequestHeaders: &HTTPHeaderModifiers{
|
|
Add: map[string]string{"foo": "bar"},
|
|
Set: map[string]string{"bar": "baz"},
|
|
Remove: []string{"qux"},
|
|
},
|
|
ResponseHeaders: &HTTPHeaderModifiers{
|
|
Add: map[string]string{"foo": "bar"},
|
|
Set: map[string]string{"bar": "baz"},
|
|
Remove: []string{"qux"},
|
|
},
|
|
},
|
|
{
|
|
Weight: 0.9,
|
|
Service: "other",
|
|
Namespace: "alt",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "service-resolver: subsets with failover",
|
|
snake: `
|
|
kind = "service-resolver"
|
|
name = "main"
|
|
meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
default_subset = "v1"
|
|
connect_timeout = "15s"
|
|
subsets = {
|
|
"v1" = {
|
|
filter = "Service.Meta.version == v1"
|
|
},
|
|
"v2" = {
|
|
filter = "Service.Meta.version == v2"
|
|
only_passing = true
|
|
},
|
|
}
|
|
failover = {
|
|
"v2" = {
|
|
service = "failcopy"
|
|
service_subset = "sure"
|
|
namespace = "neighbor"
|
|
datacenters = ["dc5", "dc14"]
|
|
},
|
|
"*" = {
|
|
datacenters = ["dc7"]
|
|
}
|
|
}`,
|
|
camel: `
|
|
Kind = "service-resolver"
|
|
Name = "main"
|
|
Meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
DefaultSubset = "v1"
|
|
ConnectTimeout = "15s"
|
|
Subsets = {
|
|
"v1" = {
|
|
Filter = "Service.Meta.version == v1"
|
|
},
|
|
"v2" = {
|
|
Filter = "Service.Meta.version == v2"
|
|
OnlyPassing = true
|
|
},
|
|
}
|
|
Failover = {
|
|
"v2" = {
|
|
Service = "failcopy"
|
|
ServiceSubset = "sure"
|
|
Namespace = "neighbor"
|
|
Datacenters = ["dc5", "dc14"]
|
|
},
|
|
"*" = {
|
|
Datacenters = ["dc7"]
|
|
}
|
|
}`,
|
|
expect: &ServiceResolverConfigEntry{
|
|
Kind: "service-resolver",
|
|
Name: "main",
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
DefaultSubset: "v1",
|
|
ConnectTimeout: 15 * time.Second,
|
|
Subsets: map[string]ServiceResolverSubset{
|
|
"v1": {
|
|
Filter: "Service.Meta.version == v1",
|
|
},
|
|
"v2": {
|
|
Filter: "Service.Meta.version == v2",
|
|
OnlyPassing: true,
|
|
},
|
|
},
|
|
Failover: map[string]ServiceResolverFailover{
|
|
"v2": {
|
|
Service: "failcopy",
|
|
ServiceSubset: "sure",
|
|
Namespace: "neighbor",
|
|
Datacenters: []string{"dc5", "dc14"},
|
|
},
|
|
"*": {
|
|
Datacenters: []string{"dc7"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "service-resolver: redirect",
|
|
snake: `
|
|
kind = "service-resolver"
|
|
name = "main"
|
|
redirect {
|
|
service = "other"
|
|
service_subset = "backup"
|
|
namespace = "alt"
|
|
datacenter = "dc9"
|
|
}
|
|
`,
|
|
camel: `
|
|
Kind = "service-resolver"
|
|
Name = "main"
|
|
Redirect {
|
|
Service = "other"
|
|
ServiceSubset = "backup"
|
|
Namespace = "alt"
|
|
Datacenter = "dc9"
|
|
}
|
|
`,
|
|
expect: &ServiceResolverConfigEntry{
|
|
Kind: "service-resolver",
|
|
Name: "main",
|
|
Redirect: &ServiceResolverRedirect{
|
|
Service: "other",
|
|
ServiceSubset: "backup",
|
|
Namespace: "alt",
|
|
Datacenter: "dc9",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "service-resolver: default",
|
|
snake: `
|
|
kind = "service-resolver"
|
|
name = "main"
|
|
`,
|
|
camel: `
|
|
Kind = "service-resolver"
|
|
Name = "main"
|
|
`,
|
|
expect: &ServiceResolverConfigEntry{
|
|
Kind: "service-resolver",
|
|
Name: "main",
|
|
},
|
|
},
|
|
{
|
|
name: "service-resolver: envoy hash lb kitchen sink",
|
|
snake: `
|
|
kind = "service-resolver"
|
|
name = "main"
|
|
load_balancer = {
|
|
policy = "ring_hash"
|
|
ring_hash_config = {
|
|
minimum_ring_size = 1
|
|
maximum_ring_size = 2
|
|
}
|
|
hash_policies = [
|
|
{
|
|
field = "cookie"
|
|
field_value = "good-cookie"
|
|
cookie_config = {
|
|
ttl = "1s"
|
|
path = "/oven"
|
|
}
|
|
terminal = true
|
|
},
|
|
{
|
|
field = "cookie"
|
|
field_value = "less-good-cookie"
|
|
cookie_config = {
|
|
session = true
|
|
path = "/toaster"
|
|
}
|
|
terminal = true
|
|
},
|
|
{
|
|
field = "header"
|
|
field_value = "x-user-id"
|
|
},
|
|
{
|
|
source_ip = true
|
|
}
|
|
]
|
|
}
|
|
`,
|
|
camel: `
|
|
Kind = "service-resolver"
|
|
Name = "main"
|
|
LoadBalancer = {
|
|
Policy = "ring_hash"
|
|
RingHashConfig = {
|
|
MinimumRingSize = 1
|
|
MaximumRingSize = 2
|
|
}
|
|
HashPolicies = [
|
|
{
|
|
Field = "cookie"
|
|
FieldValue = "good-cookie"
|
|
CookieConfig = {
|
|
TTL = "1s"
|
|
Path = "/oven"
|
|
}
|
|
Terminal = true
|
|
},
|
|
{
|
|
Field = "cookie"
|
|
FieldValue = "less-good-cookie"
|
|
CookieConfig = {
|
|
Session = true
|
|
Path = "/toaster"
|
|
}
|
|
Terminal = true
|
|
},
|
|
{
|
|
Field = "header"
|
|
FieldValue = "x-user-id"
|
|
},
|
|
{
|
|
SourceIP = true
|
|
}
|
|
]
|
|
}
|
|
`,
|
|
expect: &ServiceResolverConfigEntry{
|
|
Kind: "service-resolver",
|
|
Name: "main",
|
|
LoadBalancer: &LoadBalancer{
|
|
Policy: LBPolicyRingHash,
|
|
RingHashConfig: &RingHashConfig{
|
|
MinimumRingSize: 1,
|
|
MaximumRingSize: 2,
|
|
},
|
|
HashPolicies: []HashPolicy{
|
|
{
|
|
Field: HashPolicyCookie,
|
|
FieldValue: "good-cookie",
|
|
CookieConfig: &CookieConfig{
|
|
TTL: 1 * time.Second,
|
|
Path: "/oven",
|
|
},
|
|
Terminal: true,
|
|
},
|
|
{
|
|
Field: HashPolicyCookie,
|
|
FieldValue: "less-good-cookie",
|
|
CookieConfig: &CookieConfig{
|
|
Session: true,
|
|
Path: "/toaster",
|
|
},
|
|
Terminal: true,
|
|
},
|
|
{
|
|
Field: HashPolicyHeader,
|
|
FieldValue: "x-user-id",
|
|
},
|
|
{
|
|
SourceIP: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "service-resolver: envoy least request kitchen sink",
|
|
snake: `
|
|
kind = "service-resolver"
|
|
name = "main"
|
|
load_balancer = {
|
|
policy = "least_request"
|
|
least_request_config = {
|
|
choice_count = 2
|
|
}
|
|
}
|
|
`,
|
|
camel: `
|
|
Kind = "service-resolver"
|
|
Name = "main"
|
|
LoadBalancer = {
|
|
Policy = "least_request"
|
|
LeastRequestConfig = {
|
|
ChoiceCount = 2
|
|
}
|
|
}
|
|
`,
|
|
expect: &ServiceResolverConfigEntry{
|
|
Kind: "service-resolver",
|
|
Name: "main",
|
|
LoadBalancer: &LoadBalancer{
|
|
Policy: LBPolicyLeastRequest,
|
|
LeastRequestConfig: &LeastRequestConfig{
|
|
ChoiceCount: 2,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// TODO(rb): test SDS stuff here in both places (global/service)
|
|
name: "ingress-gateway: kitchen sink",
|
|
snake: `
|
|
kind = "ingress-gateway"
|
|
name = "ingress-web"
|
|
meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
|
|
tls {
|
|
enabled = true
|
|
tls_min_version = "TLSv1_1"
|
|
tls_max_version = "TLSv1_2"
|
|
cipher_suites = [
|
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
|
|
]
|
|
}
|
|
|
|
listeners = [
|
|
{
|
|
port = 8080
|
|
protocol = "http"
|
|
services = [
|
|
{
|
|
name = "web"
|
|
hosts = ["test.example.com", "test2.example.com"]
|
|
},
|
|
{
|
|
name = "db"
|
|
request_headers {
|
|
add {
|
|
foo = "bar"
|
|
}
|
|
set {
|
|
bar = "baz"
|
|
}
|
|
remove = ["qux"]
|
|
}
|
|
response_headers {
|
|
add {
|
|
foo = "bar"
|
|
}
|
|
set {
|
|
bar = "baz"
|
|
}
|
|
remove = ["qux"]
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
port = 9999
|
|
protocol = "tcp"
|
|
services = [
|
|
{
|
|
name = "mysql"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
port = 2234
|
|
protocol = "tcp"
|
|
services = [
|
|
{
|
|
name = "postgres"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
`,
|
|
camel: `
|
|
Kind = "ingress-gateway"
|
|
Name = "ingress-web"
|
|
Meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
TLS {
|
|
Enabled = true
|
|
TLSMinVersion = "TLSv1_1"
|
|
TLSMaxVersion = "TLSv1_2"
|
|
CipherSuites = [
|
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
|
|
]
|
|
}
|
|
Listeners = [
|
|
{
|
|
Port = 8080
|
|
Protocol = "http"
|
|
Services = [
|
|
{
|
|
Name = "web"
|
|
Hosts = ["test.example.com", "test2.example.com"]
|
|
},
|
|
{
|
|
Name = "db"
|
|
RequestHeaders {
|
|
Add {
|
|
foo = "bar"
|
|
}
|
|
Set {
|
|
bar = "baz"
|
|
}
|
|
Remove = ["qux"]
|
|
}
|
|
ResponseHeaders {
|
|
Add {
|
|
foo = "bar"
|
|
}
|
|
Set {
|
|
bar = "baz"
|
|
}
|
|
Remove = ["qux"]
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
Port = 9999
|
|
Protocol = "tcp"
|
|
Services = [
|
|
{
|
|
Name = "mysql"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
Port = 2234
|
|
Protocol = "tcp"
|
|
Services = [
|
|
{
|
|
Name = "postgres"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
`,
|
|
expect: &IngressGatewayConfigEntry{
|
|
Kind: "ingress-gateway",
|
|
Name: "ingress-web",
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
TLS: GatewayTLSConfig{
|
|
Enabled: true,
|
|
TLSMinVersion: types.TLSv1_1,
|
|
TLSMaxVersion: types.TLSv1_2,
|
|
CipherSuites: []types.TLSCipherSuite{
|
|
types.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
types.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
},
|
|
},
|
|
Listeners: []IngressListener{
|
|
{
|
|
Port: 8080,
|
|
Protocol: "http",
|
|
Services: []IngressService{
|
|
{
|
|
Name: "web",
|
|
Hosts: []string{"test.example.com", "test2.example.com"},
|
|
},
|
|
{
|
|
Name: "db",
|
|
RequestHeaders: &HTTPHeaderModifiers{
|
|
Add: map[string]string{"foo": "bar"},
|
|
Set: map[string]string{"bar": "baz"},
|
|
Remove: []string{"qux"},
|
|
},
|
|
ResponseHeaders: &HTTPHeaderModifiers{
|
|
Add: map[string]string{"foo": "bar"},
|
|
Set: map[string]string{"bar": "baz"},
|
|
Remove: []string{"qux"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Port: 9999,
|
|
Protocol: "tcp",
|
|
Services: []IngressService{
|
|
{
|
|
Name: "mysql",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Port: 2234,
|
|
Protocol: "tcp",
|
|
Services: []IngressService{
|
|
{
|
|
Name: "postgres",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "terminating-gateway: kitchen sink",
|
|
snake: `
|
|
kind = "terminating-gateway"
|
|
name = "terminating-gw-west"
|
|
meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
services = [
|
|
{
|
|
name = "payments",
|
|
ca_file = "/etc/payments/ca.pem",
|
|
cert_file = "/etc/payments/cert.pem",
|
|
key_file = "/etc/payments/tls.key",
|
|
sni = "mydomain",
|
|
},
|
|
{
|
|
name = "*",
|
|
ca_file = "/etc/all/ca.pem",
|
|
cert_file = "/etc/all/cert.pem",
|
|
key_file = "/etc/all/tls.key",
|
|
sni = "my-alt-domain",
|
|
},
|
|
]
|
|
`,
|
|
camel: `
|
|
Kind = "terminating-gateway"
|
|
Name = "terminating-gw-west"
|
|
Meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
Services = [
|
|
{
|
|
Name = "payments",
|
|
CAFile = "/etc/payments/ca.pem",
|
|
CertFile = "/etc/payments/cert.pem",
|
|
KeyFile = "/etc/payments/tls.key",
|
|
SNI = "mydomain",
|
|
},
|
|
{
|
|
Name = "*",
|
|
CAFile = "/etc/all/ca.pem",
|
|
CertFile = "/etc/all/cert.pem",
|
|
KeyFile = "/etc/all/tls.key",
|
|
SNI = "my-alt-domain",
|
|
},
|
|
]
|
|
`,
|
|
expect: &TerminatingGatewayConfigEntry{
|
|
Kind: "terminating-gateway",
|
|
Name: "terminating-gw-west",
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
Services: []LinkedService{
|
|
{
|
|
Name: "payments",
|
|
CAFile: "/etc/payments/ca.pem",
|
|
CertFile: "/etc/payments/cert.pem",
|
|
KeyFile: "/etc/payments/tls.key",
|
|
SNI: "mydomain",
|
|
},
|
|
{
|
|
Name: "*",
|
|
CAFile: "/etc/all/ca.pem",
|
|
CertFile: "/etc/all/cert.pem",
|
|
KeyFile: "/etc/all/tls.key",
|
|
SNI: "my-alt-domain",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "service-intentions: kitchen sink",
|
|
snake: `
|
|
kind = "service-intentions"
|
|
name = "web"
|
|
meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
sources = [
|
|
{
|
|
name = "foo"
|
|
action = "deny"
|
|
type = "consul"
|
|
description = "foo desc"
|
|
},
|
|
{
|
|
name = "bar"
|
|
action = "allow"
|
|
description = "bar desc"
|
|
},
|
|
{
|
|
name = "l7"
|
|
permissions = [
|
|
{
|
|
action = "deny"
|
|
http {
|
|
path_exact = "/admin"
|
|
header = [
|
|
{
|
|
name = "hdr-present"
|
|
present = true
|
|
},
|
|
{
|
|
name = "hdr-exact"
|
|
exact = "exact"
|
|
},
|
|
{
|
|
name = "hdr-prefix"
|
|
prefix = "prefix"
|
|
},
|
|
{
|
|
name = "hdr-suffix"
|
|
suffix = "suffix"
|
|
},
|
|
{
|
|
name = "hdr-regex"
|
|
regex = "regex"
|
|
},
|
|
{
|
|
name = "hdr-absent"
|
|
present = true
|
|
invert = true
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
action = "allow"
|
|
http {
|
|
path_prefix = "/v3/"
|
|
}
|
|
},
|
|
{
|
|
action = "allow"
|
|
http {
|
|
path_regex = "/v[12]/.*"
|
|
methods = ["GET", "POST"]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
sources {
|
|
name = "*"
|
|
action = "deny"
|
|
description = "wild desc"
|
|
}
|
|
`,
|
|
camel: `
|
|
Kind = "service-intentions"
|
|
Name = "web"
|
|
Meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
Sources = [
|
|
{
|
|
Name = "foo"
|
|
Action = "deny"
|
|
Type = "consul"
|
|
Description = "foo desc"
|
|
},
|
|
{
|
|
Name = "bar"
|
|
Action = "allow"
|
|
Description = "bar desc"
|
|
},
|
|
{
|
|
Name = "l7"
|
|
Permissions = [
|
|
{
|
|
Action = "deny"
|
|
HTTP {
|
|
PathExact = "/admin"
|
|
Header = [
|
|
{
|
|
Name = "hdr-present"
|
|
Present = true
|
|
},
|
|
{
|
|
Name = "hdr-exact"
|
|
Exact = "exact"
|
|
},
|
|
{
|
|
Name = "hdr-prefix"
|
|
Prefix = "prefix"
|
|
},
|
|
{
|
|
Name = "hdr-suffix"
|
|
Suffix = "suffix"
|
|
},
|
|
{
|
|
Name = "hdr-regex"
|
|
Regex = "regex"
|
|
},
|
|
{
|
|
Name = "hdr-absent"
|
|
Present = true
|
|
Invert = true
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
Action = "allow"
|
|
HTTP {
|
|
PathPrefix = "/v3/"
|
|
}
|
|
},
|
|
{
|
|
Action = "allow"
|
|
HTTP {
|
|
PathRegex = "/v[12]/.*"
|
|
Methods = ["GET", "POST"]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
Sources {
|
|
Name = "*"
|
|
Action = "deny"
|
|
Description = "wild desc"
|
|
}
|
|
`,
|
|
expect: &ServiceIntentionsConfigEntry{
|
|
Kind: "service-intentions",
|
|
Name: "web",
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
Sources: []*SourceIntention{
|
|
{
|
|
Name: "foo",
|
|
Action: "deny",
|
|
Type: "consul",
|
|
Description: "foo desc",
|
|
},
|
|
{
|
|
Name: "bar",
|
|
Action: "allow",
|
|
Description: "bar desc",
|
|
},
|
|
{
|
|
Name: "l7",
|
|
Permissions: []*IntentionPermission{
|
|
{
|
|
Action: "deny",
|
|
HTTP: &IntentionHTTPPermission{
|
|
PathExact: "/admin",
|
|
Header: []IntentionHTTPHeaderPermission{
|
|
{
|
|
Name: "hdr-present",
|
|
Present: true,
|
|
},
|
|
{
|
|
Name: "hdr-exact",
|
|
Exact: "exact",
|
|
},
|
|
{
|
|
Name: "hdr-prefix",
|
|
Prefix: "prefix",
|
|
},
|
|
{
|
|
Name: "hdr-suffix",
|
|
Suffix: "suffix",
|
|
},
|
|
{
|
|
Name: "hdr-regex",
|
|
Regex: "regex",
|
|
},
|
|
{
|
|
Name: "hdr-absent",
|
|
Present: true,
|
|
Invert: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Action: "allow",
|
|
HTTP: &IntentionHTTPPermission{
|
|
PathPrefix: "/v3/",
|
|
},
|
|
},
|
|
{
|
|
Action: "allow",
|
|
HTTP: &IntentionHTTPPermission{
|
|
PathRegex: "/v[12]/.*",
|
|
Methods: []string{"GET", "POST"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "*",
|
|
Action: "deny",
|
|
Description: "wild desc",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "service-intentions: wildcard destination",
|
|
snake: `
|
|
kind = "service-intentions"
|
|
name = "*"
|
|
sources {
|
|
name = "foo"
|
|
action = "deny"
|
|
# should be parsed, but we'll ignore it later
|
|
precedence = 6
|
|
}
|
|
`,
|
|
camel: `
|
|
Kind = "service-intentions"
|
|
Name = "*"
|
|
Sources {
|
|
Name = "foo"
|
|
Action = "deny"
|
|
# should be parsed, but we'll ignore it later
|
|
Precedence = 6
|
|
}
|
|
`,
|
|
expect: &ServiceIntentionsConfigEntry{
|
|
Kind: "service-intentions",
|
|
Name: "*",
|
|
Sources: []*SourceIntention{
|
|
{
|
|
Name: "foo",
|
|
Action: "deny",
|
|
Precedence: 6,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "mesh",
|
|
snake: `
|
|
kind = "mesh"
|
|
meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
transparent_proxy {
|
|
mesh_destinations_only = true
|
|
}
|
|
allow_enabling_permissive_mutual_tls = true
|
|
tls {
|
|
incoming {
|
|
tls_min_version = "TLSv1_1"
|
|
tls_max_version = "TLSv1_2"
|
|
cipher_suites = [
|
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
|
|
]
|
|
}
|
|
outgoing {
|
|
tls_min_version = "TLSv1_1"
|
|
tls_max_version = "TLSv1_2"
|
|
cipher_suites = [
|
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
|
|
]
|
|
}
|
|
}
|
|
http {
|
|
sanitize_x_forwarded_client_cert = true
|
|
}
|
|
peering {
|
|
peer_through_mesh_gateways = true
|
|
}
|
|
`,
|
|
camel: `
|
|
Kind = "mesh"
|
|
Meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
TransparentProxy {
|
|
MeshDestinationsOnly = true
|
|
}
|
|
AllowEnablingPermissiveMutualTLS = true
|
|
TLS {
|
|
Incoming {
|
|
TLSMinVersion = "TLSv1_1"
|
|
TLSMaxVersion = "TLSv1_2"
|
|
CipherSuites = [
|
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
|
|
]
|
|
}
|
|
Outgoing {
|
|
TLSMinVersion = "TLSv1_1"
|
|
TLSMaxVersion = "TLSv1_2"
|
|
CipherSuites = [
|
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
|
|
]
|
|
}
|
|
}
|
|
HTTP {
|
|
SanitizeXForwardedClientCert = true
|
|
}
|
|
Peering {
|
|
PeerThroughMeshGateways = true
|
|
}
|
|
`,
|
|
expect: &MeshConfigEntry{
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
TransparentProxy: TransparentProxyMeshConfig{
|
|
MeshDestinationsOnly: true,
|
|
},
|
|
AllowEnablingPermissiveMutualTLS: true,
|
|
TLS: &MeshTLSConfig{
|
|
Incoming: &MeshDirectionalTLSConfig{
|
|
TLSMinVersion: types.TLSv1_1,
|
|
TLSMaxVersion: types.TLSv1_2,
|
|
CipherSuites: []types.TLSCipherSuite{
|
|
types.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
types.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
},
|
|
},
|
|
Outgoing: &MeshDirectionalTLSConfig{
|
|
TLSMinVersion: types.TLSv1_1,
|
|
TLSMaxVersion: types.TLSv1_2,
|
|
CipherSuites: []types.TLSCipherSuite{
|
|
types.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
types.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
},
|
|
},
|
|
},
|
|
HTTP: &MeshHTTPConfig{
|
|
SanitizeXForwardedClientCert: true,
|
|
},
|
|
Peering: &PeeringMeshConfig{
|
|
PeerThroughMeshGateways: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "api-gateway",
|
|
snake: `
|
|
kind = "api-gateway"
|
|
name = "foo"
|
|
meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
`,
|
|
camel: `
|
|
Kind = "api-gateway"
|
|
Name = "foo"
|
|
Meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
`,
|
|
expect: &APIGatewayConfigEntry{
|
|
Kind: "api-gateway",
|
|
Name: "foo",
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "inline-certificate",
|
|
snake: `
|
|
kind = "inline-certificate"
|
|
name = "foo"
|
|
meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
`,
|
|
camel: `
|
|
Kind = "inline-certificate"
|
|
Name = "foo"
|
|
Meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
`,
|
|
expect: &InlineCertificateConfigEntry{
|
|
Kind: "inline-certificate",
|
|
Name: "foo",
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "http-route",
|
|
snake: `
|
|
kind = "http-route"
|
|
name = "foo"
|
|
meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
`,
|
|
camel: `
|
|
Kind = "http-route"
|
|
Name = "foo"
|
|
Meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
`,
|
|
expect: &HTTPRouteConfigEntry{
|
|
Kind: "http-route",
|
|
Name: "foo",
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "tcp-route",
|
|
snake: `
|
|
kind = "tcp-route"
|
|
name = "foo"
|
|
meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
`,
|
|
camel: `
|
|
Kind = "tcp-route"
|
|
Name = "foo"
|
|
Meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
`,
|
|
expect: &TCPRouteConfigEntry{
|
|
Kind: "tcp-route",
|
|
Name: "foo",
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "exported-services",
|
|
snake: `
|
|
kind = "exported-services"
|
|
name = "foo"
|
|
meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
services = [
|
|
{
|
|
name = "web"
|
|
namespace = "foo"
|
|
consumers = [
|
|
{
|
|
partition = "bar"
|
|
},
|
|
{
|
|
partition = "baz"
|
|
},
|
|
{
|
|
peer_name = "flarm"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
name = "db"
|
|
namespace = "bar"
|
|
consumers = [
|
|
{
|
|
partition = "zoo"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
`,
|
|
camel: `
|
|
Kind = "exported-services"
|
|
Name = "foo"
|
|
Meta {
|
|
"foo" = "bar"
|
|
"gir" = "zim"
|
|
}
|
|
Services = [
|
|
{
|
|
Name = "web"
|
|
Namespace = "foo"
|
|
Consumers = [
|
|
{
|
|
Partition = "bar"
|
|
},
|
|
{
|
|
Partition = "baz"
|
|
},
|
|
{
|
|
Peer = "flarm"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
Name = "db"
|
|
Namespace = "bar"
|
|
Consumers = [
|
|
{
|
|
Partition = "zoo"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
`,
|
|
expect: &ExportedServicesConfigEntry{
|
|
Name: "foo",
|
|
Meta: map[string]string{
|
|
"foo": "bar",
|
|
"gir": "zim",
|
|
},
|
|
Services: []ExportedService{
|
|
{
|
|
Name: "web",
|
|
Namespace: "foo",
|
|
Consumers: []ServiceConsumer{
|
|
{
|
|
Partition: "bar",
|
|
},
|
|
{
|
|
Partition: "baz",
|
|
},
|
|
{
|
|
Peer: "flarm",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "db",
|
|
Namespace: "bar",
|
|
Consumers: []ServiceConsumer{
|
|
{
|
|
Partition: "zoo",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} {
|
|
tc := tc
|
|
|
|
testbody := func(t *testing.T, body string) {
|
|
var raw map[string]interface{}
|
|
err := hcl.Decode(&raw, body)
|
|
require.NoError(t, err)
|
|
|
|
got, err := DecodeConfigEntry(raw)
|
|
if tc.expectErr != "" {
|
|
require.Nil(t, got)
|
|
require.Error(t, err)
|
|
requireContainsLower(t, err.Error(), tc.expectErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expect, got)
|
|
}
|
|
}
|
|
|
|
t.Run(tc.name+" (snake case)", func(t *testing.T) {
|
|
testbody(t, tc.snake)
|
|
})
|
|
t.Run(tc.name+" (camel case)", func(t *testing.T) {
|
|
testbody(t, tc.camel)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServiceConfigRequest(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
name string
|
|
req ServiceConfigRequest
|
|
mutate func(req *ServiceConfigRequest)
|
|
want *cache.RequestInfo
|
|
wantSame bool
|
|
}{
|
|
{
|
|
name: "basic params",
|
|
req: ServiceConfigRequest{
|
|
QueryOptions: QueryOptions{Token: "foo"},
|
|
Datacenter: "dc1",
|
|
},
|
|
want: &cache.RequestInfo{
|
|
Token: "foo",
|
|
Datacenter: "dc1",
|
|
},
|
|
wantSame: true,
|
|
},
|
|
{
|
|
name: "name should be considered",
|
|
req: ServiceConfigRequest{
|
|
Name: "web",
|
|
},
|
|
mutate: func(req *ServiceConfigRequest) {
|
|
req.Name = "db"
|
|
},
|
|
wantSame: false,
|
|
},
|
|
{
|
|
name: "upstreams should be different",
|
|
req: ServiceConfigRequest{
|
|
Name: "web",
|
|
UpstreamServiceNames: []PeeredServiceName{
|
|
{ServiceName: NewServiceName("foo", nil)},
|
|
},
|
|
},
|
|
mutate: func(req *ServiceConfigRequest) {
|
|
req.UpstreamServiceNames = []PeeredServiceName{
|
|
{ServiceName: NewServiceName("foo", nil)},
|
|
{ServiceName: NewServiceName("bar", nil)},
|
|
}
|
|
},
|
|
wantSame: false,
|
|
},
|
|
{
|
|
name: "upstreams should not depend on order",
|
|
req: ServiceConfigRequest{
|
|
Name: "web",
|
|
UpstreamServiceNames: []PeeredServiceName{
|
|
{ServiceName: NewServiceName("foo", nil)},
|
|
{ServiceName: NewServiceName("bar", nil)},
|
|
},
|
|
},
|
|
mutate: func(req *ServiceConfigRequest) {
|
|
req.UpstreamServiceNames = []PeeredServiceName{
|
|
{ServiceName: NewServiceName("foo", nil)},
|
|
{ServiceName: NewServiceName("bar", nil)},
|
|
}
|
|
},
|
|
wantSame: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
info := tc.req.CacheInfo()
|
|
if tc.mutate != nil {
|
|
tc.mutate(&tc.req)
|
|
}
|
|
afterInfo := tc.req.CacheInfo()
|
|
|
|
// Check key matches or not
|
|
if tc.wantSame {
|
|
require.Equal(t, info, afterInfo)
|
|
} else {
|
|
require.NotEqual(t, info, afterInfo)
|
|
}
|
|
|
|
if tc.want != nil {
|
|
// Reset key since we don't care about the actual hash value as long as
|
|
// it does/doesn't change appropriately (asserted with wantSame above).
|
|
info.Key = ""
|
|
require.Equal(t, *tc.want, info)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServiceConfigResponse_MsgPack(t *testing.T) {
|
|
a := ServiceConfigResponse{
|
|
ProxyConfig: map[string]interface{}{
|
|
"string": "foo",
|
|
"map": map[string]interface{}{
|
|
"baz": "bar",
|
|
},
|
|
},
|
|
UpstreamConfigs: []OpaqueUpstreamConfig{
|
|
{
|
|
Upstream: PeeredServiceName{
|
|
ServiceName: NewServiceName("a", acl.DefaultEnterpriseMeta()),
|
|
},
|
|
Config: map[string]interface{}{
|
|
"string": "aaaa",
|
|
"map": map[string]interface{}{
|
|
"baz": "aa",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Upstream: PeeredServiceName{
|
|
ServiceName: NewServiceName("b", acl.DefaultEnterpriseMeta()),
|
|
},
|
|
Config: map[string]interface{}{
|
|
"string": "bbbb",
|
|
"map": map[string]interface{}{
|
|
"baz": "bb",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
|
|
// Encode as msgPack using a regular handle i.e. NOT one with RawAsString
|
|
// since our RPC codec doesn't use that.
|
|
enc := codec.NewEncoder(&buf, MsgpackHandle)
|
|
require.NoError(t, enc.Encode(&a))
|
|
|
|
var b ServiceConfigResponse
|
|
|
|
dec := codec.NewDecoder(&buf, MsgpackHandle)
|
|
require.NoError(t, dec.Decode(&b))
|
|
|
|
require.Equal(t, a, b)
|
|
}
|
|
|
|
func TestConfigEntryResponseMarshalling(t *testing.T) {
|
|
|
|
cases := map[string]ConfigEntryResponse{
|
|
"nil entry": {},
|
|
"proxy-default entry": {
|
|
Entry: &ProxyConfigEntry{
|
|
Kind: ProxyDefaults,
|
|
Name: ProxyConfigGlobal,
|
|
Config: map[string]interface{}{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
},
|
|
"service-default entry": {
|
|
Entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "foo",
|
|
Protocol: "tcp",
|
|
// Connect: ConnectConfiguration{SideCarProxy: true},
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, tcase := range cases {
|
|
name := name
|
|
tcase := tcase
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
data, err := tcase.MarshalBinary()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, data)
|
|
|
|
var resp ConfigEntryResponse
|
|
require.NoError(t, resp.UnmarshalBinary(data))
|
|
|
|
require.Equal(t, tcase, resp)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPassiveHealthCheck_Validate(t *testing.T) {
|
|
tt := []struct {
|
|
name string
|
|
input PassiveHealthCheck
|
|
wantErr bool
|
|
wantMsg string
|
|
}{
|
|
{
|
|
name: "valid interval",
|
|
input: PassiveHealthCheck{Interval: 0 * time.Second},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "negative interval",
|
|
input: PassiveHealthCheck{Interval: -1 * time.Second},
|
|
wantErr: true,
|
|
wantMsg: "cannot be negative",
|
|
},
|
|
{
|
|
name: "valid enforcing_consecutive_5xx",
|
|
input: PassiveHealthCheck{EnforcingConsecutive5xx: uintPointer(100)},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid enforcing_consecutive_5xx",
|
|
input: PassiveHealthCheck{EnforcingConsecutive5xx: uintPointer(101)},
|
|
wantErr: true,
|
|
wantMsg: "must be a percentage",
|
|
},
|
|
{
|
|
name: "valid max_ejection_percent",
|
|
input: PassiveHealthCheck{MaxEjectionPercent: uintPointer(100)},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid max_ejection_percent",
|
|
input: PassiveHealthCheck{MaxEjectionPercent: uintPointer(101)},
|
|
wantErr: true,
|
|
wantMsg: "must be a percentage",
|
|
},
|
|
{
|
|
name: "valid base_ejection_time",
|
|
input: PassiveHealthCheck{BaseEjectionTime: durationPointer(0 * time.Second)},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "negative base_ejection_time",
|
|
input: PassiveHealthCheck{BaseEjectionTime: durationPointer(-1 * time.Second)},
|
|
wantErr: true,
|
|
wantMsg: "cannot be negative",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := tc.input.Validate()
|
|
if err == nil {
|
|
require.False(t, tc.wantErr)
|
|
return
|
|
}
|
|
require.Contains(t, err.Error(), tc.wantMsg)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpstreamLimits_Validate(t *testing.T) {
|
|
tt := []struct {
|
|
name string
|
|
input UpstreamLimits
|
|
wantErr bool
|
|
wantMsg string
|
|
}{
|
|
{
|
|
name: "valid-max-conns",
|
|
input: UpstreamLimits{MaxConnections: intPointer(0)},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "negative-max-conns",
|
|
input: UpstreamLimits{MaxConnections: intPointer(-1)},
|
|
wantErr: true,
|
|
wantMsg: "cannot be negative",
|
|
},
|
|
{
|
|
name: "valid-max-concurrent",
|
|
input: UpstreamLimits{MaxConcurrentRequests: intPointer(0)},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "negative-max-concurrent",
|
|
input: UpstreamLimits{MaxConcurrentRequests: intPointer(-1)},
|
|
wantErr: true,
|
|
wantMsg: "cannot be negative",
|
|
},
|
|
{
|
|
name: "valid-max-pending",
|
|
input: UpstreamLimits{MaxPendingRequests: intPointer(0)},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "negative-max-pending",
|
|
input: UpstreamLimits{MaxPendingRequests: intPointer(-1)},
|
|
wantErr: true,
|
|
wantMsg: "cannot be negative",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := tc.input.Validate()
|
|
if err == nil {
|
|
require.False(t, tc.wantErr)
|
|
return
|
|
}
|
|
require.Contains(t, err.Error(), tc.wantMsg)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServiceConfigEntry(t *testing.T) {
|
|
cases := map[string]configEntryTestcase{
|
|
"normalize: upstream config override no name": {
|
|
// This will do nothing to normalization, but it will fail at validation later
|
|
entry: &ServiceConfigEntry{
|
|
Name: "web",
|
|
UpstreamConfig: &UpstreamConfiguration{
|
|
Overrides: []*UpstreamConfig{
|
|
{
|
|
Name: "good",
|
|
Protocol: "grpc",
|
|
},
|
|
{
|
|
Protocol: "http2",
|
|
},
|
|
{
|
|
Name: "also-good",
|
|
Protocol: "http",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "web",
|
|
EnterpriseMeta: *DefaultEnterpriseMetaInDefaultPartition(),
|
|
UpstreamConfig: &UpstreamConfiguration{
|
|
Overrides: []*UpstreamConfig{
|
|
{
|
|
Name: "good",
|
|
EnterpriseMeta: *DefaultEnterpriseMetaInDefaultPartition(),
|
|
Protocol: "grpc",
|
|
},
|
|
{
|
|
EnterpriseMeta: *DefaultEnterpriseMetaInDefaultPartition(),
|
|
Protocol: "http2",
|
|
},
|
|
{
|
|
Name: "also-good",
|
|
EnterpriseMeta: *DefaultEnterpriseMetaInDefaultPartition(),
|
|
Protocol: "http",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
normalizeOnly: true,
|
|
},
|
|
"normalize: upstream config defaults with name": {
|
|
// This will do nothing to normalization, but it will fail at validation later
|
|
entry: &ServiceConfigEntry{
|
|
Name: "web",
|
|
UpstreamConfig: &UpstreamConfiguration{
|
|
Defaults: &UpstreamConfig{
|
|
Name: "also-good",
|
|
Protocol: "http2",
|
|
},
|
|
},
|
|
},
|
|
expected: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "web",
|
|
EnterpriseMeta: *DefaultEnterpriseMetaInDefaultPartition(),
|
|
UpstreamConfig: &UpstreamConfiguration{
|
|
Defaults: &UpstreamConfig{
|
|
Name: "also-good",
|
|
Protocol: "http2",
|
|
},
|
|
},
|
|
},
|
|
normalizeOnly: true,
|
|
},
|
|
"normalize: fill-in-kind": {
|
|
entry: &ServiceConfigEntry{
|
|
Name: "web",
|
|
},
|
|
expected: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "web",
|
|
EnterpriseMeta: *DefaultEnterpriseMetaInDefaultPartition(),
|
|
},
|
|
normalizeOnly: true,
|
|
},
|
|
"normalize: lowercase-protocol": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "web",
|
|
Protocol: "PrOtoCoL",
|
|
},
|
|
expected: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "web",
|
|
Protocol: "protocol",
|
|
EnterpriseMeta: *DefaultEnterpriseMetaInDefaultPartition(),
|
|
},
|
|
normalizeOnly: true,
|
|
},
|
|
"normalize: connect-kitchen-sink": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "web",
|
|
UpstreamConfig: &UpstreamConfiguration{
|
|
Overrides: []*UpstreamConfig{
|
|
{
|
|
Name: "redis",
|
|
Protocol: "TcP",
|
|
},
|
|
{
|
|
Name: "memcached",
|
|
ConnectTimeoutMs: -1,
|
|
},
|
|
},
|
|
Defaults: &UpstreamConfig{ConnectTimeoutMs: -20},
|
|
},
|
|
EnterpriseMeta: *DefaultEnterpriseMetaInDefaultPartition(),
|
|
},
|
|
expected: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "web",
|
|
UpstreamConfig: &UpstreamConfiguration{
|
|
Overrides: []*UpstreamConfig{
|
|
{
|
|
Name: "redis",
|
|
EnterpriseMeta: *DefaultEnterpriseMetaInDefaultPartition(),
|
|
Protocol: "tcp",
|
|
ConnectTimeoutMs: 0,
|
|
},
|
|
{
|
|
Name: "memcached",
|
|
EnterpriseMeta: *DefaultEnterpriseMetaInDefaultPartition(),
|
|
ConnectTimeoutMs: 0,
|
|
},
|
|
},
|
|
Defaults: &UpstreamConfig{
|
|
ConnectTimeoutMs: 0,
|
|
},
|
|
},
|
|
EnterpriseMeta: *DefaultEnterpriseMetaInDefaultPartition(),
|
|
},
|
|
normalizeOnly: true,
|
|
},
|
|
"wildcard name is not allowed": {
|
|
entry: &ServiceConfigEntry{
|
|
Name: WildcardSpecifier,
|
|
},
|
|
validateErr: `must be the name of a service, and not a wildcard`,
|
|
},
|
|
"upstream config override no name": {
|
|
entry: &ServiceConfigEntry{
|
|
Name: "web",
|
|
UpstreamConfig: &UpstreamConfiguration{
|
|
Overrides: []*UpstreamConfig{
|
|
{
|
|
Name: "good",
|
|
Protocol: "grpc",
|
|
},
|
|
{
|
|
Protocol: "http2",
|
|
},
|
|
{
|
|
Name: "also-good",
|
|
Protocol: "http",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
validateErr: `Name is required`,
|
|
},
|
|
"upstream config defaults with name": {
|
|
entry: &ServiceConfigEntry{
|
|
Name: "web",
|
|
UpstreamConfig: &UpstreamConfiguration{
|
|
Defaults: &UpstreamConfig{
|
|
Name: "also-good",
|
|
Protocol: "http2",
|
|
},
|
|
},
|
|
},
|
|
validateErr: `error in upstream defaults: Name must be empty`,
|
|
},
|
|
"connect-kitchen-sink": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "web",
|
|
UpstreamConfig: &UpstreamConfiguration{
|
|
Overrides: []*UpstreamConfig{
|
|
{
|
|
Name: "redis",
|
|
Protocol: "TcP",
|
|
},
|
|
{
|
|
Name: "memcached",
|
|
ConnectTimeoutMs: -1,
|
|
},
|
|
},
|
|
Defaults: &UpstreamConfig{ConnectTimeoutMs: -20},
|
|
},
|
|
EnterpriseMeta: *DefaultEnterpriseMetaInDefaultPartition(),
|
|
},
|
|
expected: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "web",
|
|
UpstreamConfig: &UpstreamConfiguration{
|
|
Overrides: []*UpstreamConfig{
|
|
{
|
|
Name: "redis",
|
|
EnterpriseMeta: *DefaultEnterpriseMetaInDefaultPartition(),
|
|
Protocol: "tcp",
|
|
ConnectTimeoutMs: 0,
|
|
},
|
|
{
|
|
Name: "memcached",
|
|
EnterpriseMeta: *DefaultEnterpriseMetaInDefaultPartition(),
|
|
ConnectTimeoutMs: 0,
|
|
},
|
|
},
|
|
Defaults: &UpstreamConfig{ConnectTimeoutMs: 0},
|
|
},
|
|
EnterpriseMeta: *DefaultEnterpriseMetaInDefaultPartition(),
|
|
},
|
|
},
|
|
"validate: nil destination address": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "external",
|
|
Protocol: "tcp",
|
|
Destination: &DestinationConfig{
|
|
Addresses: nil,
|
|
Port: 443,
|
|
},
|
|
},
|
|
validateErr: "must contain at least one valid address",
|
|
},
|
|
"validate: empty destination address": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "external",
|
|
Protocol: "tcp",
|
|
Destination: &DestinationConfig{
|
|
Addresses: []string{},
|
|
Port: 443,
|
|
},
|
|
},
|
|
validateErr: "must contain at least one valid address",
|
|
},
|
|
"validate: destination ipv4 address": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "external",
|
|
Protocol: "tcp",
|
|
Destination: &DestinationConfig{
|
|
Addresses: []string{"1.2.3.4"},
|
|
Port: 443,
|
|
},
|
|
},
|
|
},
|
|
"validate: destination ipv6 address": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "external",
|
|
Protocol: "tcp",
|
|
Destination: &DestinationConfig{
|
|
Addresses: []string{"2001:0db8:0000:8a2e:0370:7334:1234:5678"},
|
|
Port: 443,
|
|
},
|
|
},
|
|
},
|
|
"valid destination shortened ipv6 address": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "external",
|
|
Protocol: "tcp",
|
|
Destination: &DestinationConfig{
|
|
Addresses: []string{"2001:db8::8a2e:370:7334"},
|
|
Port: 443,
|
|
},
|
|
},
|
|
},
|
|
"validate: invalid destination port": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "external",
|
|
Protocol: "tcp",
|
|
Destination: &DestinationConfig{
|
|
Addresses: []string{"2001:db8::8a2e:370:7334"},
|
|
},
|
|
},
|
|
validateErr: "Invalid Port number",
|
|
},
|
|
"validate: invalid hostname 1": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "external",
|
|
Protocol: "tcp",
|
|
Destination: &DestinationConfig{
|
|
Addresses: []string{"*external.com"},
|
|
Port: 443,
|
|
},
|
|
},
|
|
validateErr: "Could not validate address",
|
|
},
|
|
"validate: invalid hostname 2": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "external",
|
|
Protocol: "tcp",
|
|
Destination: &DestinationConfig{
|
|
Addresses: []string{"..hello."},
|
|
Port: 443,
|
|
},
|
|
},
|
|
validateErr: "Could not validate address",
|
|
},
|
|
"validate: all web traffic allowed": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "external",
|
|
Protocol: "http",
|
|
Destination: &DestinationConfig{
|
|
Addresses: []string{"*"},
|
|
Port: 443,
|
|
},
|
|
},
|
|
validateErr: "Could not validate address",
|
|
},
|
|
"validate: multiple hostnames": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "external",
|
|
Protocol: "http",
|
|
Destination: &DestinationConfig{
|
|
Addresses: []string{
|
|
"api.google.com",
|
|
"web.google.com",
|
|
},
|
|
Port: 443,
|
|
},
|
|
},
|
|
},
|
|
"validate: duplicate addresses not allowed": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "external",
|
|
Protocol: "http",
|
|
Destination: &DestinationConfig{
|
|
Addresses: []string{
|
|
"api.google.com",
|
|
"api.google.com",
|
|
},
|
|
Port: 443,
|
|
},
|
|
},
|
|
validateErr: "Duplicate address",
|
|
},
|
|
"validate: invalid inbound connection balance": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "external",
|
|
Protocol: "http",
|
|
BalanceInboundConnections: "invalid",
|
|
},
|
|
validateErr: "invalid value for balance_inbound_connections",
|
|
},
|
|
"validate: invalid default outbound connection balance": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "external",
|
|
Protocol: "http",
|
|
UpstreamConfig: &UpstreamConfiguration{
|
|
Defaults: &UpstreamConfig{
|
|
BalanceOutboundConnections: "invalid",
|
|
},
|
|
},
|
|
},
|
|
validateErr: "invalid value for balance_outbound_connections",
|
|
},
|
|
"validate: invalid override outbound connection balance": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "external",
|
|
Protocol: "http",
|
|
UpstreamConfig: &UpstreamConfiguration{
|
|
Overrides: []*UpstreamConfig{
|
|
{
|
|
Name: "upstream",
|
|
BalanceOutboundConnections: "invalid",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
validateErr: "invalid value for balance_outbound_connections",
|
|
},
|
|
"validate: invalid extension": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "external",
|
|
Protocol: "http",
|
|
EnvoyExtensions: []EnvoyExtension{
|
|
{},
|
|
},
|
|
},
|
|
validateErr: "invalid EnvoyExtensions[0]: Name is required",
|
|
},
|
|
"validate: invalid extension name": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "external",
|
|
Protocol: "http",
|
|
EnvoyExtensions: []EnvoyExtension{
|
|
{
|
|
Name: "not-a-builtin",
|
|
},
|
|
},
|
|
},
|
|
validateErr: `name "not-a-builtin" is not a built-in extension`,
|
|
},
|
|
"validate: valid extension": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "external",
|
|
Protocol: "http",
|
|
EnvoyExtensions: []EnvoyExtension{
|
|
{
|
|
Name: api.BuiltinAWSLambdaExtension,
|
|
Arguments: map[string]interface{}{
|
|
"ARN": "some-arn",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"validate: invalid MutualTLSMode in service-defaults": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "web",
|
|
MutualTLSMode: MutualTLSMode("invalid-mtls-mode"),
|
|
},
|
|
validateErr: `Invalid MutualTLSMode "invalid-mtls-mode". Must be one of "", "strict", or "permissive".`,
|
|
},
|
|
"validate: invalid MutualTLSMode in proxy-defaults": {
|
|
entry: &ServiceConfigEntry{
|
|
Kind: ProxyDefaults,
|
|
Name: ProxyConfigGlobal,
|
|
MutualTLSMode: MutualTLSMode("invalid-mtls-mode"),
|
|
},
|
|
validateErr: `Invalid MutualTLSMode "invalid-mtls-mode". Must be one of "", "strict", or "permissive".`,
|
|
},
|
|
}
|
|
testConfigEntryNormalizeAndValidate(t, cases)
|
|
}
|
|
|
|
func TestUpstreamConfig_MergeInto(t *testing.T) {
|
|
tt := []struct {
|
|
name string
|
|
source UpstreamConfig
|
|
destination map[string]interface{}
|
|
want map[string]interface{}
|
|
}{
|
|
{
|
|
name: "kitchen sink",
|
|
source: UpstreamConfig{
|
|
BalanceOutboundConnections: "exact_balance",
|
|
EnvoyListenerJSON: "foo",
|
|
EnvoyClusterJSON: "bar",
|
|
ConnectTimeoutMs: 5,
|
|
Protocol: "http",
|
|
Limits: &UpstreamLimits{
|
|
MaxConnections: intPointer(3),
|
|
MaxPendingRequests: intPointer(4),
|
|
MaxConcurrentRequests: intPointer(5),
|
|
},
|
|
PassiveHealthCheck: &PassiveHealthCheck{
|
|
Interval: 2 * time.Second,
|
|
MaxFailures: 3,
|
|
EnforcingConsecutive5xx: uintPointer(4),
|
|
MaxEjectionPercent: uintPointer(5),
|
|
BaseEjectionTime: durationPointer(6 * time.Second),
|
|
},
|
|
MeshGateway: MeshGatewayConfig{Mode: MeshGatewayModeRemote},
|
|
},
|
|
destination: make(map[string]interface{}),
|
|
want: map[string]interface{}{
|
|
"balance_outbound_connections": "exact_balance",
|
|
"envoy_listener_json": "foo",
|
|
"envoy_cluster_json": "bar",
|
|
"connect_timeout_ms": 5,
|
|
"protocol": "http",
|
|
"limits": &UpstreamLimits{
|
|
MaxConnections: intPointer(3),
|
|
MaxPendingRequests: intPointer(4),
|
|
MaxConcurrentRequests: intPointer(5),
|
|
},
|
|
"passive_health_check": &PassiveHealthCheck{
|
|
Interval: 2 * time.Second,
|
|
MaxFailures: 3,
|
|
EnforcingConsecutive5xx: uintPointer(4),
|
|
MaxEjectionPercent: uintPointer(5),
|
|
BaseEjectionTime: durationPointer(6 * time.Second),
|
|
},
|
|
"mesh_gateway": MeshGatewayConfig{Mode: MeshGatewayModeRemote},
|
|
},
|
|
},
|
|
{
|
|
name: "kitchen sink override of destination",
|
|
source: UpstreamConfig{
|
|
BalanceOutboundConnections: "exact_balance",
|
|
EnvoyListenerJSON: "foo",
|
|
EnvoyClusterJSON: "bar",
|
|
ConnectTimeoutMs: 5,
|
|
Protocol: "http",
|
|
Limits: &UpstreamLimits{
|
|
MaxConnections: intPointer(3),
|
|
MaxPendingRequests: intPointer(4),
|
|
MaxConcurrentRequests: intPointer(5),
|
|
},
|
|
PassiveHealthCheck: &PassiveHealthCheck{
|
|
Interval: 2 * time.Second,
|
|
MaxFailures: 3,
|
|
EnforcingConsecutive5xx: uintPointer(4),
|
|
MaxEjectionPercent: uintPointer(5),
|
|
BaseEjectionTime: durationPointer(6 * time.Second),
|
|
},
|
|
MeshGateway: MeshGatewayConfig{Mode: MeshGatewayModeRemote},
|
|
},
|
|
destination: map[string]interface{}{
|
|
"balance_outbound_connections": "",
|
|
"envoy_listener_json": "zip",
|
|
"envoy_cluster_json": "zap",
|
|
"connect_timeout_ms": 10,
|
|
"protocol": "grpc",
|
|
"limits": &UpstreamLimits{
|
|
MaxConnections: intPointer(10),
|
|
MaxPendingRequests: intPointer(11),
|
|
MaxConcurrentRequests: intPointer(12),
|
|
},
|
|
"passive_health_check": &PassiveHealthCheck{
|
|
MaxFailures: 13,
|
|
Interval: 14 * time.Second,
|
|
EnforcingConsecutive5xx: uintPointer(15),
|
|
MaxEjectionPercent: uintPointer(16),
|
|
BaseEjectionTime: durationPointer(17 * time.Second),
|
|
},
|
|
"mesh_gateway": MeshGatewayConfig{Mode: MeshGatewayModeLocal},
|
|
},
|
|
want: map[string]interface{}{
|
|
"balance_outbound_connections": "exact_balance",
|
|
"envoy_listener_json": "foo",
|
|
"envoy_cluster_json": "bar",
|
|
"connect_timeout_ms": 5,
|
|
"protocol": "http",
|
|
"limits": &UpstreamLimits{
|
|
MaxConnections: intPointer(3),
|
|
MaxPendingRequests: intPointer(4),
|
|
MaxConcurrentRequests: intPointer(5),
|
|
},
|
|
"passive_health_check": &PassiveHealthCheck{
|
|
Interval: 2 * time.Second,
|
|
MaxFailures: 3,
|
|
EnforcingConsecutive5xx: uintPointer(4),
|
|
MaxEjectionPercent: uintPointer(5),
|
|
BaseEjectionTime: durationPointer(6 * time.Second),
|
|
},
|
|
"mesh_gateway": MeshGatewayConfig{Mode: MeshGatewayModeRemote},
|
|
},
|
|
},
|
|
{
|
|
name: "empty source leaves destination intact",
|
|
source: UpstreamConfig{},
|
|
destination: map[string]interface{}{
|
|
"balance_outbound_connections": "exact_balance",
|
|
"envoy_listener_json": "zip",
|
|
"envoy_cluster_json": "zap",
|
|
"connect_timeout_ms": 10,
|
|
"protocol": "grpc",
|
|
"limits": &UpstreamLimits{
|
|
MaxConnections: intPointer(10),
|
|
MaxPendingRequests: intPointer(11),
|
|
MaxConcurrentRequests: intPointer(12),
|
|
},
|
|
"passive_health_check": &PassiveHealthCheck{
|
|
MaxFailures: 13,
|
|
Interval: 14 * time.Second,
|
|
EnforcingConsecutive5xx: uintPointer(15),
|
|
MaxEjectionPercent: uintPointer(16),
|
|
BaseEjectionTime: durationPointer(17 * time.Second),
|
|
},
|
|
"mesh_gateway": MeshGatewayConfig{Mode: MeshGatewayModeLocal},
|
|
},
|
|
want: map[string]interface{}{
|
|
"balance_outbound_connections": "exact_balance",
|
|
"envoy_listener_json": "zip",
|
|
"envoy_cluster_json": "zap",
|
|
"connect_timeout_ms": 10,
|
|
"protocol": "grpc",
|
|
"limits": &UpstreamLimits{
|
|
MaxConnections: intPointer(10),
|
|
MaxPendingRequests: intPointer(11),
|
|
MaxConcurrentRequests: intPointer(12),
|
|
},
|
|
"passive_health_check": &PassiveHealthCheck{
|
|
MaxFailures: 13,
|
|
Interval: 14 * time.Second,
|
|
EnforcingConsecutive5xx: uintPointer(15),
|
|
MaxEjectionPercent: uintPointer(16),
|
|
BaseEjectionTime: durationPointer(17 * time.Second),
|
|
},
|
|
"mesh_gateway": MeshGatewayConfig{Mode: MeshGatewayModeLocal},
|
|
},
|
|
},
|
|
{
|
|
name: "empty source and destination is a noop",
|
|
source: UpstreamConfig{},
|
|
destination: make(map[string]interface{}),
|
|
want: map[string]interface{}{},
|
|
},
|
|
}
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
tc.source.MergeInto(tc.destination)
|
|
assert.Equal(t, tc.want, tc.destination)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseUpstreamConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input map[string]interface{}
|
|
want UpstreamConfig
|
|
}{
|
|
{
|
|
name: "defaults - nil",
|
|
input: nil,
|
|
want: UpstreamConfig{
|
|
ConnectTimeoutMs: 5000,
|
|
Protocol: "tcp",
|
|
},
|
|
},
|
|
{
|
|
name: "defaults - empty",
|
|
input: map[string]interface{}{},
|
|
want: UpstreamConfig{
|
|
ConnectTimeoutMs: 5000,
|
|
Protocol: "tcp",
|
|
},
|
|
},
|
|
{
|
|
name: "defaults - other stuff",
|
|
input: map[string]interface{}{
|
|
"foo": "bar",
|
|
"envoy_foo": "envoy_bar",
|
|
},
|
|
want: UpstreamConfig{
|
|
ConnectTimeoutMs: 5000,
|
|
Protocol: "tcp",
|
|
},
|
|
},
|
|
{
|
|
name: "protocol override",
|
|
input: map[string]interface{}{
|
|
"protocol": "http",
|
|
},
|
|
want: UpstreamConfig{
|
|
ConnectTimeoutMs: 5000,
|
|
Protocol: "http",
|
|
},
|
|
},
|
|
{
|
|
name: "connect timeout override, string",
|
|
input: map[string]interface{}{
|
|
"connect_timeout_ms": "1000",
|
|
},
|
|
want: UpstreamConfig{
|
|
ConnectTimeoutMs: 1000,
|
|
Protocol: "tcp",
|
|
},
|
|
},
|
|
{
|
|
name: "connect timeout override, float ",
|
|
input: map[string]interface{}{
|
|
"connect_timeout_ms": float64(1000.0),
|
|
},
|
|
want: UpstreamConfig{
|
|
ConnectTimeoutMs: 1000,
|
|
Protocol: "tcp",
|
|
},
|
|
},
|
|
{
|
|
name: "connect timeout override, int ",
|
|
input: map[string]interface{}{
|
|
"connect_timeout_ms": 1000,
|
|
},
|
|
want: UpstreamConfig{
|
|
ConnectTimeoutMs: 1000,
|
|
Protocol: "tcp",
|
|
},
|
|
},
|
|
{
|
|
name: "connect limits map",
|
|
input: map[string]interface{}{
|
|
"limits": map[string]interface{}{
|
|
"max_connections": 50,
|
|
"max_pending_requests": 60,
|
|
"max_concurrent_requests": 70,
|
|
},
|
|
},
|
|
want: UpstreamConfig{
|
|
ConnectTimeoutMs: 5000,
|
|
Limits: &UpstreamLimits{
|
|
MaxConnections: intPointer(50),
|
|
MaxPendingRequests: intPointer(60),
|
|
MaxConcurrentRequests: intPointer(70),
|
|
},
|
|
Protocol: "tcp",
|
|
},
|
|
},
|
|
{
|
|
name: "connect limits map zero",
|
|
input: map[string]interface{}{
|
|
"limits": map[string]interface{}{
|
|
"max_connections": 0,
|
|
"max_pending_requests": 0,
|
|
"max_concurrent_requests": 0,
|
|
},
|
|
},
|
|
want: UpstreamConfig{
|
|
ConnectTimeoutMs: 5000,
|
|
Limits: &UpstreamLimits{
|
|
MaxConnections: intPointer(0),
|
|
MaxPendingRequests: intPointer(0),
|
|
MaxConcurrentRequests: intPointer(0),
|
|
},
|
|
Protocol: "tcp",
|
|
},
|
|
},
|
|
{
|
|
name: "passive health check map",
|
|
input: map[string]interface{}{
|
|
"passive_health_check": map[string]interface{}{
|
|
"interval": "22s",
|
|
"max_failures": 7,
|
|
"enforcing_consecutive_5xx": 8,
|
|
"max_ejection_percent": 9,
|
|
"base_ejection_time": "10s",
|
|
},
|
|
},
|
|
want: UpstreamConfig{
|
|
ConnectTimeoutMs: 5000,
|
|
PassiveHealthCheck: &PassiveHealthCheck{
|
|
Interval: 22 * time.Second,
|
|
MaxFailures: 7,
|
|
EnforcingConsecutive5xx: uintPointer(8),
|
|
MaxEjectionPercent: uintPointer(9),
|
|
BaseEjectionTime: durationPointer(10 * time.Second),
|
|
},
|
|
Protocol: "tcp",
|
|
},
|
|
},
|
|
{
|
|
name: "mesh gateway map",
|
|
input: map[string]interface{}{
|
|
"mesh_gateway": map[string]interface{}{
|
|
"Mode": "remote",
|
|
},
|
|
},
|
|
want: UpstreamConfig{
|
|
ConnectTimeoutMs: 5000,
|
|
MeshGateway: MeshGatewayConfig{
|
|
Mode: MeshGatewayModeRemote,
|
|
},
|
|
Protocol: "tcp",
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := ParseUpstreamConfig(tt.input)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProxyConfigEntry(t *testing.T) {
|
|
cases := map[string]configEntryTestcase{
|
|
"proxy config name provided is not global": {
|
|
entry: &ProxyConfigEntry{
|
|
Name: "foo",
|
|
},
|
|
normalizeErr: `invalid name ("foo"), only "global" is supported`,
|
|
},
|
|
"proxy config has no name": {
|
|
entry: &ProxyConfigEntry{
|
|
Name: "",
|
|
},
|
|
expected: &ProxyConfigEntry{
|
|
Name: ProxyConfigGlobal,
|
|
Kind: ProxyDefaults,
|
|
EnterpriseMeta: *acl.DefaultEnterpriseMeta(),
|
|
},
|
|
},
|
|
"proxy config entry has invalid opaque config": {
|
|
entry: &ProxyConfigEntry{
|
|
Name: "global",
|
|
Config: map[string]interface{}{
|
|
"envoy_hcp_metrics_bind_socket_dir": "/Consul/is/a/networking/platform/that/enables/securing/your/networking/",
|
|
},
|
|
},
|
|
validateErr: "Config: envoy_hcp_metrics_bind_socket_dir length 71 exceeds max",
|
|
},
|
|
"proxy config has invalid failover policy": {
|
|
entry: &ProxyConfigEntry{
|
|
Name: "global",
|
|
FailoverPolicy: &ServiceResolverFailoverPolicy{Mode: "bad"},
|
|
},
|
|
validateErr: `Failover-policy mode must be one of '', 'sequential', or 'order-by-locality'`,
|
|
},
|
|
"proxy config with valid failover policy": {
|
|
entry: &ProxyConfigEntry{
|
|
Name: "global",
|
|
FailoverPolicy: &ServiceResolverFailoverPolicy{Mode: "order-by-locality"},
|
|
},
|
|
expected: &ProxyConfigEntry{
|
|
Name: ProxyConfigGlobal,
|
|
Kind: ProxyDefaults,
|
|
FailoverPolicy: &ServiceResolverFailoverPolicy{Mode: "order-by-locality"},
|
|
EnterpriseMeta: *acl.DefaultEnterpriseMeta(),
|
|
},
|
|
},
|
|
"proxy config has invalid access log type": {
|
|
entry: &ProxyConfigEntry{
|
|
Name: "global",
|
|
AccessLogs: AccessLogsConfig{
|
|
Enabled: true,
|
|
Type: "stdin",
|
|
},
|
|
},
|
|
validateErr: "invalid access log type: stdin",
|
|
},
|
|
"proxy config has invalid access log config - both text and json formats": {
|
|
entry: &ProxyConfigEntry{
|
|
Name: "global",
|
|
AccessLogs: AccessLogsConfig{
|
|
Enabled: true,
|
|
JSONFormat: "[%START_TIME%]",
|
|
TextFormat: "{\"start_time\": \"[%START_TIME%]\"}",
|
|
},
|
|
},
|
|
validateErr: "cannot specify both access log JSONFormat and TextFormat",
|
|
},
|
|
"proxy config has invalid access log config - file path with wrong type": {
|
|
entry: &ProxyConfigEntry{
|
|
Name: "global",
|
|
AccessLogs: AccessLogsConfig{
|
|
Enabled: true,
|
|
Path: "/tmp/logs.txt",
|
|
},
|
|
},
|
|
validateErr: "path is only valid for file type access logs",
|
|
},
|
|
"proxy config has invalid access log config - no file path specified": {
|
|
entry: &ProxyConfigEntry{
|
|
Name: "global",
|
|
AccessLogs: AccessLogsConfig{
|
|
Enabled: true,
|
|
Type: FileLogSinkType,
|
|
},
|
|
},
|
|
validateErr: "path must be specified when using file type access logs",
|
|
},
|
|
"proxy config has invalid access log JSON format": {
|
|
entry: &ProxyConfigEntry{
|
|
Name: "global",
|
|
AccessLogs: AccessLogsConfig{
|
|
Enabled: true,
|
|
JSONFormat: "{\"start_time\": \"[%START_TIME%]\"", // Missing trailing brace
|
|
},
|
|
},
|
|
validateErr: "invalid access log json for JSON format",
|
|
},
|
|
}
|
|
testConfigEntryNormalizeAndValidate(t, cases)
|
|
}
|
|
|
|
func requireContainsLower(t *testing.T, haystack, needle string) {
|
|
t.Helper()
|
|
require.Contains(t, strings.ToLower(haystack), strings.ToLower(needle))
|
|
}
|
|
|
|
func TestConfigEntryQuery_CacheInfoKey(t *testing.T) {
|
|
assertCacheInfoKeyIsComplete(t, &ConfigEntryQuery{})
|
|
}
|
|
|
|
func TestServiceConfigRequest_CacheInfoKey(t *testing.T) {
|
|
assertCacheInfoKeyIsComplete(t, &ServiceConfigRequest{})
|
|
}
|
|
|
|
func TestDiscoveryChainRequest_CacheInfoKey(t *testing.T) {
|
|
assertCacheInfoKeyIsComplete(t, &DiscoveryChainRequest{})
|
|
}
|
|
|
|
type configEntryTestcase struct {
|
|
entry ConfigEntry
|
|
normalizeOnly bool
|
|
|
|
normalizeErr string
|
|
validateErr string
|
|
|
|
// Only one of expected, expectUnchanged or check can be set.
|
|
expected ConfigEntry
|
|
expectUnchanged bool
|
|
// check is called between normalize and validate
|
|
check func(t *testing.T, entry ConfigEntry)
|
|
}
|
|
|
|
func testConfigEntryNormalizeAndValidate(t *testing.T, cases map[string]configEntryTestcase) {
|
|
t.Helper()
|
|
|
|
for name, tc := range cases {
|
|
tc := tc
|
|
t.Run(name, func(t *testing.T) {
|
|
beforeNormalize, err := copystructure.Copy(tc.entry)
|
|
require.NoError(t, err)
|
|
|
|
err = tc.entry.Normalize()
|
|
if tc.normalizeErr != "" {
|
|
testutil.RequireErrorContains(t, err, tc.normalizeErr)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
checkMethods := 0
|
|
if tc.expected != nil {
|
|
checkMethods++
|
|
}
|
|
if tc.expectUnchanged {
|
|
checkMethods++
|
|
}
|
|
if tc.check != nil {
|
|
checkMethods++
|
|
}
|
|
|
|
if checkMethods > 1 {
|
|
t.Fatal("cannot set more than one of 'expected', 'expectUnchanged' and 'check' test case fields")
|
|
}
|
|
|
|
if tc.expected != nil {
|
|
tc.expected.SetHash(tc.entry.GetHash())
|
|
require.Equal(t, tc.expected, tc.entry)
|
|
}
|
|
|
|
if tc.expectUnchanged {
|
|
// EnterpriseMeta.Normalize behaves differently in Ent and CE which
|
|
// causes an exact comparison to fail. It's still useful to assert that
|
|
// nothing else changes though during Normalize. So we ignore
|
|
// EnterpriseMeta Defaults.
|
|
opts := cmp.Options{
|
|
cmp.Comparer(func(a, b acl.EnterpriseMeta) bool {
|
|
return a.IsSame(&b)
|
|
}),
|
|
}
|
|
beforeNormalize.(ConfigEntry).SetHash(tc.entry.GetHash())
|
|
if diff := cmp.Diff(beforeNormalize, tc.entry, opts); diff != "" {
|
|
t.Fatalf("expect unchanged after Normalize, got diff:\n%s", diff)
|
|
}
|
|
}
|
|
|
|
if tc.check != nil {
|
|
tc.check(t, tc.entry)
|
|
}
|
|
|
|
if tc.normalizeOnly {
|
|
return
|
|
}
|
|
|
|
err = tc.entry.Validate()
|
|
if tc.validateErr != "" {
|
|
testutil.RequireErrorContains(t, err, tc.validateErr)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func intPointer(i int) *int {
|
|
return &i
|
|
}
|
|
|
|
func uintPointer(v uint32) *uint32 {
|
|
return &v
|
|
}
|
|
|
|
func durationPointer(d time.Duration) *time.Duration {
|
|
return &d
|
|
}
|
|
|
|
func TestValidateOpaqueConfigMap(t *testing.T) {
|
|
tt := map[string]struct {
|
|
input map[string]interface{}
|
|
expectErr string
|
|
}{
|
|
"hcp metrics socket dir is valid": {
|
|
input: map[string]interface{}{
|
|
"envoy_hcp_metrics_bind_socket_dir": "/etc/consul.d/hcp"},
|
|
expectErr: "",
|
|
},
|
|
"hcp metrics socket dir is too long": {
|
|
input: map[string]interface{}{
|
|
"envoy_hcp_metrics_bind_socket_dir": "/Consul/is/a/networking/platform/that/enables/securing/your/networking/",
|
|
},
|
|
expectErr: "envoy_hcp_metrics_bind_socket_dir length 71 exceeds max 70",
|
|
},
|
|
}
|
|
|
|
for name, tc := range tt {
|
|
t.Run(name, func(t *testing.T) {
|
|
err := validateOpaqueProxyConfig(tc.input)
|
|
if tc.expectErr != "" {
|
|
require.ErrorContains(t, err, tc.expectErr)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|