mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
836 lines
16 KiB
836 lines
16 KiB
package structs |
|
|
|
import ( |
|
"bytes" |
|
"strings" |
|
"testing" |
|
"time" |
|
|
|
"github.com/hashicorp/go-msgpack/codec" |
|
"github.com/hashicorp/hcl" |
|
"github.com/stretchr/testify/require" |
|
) |
|
|
|
// TestDecodeConfigEntry is the 'structs' mirror image of |
|
// command/config/write/config_write_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" |
|
config { |
|
"foo" = 19 |
|
"bar" = "abc" |
|
"moreconfig" { |
|
"moar" = "config" |
|
} |
|
} |
|
mesh_gateway { |
|
mode = "remote" |
|
} |
|
`, |
|
camel: ` |
|
Kind = "proxy-defaults" |
|
Name = "main" |
|
Config { |
|
"foo" = 19 |
|
"bar" = "abc" |
|
"moreconfig" { |
|
"moar" = "config" |
|
} |
|
} |
|
MeshGateway { |
|
Mode = "remote" |
|
} |
|
`, |
|
expect: &ProxyConfigEntry{ |
|
Kind: "proxy-defaults", |
|
Name: "main", |
|
Config: map[string]interface{}{ |
|
"foo": 19, |
|
"bar": "abc", |
|
"moreconfig": map[string]interface{}{ |
|
"moar": "config", |
|
}, |
|
}, |
|
MeshGateway: MeshGatewayConfig{ |
|
Mode: MeshGatewayModeRemote, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "service-defaults", |
|
snake: ` |
|
kind = "service-defaults" |
|
name = "main" |
|
protocol = "http" |
|
external_sni = "abc-123" |
|
mesh_gateway { |
|
mode = "remote" |
|
} |
|
`, |
|
camel: ` |
|
Kind = "service-defaults" |
|
Name = "main" |
|
Protocol = "http" |
|
ExternalSNI = "abc-123" |
|
MeshGateway { |
|
Mode = "remote" |
|
} |
|
`, |
|
expect: &ServiceConfigEntry{ |
|
Kind: "service-defaults", |
|
Name: "main", |
|
Protocol: "http", |
|
ExternalSNI: "abc-123", |
|
MeshGateway: MeshGatewayConfig{ |
|
Mode: MeshGatewayModeRemote, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "service-router: kitchen sink", |
|
snake: ` |
|
kind = "service-router" |
|
name = "main" |
|
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" |
|
num_retries = 12345 |
|
retry_on_connect_failure = true |
|
retry_on_status_codes = [401, 209] |
|
} |
|
}, |
|
{ |
|
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" |
|
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" |
|
NumRetries = 12345 |
|
RetryOnConnectFailure = true |
|
RetryOnStatusCodes = [401, 209] |
|
} |
|
}, |
|
{ |
|
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", |
|
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, |
|
NumRetries: 12345, |
|
RetryOnConnectFailure: true, |
|
RetryOnStatusCodes: []uint32{401, 209}, |
|
}, |
|
}, |
|
{ |
|
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" |
|
splits = [ |
|
{ |
|
weight = 99.1 |
|
service_subset = "v1" |
|
}, |
|
{ |
|
weight = 0.9 |
|
service = "other" |
|
namespace = "alt" |
|
}, |
|
] |
|
`, |
|
camel: ` |
|
Kind = "service-splitter" |
|
Name = "main" |
|
Splits = [ |
|
{ |
|
Weight = 99.1 |
|
ServiceSubset = "v1" |
|
}, |
|
{ |
|
Weight = 0.9 |
|
Service = "other" |
|
Namespace = "alt" |
|
}, |
|
] |
|
`, |
|
expect: &ServiceSplitterConfigEntry{ |
|
Kind: ServiceSplitter, |
|
Name: "main", |
|
Splits: []ServiceSplit{ |
|
{ |
|
Weight: 99.1, |
|
ServiceSubset: "v1", |
|
}, |
|
{ |
|
Weight: 0.9, |
|
Service: "other", |
|
Namespace: "alt", |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "service-resolver: subsets with failover", |
|
snake: ` |
|
kind = "service-resolver" |
|
name = "main" |
|
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" |
|
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", |
|
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: "ingress-gateway: kitchen sink", |
|
snake: ` |
|
kind = "ingress-gateway" |
|
name = "ingress-web" |
|
|
|
tls { |
|
enabled = true |
|
} |
|
|
|
listeners = [ |
|
{ |
|
port = 8080 |
|
protocol = "http" |
|
services = [ |
|
{ |
|
name = "web" |
|
hosts = ["test.example.com", "test2.example.com"] |
|
}, |
|
{ |
|
name = "db" |
|
} |
|
] |
|
}, |
|
{ |
|
port = 9999 |
|
protocol = "tcp" |
|
services = [ |
|
{ |
|
name = "mysql" |
|
} |
|
] |
|
}, |
|
{ |
|
port = 2234 |
|
protocol = "tcp" |
|
services = [ |
|
{ |
|
name = "postgres" |
|
} |
|
] |
|
} |
|
] |
|
`, |
|
camel: ` |
|
Kind = "ingress-gateway" |
|
Name = "ingress-web" |
|
TLS { |
|
Enabled = true |
|
} |
|
Listeners = [ |
|
{ |
|
Port = 8080 |
|
Protocol = "http" |
|
Services = [ |
|
{ |
|
Name = "web" |
|
Hosts = ["test.example.com", "test2.example.com"] |
|
}, |
|
{ |
|
Name = "db" |
|
} |
|
] |
|
}, |
|
{ |
|
Port = 9999 |
|
Protocol = "tcp" |
|
Services = [ |
|
{ |
|
Name = "mysql" |
|
} |
|
] |
|
}, |
|
{ |
|
Port = 2234 |
|
Protocol = "tcp" |
|
Services = [ |
|
{ |
|
Name = "postgres" |
|
} |
|
] |
|
} |
|
] |
|
`, |
|
expect: &IngressGatewayConfigEntry{ |
|
Kind: "ingress-gateway", |
|
Name: "ingress-web", |
|
TLS: GatewayTLSConfig{ |
|
Enabled: true, |
|
}, |
|
Listeners: []IngressListener{ |
|
IngressListener{ |
|
Port: 8080, |
|
Protocol: "http", |
|
Services: []IngressService{ |
|
IngressService{ |
|
Name: "web", |
|
Hosts: []string{"test.example.com", "test2.example.com"}, |
|
}, |
|
IngressService{ |
|
Name: "db", |
|
}, |
|
}, |
|
}, |
|
IngressListener{ |
|
Port: 9999, |
|
Protocol: "tcp", |
|
Services: []IngressService{ |
|
IngressService{ |
|
Name: "mysql", |
|
}, |
|
}, |
|
}, |
|
IngressListener{ |
|
Port: 2234, |
|
Protocol: "tcp", |
|
Services: []IngressService{ |
|
IngressService{ |
|
Name: "postgres", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
name: "terminating-gateway: kitchen sink", |
|
snake: ` |
|
kind = "terminating-gateway" |
|
name = "terminating-gw-west" |
|
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" |
|
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", |
|
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", |
|
}, |
|
}, |
|
}, |
|
}, |
|
} { |
|
tc := tc |
|
|
|
testbody := func(t *testing.T, body string) { |
|
t.Helper() |
|
|
|
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 TestServiceConfigResponse_MsgPack(t *testing.T) { |
|
// TODO(banks) lib.MapWalker doesn't actually fix the map[interface{}] issue |
|
// it claims to in docs yet. When it does uncomment those cases below. |
|
a := ServiceConfigResponse{ |
|
ProxyConfig: map[string]interface{}{ |
|
"string": "foo", |
|
// "map": map[string]interface{}{ |
|
// "baz": "bar", |
|
// }, |
|
}, |
|
UpstreamConfigs: map[string]map[string]interface{}{ |
|
"a": map[string]interface{}{ |
|
"string": "aaaa", |
|
// "map": map[string]interface{}{ |
|
// "baz": "aa", |
|
// }, |
|
}, |
|
"b": 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": ConfigEntryResponse{}, |
|
"proxy-default entry": ConfigEntryResponse{ |
|
Entry: &ProxyConfigEntry{ |
|
Kind: ProxyDefaults, |
|
Name: ProxyConfigGlobal, |
|
Config: map[string]interface{}{ |
|
"foo": "bar", |
|
}, |
|
}, |
|
}, |
|
"service-default entry": ConfigEntryResponse{ |
|
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 requireContainsLower(t *testing.T, haystack, needle string) { |
|
t.Helper() |
|
require.Contains(t, strings.ToLower(haystack), strings.ToLower(needle)) |
|
}
|
|
|