// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 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) }) } }