consul/agent/structs/config_entry_gateways_test.go

1791 lines
39 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package structs
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/acl"
)
func TestIngressGatewayConfigEntry(t *testing.T) {
defaultMeta := DefaultEnterpriseMetaInDefaultPartition()
cases := map[string]configEntryTestcase{
"normalize: 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: *defaultMeta,
},
normalizeOnly: true,
},
"normalize: 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: *defaultMeta,
},
normalizeOnly: true,
},
"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",
},
},
},
},
},
validateErr: "port 1111 declared on two listeners",
},
"http features: wildcard": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{
{
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: "*",
},
},
},
},
},
validateErr: "Wildcard service name is only valid for protocol",
},
"http features: multiple services": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{
{
Name: "db1",
},
{
Name: "db2",
},
},
},
},
},
},
"http features: multiple services on tcp listener": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{
{
Name: "db1",
},
{
Name: "db2",
},
},
},
},
},
validateErr: "Multiple services per listener are only supported for L7",
},
// ==========================
"tcp listener requires a defined service": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{},
},
},
},
validateErr: "No service declared for listener with port 1111",
},
"http listener requires a defined service": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{},
},
},
},
validateErr: "No service declared for listener with port 1111",
},
"empty service name not supported": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{
{},
},
},
},
},
validateErr: "Service name cannot be blank",
},
"protocol validation": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "asdf",
Services: []IngressService{
{
Name: "db",
},
},
},
},
},
validateErr: "protocol must be 'tcp', 'http', 'http2', or 'grpc'. 'asdf' is an unsupported protocol",
},
"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"},
},
},
},
},
},
validateErr: "Associating hosts to a service is not supported for the tcp protocol",
},
"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"},
},
},
},
},
},
validateErr: "Associating hosts to a wildcard service is not supported",
},
"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"},
},
},
},
},
},
validateErr: "Hosts must be unique within a specific listener",
},
"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"},
},
},
},
},
},
validateErr: `Host "example..com" must be a valid DNS hostname`,
},
"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"},
},
},
},
},
},
},
"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"},
},
},
},
},
},
validateErr: `Host "example.*.com" is not valid, a wildcard specifier is only allowed as the leftmost label`,
},
"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"},
},
},
},
},
},
validateErr: `Host "*-test.example.com" is not valid, a wildcard specifier is only allowed as the leftmost label`,
},
"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{"*"},
},
},
},
},
},
},
"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{"*"},
},
},
},
},
},
validateErr: `Host '*' is not allowed when TLS is enabled, all hosts must be valid DNS records to add as a DNSSAN`,
},
"request header manip allowed for http(ish) protocol": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{
{
Name: "web",
RequestHeaders: &HTTPHeaderModifiers{
Set: map[string]string{"x-foo": "bar"},
},
},
},
},
{
Port: 2222,
Protocol: "http2",
Services: []IngressService{
{
Name: "web2",
ResponseHeaders: &HTTPHeaderModifiers{
Set: map[string]string{"x-foo": "bar"},
},
},
},
},
{
Port: 3333,
Protocol: "grpc",
Services: []IngressService{
{
Name: "api",
ResponseHeaders: &HTTPHeaderModifiers{
Remove: []string{"x-grpc-internal"},
},
},
},
},
},
},
expectUnchanged: true,
},
"request header manip not allowed for non-http protocol": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{
{
Name: "db",
RequestHeaders: &HTTPHeaderModifiers{
Set: map[string]string{"x-foo": "bar"},
},
},
},
},
},
},
validateErr: "request headers only valid for http",
},
"response header manip not allowed for non-http protocol": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{
{
Name: "db",
ResponseHeaders: &HTTPHeaderModifiers{
Remove: []string{"x-foo"},
},
},
},
},
},
},
validateErr: "response headers only valid for http",
},
"duplicate services not allowed": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{
{
Name: "web",
},
{
Name: "web",
},
},
},
},
},
// Match only the last part of the exected error because the service name
// differs between Ent and CE default/default/web vs web
validateErr: "cannot be added multiple times (listener on port 1111)",
},
"TLS.SDS kitchen sink": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
TLS: GatewayTLSConfig{
SDS: &GatewayTLSSDSConfig{
ClusterName: "secret-service1",
CertResource: "some-ns/ingress-default",
},
},
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
TLS: &GatewayTLSConfig{
SDS: &GatewayTLSSDSConfig{
ClusterName: "secret-service2",
CertResource: "some-ns/ingress-1111",
},
},
Services: []IngressService{
{
Name: "web",
Hosts: []string{"*"},
TLS: &GatewayServiceTLSConfig{
SDS: &GatewayTLSSDSConfig{
ClusterName: "secret-service3",
CertResource: "some-ns/web",
},
},
},
},
},
},
},
},
"TLS.SDS gateway-level": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
TLS: GatewayTLSConfig{
SDS: &GatewayTLSSDSConfig{
ClusterName: "secret-service1",
CertResource: "some-ns/ingress-default",
},
},
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{
{
Name: "db",
},
},
},
},
},
expectUnchanged: true,
},
"TLS.SDS listener-level": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
TLS: &GatewayTLSConfig{
SDS: &GatewayTLSSDSConfig{
ClusterName: "secret-service1",
CertResource: "some-ns/db1",
},
},
Services: []IngressService{
{
Name: "db1",
},
},
},
{
Port: 2222,
Protocol: "tcp",
TLS: &GatewayTLSConfig{
SDS: &GatewayTLSSDSConfig{
ClusterName: "secret-service2",
CertResource: "some-ns/db2",
},
},
Services: []IngressService{
{
Name: "db2",
},
},
},
},
},
expectUnchanged: true,
},
"TLS.SDS gateway-level cluster only": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
TLS: GatewayTLSConfig{
SDS: &GatewayTLSSDSConfig{
ClusterName: "secret-service",
},
},
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
TLS: &GatewayTLSConfig{
SDS: &GatewayTLSSDSConfig{
CertResource: "some-ns/db1",
},
},
Services: []IngressService{
{
Name: "db1",
},
},
},
{
Port: 2222,
Protocol: "tcp",
TLS: &GatewayTLSConfig{
SDS: &GatewayTLSSDSConfig{
CertResource: "some-ns/db2",
},
},
Services: []IngressService{
{
Name: "db2",
},
},
},
},
},
expectUnchanged: true,
},
"TLS.SDS mixed TLS and non-TLS listeners": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
// No Gateway level TLS.Enabled or SDS config
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
TLS: &GatewayTLSConfig{
SDS: &GatewayTLSSDSConfig{
ClusterName: "sds-cluster",
CertResource: "some-ns/db1",
},
},
Services: []IngressService{
{
Name: "db1",
},
},
},
{
Port: 2222,
Protocol: "tcp",
// No TLS config
Services: []IngressService{
{
Name: "db2",
},
},
},
},
},
expectUnchanged: true,
},
"TLS.SDS only service-level mixed": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
// No Gateway level TLS.Enabled or SDS config
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
// No TLS config
Services: []IngressService{
{
Name: "web",
Hosts: []string{"www.example.com"},
TLS: &GatewayServiceTLSConfig{
SDS: &GatewayTLSSDSConfig{
ClusterName: "sds-cluster",
CertResource: "web-cert",
},
},
},
{
Name: "api",
Hosts: []string{"api.example.com"},
TLS: &GatewayServiceTLSConfig{
SDS: &GatewayTLSSDSConfig{
ClusterName: "sds-cluster",
CertResource: "api-cert",
},
},
},
},
},
{
Port: 2222,
Protocol: "http",
// No TLS config
Services: []IngressService{
{
Name: "db2",
},
},
},
},
},
expectUnchanged: true,
},
"TLS.SDS requires cluster if gateway-level cert specified": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
TLS: GatewayTLSConfig{
SDS: &GatewayTLSSDSConfig{
CertResource: "foo",
},
},
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{
{
Name: "db",
},
},
},
},
},
validateErr: "TLS.SDS.ClusterName is required if CertResource is set",
},
"TLS.SDS listener requires cluster if there is no gateway-level one": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
TLS: &GatewayTLSConfig{
SDS: &GatewayTLSSDSConfig{
CertResource: "foo",
},
},
Services: []IngressService{
{
Name: "db",
},
},
},
},
},
validateErr: "TLS.SDS.ClusterName is required if CertResource is set",
},
"TLS.SDS listener requires a cert resource if gw ClusterName set": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
TLS: GatewayTLSConfig{
SDS: &GatewayTLSSDSConfig{
ClusterName: "foo",
},
},
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
Services: []IngressService{
{
Name: "db",
},
},
},
},
},
validateErr: "TLS.SDS.CertResource is required if ClusterName is set for gateway (listener on port 1111)",
},
"TLS.SDS listener requires a cert resource if listener ClusterName set": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "tcp",
TLS: &GatewayTLSConfig{
SDS: &GatewayTLSSDSConfig{
ClusterName: "foo",
},
},
Services: []IngressService{
{
Name: "db",
},
},
},
},
},
validateErr: "TLS.SDS.CertResource is required if ClusterName is set for listener (listener on port 1111)",
},
"TLS.SDS at service level is not supported without Hosts set": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{
{
Name: "*",
TLS: &GatewayServiceTLSConfig{
SDS: &GatewayTLSSDSConfig{
CertResource: "foo",
ClusterName: "sds-cluster",
},
},
},
},
},
},
},
// Note we don't assert the last part `(service \"*\" on listener on port 1111)`
// since the service name is normalized differently on CE and Ent
validateErr: "A service specifying TLS.SDS.CertResource must have at least one item in Hosts",
},
"TLS.SDS at service level needs a cluster from somewhere": {
entry: &IngressGatewayConfigEntry{
Kind: "ingress-gateway",
Name: "ingress-web",
Listeners: []IngressListener{
{
Port: 1111,
Protocol: "http",
Services: []IngressService{
{
Name: "foo",
Hosts: []string{"foo.example.com"},
TLS: &GatewayServiceTLSConfig{
SDS: &GatewayTLSSDSConfig{
CertResource: "foo",
},
},
},
},
},
},
},
// Note we don't assert the last part `(service \"foo\" on listener on port 1111)`
// since the service name is normalized differently on CE and Ent
validateErr: "TLS.SDS.ClusterName is required if CertResource is set",
},
}
testConfigEntryNormalizeAndValidate(t, cases)
}
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 TestTerminatingGatewayConfigEntry(t *testing.T) {
cases := map[string]configEntryTestcase{
"service conflict": {
entry: &TerminatingGatewayConfigEntry{
Kind: "terminating-gateway",
Name: "terminating-gw-west",
Services: []LinkedService{
{
Name: "foo",
},
{
Name: "foo",
},
},
},
validateErr: "specified more than once",
},
"blank service name": {
entry: &TerminatingGatewayConfigEntry{
Kind: "terminating-gateway",
Name: "terminating-gw-west",
Services: []LinkedService{
{
Name: "",
},
},
},
validateErr: "Service name cannot be blank.",
},
"not all TLS options provided-1": {
entry: &TerminatingGatewayConfigEntry{
Kind: "terminating-gateway",
Name: "terminating-gw-west",
Services: []LinkedService{
{
Name: "web",
CertFile: "client.crt",
},
},
},
validateErr: "must have a CertFile, CAFile, and KeyFile",
},
"not all TLS options provided-2": {
entry: &TerminatingGatewayConfigEntry{
Kind: "terminating-gateway",
Name: "terminating-gw-west",
Services: []LinkedService{
{
Name: "web",
KeyFile: "tls.key",
},
},
},
validateErr: "must have a CertFile, CAFile, and KeyFile",
},
"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",
},
},
},
},
"only providing ca file is allowed": {
entry: &TerminatingGatewayConfigEntry{
Kind: "terminating-gateway",
Name: "terminating-gw-west",
Services: []LinkedService{
{
Name: "web",
CAFile: "ca.crt",
},
},
},
},
}
testConfigEntryNormalizeAndValidate(t, cases)
}
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",
"service.ingress.dc.alt.domain.",
},
expected: []string{
"service.ingress.dc.domain:8080",
"service.ingress.dc.alt.domain:8080",
"service.ingress.dc.alt.domain:8080",
},
},
{
name: "user-defined hosts",
input: GatewayService{
Port: 8080,
Hosts: []string{"*.test.example.com", "other.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", "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)
})
}
}
func TestAPIGateway_Listeners(t *testing.T) {
cases := map[string]configEntryTestcase{
"no listeners defined": {
entry: &APIGatewayConfigEntry{
Kind: "api-gateway",
Name: "api-gw-one",
},
validateErr: "api gateway must have at least one listener",
},
"listener name conflict": {
entry: &APIGatewayConfigEntry{
Kind: "api-gateway",
Name: "api-gw-one",
Listeners: []APIGatewayListener{
{
Port: 80,
Name: "foo",
},
{
Port: 80,
Name: "foo",
},
},
},
validateErr: "multiple listeners with the name",
},
"empty listener name": {
entry: &APIGatewayConfigEntry{
Kind: "api-gateway",
Name: "api-gw-one",
Listeners: []APIGatewayListener{
{
Port: 80,
Protocol: "tcp",
},
},
},
validateErr: "listener name \"\" is invalid, must be at least 1 character and contain only letters, numbers, or dashes",
},
"invalid listener name": {
entry: &APIGatewayConfigEntry{
Kind: "api-gateway",
Name: "api-gw-one",
Listeners: []APIGatewayListener{
{
Port: 80,
Protocol: "tcp",
Name: "/",
},
},
},
validateErr: "listener name \"/\" is invalid, must be at least 1 character and contain only letters, numbers, or dashes",
},
"merged listener protocol conflict": {
entry: &APIGatewayConfigEntry{
Kind: "api-gateway",
Name: "api-gw-two",
Listeners: []APIGatewayListener{
{
Name: "listener-one",
Port: 80,
Protocol: ListenerProtocolHTTP,
},
{
Name: "foo",
Port: 80,
Protocol: ListenerProtocolTCP,
},
},
},
validateErr: "cannot be merged",
},
"merged listener hostname conflict": {
entry: &APIGatewayConfigEntry{
Kind: "api-gateway",
Name: "api-gw-three",
Listeners: []APIGatewayListener{
{
Name: "listener",
Port: 80,
Hostname: "host.one",
},
{
Name: "foo",
Port: 80,
Hostname: "host.two",
},
},
},
validateErr: "cannot be merged",
},
"invalid protocol": {
entry: &APIGatewayConfigEntry{
Kind: "api-gateway",
Name: "api-gw-four",
Listeners: []APIGatewayListener{
{
Name: "listener",
Port: 80,
Hostname: "host.one",
Protocol: APIGatewayListenerProtocol("UDP"),
},
},
},
validateErr: "unsupported listener protocol",
},
"hostname in unsupported protocol": {
entry: &APIGatewayConfigEntry{
Kind: "api-gateway",
Name: "api-gw-five",
Listeners: []APIGatewayListener{
{
Name: "listener",
Port: 80,
Hostname: "host.one",
Protocol: APIGatewayListenerProtocol("tcp"),
},
},
},
validateErr: "hostname specification is not supported",
},
"invalid port": {
entry: &APIGatewayConfigEntry{
Kind: "api-gateway",
Name: "api-gw-six",
Listeners: []APIGatewayListener{
{
Name: "listener",
Port: -1,
Protocol: APIGatewayListenerProtocol("tcp"),
},
},
},
validateErr: "not in the range 1-65535",
},
"invalid hostname": {
entry: &APIGatewayConfigEntry{
Kind: "api-gateway",
Name: "api-gw-seven",
Listeners: []APIGatewayListener{
{
Name: "listener",
Port: 80,
Hostname: "*.*.host.one",
Protocol: APIGatewayListenerProtocol("http"),
},
},
},
validateErr: "only allowed as the left-most label",
},
"unsupported certificate kind": {
entry: &APIGatewayConfigEntry{
Kind: "api-gateway",
Name: "api-gw-eight",
Listeners: []APIGatewayListener{
{
Name: "listener",
Port: 80,
Hostname: "host.one",
Protocol: APIGatewayListenerProtocol("http"),
TLS: APIGatewayTLSConfiguration{
Certificates: []ResourceReference{{
Kind: APIGateway,
}},
},
},
},
},
validateErr: "unsupported certificate kind",
},
"unnamed certificate": {
entry: &APIGatewayConfigEntry{
Kind: "api-gateway",
Name: "api-gw-nine",
Listeners: []APIGatewayListener{
{
Name: "listener",
Port: 80,
Hostname: "host.one",
Protocol: APIGatewayListenerProtocol("http"),
TLS: APIGatewayTLSConfiguration{
Certificates: []ResourceReference{{
Kind: InlineCertificate,
}},
},
},
},
},
validateErr: "certificate reference must have a name",
},
}
testConfigEntryNormalizeAndValidate(t, cases)
}
func TestBoundAPIGateway(t *testing.T) {
cases := map[string]configEntryTestcase{
"invalid certificate, no name": {
entry: &BoundAPIGatewayConfigEntry{
Kind: BoundAPIGateway,
Name: "bound-api-gw-one",
Listeners: []BoundAPIGatewayListener{
{
Name: "one",
Certificates: []ResourceReference{{
Kind: InlineCertificate,
}},
},
},
},
validateErr: "certificate reference must have a name",
},
"invalid certificate, no kind": {
entry: &BoundAPIGatewayConfigEntry{
Kind: BoundAPIGateway,
Name: "bound-api-gw-two",
Listeners: []BoundAPIGatewayListener{
{
Name: "one",
Certificates: []ResourceReference{{
Name: "foo",
}},
},
},
},
validateErr: "unsupported certificate kind",
},
"invalid route, no name": {
entry: &BoundAPIGatewayConfigEntry{
Kind: BoundAPIGateway,
Name: "bound-api-gw-three",
Listeners: []BoundAPIGatewayListener{
{
Name: "one",
Routes: []ResourceReference{{
Kind: TCPRoute,
}},
},
},
},
validateErr: "route reference must have a name",
},
"invalid route, no kind": {
entry: &BoundAPIGatewayConfigEntry{
Kind: BoundAPIGateway,
Name: "bound-api-gw-four",
Listeners: []BoundAPIGatewayListener{
{
Name: "one",
Routes: []ResourceReference{{
Name: "foo",
}},
},
},
},
validateErr: "unsupported route kind",
},
}
testConfigEntryNormalizeAndValidate(t, cases)
}
func TestListenerBindRoute(t *testing.T) {
cases := map[string]struct {
listener BoundAPIGatewayListener
route BoundRoute
expectedListener BoundAPIGatewayListener
expectedDidBind bool
}{
"Listener has no routes": {
listener: BoundAPIGatewayListener{},
route: &TCPRouteConfigEntry{
Kind: TCPRoute,
Name: "Test Route",
},
expectedListener: BoundAPIGatewayListener{
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route",
},
},
},
expectedDidBind: true,
},
"Listener to update existing route": {
listener: BoundAPIGatewayListener{
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route 1",
},
{
Kind: TCPRoute,
Name: "Test Route 2",
},
{
Kind: TCPRoute,
Name: "Test Route 3",
},
},
},
route: &TCPRouteConfigEntry{
Kind: TCPRoute,
Name: "Test Route 2",
},
expectedListener: BoundAPIGatewayListener{
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route 1",
},
{
Kind: TCPRoute,
Name: "Test Route 2",
},
{
Kind: TCPRoute,
Name: "Test Route 3",
},
},
},
expectedDidBind: true,
},
"Listener appends new route": {
listener: BoundAPIGatewayListener{
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route 1",
},
{
Kind: TCPRoute,
Name: "Test Route 2",
},
},
},
route: &TCPRouteConfigEntry{
Kind: TCPRoute,
Name: "Test Route 3",
},
expectedListener: BoundAPIGatewayListener{
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route 1",
},
{
Kind: TCPRoute,
Name: "Test Route 2",
},
{
Kind: TCPRoute,
Name: "Test Route 3",
},
},
},
expectedDidBind: true,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
routeRef := ResourceReference{
Kind: tc.route.GetKind(),
Name: tc.route.GetName(),
EnterpriseMeta: *tc.route.GetEnterpriseMeta(),
}
actualDidBind := tc.listener.BindRoute(routeRef)
require.Equal(t, tc.expectedDidBind, actualDidBind)
require.Equal(t, tc.expectedListener.Routes, tc.listener.Routes)
})
}
}
func TestListenerUnbindRoute(t *testing.T) {
cases := map[string]struct {
listener BoundAPIGatewayListener
route BoundRoute
expectedListener BoundAPIGatewayListener
expectedDidUnbind bool
}{
"Listener has no routes": {
listener: BoundAPIGatewayListener{},
route: &TCPRouteConfigEntry{
Kind: TCPRoute,
Name: "Test Route",
},
expectedListener: BoundAPIGatewayListener{},
expectedDidUnbind: false,
},
"Listener to remove existing route": {
listener: BoundAPIGatewayListener{
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route 1",
},
{
Kind: TCPRoute,
Name: "Test Route 2",
},
{
Kind: TCPRoute,
Name: "Test Route 3",
},
},
},
route: &TCPRouteConfigEntry{
Kind: TCPRoute,
Name: "Test Route 2",
},
expectedListener: BoundAPIGatewayListener{
Routes: []ResourceReference{
{
Kind: TCPRoute,
Name: "Test Route 1",
},
{
Kind: TCPRoute,
Name: "Test Route 3",
},
},
},
expectedDidUnbind: true,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
routeRef := ResourceReference{
Kind: tc.route.GetKind(),
Name: tc.route.GetName(),
EnterpriseMeta: *tc.route.GetEnterpriseMeta(),
}
actualDidUnbind := tc.listener.UnbindRoute(routeRef)
require.Equal(t, tc.expectedDidUnbind, actualDidUnbind)
require.Equal(t, tc.expectedListener.Routes, tc.listener.Routes)
})
}
}
func Test_ServiceRouteReferences_AddService(t *testing.T) {
t.Parallel()
key := ServiceName{Name: "service", EnterpriseMeta: acl.EnterpriseMeta{}}
testCases := map[string]struct {
routeRef ResourceReference
subject ServiceRouteReferences
expectedRefs ServiceRouteReferences
}{
"key does not exist yet": {
routeRef: ResourceReference{
Kind: "http-route",
Name: "http-route",
},
subject: make(ServiceRouteReferences),
expectedRefs: ServiceRouteReferences{
key: []ResourceReference{
{
Kind: "http-route",
Name: "http-route",
},
},
},
},
"key exists adding new route": {
routeRef: ResourceReference{
Kind: "http-route",
Name: "http-route",
},
subject: ServiceRouteReferences{
key: []ResourceReference{
{
Kind: "http-route",
Name: "http-route-other",
},
},
},
expectedRefs: ServiceRouteReferences{
key: []ResourceReference{
{
Kind: "http-route",
Name: "http-route-other",
},
{
Kind: "http-route",
Name: "http-route",
},
},
},
},
"key exists adding existing route": {
routeRef: ResourceReference{
Kind: "http-route",
Name: "http-route",
},
subject: ServiceRouteReferences{
key: []ResourceReference{
{
Kind: "http-route",
Name: "http-route",
},
},
},
expectedRefs: ServiceRouteReferences{
key: []ResourceReference{
{
Kind: "http-route",
Name: "http-route",
},
},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
tc.subject.AddService(key, tc.routeRef)
require.Equal(t, tc.subject, tc.expectedRefs)
})
}
}
func Test_ServiceRouteReferences_RemoveRouteRef(t *testing.T) {
t.Parallel()
keySvcOne := ServiceName{Name: "service-one", EnterpriseMeta: acl.EnterpriseMeta{}}
keySvcTwo := ServiceName{Name: "service-two", EnterpriseMeta: acl.EnterpriseMeta{}}
testCases := map[string]struct {
routeRef ResourceReference
subject ServiceRouteReferences
expectedRefs ServiceRouteReferences
}{
"route ref exists for one service": {
routeRef: ResourceReference{
Kind: "http-route",
Name: "http-route",
},
subject: ServiceRouteReferences{
keySvcOne: []ResourceReference{
{
Kind: "http-route",
Name: "http-route",
},
{
Kind: "http-route",
Name: "http-route-other",
},
},
keySvcTwo: []ResourceReference{
{
Kind: "http-route",
Name: "http-route-other",
},
},
},
expectedRefs: ServiceRouteReferences{
keySvcOne: []ResourceReference{
{
Kind: "http-route",
Name: "http-route-other",
},
},
keySvcTwo: []ResourceReference{
{
Kind: "http-route",
Name: "http-route-other",
},
},
},
},
"route ref exists for multiple services": {
routeRef: ResourceReference{
Kind: "http-route",
Name: "http-route",
},
subject: ServiceRouteReferences{
keySvcOne: []ResourceReference{
{
Kind: "http-route",
Name: "http-route",
},
{
Kind: "http-route",
Name: "http-route-other",
},
},
keySvcTwo: []ResourceReference{
{
Kind: "http-route",
Name: "http-route",
},
{
Kind: "http-route",
Name: "http-route-other",
},
},
},
expectedRefs: ServiceRouteReferences{
keySvcOne: []ResourceReference{
{
Kind: "http-route",
Name: "http-route-other",
},
},
keySvcTwo: []ResourceReference{
{
Kind: "http-route",
Name: "http-route-other",
},
},
},
},
"route exists and is only ref for service--service is removed from refs": {
routeRef: ResourceReference{
Kind: "http-route",
Name: "http-route",
},
subject: ServiceRouteReferences{
keySvcOne: []ResourceReference{
{
Kind: "http-route",
Name: "http-route",
},
},
keySvcTwo: []ResourceReference{
{
Kind: "http-route",
Name: "http-route",
},
{
Kind: "http-route",
Name: "http-route-other",
},
},
},
expectedRefs: ServiceRouteReferences{
keySvcTwo: []ResourceReference{
{
Kind: "http-route",
Name: "http-route-other",
},
},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
tc.subject.RemoveRouteRef(tc.routeRef)
require.Equal(t, tc.subject, tc.expectedRefs)
})
}
}