mirror of
https://github.com/status-im/consul.git
synced 2025-01-09 21:35:52 +00:00
0094dbf312
This PR fixes a bug that was introduced in: https://github.com/hashicorp/consul/pull/16021 A user setting a protocol in proxy-defaults would cause tproxy implicit upstreams to not honor the upstream service's protocol set in its `ServiceDefaults.Protocol` field, and would instead always use the proxy-defaults value. Due to the fact that upstreams configured with "tcp" can successfully contact upstream "http" services, this issue was not recognized until recently (a proxy-defaults with "tcp" and a listening service with "http" would make successful requests, but not the opposite). As a temporary work-around, users experiencing this issue can explicitly set the protocol on the `ServiceDefaults.UpstreamConfig.Overrides`, which should take precedence. The fix in this PR removes the proxy-defaults protocol from the wildcard upstream that tproxy uses to configure implicit upstreams. When the protocol was included, it would always overwrite the value during discovery chain compilation, which was not correct. The discovery chain compiler also consumes proxy defaults to determine the protocol, so simply excluding it from the wildcard upstream config map resolves the issue.
305 lines
13 KiB
Go
305 lines
13 KiB
Go
// 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
|
|
}
|