diff --git a/agent/connect/sni.go b/agent/connect/sni.go index 17fce7e27c..e60740e4b1 100644 --- a/agent/connect/sni.go +++ b/agent/connect/sni.go @@ -65,6 +65,39 @@ func ServiceSNI(service string, subset string, namespace string, partition strin } } +func dotSplitLast(s string, n int) string { + tokens := strings.SplitN(s, ".", n) + if len(tokens) != n { + return "" + } + return tokens[n-1] +} + +func TrustDomainForTarget(target structs.DiscoveryTarget) string { + if target.External { + return "" + } + + switch target.Partition { + case "default": + if target.ServiceSubset == "" { + // service, namespace, datacenter, internal, trustDomain + return dotSplitLast(target.SNI, 5) + } else { + // subset, service, namespace, datacenter, internal, trustDomain + return dotSplitLast(target.SNI, 6) + } + default: + if target.ServiceSubset == "" { + // service, namespace, partition, datacenter, internalVersion, trustDomain + return dotSplitLast(target.SNI, 6) + } else { + // subset, service, namespace, partition, datacenter, internalVersion, trustDomain + return dotSplitLast(target.SNI, 7) + } + } +} + func PeeredServiceSNI(service, namespace, partition, peerName, trustDomain string) string { if peerName == "" { panic("peer name is a requirement for this function and does not make sense without it") diff --git a/agent/consul/discoverychain/gateway.go b/agent/consul/discoverychain/gateway.go index 555dc62658..cd582f1ec0 100644 --- a/agent/consul/discoverychain/gateway.go +++ b/agent/consul/discoverychain/gateway.go @@ -14,6 +14,8 @@ import ( // gateway from its configuration and multiple other discovery chains. type GatewayChainSynthesizer struct { datacenter string + trustDomain string + suffix string gateway *structs.APIGatewayConfigEntry matchesByHostname map[string][]hostnameMatch tcpRoutes []structs.TCPRouteConfigEntry @@ -27,9 +29,11 @@ type hostnameMatch struct { // NewGatewayChainSynthesizer creates a new GatewayChainSynthesizer for the // given gateway and datacenter. -func NewGatewayChainSynthesizer(datacenter string, gateway *structs.APIGatewayConfigEntry) *GatewayChainSynthesizer { +func NewGatewayChainSynthesizer(datacenter, trustDomain, suffix string, gateway *structs.APIGatewayConfigEntry) *GatewayChainSynthesizer { return &GatewayChainSynthesizer{ datacenter: datacenter, + trustDomain: trustDomain, + suffix: suffix, gateway: gateway, matchesByHostname: map[string][]hostnameMatch{}, } @@ -45,7 +49,13 @@ func (l *GatewayChainSynthesizer) AddTCPRoute(route structs.TCPRouteConfigEntry) // single hostname can be specified in multiple routes. Routing for a given // hostname must behave based on the aggregate of all rules that apply to it. func (l *GatewayChainSynthesizer) AddHTTPRoute(route structs.HTTPRouteConfigEntry) { - for _, host := range route.Hostnames { + hostnames := route.Hostnames + if len(route.Hostnames) == 0 { + // add a wildcard if there are no explicit hostnames set + hostnames = append(hostnames, "*") + } + + for _, host := range hostnames { matches, ok := l.matchesByHostname[host] if !ok { matches = []hostnameMatch{} @@ -86,41 +96,48 @@ func (l *GatewayChainSynthesizer) AddHTTPRoute(route structs.HTTPRouteConfigEntr // This is currently used to help API gateways masquarade as ingress gateways // by providing a set of virtual config entries that change the routing behavior // to upstreams referenced in the given HTTPRoutes or TCPRoutes. -func (l *GatewayChainSynthesizer) Synthesize(chains ...*structs.CompiledDiscoveryChain) ([]structs.IngressService, *structs.CompiledDiscoveryChain, error) { +func (l *GatewayChainSynthesizer) Synthesize(chains ...*structs.CompiledDiscoveryChain) ([]structs.IngressService, []*structs.CompiledDiscoveryChain, error) { if len(chains) == 0 { return nil, nil, fmt.Errorf("must provide at least one compiled discovery chain") } - services, entries := l.synthesizeEntries() + services, set := l.synthesizeEntries() - if entries.IsEmpty() { + if len(set) == 0 { // we can't actually compile a discovery chain, i.e. we're using a TCPRoute-based listener, instead, just return the ingresses - // and the first pre-compiled discovery chain - return services, chains[0], nil + // and the pre-compiled discovery chains + return services, chains, nil } - compiled, err := Compile(CompileRequest{ - ServiceName: l.gateway.Name, - EvaluateInNamespace: l.gateway.NamespaceOrDefault(), - EvaluateInPartition: l.gateway.PartitionOrDefault(), - EvaluateInDatacenter: l.datacenter, - Entries: entries, - }) - if err != nil { - return nil, nil, err - } + compiledChains := make([]*structs.CompiledDiscoveryChain, 0, len(set)) + for i, service := range services { + entries := set[i] - for _, c := range chains { - for id, target := range c.Targets { - compiled.Targets[id] = target + compiled, err := Compile(CompileRequest{ + ServiceName: service.Name, + EvaluateInNamespace: service.NamespaceOrDefault(), + EvaluateInPartition: service.PartitionOrDefault(), + EvaluateInDatacenter: l.datacenter, + EvaluateInTrustDomain: l.trustDomain, + Entries: entries, + }) + + if err != nil { + return nil, nil, err } - for id, node := range c.Nodes { - compiled.Nodes[id] = node + for _, c := range chains { + for id, target := range c.Targets { + compiled.Targets[id] = target + } + for id, node := range c.Nodes { + compiled.Nodes[id] = node + } + compiled.EnvoyExtensions = append(compiled.EnvoyExtensions, c.EnvoyExtensions...) } - compiled.EnvoyExtensions = append(compiled.EnvoyExtensions, c.EnvoyExtensions...) + compiledChains = append(compiledChains, compiled) } - return services, compiled, nil + return services, compiledChains, nil } // consolidateHTTPRoutes combines all rules into the shortest possible list of routes @@ -132,7 +149,7 @@ func (l *GatewayChainSynthesizer) consolidateHTTPRoutes() []structs.HTTPRouteCon // Create route for this hostname route := structs.HTTPRouteConfigEntry{ Kind: structs.HTTPRoute, - Name: fmt.Sprintf("%s-%s", l.gateway.Name, hostsKey(hostname)), + Name: fmt.Sprintf("%s-%s-%s", l.gateway.Name, l.suffix, hostsKey(hostname)), Hostnames: []string{hostname}, Rules: make([]structs.HTTPRouteRule, 0, len(rules)), Meta: l.gateway.Meta, @@ -170,16 +187,18 @@ func hostsKey(hosts ...string) string { return strconv.FormatUint(uint64(hostsHash.Sum32()), 16) } -func (l *GatewayChainSynthesizer) synthesizeEntries() ([]structs.IngressService, *configentry.DiscoveryChainSet) { +func (l *GatewayChainSynthesizer) synthesizeEntries() ([]structs.IngressService, []*configentry.DiscoveryChainSet) { services := []structs.IngressService{} - entries := configentry.NewDiscoveryChainSet() + entries := []*configentry.DiscoveryChainSet{} for _, route := range l.consolidateHTTPRoutes() { + entrySet := configentry.NewDiscoveryChainSet() ingress, router, splitters, defaults := synthesizeHTTPRouteDiscoveryChain(route) - entries.AddRouters(router) - entries.AddSplitters(splitters...) - entries.AddServices(defaults...) + entrySet.AddRouters(router) + entrySet.AddSplitters(splitters...) + entrySet.AddServices(defaults...) services = append(services, ingress) + entries = append(entries, entrySet) } for _, route := range l.tcpRoutes { diff --git a/agent/consul/discoverychain/gateway_httproute.go b/agent/consul/discoverychain/gateway_httproute.go index 7e4b934eb9..aaaec12e6b 100644 --- a/agent/consul/discoverychain/gateway_httproute.go +++ b/agent/consul/discoverychain/gateway_httproute.go @@ -44,7 +44,7 @@ func synthesizeHTTPRouteDiscoveryChain(route structs.HTTPRouteConfigEntry) (stru splitters := []*structs.ServiceSplitterConfigEntry{} defaults := []*structs.ServiceConfigEntry{} - router, splits := httpRouteToDiscoveryChain(route) + router, splits, upstreamDefaults := httpRouteToDiscoveryChain(route) serviceDefault := httpServiceDefault(router, meta) defaults = append(defaults, serviceDefault) for _, split := range splits { @@ -53,6 +53,7 @@ func synthesizeHTTPRouteDiscoveryChain(route structs.HTTPRouteConfigEntry) (stru defaults = append(defaults, httpServiceDefault(split, meta)) } } + defaults = append(defaults, upstreamDefaults...) ingress := structs.IngressService{ Name: router.Name, @@ -64,7 +65,7 @@ func synthesizeHTTPRouteDiscoveryChain(route structs.HTTPRouteConfigEntry) (stru return ingress, router, splitters, defaults } -func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.ServiceRouterConfigEntry, []*structs.ServiceSplitterConfigEntry) { +func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.ServiceRouterConfigEntry, []*structs.ServiceSplitterConfigEntry, []*structs.ServiceConfigEntry) { router := &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: route.GetName(), @@ -72,6 +73,7 @@ func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.Ser EnterpriseMeta: route.EnterpriseMeta, } var splitters []*structs.ServiceSplitterConfigEntry + var defaults []*structs.ServiceConfigEntry for idx, rule := range route.Rules { modifier := httpRouteFiltersToServiceRouteHeaderModifier(rule.Filters.Headers) @@ -96,6 +98,15 @@ func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.Ser destination.Partition = service.PartitionOrDefault() destination.PrefixRewrite = servicePrefixRewrite destination.RequestHeaders = modifier + + // since we have already validated the protocol elsewhere, we + // create a new service defaults here to make sure we pass validation + defaults = append(defaults, &structs.ServiceConfigEntry{ + Kind: structs.ServiceDefaults, + Name: service.Name, + Protocol: "http", + EnterpriseMeta: service.EnterpriseMeta, + }) } else { // create a virtual service to split destination.Service = fmt.Sprintf("%s-%d", route.GetName(), idx) @@ -133,6 +144,15 @@ func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.Ser split.Namespace = service.NamespaceOrDefault() split.Partition = service.PartitionOrDefault() splitter.Splits = append(splitter.Splits, split) + + // since we have already validated the protocol elsewhere, we + // create a new service defaults here to make sure we pass validation + defaults = append(defaults, &structs.ServiceConfigEntry{ + Kind: structs.ServiceDefaults, + Name: service.Name, + Protocol: "http", + EnterpriseMeta: service.EnterpriseMeta, + }) } if len(splitter.Splits) > 0 { splitters = append(splitters, splitter) @@ -153,7 +173,7 @@ func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.Ser } } - return router, splitters + return router, splitters, defaults } func httpRouteFiltersToDestinationPrefixRewrite(rewrites []structs.URLRewrite) string { diff --git a/agent/consul/discoverychain/gateway_test.go b/agent/consul/discoverychain/gateway_test.go index d56314ace5..1d6ec78d24 100644 --- a/agent/consul/discoverychain/gateway_test.go +++ b/agent/consul/discoverychain/gateway_test.go @@ -23,13 +23,15 @@ func TestGatewayChainSynthesizer_AddTCPRoute(t *testing.T) { expected := GatewayChainSynthesizer{ datacenter: datacenter, gateway: gateway, + trustDomain: "domain", + suffix: "suffix", matchesByHostname: map[string][]hostnameMatch{}, tcpRoutes: []structs.TCPRouteConfigEntry{ route, }, } - gatewayChainSynthesizer := NewGatewayChainSynthesizer(datacenter, gateway) + gatewayChainSynthesizer := NewGatewayChainSynthesizer(datacenter, "domain", "suffix", gateway) // Add a TCP route gatewayChainSynthesizer.AddTCPRoute(route) @@ -49,7 +51,9 @@ func TestGatewayChainSynthesizer_AddHTTPRoute(t *testing.T) { Kind: structs.HTTPRoute, Name: "route", }, - expectedMatchesByHostname: map[string][]hostnameMatch{}, + expectedMatchesByHostname: map[string][]hostnameMatch{ + "*": {}, + }, }, "single hostname with no rules": { route: structs.HTTPRouteConfigEntry{ @@ -453,7 +457,7 @@ func TestGatewayChainSynthesizer_AddHTTPRoute(t *testing.T) { Name: "gateway", } - gatewayChainSynthesizer := NewGatewayChainSynthesizer(datacenter, gateway) + gatewayChainSynthesizer := NewGatewayChainSynthesizer(datacenter, "domain", "suffix", gateway) gatewayChainSynthesizer.AddHTTPRoute(tc.route) @@ -472,11 +476,11 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) { chain *structs.CompiledDiscoveryChain extra []*structs.CompiledDiscoveryChain expectedIngressServices []structs.IngressService - expectedDiscoveryChain *structs.CompiledDiscoveryChain + expectedDiscoveryChains []*structs.CompiledDiscoveryChain }{ // TODO Add tests for other synthesizer types. "TCPRoute-based listener": { - synthesizer: NewGatewayChainSynthesizer("dc1", &structs.APIGatewayConfigEntry{ + synthesizer: NewGatewayChainSynthesizer("dc1", "domain", "suffix", &structs.APIGatewayConfigEntry{ Kind: structs.APIGateway, Name: "gateway", }), @@ -493,14 +497,14 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) { }, extra: []*structs.CompiledDiscoveryChain{}, expectedIngressServices: []structs.IngressService{}, - expectedDiscoveryChain: &structs.CompiledDiscoveryChain{ + expectedDiscoveryChains: []*structs.CompiledDiscoveryChain{{ ServiceName: "foo", Namespace: "default", Datacenter: "dc1", - }, + }}, }, "HTTPRoute-based listener": { - synthesizer: NewGatewayChainSynthesizer("dc1", &structs.APIGatewayConfigEntry{ + synthesizer: NewGatewayChainSynthesizer("dc1", "domain", "suffix", &structs.APIGatewayConfigEntry{ Kind: structs.APIGateway, Name: "gateway", }), @@ -508,6 +512,11 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) { { Kind: structs.HTTPRoute, Name: "http-route", + Rules: []structs.HTTPRouteRule{{ + Services: []structs.HTTPService{{ + Name: "foo", + }}, + }}, }, }, chain: &structs.CompiledDiscoveryChain{ @@ -515,13 +524,98 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) { Namespace: "default", Datacenter: "dc1", }, - extra: []*structs.CompiledDiscoveryChain{}, - expectedIngressServices: []structs.IngressService{}, - expectedDiscoveryChain: &structs.CompiledDiscoveryChain{ - ServiceName: "foo", + extra: []*structs.CompiledDiscoveryChain{}, + expectedIngressServices: []structs.IngressService{{ + Name: "gateway-suffix-9b9265b", + Hosts: []string{"*"}, + }}, + expectedDiscoveryChains: []*structs.CompiledDiscoveryChain{{ + ServiceName: "gateway-suffix-9b9265b", + Partition: "default", Namespace: "default", Datacenter: "dc1", - }, + Protocol: "http", + StartNode: "router:gateway-suffix-9b9265b.default.default", + Nodes: map[string]*structs.DiscoveryGraphNode{ + "resolver:gateway-suffix-9b9265b.default.default.dc1": { + Type: "resolver", + Name: "gateway-suffix-9b9265b.default.default.dc1", + Resolver: &structs.DiscoveryResolver{ + Target: "gateway-suffix-9b9265b.default.default.dc1", + Default: true, + ConnectTimeout: 5000000000, + }, + }, + "router:gateway-suffix-9b9265b.default.default": { + Type: "router", + Name: "gateway-suffix-9b9265b.default.default", + Routes: []*structs.DiscoveryRoute{{ + Definition: &structs.ServiceRoute{ + Match: &structs.ServiceRouteMatch{ + HTTP: &structs.ServiceRouteHTTPMatch{ + PathPrefix: "/", + }, + }, + Destination: &structs.ServiceRouteDestination{ + Service: "foo", + Partition: "default", + Namespace: "default", + RequestHeaders: &structs.HTTPHeaderModifiers{ + Add: make(map[string]string), + Set: make(map[string]string), + }, + }, + }, + NextNode: "resolver:foo.default.default.dc1", + }, { + Definition: &structs.ServiceRoute{ + Match: &structs.ServiceRouteMatch{ + HTTP: &structs.ServiceRouteHTTPMatch{ + PathPrefix: "/", + }, + }, + Destination: &structs.ServiceRouteDestination{ + Service: "gateway-suffix-9b9265b", + Partition: "default", + Namespace: "default", + }, + }, + NextNode: "resolver:gateway-suffix-9b9265b.default.default.dc1", + }}, + }, + "resolver:foo.default.default.dc1": { + Type: "resolver", + Name: "foo.default.default.dc1", + Resolver: &structs.DiscoveryResolver{ + Target: "foo.default.default.dc1", + Default: true, + ConnectTimeout: 5000000000, + }, + }, + }, + Targets: map[string]*structs.DiscoveryTarget{ + "gateway-suffix-9b9265b.default.default.dc1": { + ID: "gateway-suffix-9b9265b.default.default.dc1", + Service: "gateway-suffix-9b9265b", + Datacenter: "dc1", + Partition: "default", + Namespace: "default", + ConnectTimeout: 5000000000, + SNI: "gateway-suffix-9b9265b.default.dc1.internal.domain", + Name: "gateway-suffix-9b9265b.default.dc1.internal.domain", + }, + "foo.default.default.dc1": { + ID: "foo.default.default.dc1", + Service: "foo", + Datacenter: "dc1", + Partition: "default", + Namespace: "default", + ConnectTimeout: 5000000000, + SNI: "foo.default.dc1.internal.domain", + Name: "foo.default.dc1.internal.domain", + }, + }, + }}, }, } @@ -535,11 +629,11 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) { } chains := append([]*structs.CompiledDiscoveryChain{tc.chain}, tc.extra...) - ingressServices, discoveryChain, err := tc.synthesizer.Synthesize(chains...) + ingressServices, discoveryChains, err := tc.synthesizer.Synthesize(chains...) require.NoError(t, err) require.Equal(t, tc.expectedIngressServices, ingressServices) - require.Equal(t, tc.expectedDiscoveryChain, discoveryChain) + require.Equal(t, tc.expectedDiscoveryChains, discoveryChains) }) } } diff --git a/agent/consul/gateways/controller_gateways.go b/agent/consul/gateways/controller_gateways.go index 40faa33224..53d8c9a888 100644 --- a/agent/consul/gateways/controller_gateways.go +++ b/agent/consul/gateways/controller_gateways.go @@ -684,7 +684,7 @@ func (g *gatewayMeta) updateRouteBinding(route structs.BoundRoute) (bool, []stru // shouldBindRoute returns whether a Route's parent reference references the Gateway // that we wrap. func (g *gatewayMeta) shouldBindRoute(ref structs.ResourceReference) bool { - return ref.Kind == structs.APIGateway && g.Gateway.Name == ref.Name && g.Gateway.EnterpriseMeta.IsSame(&ref.EnterpriseMeta) + return (ref.Kind == structs.APIGateway || ref.Kind == "") && g.Gateway.Name == ref.Name && g.Gateway.EnterpriseMeta.IsSame(&ref.EnterpriseMeta) } // shouldBindRouteToListener returns whether a Route's parent reference should attempt diff --git a/agent/consul/gateways/controller_gateways_test.go b/agent/consul/gateways/controller_gateways_test.go index aa7b747fd1..0c47809c76 100644 --- a/agent/consul/gateways/controller_gateways_test.go +++ b/agent/consul/gateways/controller_gateways_test.go @@ -181,6 +181,7 @@ func TestBoundAPIGatewayBindRoute(t *testing.T) { Name: "Route", Parents: []structs.ResourceReference{ { + Kind: "Foo", Name: "Gateway", SectionName: "Listener", }, diff --git a/agent/proxycfg/snapshot.go b/agent/proxycfg/snapshot.go index 8997763868..13c8540056 100644 --- a/agent/proxycfg/snapshot.go +++ b/agent/proxycfg/snapshot.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/consul/discoverychain" "github.com/hashicorp/consul/agent/proxycfg/internal/watch" "github.com/hashicorp/consul/agent/structs" @@ -745,7 +746,11 @@ type configSnapshotAPIGateway struct { func (c *configSnapshotAPIGateway) ToIngress(datacenter string) (configSnapshotIngressGateway, error) { // Convert API Gateway Listeners to Ingress Listeners. ingressListeners := make(map[IngressListenerKey]structs.IngressListener, len(c.Listeners)) + ingressUpstreams := make(map[IngressListenerKey]structs.Upstreams, len(c.Listeners)) synthesizedChains := map[UpstreamID]*structs.CompiledDiscoveryChain{} + watchedUpstreamEndpoints := make(map[UpstreamID]map[string]structs.CheckServiceNodes) + watchedGatewayEndpoints := make(map[UpstreamID]map[string]structs.CheckServiceNodes) + for name, listener := range c.Listeners { boundListener, ok := c.BoundListeners[name] if !ok { @@ -764,14 +769,37 @@ func (c *configSnapshotAPIGateway) ToIngress(datacenter string) (configSnapshotI } // Create a synthesized discovery chain for each service. - services, compiled, err := c.synthesizeChains(datacenter, listener.Protocol, boundListener) + services, upstreams, compiled, err := c.synthesizeChains(datacenter, listener.Protocol, listener.Port, listener.Name, boundListener) if err != nil { return configSnapshotIngressGateway{}, err } + + if len(upstreams) == 0 { + // skip if we can't construct any upstreams + continue + } + ingressListener.Services = services - for _, service := range services { + for i, service := range services { id := NewUpstreamIDFromServiceName(structs.NewServiceName(service.Name, &service.EnterpriseMeta)) - synthesizedChains[id] = compiled + upstreamEndpoints := make(map[string]structs.CheckServiceNodes) + gatewayEndpoints := make(map[string]structs.CheckServiceNodes) + + // add the watched endpoints and gateway endpoints under the new upstream + for _, endpoints := range c.WatchedUpstreamEndpoints { + for targetID, endpoint := range endpoints { + upstreamEndpoints[targetID] = endpoint + } + } + for _, endpoints := range c.WatchedGatewayEndpoints { + for targetID, endpoint := range endpoints { + gatewayEndpoints[targetID] = endpoint + } + } + + synthesizedChains[id] = compiled[i] + watchedUpstreamEndpoints[id] = upstreamEndpoints + watchedGatewayEndpoints[id] = gatewayEndpoints } // Configure TLS for the ingress listener @@ -786,21 +814,39 @@ func (c *configSnapshotAPIGateway) ToIngress(datacenter string) (configSnapshotI Protocol: string(listener.Protocol), } ingressListeners[key] = ingressListener + ingressUpstreams[key] = upstreams } + snapshotUpstreams := c.DeepCopy().ConfigSnapshotUpstreams snapshotUpstreams.DiscoveryChain = synthesizedChains + snapshotUpstreams.WatchedUpstreamEndpoints = watchedUpstreamEndpoints + snapshotUpstreams.WatchedGatewayEndpoints = watchedGatewayEndpoints return configSnapshotIngressGateway{ - Upstreams: c.Upstreams.toUpstreams(), + Upstreams: ingressUpstreams, ConfigSnapshotUpstreams: snapshotUpstreams, GatewayConfigLoaded: true, Listeners: ingressListeners, }, nil } -func (c *configSnapshotAPIGateway) synthesizeChains(datacenter string, protocol structs.APIGatewayListenerProtocol, boundListener structs.BoundAPIGatewayListener) ([]structs.IngressService, *structs.CompiledDiscoveryChain, error) { +func (c *configSnapshotAPIGateway) synthesizeChains(datacenter string, protocol structs.APIGatewayListenerProtocol, port int, name string, boundListener structs.BoundAPIGatewayListener) ([]structs.IngressService, structs.Upstreams, []*structs.CompiledDiscoveryChain, error) { chains := []*structs.CompiledDiscoveryChain{} - synthesizer := discoverychain.NewGatewayChainSynthesizer(datacenter, c.GatewayConfig) + trustDomain := "" + +DOMAIN_LOOP: + for _, chain := range c.DiscoveryChain { + for _, target := range chain.Targets { + if !target.External { + trustDomain = connect.TrustDomainForTarget(*target) + if trustDomain != "" { + break DOMAIN_LOOP + } + } + } + } + + synthesizer := discoverychain.NewGatewayChainSynthesizer(datacenter, trustDomain, name, c.GatewayConfig) for _, routeRef := range boundListener.Routes { switch routeRef.Kind { case structs.HTTPRoute: @@ -828,15 +874,35 @@ func (c *configSnapshotAPIGateway) synthesizeChains(datacenter string, protocol } } default: - return nil, nil, fmt.Errorf("unknown route kind %q", routeRef.Kind) + return nil, nil, nil, fmt.Errorf("unknown route kind %q", routeRef.Kind) } } if len(chains) == 0 { - return nil, nil, nil + return nil, nil, nil, nil } - return synthesizer.Synthesize(chains...) + services, compiled, err := synthesizer.Synthesize(chains...) + if err != nil { + return nil, nil, nil, err + } + + // reconstruct the upstreams + upstreams := make([]structs.Upstream, 0, len(services)) + for _, service := range services { + upstreams = append(upstreams, structs.Upstream{ + DestinationName: service.Name, + DestinationNamespace: service.NamespaceOrDefault(), + DestinationPartition: service.PartitionOrDefault(), + IngressHosts: service.Hosts, + LocalBindPort: port, + Config: map[string]interface{}{ + "protocol": string(protocol), + }, + }) + } + + return services, upstreams, compiled, err } func (c *configSnapshotAPIGateway) toIngressTLS() (*structs.GatewayTLSConfig, error) { diff --git a/agent/proxycfg/snapshot_test.go b/agent/proxycfg/snapshot_test.go index 8c4778cc7d..92f2ac19f8 100644 --- a/agent/proxycfg/snapshot_test.go +++ b/agent/proxycfg/snapshot_test.go @@ -68,6 +68,8 @@ func TestAPIGatewaySnapshotToIngressGatewaySnapshot(t *testing.T) { ConfigSnapshotUpstreams: ConfigSnapshotUpstreams{ PeerUpstreamEndpoints: watch.NewMap[UpstreamID, structs.CheckServiceNodes](), WatchedLocalGWEndpoints: watch.NewMap[string, structs.CheckServiceNodes](), + WatchedGatewayEndpoints: map[UpstreamID]map[string]structs.CheckServiceNodes{}, + WatchedUpstreamEndpoints: map[UpstreamID]map[string]structs.CheckServiceNodes{}, UpstreamPeerTrustBundles: watch.NewMap[string, *pbpeering.PeeringTrustBundle](), DiscoveryChain: map[UpstreamID]*structs.CompiledDiscoveryChain{}, }, diff --git a/api/config_entry_routes.go b/api/config_entry_routes.go index d074405710..8561c02eaf 100644 --- a/api/config_entry_routes.go +++ b/api/config_entry_routes.go @@ -71,6 +71,14 @@ type HTTPRouteConfigEntry struct { // Name is used to match the config entry with its associated http-route. Name string + // Parents is a list of gateways that this route should be bound to + Parents []ResourceReference + // Rules are a list of HTTP-based routing rules that this route should + // use for constructing a routing table. + Rules []HTTPRouteRule + // Hostnames are the hostnames for which this HTTPRoute should respond to requests. + Hostnames []string + Meta map[string]string `json:",omitempty"` // CreateIndex is the Raft index this entry was created at. This is a @@ -101,3 +109,137 @@ func (r *HTTPRouteConfigEntry) GetNamespace() string { return r.Namespace func (r *HTTPRouteConfigEntry) GetMeta() map[string]string { return r.Meta } func (r *HTTPRouteConfigEntry) GetCreateIndex() uint64 { return r.CreateIndex } func (r *HTTPRouteConfigEntry) GetModifyIndex() uint64 { return r.ModifyIndex } + +// HTTPMatch specifies the criteria that should be +// used in determining whether or not a request should +// be routed to a given set of services. +type HTTPMatch struct { + Headers []HTTPHeaderMatch + Method HTTPMatchMethod + Path HTTPPathMatch + Query []HTTPQueryMatch +} + +// HTTPMatchMethod specifies which type of HTTP verb should +// be used for matching a given request. +type HTTPMatchMethod string + +const ( + HTTPMatchMethodAll HTTPMatchMethod = "" + HTTPMatchMethodConnect HTTPMatchMethod = "CONNECT" + HTTPMatchMethodDelete HTTPMatchMethod = "DELETE" + HTTPMatchMethodGet HTTPMatchMethod = "GET" + HTTPMatchMethodHead HTTPMatchMethod = "HEAD" + HTTPMatchMethodOptions HTTPMatchMethod = "OPTIONS" + HTTPMatchMethodPatch HTTPMatchMethod = "PATCH" + HTTPMatchMethodPost HTTPMatchMethod = "POST" + HTTPMatchMethodPut HTTPMatchMethod = "PUT" + HTTPMatchMethodTrace HTTPMatchMethod = "TRACE" +) + +// HTTPHeaderMatchType specifies how header matching criteria +// should be applied to a request. +type HTTPHeaderMatchType string + +const ( + HTTPHeaderMatchExact HTTPHeaderMatchType = "exact" + HTTPHeaderMatchPrefix HTTPHeaderMatchType = "prefix" + HTTPHeaderMatchPresent HTTPHeaderMatchType = "present" + HTTPHeaderMatchRegularExpression HTTPHeaderMatchType = "regex" + HTTPHeaderMatchSuffix HTTPHeaderMatchType = "suffix" +) + +// HTTPHeaderMatch specifies how a match should be done +// on a request's headers. +type HTTPHeaderMatch struct { + Match HTTPHeaderMatchType + Name string + Value string +} + +// HTTPPathMatchType specifies how path matching criteria +// should be applied to a request. +type HTTPPathMatchType string + +const ( + HTTPPathMatchExact HTTPPathMatchType = "exact" + HTTPPathMatchPrefix HTTPPathMatchType = "prefix" + HTTPPathMatchRegularExpression HTTPPathMatchType = "regex" +) + +// HTTPPathMatch specifies how a match should be done +// on a request's path. +type HTTPPathMatch struct { + Match HTTPPathMatchType + Value string +} + +// HTTPQueryMatchType specifies how querys matching criteria +// should be applied to a request. +type HTTPQueryMatchType string + +const ( + HTTPQueryMatchExact HTTPQueryMatchType = "exact" + HTTPQueryMatchPresent HTTPQueryMatchType = "present" + HTTPQueryMatchRegularExpression HTTPQueryMatchType = "regex" +) + +// HTTPQueryMatch specifies how a match should be done +// on a request's query parameters. +type HTTPQueryMatch struct { + Match HTTPQueryMatchType + Name string + Value string +} + +// HTTPFilters specifies a list of filters used to modify a request +// before it is routed to an upstream. +type HTTPFilters struct { + Headers []HTTPHeaderFilter + URLRewrites []URLRewrite +} + +// HTTPHeaderFilter specifies how HTTP headers should be modified. +type HTTPHeaderFilter struct { + Add map[string]string + Remove []string + Set map[string]string +} + +type URLRewrite struct { + Path string +} + +// HTTPRouteRule specifies the routing rules used to determine what upstream +// service an HTTP request is routed to. +type HTTPRouteRule struct { + // Filters is a list of HTTP-based filters used to modify a request prior + // to routing it to the upstream service + Filters HTTPFilters + // Matches specified the matching criteria used in the routing table. If a + // request matches the given HTTPMatch configuration, then traffic is routed + // to services specified in the Services field. + Matches []HTTPMatch + // Services is a list of HTTP-based services to route to if the request matches + // the rules specified in the Matches field. + Services []HTTPService +} + +// HTTPService is a service reference for HTTP-based routing rules +type HTTPService struct { + Name string + // Weight is an arbitrary integer used in calculating how much + // traffic should be sent to the given service. + Weight int + // Filters is a list of HTTP-based filters used to modify a request prior + // to routing it to the upstream service + Filters HTTPFilters + + // Partition is the partition the config entry is associated with. + // Partitioning is a Consul Enterprise feature. + Partition string `json:",omitempty"` + + // Namespace is the namespace the config entry is associated with. + // Namespacing is a Consul Enterprise feature. + Namespace string `json:",omitempty"` +} diff --git a/test/integration/connect/envoy/case-api-gateway-http-simple/capture.sh b/test/integration/connect/envoy/case-api-gateway-http-simple/capture.sh new file mode 100644 index 0000000000..8ba0e0ddab --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-simple/capture.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +snapshot_envoy_admin localhost:20000 api-gateway primary || true \ No newline at end of file diff --git a/test/integration/connect/envoy/case-api-gateway-http-simple/service_gateway.hcl b/test/integration/connect/envoy/case-api-gateway-http-simple/service_gateway.hcl new file mode 100644 index 0000000000..486c25c59e --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-simple/service_gateway.hcl @@ -0,0 +1,4 @@ +services { + name = "api-gateway" + kind = "api-gateway" +} diff --git a/test/integration/connect/envoy/case-api-gateway-http-simple/setup.sh b/test/integration/connect/envoy/case-api-gateway-http-simple/setup.sh new file mode 100644 index 0000000000..8d0513553d --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-simple/setup.sh @@ -0,0 +1,92 @@ +#!/bin/bash + +set -euo pipefail + +upsert_config_entry primary ' +kind = "api-gateway" +name = "api-gateway" +listeners = [ + { + name = "listener-one" + port = 9999 + protocol = "http" + }, + { + name = "listener-two" + port = 9998 + protocol = "http" + } +] +' + +upsert_config_entry primary ' +Kind = "proxy-defaults" +Name = "global" +Config { + protocol = "http" +} +' + +upsert_config_entry primary ' +kind = "http-route" +name = "api-gateway-route-one" +rules = [ + { + services = [ + { + name = "s1" + } + ] + } +] +parents = [ + { + name = "api-gateway" + sectionName = "listener-one" + } +] +' + +upsert_config_entry primary ' +kind = "http-route" +name = "api-gateway-route-two" +rules = [ + { + services = [ + { + name = "s2" + } + ] + } +] +parents = [ + { + name = "api-gateway" + sectionName = "listener-two" + } +] +' + +upsert_config_entry primary ' +kind = "service-intentions" +name = "s1" +sources { + name = "api-gateway" + action = "allow" +} +' + +upsert_config_entry primary ' +kind = "service-intentions" +name = "s2" +sources { + name = "api-gateway" + action = "deny" +} +' + +register_services primary + +gen_envoy_bootstrap api-gateway 20000 primary true +gen_envoy_bootstrap s1 19000 +gen_envoy_bootstrap s2 19001 \ No newline at end of file diff --git a/test/integration/connect/envoy/case-api-gateway-http-simple/vars.sh b/test/integration/connect/envoy/case-api-gateway-http-simple/vars.sh new file mode 100644 index 0000000000..38a47d8527 --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-simple/vars.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +export REQUIRED_SERVICES="$DEFAULT_REQUIRED_SERVICES api-gateway-primary" diff --git a/test/integration/connect/envoy/case-api-gateway-http-simple/verify.bats b/test/integration/connect/envoy/case-api-gateway-http-simple/verify.bats new file mode 100644 index 0000000000..c7378e55bf --- /dev/null +++ b/test/integration/connect/envoy/case-api-gateway-http-simple/verify.bats @@ -0,0 +1,34 @@ +#!/usr/bin/env bats + +load helpers + +@test "api gateway proxy admin is up on :20000" { + retry_default curl -f -s localhost:20000/stats -o /dev/null +} + +@test "api gateway should have be accepted and not conflicted" { + assert_config_entry_status Accepted True Accepted primary api-gateway api-gateway + assert_config_entry_status Conflicted False NoConflict primary api-gateway api-gateway +} + +@test "api gateway should have healthy endpoints for s1" { + assert_config_entry_status Bound True Bound primary http-route api-gateway-route-one + assert_upstream_has_endpoints_in_status 127.0.0.1:20000 s1 HEALTHY 1 +} + +@test "api gateway should have healthy endpoints for s2" { + assert_config_entry_status Bound True Bound primary http-route api-gateway-route-two + assert_upstream_has_endpoints_in_status 127.0.0.1:20000 s2 HEALTHY 1 +} + +@test "api gateway should be able to connect to s1 via configured port" { + run retry_long curl -s -f -d hello localhost:9999 + [ "$status" -eq 0 ] + [[ "$output" == *"hello"* ]] +} + +@test "api gateway should get an intentions error connecting to s2 via configured port" { + run retry_default sh -c "curl -s localhost:9998 | grep RBAC" + [ "$status" -eq 0 ] + [[ "$output" == "RBAC: access denied" ]] +} \ No newline at end of file