5abaaead05
* Add Peer field to service-defaults upstream overrides. * add api changes, compat mode for service default overrides * Fixes based on testing --------- Co-authored-by: DanStough <dan.stough@hashicorp.com>
316 lines
13 KiB
Go
316 lines
13 KiB
Go
package configentry
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/imdario/mergo"
|
|
"github.com/mitchellh/copystructure"
|
|
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
)
|
|
|
|
func ComputeResolvedServiceConfig(
|
|
args *structs.ServiceConfigRequest,
|
|
entries *ResolvedServiceConfigSet,
|
|
logger hclog.Logger,
|
|
) (*structs.ServiceConfigResponse, error) {
|
|
var thisReply structs.ServiceConfigResponse
|
|
|
|
thisReply.MeshGateway.Mode = structs.MeshGatewayModeDefault
|
|
|
|
// Store the upstream defaults under a wildcard key so that they can be applied to
|
|
// upstreams that are inferred from intentions and do not have explicit upstream configuration.
|
|
wildcard := structs.PeeredServiceName{
|
|
ServiceName: structs.NewServiceName(structs.WildcardSpecifier, args.WithWildcardNamespace()),
|
|
}
|
|
wildcardUpstreamDefaults := make(map[string]interface{})
|
|
// resolvedConfigs stores the opaque config map for each upstream and is keyed on the upstream's ID.
|
|
resolvedConfigs := make(map[structs.PeeredServiceName]map[string]interface{})
|
|
|
|
// TODO(freddy) Refactor this into smaller set of state store functions
|
|
// Pass the WatchSet to both the service and proxy config lookups. If either is updated during the
|
|
// blocking query, this function will be rerun and these state store lookups will both be current.
|
|
// We use the default enterprise meta to look up the global proxy defaults because they are not namespaced.
|
|
|
|
proxyConf := entries.GetProxyDefaults(args.PartitionOrDefault())
|
|
if proxyConf != nil {
|
|
// Apply the proxy defaults to the sidecar's proxy config
|
|
mapCopy, err := copystructure.Copy(proxyConf.Config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to copy global proxy-defaults: %v", err)
|
|
}
|
|
|
|
thisReply.ProxyConfig = mapCopy.(map[string]interface{})
|
|
thisReply.Mode = proxyConf.Mode
|
|
thisReply.TransparentProxy = proxyConf.TransparentProxy
|
|
thisReply.MeshGateway = proxyConf.MeshGateway
|
|
thisReply.Expose = proxyConf.Expose
|
|
thisReply.EnvoyExtensions = proxyConf.EnvoyExtensions
|
|
thisReply.AccessLogs = proxyConf.AccessLogs
|
|
|
|
// Only MeshGateway and Protocol should affect upstreams.
|
|
// MeshGateway is strange. It's marshaled into UpstreamConfigs via the arbitrary map, but it
|
|
// uses concrete fields everywhere else. We always take the explicit definition here for
|
|
// wildcard upstreams and discard the user setting it via arbitrary map in proxy-defaults.
|
|
if mgw, ok := thisReply.ProxyConfig["mesh_gateway"]; ok {
|
|
wildcardUpstreamDefaults["mesh_gateway"] = mgw
|
|
}
|
|
if !proxyConf.MeshGateway.IsZero() {
|
|
wildcardUpstreamDefaults["mesh_gateway"] = proxyConf.MeshGateway
|
|
}
|
|
if protocol, ok := thisReply.ProxyConfig["protocol"]; ok {
|
|
wildcardUpstreamDefaults["protocol"] = protocol
|
|
}
|
|
}
|
|
|
|
serviceConf := entries.GetServiceDefaults(
|
|
structs.NewServiceID(args.Name, &args.EnterpriseMeta),
|
|
)
|
|
if serviceConf != nil {
|
|
|
|
if serviceConf.Expose.Checks {
|
|
thisReply.Expose.Checks = true
|
|
}
|
|
if len(serviceConf.Expose.Paths) >= 1 {
|
|
thisReply.Expose.Paths = serviceConf.Expose.Paths
|
|
}
|
|
if serviceConf.MeshGateway.Mode != structs.MeshGatewayModeDefault {
|
|
thisReply.MeshGateway.Mode = serviceConf.MeshGateway.Mode
|
|
wildcardUpstreamDefaults["mesh_gateway"] = serviceConf.MeshGateway
|
|
}
|
|
if serviceConf.TransparentProxy.OutboundListenerPort != 0 {
|
|
thisReply.TransparentProxy.OutboundListenerPort = serviceConf.TransparentProxy.OutboundListenerPort
|
|
}
|
|
if serviceConf.TransparentProxy.DialedDirectly {
|
|
thisReply.TransparentProxy.DialedDirectly = serviceConf.TransparentProxy.DialedDirectly
|
|
}
|
|
if serviceConf.Mode != structs.ProxyModeDefault {
|
|
thisReply.Mode = serviceConf.Mode
|
|
}
|
|
if serviceConf.Destination != nil {
|
|
thisReply.Destination = *serviceConf.Destination
|
|
}
|
|
|
|
// Populate values for the proxy config map
|
|
proxyConf := thisReply.ProxyConfig
|
|
if proxyConf == nil {
|
|
proxyConf = make(map[string]interface{})
|
|
}
|
|
if serviceConf.Protocol != "" {
|
|
proxyConf["protocol"] = serviceConf.Protocol
|
|
}
|
|
if serviceConf.BalanceInboundConnections != "" {
|
|
proxyConf["balance_inbound_connections"] = serviceConf.BalanceInboundConnections
|
|
}
|
|
if serviceConf.MaxInboundConnections > 0 {
|
|
proxyConf["max_inbound_connections"] = serviceConf.MaxInboundConnections
|
|
}
|
|
if serviceConf.LocalConnectTimeoutMs > 0 {
|
|
proxyConf["local_connect_timeout_ms"] = serviceConf.LocalConnectTimeoutMs
|
|
}
|
|
if serviceConf.LocalRequestTimeoutMs > 0 {
|
|
proxyConf["local_request_timeout_ms"] = serviceConf.LocalRequestTimeoutMs
|
|
}
|
|
// Add the proxy conf to the response if any fields were populated
|
|
if len(proxyConf) > 0 {
|
|
thisReply.ProxyConfig = proxyConf
|
|
}
|
|
|
|
thisReply.Meta = serviceConf.Meta
|
|
// Service defaults' envoy extensions are appended to the proxy defaults extensions so that proxy defaults
|
|
// extensions are applied first.
|
|
thisReply.EnvoyExtensions = append(thisReply.EnvoyExtensions, serviceConf.EnvoyExtensions...)
|
|
}
|
|
|
|
// First collect all upstreams into a set of seen upstreams.
|
|
// Upstreams can come from:
|
|
// - Explicitly from proxy registrations, and therefore as an argument to this RPC endpoint
|
|
// - Implicitly from centralized upstream config in service-defaults
|
|
seenUpstreams := map[structs.PeeredServiceName]struct{}{}
|
|
|
|
var (
|
|
noUpstreamArgs = len(args.UpstreamServiceNames) == 0 && len(args.UpstreamIDs) == 0
|
|
|
|
// Check the args and the resolved value. If it was exclusively set via a config entry, then args.Mode
|
|
// will never be transparent because the service config request does not use the resolved value.
|
|
tproxy = args.Mode == structs.ProxyModeTransparent || thisReply.Mode == structs.ProxyModeTransparent
|
|
)
|
|
|
|
// The upstreams passed as arguments to this endpoint are the upstreams explicitly defined in a proxy registration.
|
|
// If no upstreams were passed, then we should only return the resolved config if the proxy is in transparent mode.
|
|
// Otherwise we would return a resolved upstream config to a proxy with no configured upstreams.
|
|
if noUpstreamArgs && !tproxy {
|
|
return &thisReply, nil
|
|
}
|
|
|
|
// First store all upstreams that were provided in the request
|
|
for _, psn := range args.UpstreamServiceNames {
|
|
if _, ok := seenUpstreams[psn]; !ok {
|
|
seenUpstreams[psn] = struct{}{}
|
|
}
|
|
}
|
|
// For 1.14, service-defaults overrides would apply to peer upstreams incorrectly
|
|
// because the config merging logic was oblivious to the concept of a peer.
|
|
// We replicate this behavior on legacy calls for backwards-compatibility.
|
|
for _, sid := range args.UpstreamIDs {
|
|
psn := structs.PeeredServiceName{
|
|
ServiceName: structs.NewServiceName(sid.ID, &sid.EnterpriseMeta),
|
|
}
|
|
seenUpstreams[psn] = struct{}{}
|
|
}
|
|
|
|
// Then store upstreams inferred from service-defaults and mapify the overrides.
|
|
var (
|
|
upstreamDefaults *structs.UpstreamConfig
|
|
upstreamOverrides = make(map[structs.PeeredServiceName]*structs.UpstreamConfig)
|
|
)
|
|
if serviceConf != nil && serviceConf.UpstreamConfig != nil {
|
|
for i, override := range serviceConf.UpstreamConfig.Overrides {
|
|
if override.Name == "" {
|
|
logger.Warn(
|
|
"Skipping UpstreamConfig.Overrides entry without a required name field",
|
|
"entryIndex", i,
|
|
"kind", serviceConf.GetKind(),
|
|
"name", serviceConf.GetName(),
|
|
"namespace", serviceConf.GetEnterpriseMeta().NamespaceOrEmpty(),
|
|
)
|
|
continue // skip this impossible condition
|
|
}
|
|
psn := override.PeeredServiceName()
|
|
seenUpstreams[psn] = struct{}{}
|
|
upstreamOverrides[psn] = override
|
|
}
|
|
if serviceConf.UpstreamConfig.Defaults != nil {
|
|
upstreamDefaults = serviceConf.UpstreamConfig.Defaults
|
|
if upstreamDefaults.MeshGateway.Mode == structs.MeshGatewayModeDefault {
|
|
upstreamDefaults.MeshGateway.Mode = thisReply.MeshGateway.Mode
|
|
}
|
|
upstreamDefaults.MergeInto(wildcardUpstreamDefaults)
|
|
// Always add the wildcard upstream if a service-defaults default-upstream was configured.
|
|
resolvedConfigs[wildcard] = wildcardUpstreamDefaults
|
|
}
|
|
}
|
|
|
|
if !args.MeshGateway.IsZero() {
|
|
wildcardUpstreamDefaults["mesh_gateway"] = args.MeshGateway
|
|
}
|
|
|
|
// Add the wildcard upstream if any fields were populated (it may have been already
|
|
// added if a service-defaults exists). We likely could always add it without issues,
|
|
// but this has been existing behavior, and many unit tests would break.
|
|
if len(wildcardUpstreamDefaults) > 0 {
|
|
resolvedConfigs[wildcard] = wildcardUpstreamDefaults
|
|
}
|
|
|
|
// For Consul 1.14.x, service-defaults would apply to either local or peer services as long
|
|
// as the `name` matched. We introduce `legacyUpstreams` as a compatibility mode for:
|
|
// 1. old agents, that are using the deprecated UpstreamIDs api
|
|
// 2. Migrations to 1.15 that do not specify the "peer" field. The behavior should remain the same
|
|
// until the config entries are updates.
|
|
//
|
|
// This should be remove in Consul 1.16
|
|
var hasPeerUpstream bool
|
|
for _, override := range upstreamOverrides {
|
|
if override.Peer != "" {
|
|
hasPeerUpstream = true
|
|
break
|
|
}
|
|
}
|
|
legacyUpstreams := len(args.UpstreamIDs) > 0 || !hasPeerUpstream
|
|
|
|
for upstream := range seenUpstreams {
|
|
resolvedCfg := make(map[string]interface{})
|
|
|
|
// The protocol of an upstream is resolved in this order:
|
|
// 1. Default protocol from proxy-defaults (how all services should be addressed)
|
|
// 2. Protocol for upstream service defined in its service-defaults (how the upstream wants to be addressed)
|
|
// 3. Protocol defined for the upstream in the service-defaults.(upstream_config.defaults|upstream_config.overrides) of the downstream
|
|
// (how the downstream wants to address it)
|
|
if err := mergo.MergeWithOverwrite(&resolvedCfg, wildcardUpstreamDefaults); err != nil {
|
|
return nil, fmt.Errorf("failed to merge wildcard defaults into upstream: %v", err)
|
|
}
|
|
|
|
upstreamSvcDefaults := entries.GetServiceDefaults(upstream.ServiceName.ToServiceID())
|
|
if upstreamSvcDefaults != nil {
|
|
if upstreamSvcDefaults.Protocol != "" {
|
|
resolvedCfg["protocol"] = upstreamSvcDefaults.Protocol
|
|
}
|
|
}
|
|
|
|
// When dialing an upstream, the goal is to flatten the mesh gateway mode in this order
|
|
// (larger number wins):
|
|
// 1. Value from the proxy-defaults
|
|
// 2. Value from top-level of service-defaults (ServiceDefaults.MeshGateway)
|
|
// 3. Value from centralized upstream defaults (ServiceDefaults.UpstreamConfig.Defaults)
|
|
// 4. Value from local proxy registration (NodeService.Proxy.MeshGateway)
|
|
// 5. Value from centralized upstream override (ServiceDefaults.UpstreamConfig.Overrides)
|
|
// 6. Value from local upstream definition (NodeService.Proxy.Upstreams[].MeshGateway)
|
|
//
|
|
// The MeshGateway value from upstream definitions in the proxy registration override
|
|
// the one from UpstreamConfig.Defaults and UpstreamConfig.Overrides because they are
|
|
// specific to the proxy instance.
|
|
//
|
|
// Step 6 is handled by the dialer's ServiceManager in MergeServiceConfig.
|
|
|
|
// Start with the merged value from proxyConf and serviceConf. (steps 1-2)
|
|
if !thisReply.MeshGateway.IsZero() {
|
|
resolvedCfg["mesh_gateway"] = thisReply.MeshGateway
|
|
}
|
|
|
|
// Merge in the upstream defaults (step 3).
|
|
if upstreamDefaults != nil {
|
|
upstreamDefaults.MergeInto(resolvedCfg)
|
|
}
|
|
|
|
// Merge in the top-level mode from the proxy instance (step 4).
|
|
if !args.MeshGateway.IsZero() {
|
|
// This means each upstream inherits the value from the `NodeService.Proxy.MeshGateway` field.
|
|
resolvedCfg["mesh_gateway"] = args.MeshGateway
|
|
}
|
|
|
|
// Merge in Overrides for the upstream (step 5).
|
|
// In the legacy case, overrides only match on name. We remove the peer and try to match against
|
|
// our map of overrides. We still want to check the full PSN in the map in case there is a specific
|
|
// override that applies to peers.
|
|
if legacyUpstreams {
|
|
peerlessUpstream := upstream
|
|
peerlessUpstream.Peer = ""
|
|
if upstreamOverrides[peerlessUpstream] != nil {
|
|
upstreamOverrides[peerlessUpstream].MergeInto(resolvedCfg)
|
|
}
|
|
}
|
|
if upstreamOverrides[upstream] != nil {
|
|
upstreamOverrides[upstream].MergeInto(resolvedCfg)
|
|
}
|
|
|
|
if len(resolvedCfg) > 0 {
|
|
resolvedConfigs[upstream] = resolvedCfg
|
|
}
|
|
}
|
|
|
|
// don't allocate the slices just to not fill them
|
|
if len(resolvedConfigs) == 0 {
|
|
return &thisReply, nil
|
|
}
|
|
|
|
if len(args.UpstreamIDs) > 0 {
|
|
// DEPRECATED: Remove these legacy upstreams in Consul v1.16
|
|
thisReply.UpstreamIDConfigs = make(structs.OpaqueUpstreamConfigsDeprecated, 0, len(resolvedConfigs))
|
|
|
|
for us, conf := range resolvedConfigs {
|
|
thisReply.UpstreamIDConfigs = append(thisReply.UpstreamIDConfigs,
|
|
structs.OpaqueUpstreamConfigDeprecated{Upstream: us.ServiceName.ToServiceID(), Config: conf})
|
|
}
|
|
} else {
|
|
thisReply.UpstreamConfigs = make(structs.OpaqueUpstreamConfigs, 0, len(resolvedConfigs))
|
|
|
|
for us, conf := range resolvedConfigs {
|
|
thisReply.UpstreamConfigs = append(thisReply.UpstreamConfigs,
|
|
structs.OpaqueUpstreamConfig{Upstream: us, Config: conf})
|
|
}
|
|
}
|
|
|
|
return &thisReply, nil
|
|
}
|