324 lines
9.1 KiB
Go
324 lines
9.1 KiB
Go
package consul
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/consul/api"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
// newConnect creates a new Consul AgentServiceConnect struct based on a Nomad
|
|
// Connect struct. If the nomad Connect struct is nil, nil will be returned to
|
|
// disable Connect for this service.
|
|
func newConnect(serviceID string, info structs.AllocInfo, serviceName string, nc *structs.ConsulConnect, networks structs.Networks, ports structs.AllocatedPorts) (*api.AgentServiceConnect, error) {
|
|
switch {
|
|
case nc == nil:
|
|
// no connect stanza means there is no connect service to register
|
|
return nil, nil
|
|
|
|
case nc.IsGateway():
|
|
// gateway settings are configured on the service block on the consul side
|
|
return nil, nil
|
|
|
|
case nc.IsNative():
|
|
// the service is connect native
|
|
return &api.AgentServiceConnect{Native: true}, nil
|
|
|
|
case nc.HasSidecar():
|
|
// must register the sidecar for this service
|
|
if nc.SidecarService.Port == "" {
|
|
nc.SidecarService.Port = fmt.Sprintf("%s-%s", structs.ConnectProxyPrefix, serviceName)
|
|
}
|
|
sidecarReg, err := connectSidecarRegistration(serviceID, info, nc.SidecarService, networks, ports)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &api.AgentServiceConnect{SidecarService: sidecarReg}, nil
|
|
|
|
default:
|
|
// a non-nil but empty connect block makes no sense
|
|
return nil, fmt.Errorf("Connect configuration empty for service %s", serviceName)
|
|
}
|
|
}
|
|
|
|
// newConnectGateway creates a new Consul AgentServiceConnectProxyConfig struct based on
|
|
// a Nomad Connect struct. If the Nomad Connect struct does not contain a gateway, nil
|
|
// will be returned as this service is not a gateway.
|
|
func newConnectGateway(connect *structs.ConsulConnect) *api.AgentServiceConnectProxyConfig {
|
|
if !connect.IsGateway() {
|
|
return nil
|
|
}
|
|
|
|
var envoyConfig map[string]interface{}
|
|
|
|
// Populate the envoy configuration from the gateway.proxy stanza, if
|
|
// such configuration is provided.
|
|
if proxy := connect.Gateway.Proxy; proxy != nil {
|
|
envoyConfig = make(map[string]interface{})
|
|
|
|
if len(proxy.EnvoyGatewayBindAddresses) > 0 {
|
|
envoyConfig["envoy_gateway_bind_addresses"] = proxy.EnvoyGatewayBindAddresses
|
|
}
|
|
|
|
if proxy.EnvoyGatewayNoDefaultBind {
|
|
envoyConfig["envoy_gateway_no_default_bind"] = true
|
|
}
|
|
|
|
if proxy.EnvoyGatewayBindTaggedAddresses {
|
|
envoyConfig["envoy_gateway_bind_tagged_addresses"] = true
|
|
}
|
|
|
|
if proxy.EnvoyDNSDiscoveryType != "" {
|
|
envoyConfig["envoy_dns_discovery_type"] = proxy.EnvoyDNSDiscoveryType
|
|
}
|
|
|
|
if proxy.ConnectTimeout != nil {
|
|
envoyConfig["connect_timeout_ms"] = proxy.ConnectTimeout.Milliseconds()
|
|
}
|
|
|
|
if len(proxy.Config) > 0 {
|
|
for k, v := range proxy.Config {
|
|
envoyConfig[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
return &api.AgentServiceConnectProxyConfig{Config: envoyConfig}
|
|
}
|
|
|
|
func connectSidecarRegistration(serviceID string, info structs.AllocInfo, css *structs.ConsulSidecarService, networks structs.Networks, ports structs.AllocatedPorts) (*api.AgentServiceRegistration, error) {
|
|
if css == nil {
|
|
// no sidecar stanza means there is no sidecar service to register
|
|
return nil, nil
|
|
}
|
|
|
|
cMapping, err := connectPort(css.Port, networks, ports)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
proxy, err := connectSidecarProxy(info, css.Proxy, cMapping.To, networks)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// if the service has a TCP check that's failing, we need an alias to
|
|
// ensure service discovery excludes this sidecar from queries
|
|
// (ex. in the case of Connect upstreams)
|
|
checks := api.AgentServiceChecks{{
|
|
Name: "Connect Sidecar Aliasing " + serviceID,
|
|
AliasService: serviceID,
|
|
}}
|
|
if !css.DisableDefaultTCPCheck {
|
|
checks = append(checks, &api.AgentServiceCheck{
|
|
Name: "Connect Sidecar Listening",
|
|
TCP: net.JoinHostPort(cMapping.HostIP, strconv.Itoa(cMapping.Value)),
|
|
Interval: "10s",
|
|
})
|
|
}
|
|
|
|
return &api.AgentServiceRegistration{
|
|
Tags: slices.Clone(css.Tags),
|
|
Port: cMapping.Value,
|
|
Address: cMapping.HostIP,
|
|
Proxy: proxy,
|
|
Checks: checks,
|
|
}, nil
|
|
}
|
|
|
|
func connectSidecarProxy(info structs.AllocInfo, proxy *structs.ConsulProxy, cPort int, networks structs.Networks) (*api.AgentServiceConnectProxyConfig, error) {
|
|
if proxy == nil {
|
|
proxy = new(structs.ConsulProxy)
|
|
}
|
|
|
|
expose, err := connectProxyExpose(proxy.Expose, networks)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &api.AgentServiceConnectProxyConfig{
|
|
LocalServiceAddress: proxy.LocalServiceAddress,
|
|
LocalServicePort: proxy.LocalServicePort,
|
|
Config: connectProxyConfig(proxy.Config, cPort, info),
|
|
Upstreams: connectUpstreams(proxy.Upstreams),
|
|
Expose: expose,
|
|
}, nil
|
|
}
|
|
|
|
func connectProxyExpose(expose *structs.ConsulExposeConfig, networks structs.Networks) (api.ExposeConfig, error) {
|
|
if expose == nil {
|
|
return api.ExposeConfig{}, nil
|
|
}
|
|
|
|
paths, err := connectProxyExposePaths(expose.Paths, networks)
|
|
if err != nil {
|
|
return api.ExposeConfig{}, err
|
|
}
|
|
|
|
return api.ExposeConfig{
|
|
Checks: false,
|
|
Paths: paths,
|
|
}, nil
|
|
}
|
|
|
|
func connectProxyExposePaths(in []structs.ConsulExposePath, networks structs.Networks) ([]api.ExposePath, error) {
|
|
if len(in) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
paths := make([]api.ExposePath, len(in))
|
|
for i, path := range in {
|
|
if _, exposedPort, err := connectExposePathPort(path.ListenerPort, networks); err != nil {
|
|
return nil, err
|
|
} else {
|
|
paths[i] = api.ExposePath{
|
|
ListenerPort: exposedPort,
|
|
Path: path.Path,
|
|
LocalPathPort: path.LocalPathPort,
|
|
Protocol: path.Protocol,
|
|
ParsedFromCheck: false,
|
|
}
|
|
}
|
|
}
|
|
return paths, nil
|
|
}
|
|
|
|
func connectUpstreams(in []structs.ConsulUpstream) []api.Upstream {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
|
|
upstreams := make([]api.Upstream, len(in))
|
|
for i, upstream := range in {
|
|
upstreams[i] = api.Upstream{
|
|
DestinationName: upstream.DestinationName,
|
|
DestinationNamespace: upstream.DestinationNamespace,
|
|
LocalBindPort: upstream.LocalBindPort,
|
|
Datacenter: upstream.Datacenter,
|
|
LocalBindAddress: upstream.LocalBindAddress,
|
|
MeshGateway: connectMeshGateway(upstream.MeshGateway),
|
|
}
|
|
}
|
|
return upstreams
|
|
}
|
|
|
|
// connectMeshGateway creates an api.MeshGatewayConfig from the nomad upstream
|
|
// block. A non-existent config or unsupported gateway mode will default to the
|
|
// Consul default mode.
|
|
func connectMeshGateway(in structs.ConsulMeshGateway) api.MeshGatewayConfig {
|
|
gw := api.MeshGatewayConfig{
|
|
Mode: api.MeshGatewayModeDefault,
|
|
}
|
|
|
|
switch in.Mode {
|
|
case "local":
|
|
gw.Mode = api.MeshGatewayModeLocal
|
|
case "remote":
|
|
gw.Mode = api.MeshGatewayModeRemote
|
|
case "none":
|
|
gw.Mode = api.MeshGatewayModeNone
|
|
}
|
|
|
|
return gw
|
|
}
|
|
|
|
func connectProxyConfig(cfg map[string]interface{}, port int, info structs.AllocInfo) map[string]interface{} {
|
|
if cfg == nil {
|
|
cfg = make(map[string]interface{})
|
|
}
|
|
cfg["bind_address"] = "0.0.0.0"
|
|
cfg["bind_port"] = port
|
|
|
|
tags := map[string]string{
|
|
"nomad.group=": info.Group,
|
|
"nomad.job=": info.JobID,
|
|
"nomad.namespace=": info.Namespace,
|
|
"nomad.alloc_id=": info.AllocID,
|
|
}
|
|
injectNomadInfo(cfg, tags)
|
|
return cfg
|
|
}
|
|
|
|
// injectNomadInfo merges nomad information into cfg=>envoy_stats_tags
|
|
//
|
|
// cfg must not be nil
|
|
func injectNomadInfo(cfg map[string]interface{}, defaultTags map[string]string) {
|
|
const configKey = "envoy_stats_tags"
|
|
|
|
existingTagsI := cfg[configKey]
|
|
switch existingTags := existingTagsI.(type) {
|
|
case []string:
|
|
if len(existingTags) == 0 {
|
|
break
|
|
}
|
|
OUTER:
|
|
for key, value := range defaultTags {
|
|
for _, tag := range existingTags {
|
|
if strings.HasPrefix(tag, key) {
|
|
continue OUTER
|
|
}
|
|
}
|
|
existingTags = append(existingTags, key+value)
|
|
}
|
|
cfg[configKey] = existingTags
|
|
return
|
|
}
|
|
|
|
// common case.
|
|
var tags []string
|
|
for key, value := range defaultTags {
|
|
if value == "" {
|
|
continue
|
|
}
|
|
tag := key + value
|
|
tags = append(tags, tag)
|
|
}
|
|
sort.Strings(tags) // mostly for test stability
|
|
cfg[configKey] = tags
|
|
}
|
|
|
|
func connectNetworkInvariants(networks structs.Networks) error {
|
|
if n := len(networks); n != 1 {
|
|
return fmt.Errorf("Connect only supported with exactly 1 network (found %d)", n)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// connectPort returns the network and port for the Connect proxy sidecar
|
|
// defined for this service. An error is returned if the network and port
|
|
// cannot be determined.
|
|
func connectPort(portLabel string, networks structs.Networks, ports structs.AllocatedPorts) (structs.AllocatedPortMapping, error) {
|
|
if err := connectNetworkInvariants(networks); err != nil {
|
|
return structs.AllocatedPortMapping{}, err
|
|
}
|
|
mapping, ok := ports.Get(portLabel)
|
|
if !ok {
|
|
mapping = networks.Port(portLabel)
|
|
if mapping.Value > 0 {
|
|
return mapping, nil
|
|
}
|
|
return structs.AllocatedPortMapping{}, fmt.Errorf("No port of label %q defined", portLabel)
|
|
}
|
|
return mapping, nil
|
|
}
|
|
|
|
// connectExposePathPort returns the port for the exposed path for the exposed
|
|
// proxy path.
|
|
func connectExposePathPort(portLabel string, networks structs.Networks) (string, int, error) {
|
|
if err := connectNetworkInvariants(networks); err != nil {
|
|
return "", 0, err
|
|
}
|
|
|
|
mapping := networks.Port(portLabel)
|
|
if mapping.Value == 0 {
|
|
return "", 0, fmt.Errorf("No port of label %q defined", portLabel)
|
|
}
|
|
|
|
return mapping.HostIP, mapping.Value, nil
|
|
}
|