open-consul/agent/structs/config_entry_gateways_test.go

693 lines
14 KiB
Go
Raw Normal View History

package structs
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestIngressConfigEntry_Normalize(t *testing.T) {
cases := []struct {
name string
entry IngressGatewayConfigEntry
expected IngressGatewayConfigEntry
}{
{
name: "empty protocol",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "",
Services: []IngressService{},
},
},
},
expected: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{},
},
},
EnterpriseMeta: *DefaultEnterpriseMeta(),
},
},
{
name: "lowercase protocols",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "TCP",
Services: []IngressService{},
},
{
Port: 1112,
Protocol: "HtTP",
Services: []IngressService{},
},
},
},
expected: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{},
},
{
Port: 1112,
Protocol: "http",
Services: []IngressService{},
},
},
EnterpriseMeta: *DefaultEnterpriseMeta(),
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := tc.entry.Normalize()
require.NoError(t, err)
require.Equal(t, tc.expected, tc.entry)
})
}
}
connect: use stronger validation that ingress gateways have compatible protocols defined for their upstreams (#8470) Fixes #8466 Since Consul 1.8.0 there was a bug in how ingress gateway protocol compatibility was enforced. At the point in time that an ingress-gateway config entry was modified the discovery chain for each upstream was checked to ensure the ingress gateway protocol matched. Unfortunately future modifications of other config entries were not validated against existing ingress-gateway definitions, such as: 1. create tcp ingress-gateway pointing to 'api' (ok) 2. create service-defaults for 'api' setting protocol=http (worked, but not ok) 3. create service-splitter or service-router for 'api' (worked, but caused an agent panic) If you were to do these in a different order, it would fail without a crash: 1. create service-defaults for 'api' setting protocol=http (ok) 2. create service-splitter or service-router for 'api' (ok) 3. create tcp ingress-gateway pointing to 'api' (fail with message about protocol mismatch) This PR introduces the missing validation. The two new behaviors are: 1. create tcp ingress-gateway pointing to 'api' (ok) 2. (NEW) create service-defaults for 'api' setting protocol=http ("ok" for back compat) 3. (NEW) create service-splitter or service-router for 'api' (fail with message about protocol mismatch) In consideration for any existing users that may be inadvertently be falling into item (2) above, that is now officiall a valid configuration to be in. For anyone falling into item (3) above while you cannot use the API to manufacture that scenario anymore, anyone that has old (now bad) data will still be able to have the agent use them just enough to generate a new agent/proxycfg error message rather than a panic. Unfortunately we just don't have enough information to properly fix the config entries.
2020-08-12 16:19:20 +00:00
func TestIngressConfigEntry_ListRelatedServices(t *testing.T) {
type testcase struct {
entry IngressGatewayConfigEntry
expectServices []ServiceID
}
cases := map[string]testcase{
"one exact": {
entry: IngressGatewayConfigEntry{
Kind: IngressGateway,
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{
{Name: "web"},
},
},
},
},
expectServices: []ServiceID{NewServiceID("web", nil)},
},
"one wild": {
entry: IngressGatewayConfigEntry{
Kind: IngressGateway,
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{
{Name: "*"},
},
},
},
},
expectServices: nil,
},
"kitchen sink": {
entry: IngressGatewayConfigEntry{
Kind: IngressGateway,
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{
{Name: "api"},
{Name: "web"},
},
},
{
Port: 2222,
Protocol: "tcp",
Services: []IngressService{
{Name: "web"},
{Name: "*"},
{Name: "db"},
{Name: "blah"},
},
},
},
},
expectServices: []ServiceID{
NewServiceID("api", nil),
NewServiceID("blah", nil),
NewServiceID("db", nil),
NewServiceID("web", nil),
},
},
}
for name, tc := range cases {
tc := tc
t.Run(name, func(t *testing.T) {
got := tc.entry.ListRelatedServices()
require.Equal(t, tc.expectServices, got)
})
}
}
func TestIngressConfigEntry_Validate(t *testing.T) {
cases := []struct {
name string
entry IngressGatewayConfigEntry
expectErr string
}{
{
name: "port conflict",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{
{
Name: "mysql",
},
},
},
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{
{
Name: "postgres",
},
},
},
},
},
expectErr: "port 1111 declared on two listeners",
},
{
name: "http features: wildcard",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{
{
Name: "*",
},
},
},
},
},
},
{
name: "http features: wildcard service on invalid protocol",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{
{
Name: "*",
},
},
},
},
},
expectErr: "Wildcard service name is only valid for protocol",
},
{
name: "http features: multiple services",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{
{
Name: "db1",
},
{
Name: "db2",
},
},
},
},
},
expectErr: "multiple services per listener are only supported for protocol",
},
{
name: "tcp listener requires a defined service",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{},
},
},
},
expectErr: "no service declared for listener with port 1111",
},
{
name: "http listener requires a defined service",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{},
},
},
},
expectErr: "no service declared for listener with port 1111",
},
{
name: "empty service name not supported",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{
{},
},
},
},
},
expectErr: "Service name cannot be blank",
},
{
name: "protocol validation",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "asdf",
Services: []IngressService{
{
Name: "db",
},
},
},
},
},
expectErr: "protocol must be 'tcp', 'http', 'http2', or 'grpc'. 'asdf' is an unsupported protocol",
},
{
name: "hosts cannot be set on a tcp listener",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{
{
Name: "db",
Hosts: []string{"db.example.com"},
},
},
},
},
},
expectErr: "Associating hosts to a service is not supported for the tcp protocol",
},
{
name: "hosts cannot be set on a wildcard specifier",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{
{
Name: "*",
Hosts: []string{"db.example.com"},
},
},
},
},
},
expectErr: "Associating hosts to a wildcard service is not supported",
},
{
name: "hosts must be unique per listener",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{
{
Name: "db",
Hosts: []string{"test.example.com"},
},
{
Name: "api",
Hosts: []string{"test.example.com"},
},
},
},
},
},
expectErr: "Hosts must be unique within a specific listener",
},
{
name: "hosts must be a valid DNS name",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{
{
Name: "db",
Hosts: []string{"example..com"},
},
},
},
},
},
expectErr: `Host "example..com" must be a valid DNS hostname`,
},
{
name: "wildcard specifier is only allowed in the leftmost label",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{
{
Name: "db",
Hosts: []string{"*.example.com"},
},
},
},
},
},
},
{
name: "wildcard specifier is not allowed in non-leftmost labels",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{
{
Name: "db",
Hosts: []string{"example.*.com"},
},
},
},
},
},
expectErr: `Host "example.*.com" is not valid, a wildcard specifier is only allowed as the leftmost label`,
},
{
name: "wildcard specifier is not allowed in leftmost labels as a partial",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{
{
Name: "db",
Hosts: []string{"*-test.example.com"},
},
},
},
},
},
expectErr: `Host "*-test.example.com" is not valid, a wildcard specifier is only allowed as the leftmost label`,
},
{
name: "wildcard specifier is allowed for hosts when TLS is disabled",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{
{
Name: "db",
Hosts: []string{"*"},
},
},
},
},
},
},
{
name: "wildcard specifier is not allowed for hosts when TLS is enabled",
entry: IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
TLS: GatewayTLSConfig{
Enabled: true,
},
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{
{
Name: "db",
Hosts: []string{"*"},
},
},
},
},
},
expectErr: `Host '*' is not allowed when TLS is enabled, all hosts must be valid DNS records to add as a DNSSAN`,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := tc.entry.Validate()
if tc.expectErr != "" {
require.Error(t, err)
requireContainsLower(t, err.Error(), tc.expectErr)
} else {
require.NoError(t, err)
}
})
}
}
func TestTerminatingConfigEntry_Validate(t *testing.T) {
cases := []struct {
name string
entry TerminatingGatewayConfigEntry
expectErr string
}{
{
name: "service conflict",
entry: TerminatingGatewayConfigEntry{
Kind: "terminating-gateway",
Name: "terminating-gw-west",
Services: []LinkedService{
{
Name: "foo",
},
{
Name: "foo",
},
},
},
expectErr: "specified more than once",
},
{
name: "blank service name",
entry: TerminatingGatewayConfigEntry{
Kind: "terminating-gateway",
Name: "terminating-gw-west",
Services: []LinkedService{
{
Name: "",
},
},
},
expectErr: "Service name cannot be blank.",
},
{
name: "not all TLS options provided-1",
entry: TerminatingGatewayConfigEntry{
Kind: "terminating-gateway",
Name: "terminating-gw-west",
Services: []LinkedService{
{
Name: "web",
CertFile: "client.crt",
},
},
},
expectErr: "must have a CertFile, CAFile, and KeyFile",
},
{
name: "not all TLS options provided-2",
entry: TerminatingGatewayConfigEntry{
Kind: "terminating-gateway",
Name: "terminating-gw-west",
Services: []LinkedService{
{
Name: "web",
KeyFile: "tls.key",
},
},
},
expectErr: "must have a CertFile, CAFile, and KeyFile",
},
{
name: "all TLS options provided",
entry: TerminatingGatewayConfigEntry{
Kind: "terminating-gateway",
Name: "terminating-gw-west",
Services: []LinkedService{
{
Name: "web",
CAFile: "ca.crt",
CertFile: "client.crt",
KeyFile: "tls.key",
},
},
},
},
{
name: "only providing ca file is allowed",
entry: TerminatingGatewayConfigEntry{
Kind: "terminating-gateway",
Name: "terminating-gw-west",
Services: []LinkedService{
{
Name: "web",
CAFile: "ca.crt",
},
},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := tc.entry.Validate()
if tc.expectErr != "" {
require.Error(t, err)
requireContainsLower(t, err.Error(), tc.expectErr)
} else {
require.NoError(t, err)
}
})
}
}
func TestGatewayService_Addresses(t *testing.T) {
cases := []struct {
name string
input GatewayService
argument []string
expected []string
}{
{
name: "port is zero",
input: GatewayService{},
expected: nil,
},
{
name: "no hosts with empty array",
input: GatewayService{
Port: 8080,
},
expected: nil,
},
{
name: "no hosts with default",
input: GatewayService{
Port: 8080,
},
argument: []string{
"service.ingress.dc.domain",
"service.ingress.dc.alt.domain",
},
expected: []string{
"service.ingress.dc.domain:8080",
"service.ingress.dc.alt.domain:8080",
},
},
{
name: "user-defined hosts",
input: GatewayService{
Port: 8080,
Hosts: []string{"*.test.example.com", "other.example.com"},
},
argument: []string{
"service.ingress.dc.domain",
"service.ingress.alt.domain",
},
expected: []string{"*.test.example.com:8080", "other.example.com:8080"},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
addresses := tc.input.Addresses(tc.argument)
require.ElementsMatch(t, tc.expected, addresses)
})
}
}