265 lines
9.1 KiB
Go
265 lines
9.1 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package configentry
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
memdb "github.com/hashicorp/go-memdb"
|
|
"github.com/imdario/mergo"
|
|
"github.com/mitchellh/copystructure"
|
|
|
|
"github.com/hashicorp/consul/acl"
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
)
|
|
|
|
type StateStore interface {
|
|
ReadResolvedServiceConfigEntries(memdb.WatchSet, string, *acl.EnterpriseMeta, []structs.ServiceID, structs.ProxyMode) (uint64, *ResolvedServiceConfigSet, error)
|
|
}
|
|
|
|
// MergeNodeServiceWithCentralConfig merges a service instance (NodeService) with the
|
|
// proxy-defaults/global and service-defaults/:service config entries.
|
|
// This common helper is used by the blocking query function of different RPC endpoints
|
|
// that need to return a fully resolved service defintion.
|
|
func MergeNodeServiceWithCentralConfig(
|
|
ws memdb.WatchSet,
|
|
state StateStore,
|
|
ns *structs.NodeService,
|
|
logger hclog.Logger) (uint64, *structs.NodeService, error) {
|
|
|
|
serviceName := ns.Service
|
|
var upstreams []structs.PeeredServiceName
|
|
if ns.IsSidecarProxy() {
|
|
// This is a sidecar proxy, ignore the proxy service's config since we are
|
|
// managed by the target service config.
|
|
serviceName = ns.Proxy.DestinationServiceName
|
|
|
|
// Also if we have any upstreams defined, add them to the defaults lookup request
|
|
// so we can learn about their configs.
|
|
for _, us := range ns.Proxy.Upstreams {
|
|
if us.DestinationType == "" || us.DestinationType == structs.UpstreamDestTypeService {
|
|
psn := us.DestinationID()
|
|
if psn.Peer == "" {
|
|
psn.ServiceName.EnterpriseMeta.Merge(&ns.EnterpriseMeta)
|
|
} else {
|
|
// Peer services should not have their namespace overwritten.
|
|
psn.ServiceName.EnterpriseMeta.OverridePartition(ns.EnterpriseMeta.PartitionOrDefault())
|
|
}
|
|
upstreams = append(upstreams, psn)
|
|
}
|
|
}
|
|
}
|
|
|
|
configReq := &structs.ServiceConfigRequest{
|
|
Name: serviceName,
|
|
MeshGateway: ns.Proxy.MeshGateway,
|
|
Mode: ns.Proxy.Mode,
|
|
UpstreamServiceNames: upstreams,
|
|
EnterpriseMeta: ns.EnterpriseMeta,
|
|
}
|
|
|
|
// prefer using this vs directly calling the ConfigEntry.ResolveServiceConfig RPC
|
|
// so as to pass down the same watch set to also watch on changes to
|
|
// proxy-defaults/global and service-defaults.
|
|
cfgIndex, configEntries, err := state.ReadResolvedServiceConfigEntries(
|
|
ws,
|
|
configReq.Name,
|
|
&configReq.EnterpriseMeta,
|
|
configReq.GetLocalUpstreamIDs(),
|
|
configReq.Mode,
|
|
)
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("Failure looking up service config entries for %s: %v",
|
|
ns.ID, err)
|
|
}
|
|
|
|
defaults, err := ComputeResolvedServiceConfig(
|
|
configReq,
|
|
configEntries,
|
|
logger,
|
|
)
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("Failure computing service defaults for %s: %v",
|
|
ns.ID, err)
|
|
}
|
|
|
|
mergedns, err := MergeServiceConfig(defaults, ns)
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("Failure merging service definition with config entry defaults for %s: %v",
|
|
ns.ID, err)
|
|
}
|
|
|
|
return cfgIndex, mergedns, nil
|
|
}
|
|
|
|
// MergeServiceConfig merges the service into defaults to produce the final effective
|
|
// config for the specified service.
|
|
func MergeServiceConfig(defaults *structs.ServiceConfigResponse, service *structs.NodeService) (*structs.NodeService, error) {
|
|
if defaults == nil {
|
|
return service, nil
|
|
}
|
|
|
|
// We don't want to change s.registration in place since it is our source of
|
|
// truth about what was actually registered before defaults applied. So copy
|
|
// it first.
|
|
nsRaw, err := copystructure.Copy(service)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Merge proxy defaults
|
|
ns := nsRaw.(*structs.NodeService)
|
|
|
|
if err := mergo.Merge(&ns.Proxy.Config, defaults.ProxyConfig); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := mergo.Merge(&ns.Proxy.Expose, defaults.Expose); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := mergo.Merge(&ns.Proxy.AccessLogs, defaults.AccessLogs); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// defaults.EnvoyExtensions contains the extensions from the proxy defaults config entry followed by extensions from
|
|
// the service defaults config entry. This adds the extensions to structs.NodeService.Proxy which in turn is copied
|
|
// into the proxycfg snapshot to ensure the local service's extensions are accessible from the snapshot.
|
|
//
|
|
// This will replace any existing extensions in the NodeService but that is ok because defaults.EnvoyExtensions
|
|
// should have the latest extensions computed from service defaults and proxy defaults.
|
|
ns.Proxy.EnvoyExtensions = nil
|
|
if len(defaults.EnvoyExtensions) > 0 {
|
|
nsExtensions := make([]structs.EnvoyExtension, len(defaults.EnvoyExtensions))
|
|
for i, ext := range defaults.EnvoyExtensions {
|
|
nsExtensions[i] = structs.EnvoyExtension{
|
|
Name: ext.Name,
|
|
Required: ext.Required,
|
|
Arguments: ext.Arguments,
|
|
}
|
|
}
|
|
ns.Proxy.EnvoyExtensions = nsExtensions
|
|
}
|
|
|
|
if ns.Proxy.MeshGateway.Mode == structs.MeshGatewayModeDefault {
|
|
ns.Proxy.MeshGateway.Mode = defaults.MeshGateway.Mode
|
|
}
|
|
if ns.Proxy.Mode == structs.ProxyModeDefault {
|
|
ns.Proxy.Mode = defaults.Mode
|
|
}
|
|
if ns.Proxy.TransparentProxy.OutboundListenerPort == 0 {
|
|
ns.Proxy.TransparentProxy.OutboundListenerPort = defaults.TransparentProxy.OutboundListenerPort
|
|
}
|
|
if !ns.Proxy.TransparentProxy.DialedDirectly {
|
|
ns.Proxy.TransparentProxy.DialedDirectly = defaults.TransparentProxy.DialedDirectly
|
|
}
|
|
|
|
if ns.Proxy.MutualTLSMode == structs.MutualTLSModeDefault {
|
|
ns.Proxy.MutualTLSMode = defaults.MutualTLSMode
|
|
}
|
|
|
|
// remoteUpstreams contains synthetic Upstreams generated from central config (service-defaults.UpstreamConfigs).
|
|
remoteUpstreams := make(map[structs.PeeredServiceName]structs.Upstream)
|
|
|
|
// If the arguments did not fully normalize tenancy stuff, take care of that now.
|
|
entMeta := ns.EnterpriseMeta
|
|
entMeta.Normalize()
|
|
|
|
for _, us := range defaults.UpstreamConfigs {
|
|
parsed, err := structs.ParseUpstreamConfigNoDefaults(us.Config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse upstream config map for %s: %v", us.Upstream.String(), err)
|
|
}
|
|
|
|
// If the defaults did not fully normalize tenancy stuff, take care of
|
|
// that now too.
|
|
psn := us.Upstream // only normalize the copy
|
|
psn.ServiceName.EnterpriseMeta.Normalize()
|
|
|
|
// Normalize the partition field specially.
|
|
if psn.Peer != "" {
|
|
psn.ServiceName.OverridePartition(entMeta.PartitionOrDefault())
|
|
}
|
|
|
|
remoteUpstreams[psn] = structs.Upstream{
|
|
DestinationNamespace: psn.ServiceName.NamespaceOrDefault(),
|
|
DestinationPartition: psn.ServiceName.PartitionOrDefault(),
|
|
DestinationName: psn.ServiceName.Name,
|
|
DestinationPeer: psn.Peer,
|
|
Config: us.Config,
|
|
MeshGateway: parsed.MeshGateway,
|
|
CentrallyConfigured: true,
|
|
}
|
|
}
|
|
|
|
// localUpstreams stores the upstreams seen from the local registration so that we can merge in the synthetic entries.
|
|
// In transparent proxy mode ns.Proxy.Upstreams will likely be empty because users do not need to define upstreams explicitly.
|
|
// So to store upstream-specific flags from central config, we add entries to ns.Proxy.Upstreams with those values.
|
|
localUpstreams := make(map[structs.PeeredServiceName]struct{})
|
|
|
|
// Merge upstream defaults into the local registration
|
|
for i := range ns.Proxy.Upstreams {
|
|
// Get a pointer not a value copy of the upstream struct
|
|
us := &ns.Proxy.Upstreams[i]
|
|
if us.DestinationType != "" && us.DestinationType != structs.UpstreamDestTypeService {
|
|
continue
|
|
}
|
|
|
|
uid := us.DestinationID()
|
|
|
|
// Normalize the partition field specially.
|
|
if uid.Peer != "" {
|
|
uid.ServiceName.OverridePartition(entMeta.PartitionOrDefault())
|
|
}
|
|
|
|
localUpstreams[uid] = struct{}{}
|
|
remoteCfg, ok := remoteUpstreams[uid]
|
|
if !ok {
|
|
// No config defaults to merge
|
|
continue
|
|
}
|
|
|
|
// The local upstream config mode has the highest precedence, so only overwrite when it's set to the default
|
|
if us.MeshGateway.Mode == structs.MeshGatewayModeDefault {
|
|
us.MeshGateway.Mode = remoteCfg.MeshGateway.Mode
|
|
}
|
|
|
|
preMergeProtocol, found := us.Config["protocol"]
|
|
// Merge in everything else that is read from the map
|
|
if err := mergo.Merge(&us.Config, remoteCfg.Config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Reset the protocol to its pre-merged version for peering upstreams.
|
|
if us.DestinationPeer != "" {
|
|
if found {
|
|
us.Config["protocol"] = preMergeProtocol
|
|
} else {
|
|
delete(us.Config, "protocol")
|
|
}
|
|
}
|
|
|
|
// Delete the mesh gateway key from opaque config since this is the value that was resolved from
|
|
// the servers and NOT the final merged value for this upstream.
|
|
// Note that we use the "mesh_gateway" key and not other variants like "MeshGateway" because
|
|
// UpstreamConfig.MergeInto and ResolveServiceConfig only use "mesh_gateway".
|
|
delete(us.Config, "mesh_gateway")
|
|
}
|
|
|
|
// Ensure upstreams present in central config are represented in the local configuration.
|
|
// This does not apply outside of transparent mode because in that situation every possible upstream already exists
|
|
// inside of ns.Proxy.Upstreams.
|
|
if ns.Proxy.Mode == structs.ProxyModeTransparent {
|
|
for id, remote := range remoteUpstreams {
|
|
if _, ok := localUpstreams[id]; ok {
|
|
// Remote upstream is already present locally
|
|
continue
|
|
}
|
|
|
|
ns.Proxy.Upstreams = append(ns.Proxy.Upstreams, remote)
|
|
}
|
|
}
|
|
|
|
return ns, err
|
|
}
|