mirror of https://github.com/hashicorp/consul
335 lines
12 KiB
Go
335 lines
12 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package discoverychain
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
)
|
|
|
|
// compareHTTPRules implements the non-hostname order of precedence for routes specified by the K8s Gateway API spec.
|
|
// https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRouteRule
|
|
//
|
|
// Ordering prefers matches based on the largest number of:
|
|
//
|
|
// 1. characters in a matching non-wildcard hostname
|
|
// 2. characters in a matching hostname
|
|
// 3. characters in a matching path
|
|
// 4. header matches
|
|
// 5. query param matches
|
|
//
|
|
// The hostname-specific comparison (1+2) occur in Envoy outside of our control:
|
|
// https://github.com/envoyproxy/envoy/blob/5c4d4bd957f9402eca80bef82e7cc3ae714e04b4/source/common/router/config_impl.cc#L1645-L1682
|
|
func compareHTTPRules(ruleA, ruleB structs.HTTPMatch) bool {
|
|
if len(ruleA.Path.Value) != len(ruleB.Path.Value) {
|
|
return len(ruleA.Path.Value) > len(ruleB.Path.Value)
|
|
}
|
|
if len(ruleA.Headers) != len(ruleB.Headers) {
|
|
return len(ruleA.Headers) > len(ruleB.Headers)
|
|
}
|
|
return len(ruleA.Query) > len(ruleB.Query)
|
|
}
|
|
|
|
func httpServiceDefault(entry structs.ConfigEntry, meta map[string]string) *structs.ServiceConfigEntry {
|
|
return &structs.ServiceConfigEntry{
|
|
Kind: structs.ServiceDefaults,
|
|
Name: entry.GetName(),
|
|
Protocol: "http",
|
|
Meta: meta,
|
|
EnterpriseMeta: *entry.GetEnterpriseMeta(),
|
|
}
|
|
}
|
|
|
|
func synthesizeHTTPRouteDiscoveryChain(route structs.HTTPRouteConfigEntry) (structs.IngressService, *structs.ServiceRouterConfigEntry, []*structs.ServiceSplitterConfigEntry, []*structs.ServiceConfigEntry) {
|
|
meta := route.GetMeta()
|
|
splitters := []*structs.ServiceSplitterConfigEntry{}
|
|
defaults := []*structs.ServiceConfigEntry{}
|
|
|
|
router, splits, upstreamDefaults := httpRouteToDiscoveryChain(route)
|
|
serviceDefault := httpServiceDefault(router, meta)
|
|
defaults = append(defaults, serviceDefault)
|
|
for _, split := range splits {
|
|
splitters = append(splitters, split)
|
|
if split.Name != serviceDefault.Name {
|
|
defaults = append(defaults, httpServiceDefault(split, meta))
|
|
}
|
|
}
|
|
defaults = append(defaults, upstreamDefaults...)
|
|
|
|
ingress := structs.IngressService{
|
|
Name: router.Name,
|
|
Hosts: route.Hostnames,
|
|
Meta: route.Meta,
|
|
EnterpriseMeta: route.EnterpriseMeta,
|
|
}
|
|
|
|
return ingress, router, splitters, defaults
|
|
}
|
|
|
|
func httpRouteToDiscoveryChain(route structs.HTTPRouteConfigEntry) (*structs.ServiceRouterConfigEntry, []*structs.ServiceSplitterConfigEntry, []*structs.ServiceConfigEntry) {
|
|
router := &structs.ServiceRouterConfigEntry{
|
|
Kind: structs.ServiceRouter,
|
|
Name: route.GetName(),
|
|
Meta: route.GetMeta(),
|
|
EnterpriseMeta: route.EnterpriseMeta,
|
|
}
|
|
var splitters []*structs.ServiceSplitterConfigEntry
|
|
var defaults []*structs.ServiceConfigEntry
|
|
|
|
for idx, rule := range route.Rules {
|
|
requestModifier := httpRouteFiltersToServiceRouteHeaderModifier(rule.Filters.Headers)
|
|
responseModifier := httpRouteFiltersToServiceRouteHeaderModifier(rule.ResponseFilters.Headers)
|
|
prefixRewrite := httpRouteFiltersToDestinationPrefixRewrite(rule.Filters.URLRewrite)
|
|
|
|
var destination structs.ServiceRouteDestination
|
|
if len(rule.Services) == 1 {
|
|
service := rule.Services[0]
|
|
|
|
servicePrefixRewrite := httpRouteFiltersToDestinationPrefixRewrite(service.Filters.URLRewrite)
|
|
if service.Filters.URLRewrite == nil {
|
|
servicePrefixRewrite = prefixRewrite
|
|
}
|
|
|
|
// Merge service request header modifier(s) onto route rule modifiers
|
|
// Note: Removals for the same header may exist on the rule + the service and
|
|
// will result in idempotent duplicate values in the modifier w/ service coming last
|
|
serviceRequestModifier := httpRouteFiltersToServiceRouteHeaderModifier(service.Filters.Headers)
|
|
requestModifier.Add = mergeMaps(requestModifier.Add, serviceRequestModifier.Add)
|
|
requestModifier.Set = mergeMaps(requestModifier.Set, serviceRequestModifier.Set)
|
|
requestModifier.Remove = append(requestModifier.Remove, serviceRequestModifier.Remove...)
|
|
|
|
// Merge service response header modifier(s) onto route rule modifiers
|
|
// Note: Removals for the same header may exist on the rule + the service and
|
|
// will result in idempotent duplicate values in the modifier w/ service coming last
|
|
serviceResponseModifier := httpRouteFiltersToServiceRouteHeaderModifier(service.ResponseFilters.Headers)
|
|
responseModifier.Add = mergeMaps(responseModifier.Add, serviceResponseModifier.Add)
|
|
responseModifier.Set = mergeMaps(responseModifier.Set, serviceResponseModifier.Set)
|
|
responseModifier.Remove = append(responseModifier.Remove, serviceResponseModifier.Remove...)
|
|
|
|
destination.Service = service.Name
|
|
destination.Namespace = service.NamespaceOrDefault()
|
|
destination.Partition = service.PartitionOrDefault()
|
|
destination.PrefixRewrite = servicePrefixRewrite
|
|
destination.RequestHeaders = requestModifier
|
|
destination.ResponseHeaders = responseModifier
|
|
|
|
// 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)
|
|
destination.Namespace = route.NamespaceOrDefault()
|
|
destination.Partition = route.PartitionOrDefault()
|
|
destination.PrefixRewrite = prefixRewrite
|
|
destination.RequestHeaders = requestModifier
|
|
destination.ResponseHeaders = responseModifier
|
|
|
|
splitter := &structs.ServiceSplitterConfigEntry{
|
|
Kind: structs.ServiceSplitter,
|
|
Name: destination.Service,
|
|
Splits: []structs.ServiceSplit{},
|
|
Meta: route.GetMeta(),
|
|
EnterpriseMeta: route.EnterpriseMeta,
|
|
}
|
|
|
|
totalWeight := 0
|
|
for _, service := range rule.Services {
|
|
totalWeight += service.Weight
|
|
}
|
|
|
|
for _, service := range rule.Services {
|
|
if service.Weight == 0 {
|
|
continue
|
|
}
|
|
|
|
modifier := httpRouteFiltersToServiceRouteHeaderModifier(service.Filters.Headers)
|
|
|
|
weightPercentage := float32(service.Weight) / float32(totalWeight)
|
|
split := structs.ServiceSplit{
|
|
RequestHeaders: modifier,
|
|
Weight: weightPercentage * 100,
|
|
}
|
|
split.Service = service.Name
|
|
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)
|
|
}
|
|
}
|
|
|
|
if rule.Filters.RetryFilter != nil {
|
|
if rule.Filters.RetryFilter.NumRetries != nil {
|
|
destination.NumRetries = *rule.Filters.RetryFilter.NumRetries
|
|
}
|
|
if rule.Filters.RetryFilter.RetryOnConnectFailure != nil {
|
|
destination.RetryOnConnectFailure = *rule.Filters.RetryFilter.RetryOnConnectFailure
|
|
}
|
|
|
|
if len(rule.Filters.RetryFilter.RetryOn) > 0 {
|
|
destination.RetryOn = rule.Filters.RetryFilter.RetryOn
|
|
}
|
|
|
|
if len(rule.Filters.RetryFilter.RetryOnStatusCodes) > 0 {
|
|
destination.RetryOnStatusCodes = rule.Filters.RetryFilter.RetryOnStatusCodes
|
|
}
|
|
}
|
|
|
|
if rule.Filters.TimeoutFilter != nil {
|
|
destination.IdleTimeout = rule.Filters.TimeoutFilter.IdleTimeout
|
|
destination.RequestTimeout = rule.Filters.TimeoutFilter.RequestTimeout
|
|
}
|
|
|
|
// for each match rule a ServiceRoute is created for the service-router
|
|
// if there are no rules a single route with the destination is set
|
|
if len(rule.Matches) == 0 {
|
|
router.Routes = append(router.Routes, structs.ServiceRoute{Destination: &destination})
|
|
}
|
|
|
|
for _, match := range rule.Matches {
|
|
router.Routes = append(router.Routes, structs.ServiceRoute{
|
|
Match: &structs.ServiceRouteMatch{HTTP: httpRouteMatchToServiceRouteHTTPMatch(match)},
|
|
Destination: &destination,
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
return router, splitters, defaults
|
|
}
|
|
|
|
func httpRouteFiltersToDestinationPrefixRewrite(rewrite *structs.URLRewrite) string {
|
|
if rewrite == nil {
|
|
return ""
|
|
}
|
|
return rewrite.Path
|
|
}
|
|
|
|
// httpRouteFiltersToServiceRouteHeaderModifier will consolidate a list of HTTP filters
|
|
// into a single set of header modifications for Consul to make as a request passes through.
|
|
func httpRouteFiltersToServiceRouteHeaderModifier(filters []structs.HTTPHeaderFilter) *structs.HTTPHeaderModifiers {
|
|
modifier := &structs.HTTPHeaderModifiers{
|
|
Add: make(map[string]string),
|
|
Set: make(map[string]string),
|
|
}
|
|
for _, filter := range filters {
|
|
// If we have multiple filters specified, then we can potentially clobber
|
|
// "Add" and "Set" here -- as far as K8S gateway spec is concerned, this
|
|
// is all implementation-specific behavior and undefined by the spec.
|
|
modifier.Add = mergeMaps(modifier.Add, filter.Add)
|
|
modifier.Set = mergeMaps(modifier.Set, filter.Set)
|
|
modifier.Remove = append(modifier.Remove, filter.Remove...)
|
|
}
|
|
return modifier
|
|
}
|
|
|
|
func mergeMaps(a, b map[string]string) map[string]string {
|
|
for k, v := range b {
|
|
a[k] = v
|
|
}
|
|
return a
|
|
}
|
|
|
|
func httpRouteMatchToServiceRouteHTTPMatch(match structs.HTTPMatch) *structs.ServiceRouteHTTPMatch {
|
|
var consulMatch structs.ServiceRouteHTTPMatch
|
|
switch match.Path.Match {
|
|
case structs.HTTPPathMatchExact:
|
|
consulMatch.PathExact = match.Path.Value
|
|
case structs.HTTPPathMatchPrefix:
|
|
consulMatch.PathPrefix = match.Path.Value
|
|
case structs.HTTPPathMatchRegularExpression:
|
|
consulMatch.PathRegex = match.Path.Value
|
|
}
|
|
|
|
for _, header := range match.Headers {
|
|
switch header.Match {
|
|
case structs.HTTPHeaderMatchExact:
|
|
consulMatch.Header = append(consulMatch.Header, structs.ServiceRouteHTTPMatchHeader{
|
|
Name: header.Name,
|
|
Exact: header.Value,
|
|
})
|
|
case structs.HTTPHeaderMatchPrefix:
|
|
consulMatch.Header = append(consulMatch.Header, structs.ServiceRouteHTTPMatchHeader{
|
|
Name: header.Name,
|
|
Prefix: header.Value,
|
|
})
|
|
case structs.HTTPHeaderMatchSuffix:
|
|
consulMatch.Header = append(consulMatch.Header, structs.ServiceRouteHTTPMatchHeader{
|
|
Name: header.Name,
|
|
Suffix: header.Value,
|
|
})
|
|
case structs.HTTPHeaderMatchPresent:
|
|
consulMatch.Header = append(consulMatch.Header, structs.ServiceRouteHTTPMatchHeader{
|
|
Name: header.Name,
|
|
Present: true,
|
|
})
|
|
case structs.HTTPHeaderMatchRegularExpression:
|
|
consulMatch.Header = append(consulMatch.Header, structs.ServiceRouteHTTPMatchHeader{
|
|
Name: header.Name,
|
|
Regex: header.Value,
|
|
})
|
|
}
|
|
}
|
|
|
|
for _, query := range match.Query {
|
|
switch query.Match {
|
|
case structs.HTTPQueryMatchExact:
|
|
consulMatch.QueryParam = append(consulMatch.QueryParam, structs.ServiceRouteHTTPMatchQueryParam{
|
|
Name: query.Name,
|
|
Exact: query.Value,
|
|
})
|
|
case structs.HTTPQueryMatchPresent:
|
|
consulMatch.QueryParam = append(consulMatch.QueryParam, structs.ServiceRouteHTTPMatchQueryParam{
|
|
Name: query.Name,
|
|
Present: true,
|
|
})
|
|
case structs.HTTPQueryMatchRegularExpression:
|
|
consulMatch.QueryParam = append(consulMatch.QueryParam, structs.ServiceRouteHTTPMatchQueryParam{
|
|
Name: query.Name,
|
|
Regex: query.Value,
|
|
})
|
|
}
|
|
}
|
|
|
|
switch match.Method {
|
|
case structs.HTTPMatchMethodConnect:
|
|
consulMatch.Methods = append(consulMatch.Methods, "CONNECT")
|
|
case structs.HTTPMatchMethodDelete:
|
|
consulMatch.Methods = append(consulMatch.Methods, "DELETE")
|
|
case structs.HTTPMatchMethodGet:
|
|
consulMatch.Methods = append(consulMatch.Methods, "GET")
|
|
case structs.HTTPMatchMethodHead:
|
|
consulMatch.Methods = append(consulMatch.Methods, "HEAD")
|
|
case structs.HTTPMatchMethodOptions:
|
|
consulMatch.Methods = append(consulMatch.Methods, "OPTIONS")
|
|
case structs.HTTPMatchMethodPatch:
|
|
consulMatch.Methods = append(consulMatch.Methods, "PATCH")
|
|
case structs.HTTPMatchMethodPost:
|
|
consulMatch.Methods = append(consulMatch.Methods, "POST")
|
|
case structs.HTTPMatchMethodPut:
|
|
consulMatch.Methods = append(consulMatch.Methods, "PUT")
|
|
case structs.HTTPMatchMethodTrace:
|
|
consulMatch.Methods = append(consulMatch.Methods, "TRACE")
|
|
}
|
|
|
|
return &consulMatch
|
|
}
|