2022-03-08 19:37:24 +00:00
|
|
|
package xdscommon
|
|
|
|
|
|
|
|
import (
|
2023-01-11 14:39:10 +00:00
|
|
|
"google.golang.org/protobuf/proto"
|
2022-03-15 14:07:40 +00:00
|
|
|
|
|
|
|
"github.com/hashicorp/consul/agent/connect"
|
|
|
|
"github.com/hashicorp/consul/agent/proxycfg"
|
|
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
|
|
"github.com/hashicorp/consul/api"
|
2022-03-08 19:37:24 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// Resource types in xDS v3. These are copied from
|
|
|
|
// envoyproxy/go-control-plane/pkg/resource/v3/resource.go since we don't need any of
|
|
|
|
// the rest of that package.
|
|
|
|
apiTypePrefix = "type.googleapis.com/"
|
|
|
|
|
|
|
|
// EndpointType is the TypeURL for Endpoint discovery responses.
|
|
|
|
EndpointType = apiTypePrefix + "envoy.config.endpoint.v3.ClusterLoadAssignment"
|
|
|
|
|
|
|
|
// ClusterType is the TypeURL for Cluster discovery responses.
|
|
|
|
ClusterType = apiTypePrefix + "envoy.config.cluster.v3.Cluster"
|
|
|
|
|
|
|
|
// RouteType is the TypeURL for Route discovery responses.
|
|
|
|
RouteType = apiTypePrefix + "envoy.config.route.v3.RouteConfiguration"
|
|
|
|
|
|
|
|
// ListenerType is the TypeURL for Listener discovery responses.
|
|
|
|
ListenerType = apiTypePrefix + "envoy.config.listener.v3.Listener"
|
2023-01-06 17:13:40 +00:00
|
|
|
|
|
|
|
// PublicListenerName is the name we give the public listener in Envoy config.
|
|
|
|
PublicListenerName = "public_listener"
|
|
|
|
|
|
|
|
// LocalAppClusterName is the name we give the local application "cluster" in
|
|
|
|
// Envoy config. Note that all cluster names may collide with service names
|
|
|
|
// since we want cluster names and service names to match to enable nice
|
|
|
|
// metrics correlation without massaging prefixes on cluster names.
|
|
|
|
//
|
|
|
|
// We should probably make this more unlikely to collide however changing it
|
|
|
|
// potentially breaks upgrade compatibility without restarting all Envoy's as
|
|
|
|
// it will no longer match their existing cluster name. Changing this will
|
|
|
|
// affect metrics output so could break dashboards (for local app traffic).
|
|
|
|
//
|
|
|
|
// We should probably just make it configurable if anyone actually has
|
|
|
|
// services named "local_app" in the future.
|
|
|
|
LocalAppClusterName = "local_app"
|
2022-03-08 19:37:24 +00:00
|
|
|
)
|
|
|
|
|
2023-01-06 17:13:40 +00:00
|
|
|
type EnvoyExtension interface {
|
|
|
|
Extend(*IndexedResources, ExtensionConfiguration) (*IndexedResources, error)
|
|
|
|
Validate(ExtensionConfiguration) error
|
|
|
|
}
|
|
|
|
|
2022-03-08 19:37:24 +00:00
|
|
|
type IndexedResources struct {
|
|
|
|
// Index is a map of typeURL => resourceName => resource
|
|
|
|
Index map[string]map[string]proto.Message
|
|
|
|
|
|
|
|
// ChildIndex is a map of typeURL => parentResourceName => list of
|
|
|
|
// childResourceNames. This only applies if the child and parent do not
|
|
|
|
// share a name.
|
|
|
|
ChildIndex map[string]map[string][]string
|
|
|
|
}
|
|
|
|
|
|
|
|
func EmptyIndexedResources() *IndexedResources {
|
|
|
|
return &IndexedResources{
|
|
|
|
Index: map[string]map[string]proto.Message{
|
|
|
|
ListenerType: make(map[string]proto.Message),
|
|
|
|
RouteType: make(map[string]proto.Message),
|
|
|
|
ClusterType: make(map[string]proto.Message),
|
|
|
|
EndpointType: make(map[string]proto.Message),
|
|
|
|
},
|
|
|
|
ChildIndex: map[string]map[string][]string{
|
|
|
|
ListenerType: make(map[string][]string),
|
|
|
|
ClusterType: make(map[string][]string),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
2022-03-15 14:07:40 +00:00
|
|
|
|
|
|
|
type ServiceConfig struct {
|
|
|
|
// Kind identifies the final proxy kind that will make the request to the
|
|
|
|
// destination service.
|
2022-12-19 20:19:37 +00:00
|
|
|
Kind api.ServiceKind
|
|
|
|
EnvoyExtensions []api.EnvoyExtension
|
2022-03-15 14:07:40 +00:00
|
|
|
}
|
|
|
|
|
2022-12-21 06:26:20 +00:00
|
|
|
// ExtensionConfiguration is the configuration for an extension attached to a service on the local proxy. Currently, it
|
|
|
|
// is only created for the local proxy's upstream service if the upstream service has an extension configured. In the
|
|
|
|
// future it will also include information about the service local to the local proxy as well. It should depend on the
|
|
|
|
// API client rather than the structs package because the API client is meant to be public.
|
|
|
|
type ExtensionConfiguration struct {
|
|
|
|
// EnvoyExtension is the extension that will patch Envoy resources.
|
|
|
|
EnvoyExtension api.EnvoyExtension
|
|
|
|
|
|
|
|
// ServiceName is the name of the service the EnvoyExtension is being applied to. It could be the local service or
|
|
|
|
// an upstream of the local service.
|
|
|
|
ServiceName api.CompoundServiceName
|
|
|
|
|
|
|
|
// Upstreams will only be configured on the ExtensionConfiguration if the EnvoyExtension is being applied to an
|
|
|
|
// upstream. If there are no Upstreams, then EnvoyExtension is being applied to the local service's resources.
|
|
|
|
Upstreams map[api.CompoundServiceName]UpstreamData
|
|
|
|
|
|
|
|
// Kind is mode the local Envoy proxy is running in. For now, only connect proxy and
|
2022-03-31 20:24:46 +00:00
|
|
|
// terminating gateways are supported.
|
2022-03-15 14:07:40 +00:00
|
|
|
Kind api.ServiceKind
|
|
|
|
}
|
|
|
|
|
2022-12-21 06:26:20 +00:00
|
|
|
// UpstreamData has the SNI, EnvoyID, and OutgoingProxyKind of the upstream services for the local proxy and this data
|
|
|
|
// is used to choose which Envoy resources to patch.
|
|
|
|
type UpstreamData struct {
|
|
|
|
// SNI is the SNI header used to reach an upstream service.
|
|
|
|
SNI map[string]struct{}
|
|
|
|
// EnvoyID is the envoy ID of an upstream service, structured <service> or <partition>/<ns>/<service> when using a
|
|
|
|
// non-default namespace or partition.
|
|
|
|
EnvoyID string
|
|
|
|
// OutgoingProxyKind is the type of proxy of the upstream service. However, if the upstream is "typical" this will
|
|
|
|
// be set to "connect-proxy" instead.
|
|
|
|
OutgoingProxyKind api.ServiceKind
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ec ExtensionConfiguration) IsUpstream() bool {
|
|
|
|
_, ok := ec.Upstreams[ec.ServiceName]
|
|
|
|
return ok
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ec ExtensionConfiguration) MatchesUpstreamServiceSNI(sni string) bool {
|
|
|
|
u := ec.Upstreams[ec.ServiceName]
|
|
|
|
_, match := u.SNI[sni]
|
|
|
|
return match
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ec ExtensionConfiguration) EnvoyID() string {
|
|
|
|
u := ec.Upstreams[ec.ServiceName]
|
|
|
|
return u.EnvoyID
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ec ExtensionConfiguration) OutgoingProxyKind() api.ServiceKind {
|
|
|
|
u := ec.Upstreams[ec.ServiceName]
|
|
|
|
return u.OutgoingProxyKind
|
|
|
|
}
|
|
|
|
|
|
|
|
func GetExtensionConfigurations(cfgSnap *proxycfg.ConfigSnapshot) map[api.CompoundServiceName][]ExtensionConfiguration {
|
|
|
|
extensionsMap := make(map[api.CompoundServiceName][]api.EnvoyExtension)
|
|
|
|
upstreamMap := make(map[api.CompoundServiceName]UpstreamData)
|
|
|
|
var kind api.ServiceKind
|
2022-12-22 18:03:33 +00:00
|
|
|
extensionConfigurationsMap := make(map[api.CompoundServiceName][]ExtensionConfiguration)
|
2022-03-15 14:07:40 +00:00
|
|
|
|
|
|
|
trustDomain := ""
|
|
|
|
if cfgSnap.Roots != nil {
|
|
|
|
trustDomain = cfgSnap.Roots.TrustDomain
|
|
|
|
}
|
|
|
|
|
|
|
|
switch cfgSnap.Kind {
|
2022-05-05 20:39:39 +00:00
|
|
|
case structs.ServiceKindConnectProxy:
|
2022-12-21 06:26:20 +00:00
|
|
|
kind = api.ServiceKindConnectProxy
|
|
|
|
outgoingKindByService := make(map[api.CompoundServiceName]api.ServiceKind)
|
2022-05-05 20:39:39 +00:00
|
|
|
for uid, upstreamData := range cfgSnap.ConnectProxy.WatchedUpstreamEndpoints {
|
2022-12-21 06:26:20 +00:00
|
|
|
sn := upstreamIDToCompoundServiceName(uid)
|
|
|
|
|
2022-05-05 20:39:39 +00:00
|
|
|
for _, serviceNodes := range upstreamData {
|
|
|
|
for _, serviceNode := range serviceNodes {
|
2022-12-21 06:26:20 +00:00
|
|
|
if serviceNode.Service == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// Store the upstream's kind, and for ServiceKindTypical we don't do anything because we'll default
|
|
|
|
// any unset upstreams to ServiceKindConnectProxy below.
|
|
|
|
switch serviceNode.Service.Kind {
|
|
|
|
case structs.ServiceKindTypical:
|
|
|
|
default:
|
|
|
|
outgoingKindByService[sn] = api.ServiceKind(serviceNode.Service.Kind)
|
2022-05-05 20:39:39 +00:00
|
|
|
}
|
2022-12-21 06:26:20 +00:00
|
|
|
// We only need the kind from one instance, so break once we find it.
|
|
|
|
break
|
2022-05-05 20:39:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-03 21:42:50 +00:00
|
|
|
// TODO(peering): consider PeerUpstreamEndpoints in addition to DiscoveryChain
|
2022-12-21 06:26:20 +00:00
|
|
|
// These are the discovery chains for upstreams which have the Envoy Extensions applied to the local service.
|
2022-05-05 20:39:39 +00:00
|
|
|
for uid, dc := range cfgSnap.ConnectProxy.DiscoveryChain {
|
|
|
|
compoundServiceName := upstreamIDToCompoundServiceName(uid)
|
2022-12-21 06:26:20 +00:00
|
|
|
extensionsMap[compoundServiceName] = convertEnvoyExtensions(dc.EnvoyExtensions)
|
|
|
|
|
2022-05-05 20:39:39 +00:00
|
|
|
meta := uid.EnterpriseMeta
|
|
|
|
sni := connect.ServiceSNI(uid.Name, "", meta.NamespaceOrDefault(), meta.PartitionOrDefault(), cfgSnap.Datacenter, trustDomain)
|
2022-12-21 06:26:20 +00:00
|
|
|
outgoingKind, ok := outgoingKindByService[compoundServiceName]
|
|
|
|
if !ok {
|
|
|
|
outgoingKind = api.ServiceKindConnectProxy
|
|
|
|
}
|
|
|
|
|
|
|
|
upstreamMap[compoundServiceName] = UpstreamData{
|
|
|
|
SNI: map[string]struct{}{sni: {}},
|
|
|
|
EnvoyID: uid.EnvoyID(),
|
|
|
|
OutgoingProxyKind: outgoingKind,
|
|
|
|
}
|
2022-05-05 20:39:39 +00:00
|
|
|
}
|
2022-12-22 18:03:33 +00:00
|
|
|
// Adds extensions configured for the local service to the ExtensionConfiguration. This only applies to
|
|
|
|
// connect-proxies because extensions are either global or tied to a specific service, so the terminating
|
|
|
|
// gateway's Envoy resources for the local service (i.e not to upstreams) would never need to be modified.
|
|
|
|
localSvc := api.CompoundServiceName{
|
|
|
|
Name: cfgSnap.Proxy.DestinationServiceName,
|
|
|
|
Namespace: cfgSnap.ProxyID.NamespaceOrDefault(),
|
|
|
|
Partition: cfgSnap.ProxyID.PartitionOrEmpty(),
|
|
|
|
}
|
|
|
|
extensionConfigurationsMap[localSvc] = []ExtensionConfiguration{}
|
|
|
|
cfgSnapExts := convertEnvoyExtensions(cfgSnap.Proxy.EnvoyExtensions)
|
|
|
|
for _, ext := range cfgSnapExts {
|
|
|
|
extCfg := ExtensionConfiguration{
|
|
|
|
EnvoyExtension: ext,
|
|
|
|
ServiceName: localSvc,
|
|
|
|
// Upstreams is nil to signify this extension is not being applied to an upstream service, but rather to the local service.
|
|
|
|
Upstreams: nil,
|
|
|
|
Kind: kind,
|
|
|
|
}
|
|
|
|
extensionConfigurationsMap[localSvc] = append(extensionConfigurationsMap[localSvc], extCfg)
|
|
|
|
}
|
2022-03-15 14:07:40 +00:00
|
|
|
case structs.ServiceKindTerminatingGateway:
|
2022-12-21 06:26:20 +00:00
|
|
|
kind = api.ServiceKindTerminatingGateway
|
2022-03-15 14:07:40 +00:00
|
|
|
for svc, c := range cfgSnap.TerminatingGateway.ServiceConfigs {
|
|
|
|
compoundServiceName := serviceNameToCompoundServiceName(svc)
|
2022-12-21 06:26:20 +00:00
|
|
|
extensionsMap[compoundServiceName] = convertEnvoyExtensions(c.EnvoyExtensions)
|
2022-03-15 14:07:40 +00:00
|
|
|
|
|
|
|
sni := connect.ServiceSNI(svc.Name, "", svc.NamespaceOrDefault(), svc.PartitionOrDefault(), cfgSnap.Datacenter, trustDomain)
|
|
|
|
envoyID := proxycfg.NewUpstreamIDFromServiceName(svc)
|
2022-12-21 06:26:20 +00:00
|
|
|
|
|
|
|
snis := map[string]struct{}{sni: {}}
|
2022-04-13 15:45:25 +00:00
|
|
|
|
|
|
|
resolver, hasResolver := cfgSnap.TerminatingGateway.ServiceResolvers[svc]
|
|
|
|
if hasResolver {
|
|
|
|
for subsetName := range resolver.Subsets {
|
|
|
|
sni := connect.ServiceSNI(svc.Name, subsetName, svc.NamespaceOrDefault(), svc.PartitionOrDefault(), cfgSnap.Datacenter, trustDomain)
|
2022-12-21 06:26:20 +00:00
|
|
|
snis[sni] = struct{}{}
|
2022-04-13 15:45:25 +00:00
|
|
|
}
|
|
|
|
}
|
2022-12-21 06:26:20 +00:00
|
|
|
|
|
|
|
upstreamMap[compoundServiceName] = UpstreamData{
|
|
|
|
SNI: snis,
|
|
|
|
EnvoyID: envoyID.EnvoyID(),
|
|
|
|
OutgoingProxyKind: api.ServiceKindTerminatingGateway,
|
|
|
|
}
|
|
|
|
|
2022-03-15 14:07:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-21 06:26:20 +00:00
|
|
|
for svc, exts := range extensionsMap {
|
|
|
|
extensionConfigurationsMap[svc] = []ExtensionConfiguration{}
|
|
|
|
for _, ext := range exts {
|
|
|
|
extCfg := ExtensionConfiguration{
|
|
|
|
EnvoyExtension: ext,
|
|
|
|
Kind: kind,
|
|
|
|
ServiceName: svc,
|
|
|
|
Upstreams: upstreamMap,
|
|
|
|
}
|
|
|
|
extensionConfigurationsMap[svc] = append(extensionConfigurationsMap[svc], extCfg)
|
|
|
|
}
|
2022-03-15 14:07:40 +00:00
|
|
|
}
|
2022-12-21 06:26:20 +00:00
|
|
|
|
|
|
|
return extensionConfigurationsMap
|
2022-03-15 14:07:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func serviceNameToCompoundServiceName(svc structs.ServiceName) api.CompoundServiceName {
|
|
|
|
return api.CompoundServiceName{
|
|
|
|
Name: svc.Name,
|
|
|
|
Partition: svc.PartitionOrDefault(),
|
|
|
|
Namespace: svc.NamespaceOrDefault(),
|
|
|
|
}
|
|
|
|
}
|
2022-05-05 20:39:39 +00:00
|
|
|
|
|
|
|
func upstreamIDToCompoundServiceName(uid proxycfg.UpstreamID) api.CompoundServiceName {
|
|
|
|
return api.CompoundServiceName{
|
|
|
|
Name: uid.Name,
|
|
|
|
Partition: uid.PartitionOrDefault(),
|
|
|
|
Namespace: uid.NamespaceOrDefault(),
|
|
|
|
}
|
|
|
|
}
|
2022-12-19 20:19:37 +00:00
|
|
|
|
2022-12-21 06:26:20 +00:00
|
|
|
func convertEnvoyExtensions(structExtensions structs.EnvoyExtensions) []api.EnvoyExtension {
|
|
|
|
return structExtensions.ToAPI()
|
2022-12-19 20:19:37 +00:00
|
|
|
}
|