mirror of
https://github.com/status-im/consul.git
synced 2025-01-11 14:24:39 +00:00
8728a4496c
* Add Tproxy support to Envoy Extensions (this is needed for service to service validation) * Add validation for Envoy configuration for an upstream service * Use both /config_dump and /cluster to validate Envoy configuration This is because of a bug in Envoy where the EndpointsConfigDump does not include a cluster_name, making it impossible to match an endpoint to verify it exists. This removes endpoints support for builtin extensions since only the validate plugin was using it, and it is no longer used. It also removes test cases for endpoint validation. Endpoints validation now only occurs in the top level test from config_dump and clusters json files. Co-authored-by: Eric <eric@haberkorn.co>
291 lines
11 KiB
Go
291 lines
11 KiB
Go
package xdscommon
|
|
|
|
import (
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/hashicorp/consul/agent/connect"
|
|
"github.com/hashicorp/consul/agent/proxycfg"
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
"github.com/hashicorp/consul/api"
|
|
)
|
|
|
|
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"
|
|
|
|
// 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"
|
|
|
|
// OutboundListenerName is the name we give the outbound Envoy listener when transparent proxy mode is enabled.
|
|
OutboundListenerName = "outbound_listener"
|
|
)
|
|
|
|
type EnvoyExtension interface {
|
|
Extend(*IndexedResources, ExtensionConfiguration) (*IndexedResources, error)
|
|
Validate(ExtensionConfiguration) error
|
|
}
|
|
|
|
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),
|
|
},
|
|
}
|
|
}
|
|
|
|
type ServiceConfig struct {
|
|
// Kind identifies the final proxy kind that will make the request to the
|
|
// destination service.
|
|
Kind api.ServiceKind
|
|
EnvoyExtensions []api.EnvoyExtension
|
|
}
|
|
|
|
// 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
|
|
// terminating gateways are supported.
|
|
Kind api.ServiceKind
|
|
}
|
|
|
|
// 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 {
|
|
// VIP is the tproxy virtual IP used to reach an upstream service.
|
|
VIP string
|
|
// 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
|
|
extensionConfigurationsMap := make(map[api.CompoundServiceName][]ExtensionConfiguration)
|
|
|
|
trustDomain := ""
|
|
if cfgSnap.Roots != nil {
|
|
trustDomain = cfgSnap.Roots.TrustDomain
|
|
}
|
|
|
|
switch cfgSnap.Kind {
|
|
case structs.ServiceKindConnectProxy:
|
|
kind = api.ServiceKindConnectProxy
|
|
outgoingKindByService := make(map[api.CompoundServiceName]api.ServiceKind)
|
|
vipForService := make(map[api.CompoundServiceName]string)
|
|
for uid, upstreamData := range cfgSnap.ConnectProxy.WatchedUpstreamEndpoints {
|
|
sn := upstreamIDToCompoundServiceName(uid)
|
|
|
|
for _, serviceNodes := range upstreamData {
|
|
for _, serviceNode := range serviceNodes {
|
|
if serviceNode.Service == nil {
|
|
continue
|
|
}
|
|
vip := serviceNode.Service.TaggedAddresses[structs.TaggedAddressVirtualIP].Address
|
|
if vip != "" {
|
|
if _, ok := vipForService[sn]; !ok {
|
|
vipForService[sn] = vip
|
|
}
|
|
}
|
|
// 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)
|
|
}
|
|
// We only need the kind from one instance, so break once we find it.
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO(peering): consider PeerUpstreamEndpoints in addition to DiscoveryChain
|
|
// These are the discovery chains for upstreams which have the Envoy Extensions applied to the local service.
|
|
for uid, dc := range cfgSnap.ConnectProxy.DiscoveryChain {
|
|
compoundServiceName := upstreamIDToCompoundServiceName(uid)
|
|
extensionsMap[compoundServiceName] = convertEnvoyExtensions(dc.EnvoyExtensions)
|
|
|
|
meta := uid.EnterpriseMeta
|
|
sni := connect.ServiceSNI(uid.Name, "", meta.NamespaceOrDefault(), meta.PartitionOrDefault(), cfgSnap.Datacenter, trustDomain)
|
|
outgoingKind, ok := outgoingKindByService[compoundServiceName]
|
|
if !ok {
|
|
outgoingKind = api.ServiceKindConnectProxy
|
|
}
|
|
|
|
upstreamMap[compoundServiceName] = UpstreamData{
|
|
SNI: map[string]struct{}{sni: {}},
|
|
VIP: vipForService[compoundServiceName],
|
|
EnvoyID: uid.EnvoyID(),
|
|
OutgoingProxyKind: outgoingKind,
|
|
}
|
|
}
|
|
// 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)
|
|
}
|
|
case structs.ServiceKindTerminatingGateway:
|
|
kind = api.ServiceKindTerminatingGateway
|
|
for svc, c := range cfgSnap.TerminatingGateway.ServiceConfigs {
|
|
compoundServiceName := serviceNameToCompoundServiceName(svc)
|
|
extensionsMap[compoundServiceName] = convertEnvoyExtensions(c.EnvoyExtensions)
|
|
|
|
sni := connect.ServiceSNI(svc.Name, "", svc.NamespaceOrDefault(), svc.PartitionOrDefault(), cfgSnap.Datacenter, trustDomain)
|
|
envoyID := proxycfg.NewUpstreamIDFromServiceName(svc)
|
|
|
|
snis := map[string]struct{}{sni: {}}
|
|
|
|
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)
|
|
snis[sni] = struct{}{}
|
|
}
|
|
}
|
|
|
|
upstreamMap[compoundServiceName] = UpstreamData{
|
|
SNI: snis,
|
|
EnvoyID: envoyID.EnvoyID(),
|
|
OutgoingProxyKind: api.ServiceKindTerminatingGateway,
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
return extensionConfigurationsMap
|
|
}
|
|
|
|
func serviceNameToCompoundServiceName(svc structs.ServiceName) api.CompoundServiceName {
|
|
return api.CompoundServiceName{
|
|
Name: svc.Name,
|
|
Partition: svc.PartitionOrDefault(),
|
|
Namespace: svc.NamespaceOrDefault(),
|
|
}
|
|
}
|
|
|
|
func upstreamIDToCompoundServiceName(uid proxycfg.UpstreamID) api.CompoundServiceName {
|
|
return api.CompoundServiceName{
|
|
Name: uid.Name,
|
|
Partition: uid.PartitionOrDefault(),
|
|
Namespace: uid.NamespaceOrDefault(),
|
|
}
|
|
}
|
|
|
|
func convertEnvoyExtensions(structExtensions structs.EnvoyExtensions) []api.EnvoyExtension {
|
|
return structExtensions.ToAPI()
|
|
}
|