9b0984e5a6
Fix configuration merging for implicit tproxy upstreams. Change the merging logic so that the wildcard upstream has correct proxy-defaults and service-defaults values combined into it. It did not previously merge all fields, and the wildcard upstream did not exist unless service-defaults existed (it ignored proxy-defaults, essentially). Change the way we fetch upstream configuration in the xDS layer so that it falls back to the wildcard when no matching upstream is found. This is what allows implicit peer upstreams to have the correct "merged" config. Change proxycfg to always watch local mesh gateway endpoints whenever a peer upstream is found. This simplifies the logic so that we do not have to inspect the "merged" configuration on peer upstreams to extract the mesh gateway mode.
287 lines
11 KiB
Go
287 lines
11 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,
|
|
upstreamIDs []structs.ServiceID,
|
|
legacyUpstreams bool,
|
|
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.NewServiceID(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.ServiceID]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.
|
|
|
|
var proxyConfGlobalProtocol string
|
|
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
|
|
|
|
parsed, err := structs.ParseUpstreamConfigNoDefaults(mapCopy.(map[string]interface{}))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse upstream config map for proxy-defaults: %v", err)
|
|
}
|
|
proxyConfGlobalProtocol = parsed.Protocol
|
|
|
|
// 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 err := mergo.MergeWithOverwrite(&wildcardUpstreamDefaults, mapCopy); err != nil {
|
|
return nil, fmt.Errorf("failed to merge upstream config map for proxy-defaults: %v", err)
|
|
}
|
|
if !proxyConf.MeshGateway.IsZero() {
|
|
wildcardUpstreamDefaults["mesh_gateway"] = proxyConf.MeshGateway
|
|
}
|
|
}
|
|
|
|
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.ServiceID]struct{}{}
|
|
|
|
var (
|
|
noUpstreamArgs = len(upstreamIDs) == 0 && len(args.Upstreams) == 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 _, sid := range upstreamIDs {
|
|
if _, ok := seenUpstreams[sid]; !ok {
|
|
seenUpstreams[sid] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// Then store upstreams inferred from service-defaults and mapify the overrides.
|
|
var (
|
|
upstreamDefaults *structs.UpstreamConfig
|
|
upstreamOverrides = make(map[structs.ServiceID]*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
|
|
}
|
|
seenUpstreams[override.ServiceID()] = struct{}{}
|
|
upstreamOverrides[override.ServiceID()] = 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 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)
|
|
protocol := proxyConfGlobalProtocol
|
|
|
|
upstreamSvcDefaults := entries.GetServiceDefaults(
|
|
structs.NewServiceID(upstream.ID, &upstream.EnterpriseMeta),
|
|
)
|
|
if upstreamSvcDefaults != nil {
|
|
if upstreamSvcDefaults.Protocol != "" {
|
|
protocol = upstreamSvcDefaults.Protocol
|
|
}
|
|
}
|
|
|
|
if protocol != "" {
|
|
resolvedCfg["protocol"] = 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).
|
|
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 legacyUpstreams {
|
|
// For legacy upstreams we return a map that is only keyed on the string ID, since they precede namespaces
|
|
thisReply.UpstreamConfigs = make(map[string]map[string]interface{})
|
|
|
|
for us, conf := range resolvedConfigs {
|
|
thisReply.UpstreamConfigs[us.ID] = conf
|
|
}
|
|
|
|
} else {
|
|
thisReply.UpstreamIDConfigs = make(structs.OpaqueUpstreamConfigs, 0, len(resolvedConfigs))
|
|
|
|
for us, conf := range resolvedConfigs {
|
|
thisReply.UpstreamIDConfigs = append(thisReply.UpstreamIDConfigs,
|
|
structs.OpaqueUpstreamConfig{Upstream: us, Config: conf})
|
|
}
|
|
}
|
|
|
|
return &thisReply, nil
|
|
}
|