open-consul/agent/consul/discoverychain/gateway.go

276 lines
8.5 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package discoverychain
import (
"fmt"
"hash/crc32"
"sort"
"strconv"
"strings"
"github.com/hashicorp/consul/agent/configentry"
"github.com/hashicorp/consul/agent/structs"
)
// GatewayChainSynthesizer is used to synthesize a discovery chain for a
// gateway from its configuration and multiple other discovery chains.
type GatewayChainSynthesizer struct {
datacenter string
trustDomain string
suffix string
gateway *structs.APIGatewayConfigEntry
hostname string
matchesByHostname map[string][]hostnameMatch
tcpRoutes []structs.TCPRouteConfigEntry
}
type hostnameMatch struct {
match structs.HTTPMatch
filters structs.HTTPFilters
services []structs.HTTPService
}
// NewGatewayChainSynthesizer creates a new GatewayChainSynthesizer for the
// given gateway and datacenter.
func NewGatewayChainSynthesizer(datacenter, trustDomain, suffix string, gateway *structs.APIGatewayConfigEntry) *GatewayChainSynthesizer {
return &GatewayChainSynthesizer{
datacenter: datacenter,
trustDomain: trustDomain,
suffix: suffix,
gateway: gateway,
matchesByHostname: map[string][]hostnameMatch{},
}
}
// AddTCPRoute adds a TCPRoute to use in synthesizing a discovery chain
func (l *GatewayChainSynthesizer) AddTCPRoute(route structs.TCPRouteConfigEntry) {
l.tcpRoutes = append(l.tcpRoutes, route)
}
// SetHostname sets the base hostname for a listener that this is being synthesized for
func (l *GatewayChainSynthesizer) SetHostname(hostname string) {
l.hostname = hostname
}
// AddHTTPRoute takes a new route and flattens its rule matches out per hostname.
// This is required since a single route can specify multiple hostnames, and a
// 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) {
hostnames := route.FilteredHostnames(l.hostname)
for _, host := range hostnames {
matches, ok := l.matchesByHostname[host]
if !ok {
matches = []hostnameMatch{}
}
for _, rule := range route.Rules {
// If a rule has no matches defined, add default match
if rule.Matches == nil {
rule.Matches = []structs.HTTPMatch{}
}
if len(rule.Matches) == 0 {
rule.Matches = []structs.HTTPMatch{{
Path: structs.HTTPPathMatch{
Match: structs.HTTPPathMatchPrefix,
Value: "/",
},
}}
}
// Add all matches for this rule to the list for this hostname
for _, match := range rule.Matches {
matches = append(matches, hostnameMatch{
match: match,
filters: rule.Filters,
services: rule.Services,
})
}
}
l.matchesByHostname[host] = matches
}
}
// Synthesize assembles a synthetic discovery chain from multiple other discovery chains
// that have StartNodes that are referenced by routers or splitters in the entries for the
// given CompileRequest.
//
// 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) {
if len(chains) == 0 {
return nil, nil, fmt.Errorf("must provide at least one compiled discovery chain")
}
services, set := l.synthesizeEntries()
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 pre-compiled discovery chains
return services, chains, nil
}
compiledChains := make([]*structs.CompiledDiscoveryChain, 0, len(set))
for i, service := range services {
entries := set[i]
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
}
node := compiled.Nodes[compiled.StartNode]
if node.IsRouter() {
resolverPrefix := structs.DiscoveryGraphNodeTypeResolver + ":" + node.Name
// clean out the clusters that will get added for the router
for name := range compiled.Nodes {
if strings.HasPrefix(name, resolverPrefix) {
delete(compiled.Nodes, name)
}
}
// clean out the route rules that'll get added for the router
filtered := []*structs.DiscoveryRoute{}
for _, route := range node.Routes {
if strings.HasPrefix(route.NextNode, resolverPrefix) {
continue
}
filtered = append(filtered, route)
}
node.Routes = filtered
}
compiled.Nodes[compiled.StartNode] = node
// fix up the nodes for the terminal targets to either be a splitter or resolver if there is no splitter present
for name, node := range compiled.Nodes {
switch node.Type {
// we should only have these two types
case structs.DiscoveryGraphNodeTypeRouter:
for i, route := range node.Routes {
node.Routes[i].NextNode = targetForResolverNode(route.NextNode, chains)
}
case structs.DiscoveryGraphNodeTypeSplitter:
for i, split := range node.Splits {
node.Splits[i].NextNode = targetForResolverNode(split.NextNode, chains)
}
}
compiled.Nodes[name] = 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...)
}
compiledChains = append(compiledChains, compiled)
}
return services, compiledChains, nil
}
// consolidateHTTPRoutes combines all rules into the shortest possible list of routes
// with one route per hostname containing all rules for that hostname.
func (l *GatewayChainSynthesizer) consolidateHTTPRoutes() []structs.HTTPRouteConfigEntry {
var routes []structs.HTTPRouteConfigEntry
for hostname, rules := range l.matchesByHostname {
// Create route for this hostname
route := structs.HTTPRouteConfigEntry{
Kind: structs.HTTPRoute,
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,
EnterpriseMeta: l.gateway.EnterpriseMeta,
}
// Sort rules for this hostname in order of precedence
sort.SliceStable(rules, func(i, j int) bool {
return compareHTTPRules(rules[i].match, rules[j].match)
})
// Add all rules for this hostname
for _, rule := range rules {
route.Rules = append(route.Rules, structs.HTTPRouteRule{
Matches: []structs.HTTPMatch{rule.match},
Filters: rule.filters,
Services: rule.services,
})
}
routes = append(routes, route)
}
return routes
}
func targetForResolverNode(nodeName string, chains []*structs.CompiledDiscoveryChain) string {
resolverPrefix := structs.DiscoveryGraphNodeTypeResolver + ":"
splitterPrefix := structs.DiscoveryGraphNodeTypeSplitter + ":"
if !strings.HasPrefix(nodeName, resolverPrefix) {
return nodeName
}
splitterName := splitterPrefix + strings.TrimPrefix(nodeName, resolverPrefix)
for _, c := range chains {
for name, node := range c.Nodes {
if node.IsSplitter() && strings.HasPrefix(splitterName, name) {
return name
}
}
}
return nodeName
}
func hostsKey(hosts ...string) string {
sort.Strings(hosts)
hostsHash := crc32.NewIEEE()
for _, h := range hosts {
if _, err := hostsHash.Write([]byte(h)); err != nil {
continue
}
}
return strconv.FormatUint(uint64(hostsHash.Sum32()), 16)
}
func (l *GatewayChainSynthesizer) synthesizeEntries() ([]structs.IngressService, []*configentry.DiscoveryChainSet) {
services := []structs.IngressService{}
entries := []*configentry.DiscoveryChainSet{}
for _, route := range l.consolidateHTTPRoutes() {
entrySet := configentry.NewDiscoveryChainSet()
ingress, router, splitters, defaults := synthesizeHTTPRouteDiscoveryChain(route)
entrySet.AddRouters(router)
entrySet.AddSplitters(splitters...)
entrySet.AddServices(defaults...)
services = append(services, ingress)
entries = append(entries, entrySet)
}
for _, route := range l.tcpRoutes {
services = append(services, synthesizeTCPRouteDiscoveryChain(route)...)
}
return services, entries
}