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

191 lines
5.9 KiB
Go
Raw Normal View History

API Gateway to Ingress Gateway Snapshot Translation and Routes to Virtual Routers and Splitters (#16127) * Stub proxycfg handler for API gateway * Add Service Kind constants/handling for API Gateway * Begin stubbing for SDS * Add new Secret type to xDS order of operations * Continue stubbing of SDS * Iterate on proxycfg handler for API gateway * Handle BoundAPIGateway config entry subscription in proxycfg-glue * Add API gateway to config snapshot validation * Add API gateway to config snapshot clone, leaf, etc. * Subscribe to bound route + cert config entries on bound-api-gateway * Track routes + certs on API gateway config snapshot * Generate DeepCopy() for types used in watch.Map * Watch all active references on api-gateway, unwatch inactive * Track loading of initial bound-api-gateway config entry * Use proper proto package for SDS mapping * Use ResourceReference instead of ServiceName, collect resources * Fix typo, add + remove TODOs * Watch discovery chains for TCPRoute * Add TODO for updating gateway services for api-gateway * make proto * Regenerate deep-copy for proxycfg * Set datacenter on upstream ID from query source * Watch discovery chains for http-route service backends * Add ServiceName getter to HTTP+TCP Service structs * Clean up unwatched discovery chains on API Gateway * Implement watch for ingress leaf certificate * Collect upstreams on http-route + tcp-route updates * Remove unused GatewayServices update handler * Remove unnecessary gateway services logic for API Gateway * Remove outdate TODO * Use .ToIngress where appropriate, including TODO for cleaning up * Cancel before returning error * Remove GatewayServices subscription * Add godoc for handlerAPIGateway functions * Update terminology from Connect => Consul Service Mesh Consistent with terminology changes in https://github.com/hashicorp/consul/pull/12690 * Add missing TODO * Remove duplicate switch case * Rerun deep-copy generator * Use correct property on config snapshot * Remove unnecessary leaf cert watch * Clean up based on code review feedback * Note handler properties that are initialized but set elsewhere * Add TODO for moving helper func into structs pkg * Update generated DeepCopy code * gofmt * Begin stubbing for SDS * Start adding tests * Remove second BoundAPIGateway case in glue * TO BE PICKED: fix formatting of str * WIP * Fix merge conflict * Implement HTTP Route to Discovery Chain config entries * Stub out function to create discovery chain * Add discovery chain merging code (#16131) * Test adding TCP and HTTP routes * Add some tests for the synthesizer * Run go mod tidy * Pairing with N8 * Run deep copy * Clean up GatewayChainSynthesizer * Fix missing assignment of BoundAPIGateway topic * Separate out synthesizeChains and toIngressTLS * Fix build errors * Ensure synthesizer skips non-matching routes by protocol * Rebase on N8s work * Generate DeepCopy() for API gateway listener types * Improve variable name * Regenerate DeepCopy() code * Fix linting issue * fix protobuf import * Fix more merge conflict errors * Fix synthesize test * Run deep copy * Add URLRewrite to proto * Update agent/consul/discoverychain/gateway_tcproute.go Co-authored-by: Nathan Coleman <nathan.coleman@hashicorp.com> * Remove APIGatewayConfigEntry that was extra * Error out if route kind is unknown * Fix formatting errors in proto --------- Co-authored-by: Nathan Coleman <nathan.coleman@hashicorp.com> Co-authored-by: Andrew Stucki <andrew.stucki@hashicorp.com>
2023-02-09 17:58:55 +00:00
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
}