mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
304 lines
13 KiB
304 lines
13 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: MPL-2.0 |
|
|
|
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. |
|
|
|
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.MutualTLSMode = proxyConf.MutualTLSMode |
|
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 |
|
} |
|
|
|
// We explicitly DO NOT merge the protocol from proxy-defaults into the wildcard upstream here. |
|
// TProxy will try to use the data from the `wildcardUpstreamDefaults` as a source of truth, which is |
|
// normally correct to inherit from proxy-defaults. However, it is NOT correct for protocol. |
|
// |
|
// This edge-case is different for `protocol` from other fields, since the protocol can be |
|
// set on both the local `ServiceDefaults.UpstreamOverrides` and upstream `ServiceDefaults.Protocol`. |
|
// This means that when proxy-defaults is set, it would always be treated as an explicit override, |
|
// and take precedence over the protocol that is set on the discovery chain (which comes from the |
|
// service's preference in its service-defaults), which is wrong. |
|
// |
|
// When the upstream is not explicitly defined, we should only get the protocol from one of these locations: |
|
// 1. For tproxy non-peering services, it can be fetched via the discovery chain. |
|
// The chain compiler merges the proxy-defaults protocol with the upstream's preferred service-defaults protocol. |
|
// 2. For tproxy non-peering services with default upstream overrides, it will come from the wildcard upstream overrides. |
|
// 3. For tproxy non-peering services with specific upstream overrides, it will come from the specific upstream override defined. |
|
// 4. For tproxy peering services, they do not honor the proxy-defaults, since they reside in a different cluster. |
|
// The data will come from a separate peerMeta field. |
|
// In all of these cases, it is not necessary for the proxy-defaults to exist in the wildcard upstream. |
|
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 |
|
} |
|
|
|
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 |
|
} |
|
|
|
if serviceConf.MutualTLSMode != structs.MutualTLSModeDefault { |
|
thisReply.MutualTLSMode = serviceConf.MutualTLSMode |
|
} |
|
|
|
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 |
|
|
|
// 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{}{} |
|
} |
|
} |
|
|
|
// 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 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 proxyConfGlobalProtocol != "" { |
|
resolvedCfg["protocol"] = proxyConfGlobalProtocol |
|
} |
|
|
|
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). |
|
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 |
|
} |
|
|
|
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 |
|
}
|
|
|