191 lines
5.9 KiB
Go
191 lines
5.9 KiB
Go
package discoverychain
|
|
|
|
import (
|
|
"fmt"
|
|
"hash/crc32"
|
|
"sort"
|
|
"strconv"
|
|
|
|
"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
|
|
gateway *structs.APIGatewayConfigEntry
|
|
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 string, gateway *structs.APIGatewayConfigEntry) *GatewayChainSynthesizer {
|
|
return &GatewayChainSynthesizer{
|
|
datacenter: datacenter,
|
|
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)
|
|
}
|
|
|
|
// 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) {
|
|
for _, host := range route.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, entries := l.synthesizeEntries()
|
|
|
|
if entries.IsEmpty() {
|
|
// 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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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...)
|
|
}
|
|
|
|
return services, compiled, 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", l.gateway.Name, 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 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.NewDiscoveryChainSet()
|
|
|
|
for _, route := range l.consolidateHTTPRoutes() {
|
|
ingress, router, splitters, defaults := synthesizeHTTPRouteDiscoveryChain(route)
|
|
entries.AddRouters(router)
|
|
entries.AddSplitters(splitters...)
|
|
entries.AddServices(defaults...)
|
|
services = append(services, ingress)
|
|
}
|
|
|
|
for _, route := range l.tcpRoutes {
|
|
services = append(services, synthesizeTCPRouteDiscoveryChain(route)...)
|
|
}
|
|
|
|
return services, entries
|
|
}
|