github.com/hernad/nomad@v1.6.112/command/agent/consul/connect.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package consul
     5  
     6  import (
     7  	"fmt"
     8  	"net"
     9  	"sort"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/hashicorp/consul/api"
    14  	"github.com/hernad/nomad/nomad/structs"
    15  	"golang.org/x/exp/maps"
    16  	"golang.org/x/exp/slices"
    17  )
    18  
    19  // newConnect creates a new Consul AgentServiceConnect struct based on a Nomad
    20  // Connect struct. If the nomad Connect struct is nil, nil will be returned to
    21  // disable Connect for this service.
    22  func newConnect(serviceID string, info structs.AllocInfo, serviceName string, nc *structs.ConsulConnect, networks structs.Networks, ports structs.AllocatedPorts) (*api.AgentServiceConnect, error) {
    23  	switch {
    24  	case nc == nil:
    25  		// no connect block means there is no connect service to register
    26  		return nil, nil
    27  
    28  	case nc.IsGateway():
    29  		// gateway settings are configured on the service block on the consul side
    30  		return nil, nil
    31  
    32  	case nc.IsNative():
    33  		// the service is connect native
    34  		return &api.AgentServiceConnect{Native: true}, nil
    35  
    36  	case nc.HasSidecar():
    37  		// must register the sidecar for this service
    38  		if nc.SidecarService.Port == "" {
    39  			nc.SidecarService.Port = fmt.Sprintf("%s-%s", structs.ConnectProxyPrefix, serviceName)
    40  		}
    41  		sidecarReg, err := connectSidecarRegistration(serviceID, info, nc.SidecarService, networks, ports)
    42  		if err != nil {
    43  			return nil, err
    44  		}
    45  		return &api.AgentServiceConnect{SidecarService: sidecarReg}, nil
    46  
    47  	default:
    48  		// a non-nil but empty connect block makes no sense
    49  		return nil, fmt.Errorf("Connect configuration empty for service %s", serviceName)
    50  	}
    51  }
    52  
    53  // newConnectGateway creates a new Consul AgentServiceConnectProxyConfig struct based on
    54  // a Nomad Connect struct. If the Nomad Connect struct does not contain a gateway, nil
    55  // will be returned as this service is not a gateway.
    56  func newConnectGateway(connect *structs.ConsulConnect) *api.AgentServiceConnectProxyConfig {
    57  	if !connect.IsGateway() {
    58  		return nil
    59  	}
    60  
    61  	var envoyConfig map[string]interface{}
    62  
    63  	// Populate the envoy configuration from the gateway.proxy block, if
    64  	// such configuration is provided.
    65  	if proxy := connect.Gateway.Proxy; proxy != nil {
    66  		envoyConfig = make(map[string]interface{})
    67  
    68  		if len(proxy.EnvoyGatewayBindAddresses) > 0 {
    69  			envoyConfig["envoy_gateway_bind_addresses"] = proxy.EnvoyGatewayBindAddresses
    70  		}
    71  
    72  		if proxy.EnvoyGatewayNoDefaultBind {
    73  			envoyConfig["envoy_gateway_no_default_bind"] = true
    74  		}
    75  
    76  		if proxy.EnvoyGatewayBindTaggedAddresses {
    77  			envoyConfig["envoy_gateway_bind_tagged_addresses"] = true
    78  		}
    79  
    80  		if proxy.EnvoyDNSDiscoveryType != "" {
    81  			envoyConfig["envoy_dns_discovery_type"] = proxy.EnvoyDNSDiscoveryType
    82  		}
    83  
    84  		if proxy.ConnectTimeout != nil {
    85  			envoyConfig["connect_timeout_ms"] = proxy.ConnectTimeout.Milliseconds()
    86  		}
    87  
    88  		if len(proxy.Config) > 0 {
    89  			for k, v := range proxy.Config {
    90  				envoyConfig[k] = v
    91  			}
    92  		}
    93  	}
    94  
    95  	return &api.AgentServiceConnectProxyConfig{Config: envoyConfig}
    96  }
    97  
    98  func connectSidecarRegistration(serviceID string, info structs.AllocInfo, css *structs.ConsulSidecarService, networks structs.Networks, ports structs.AllocatedPorts) (*api.AgentServiceRegistration, error) {
    99  	if css == nil {
   100  		// no sidecar block means there is no sidecar service to register
   101  		return nil, nil
   102  	}
   103  
   104  	cMapping, err := connectPort(css.Port, networks, ports)
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  
   109  	proxy, err := connectSidecarProxy(info, css.Proxy, cMapping.To, networks)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	// if the service has a TCP check that's failing, we need an alias to
   115  	// ensure service discovery excludes this sidecar from queries
   116  	// (ex. in the case of Connect upstreams)
   117  	checks := api.AgentServiceChecks{{
   118  		Name:         "Connect Sidecar Aliasing " + serviceID,
   119  		AliasService: serviceID,
   120  	}}
   121  	if !css.DisableDefaultTCPCheck {
   122  		checks = append(checks, &api.AgentServiceCheck{
   123  			Name:     "Connect Sidecar Listening",
   124  			TCP:      net.JoinHostPort(cMapping.HostIP, strconv.Itoa(cMapping.Value)),
   125  			Interval: "10s",
   126  		})
   127  	}
   128  
   129  	return &api.AgentServiceRegistration{
   130  		Tags:    slices.Clone(css.Tags),
   131  		Port:    cMapping.Value,
   132  		Address: cMapping.HostIP,
   133  		Proxy:   proxy,
   134  		Checks:  checks,
   135  		Meta:    maps.Clone(css.Meta),
   136  	}, nil
   137  }
   138  
   139  func connectSidecarProxy(info structs.AllocInfo, proxy *structs.ConsulProxy, cPort int, networks structs.Networks) (*api.AgentServiceConnectProxyConfig, error) {
   140  	if proxy == nil {
   141  		proxy = new(structs.ConsulProxy)
   142  	}
   143  
   144  	expose, err := connectProxyExpose(proxy.Expose, networks)
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  
   149  	return &api.AgentServiceConnectProxyConfig{
   150  		LocalServiceAddress: proxy.LocalServiceAddress,
   151  		LocalServicePort:    proxy.LocalServicePort,
   152  		Config:              connectProxyConfig(proxy.Config, cPort, info),
   153  		Upstreams:           connectUpstreams(proxy.Upstreams),
   154  		Expose:              expose,
   155  	}, nil
   156  }
   157  
   158  func connectProxyExpose(expose *structs.ConsulExposeConfig, networks structs.Networks) (api.ExposeConfig, error) {
   159  	if expose == nil {
   160  		return api.ExposeConfig{}, nil
   161  	}
   162  
   163  	paths, err := connectProxyExposePaths(expose.Paths, networks)
   164  	if err != nil {
   165  		return api.ExposeConfig{}, err
   166  	}
   167  
   168  	return api.ExposeConfig{
   169  		Checks: false,
   170  		Paths:  paths,
   171  	}, nil
   172  }
   173  
   174  func connectProxyExposePaths(in []structs.ConsulExposePath, networks structs.Networks) ([]api.ExposePath, error) {
   175  	if len(in) == 0 {
   176  		return nil, nil
   177  	}
   178  
   179  	paths := make([]api.ExposePath, len(in))
   180  	for i, path := range in {
   181  		if _, exposedPort, err := connectExposePathPort(path.ListenerPort, networks); err != nil {
   182  			return nil, err
   183  		} else {
   184  			paths[i] = api.ExposePath{
   185  				ListenerPort:    exposedPort,
   186  				Path:            path.Path,
   187  				LocalPathPort:   path.LocalPathPort,
   188  				Protocol:        path.Protocol,
   189  				ParsedFromCheck: false,
   190  			}
   191  		}
   192  	}
   193  	return paths, nil
   194  }
   195  
   196  func connectUpstreams(in []structs.ConsulUpstream) []api.Upstream {
   197  	if len(in) == 0 {
   198  		return nil
   199  	}
   200  
   201  	upstreams := make([]api.Upstream, len(in))
   202  	for i, upstream := range in {
   203  		upstreams[i] = api.Upstream{
   204  			DestinationName:      upstream.DestinationName,
   205  			DestinationNamespace: upstream.DestinationNamespace,
   206  			LocalBindPort:        upstream.LocalBindPort,
   207  			Datacenter:           upstream.Datacenter,
   208  			LocalBindAddress:     upstream.LocalBindAddress,
   209  			MeshGateway:          connectMeshGateway(upstream.MeshGateway),
   210  			Config:               maps.Clone(upstream.Config),
   211  		}
   212  	}
   213  	return upstreams
   214  }
   215  
   216  // connectMeshGateway creates an api.MeshGatewayConfig from the nomad upstream
   217  // block. A non-existent config or unsupported gateway mode will default to the
   218  // Consul default mode.
   219  func connectMeshGateway(in structs.ConsulMeshGateway) api.MeshGatewayConfig {
   220  	gw := api.MeshGatewayConfig{
   221  		Mode: api.MeshGatewayModeDefault,
   222  	}
   223  
   224  	switch in.Mode {
   225  	case "local":
   226  		gw.Mode = api.MeshGatewayModeLocal
   227  	case "remote":
   228  		gw.Mode = api.MeshGatewayModeRemote
   229  	case "none":
   230  		gw.Mode = api.MeshGatewayModeNone
   231  	}
   232  
   233  	return gw
   234  }
   235  
   236  func connectProxyConfig(cfg map[string]interface{}, port int, info structs.AllocInfo) map[string]interface{} {
   237  	if cfg == nil {
   238  		cfg = make(map[string]interface{})
   239  	}
   240  	cfg["bind_address"] = "0.0.0.0"
   241  	cfg["bind_port"] = port
   242  
   243  	tags := map[string]string{
   244  		"nomad.group=":     info.Group,
   245  		"nomad.job=":       info.JobID,
   246  		"nomad.namespace=": info.Namespace,
   247  		"nomad.alloc_id=":  info.AllocID,
   248  	}
   249  	injectNomadInfo(cfg, tags)
   250  	return cfg
   251  }
   252  
   253  // injectNomadInfo merges nomad information into cfg=>envoy_stats_tags
   254  //
   255  // cfg must not be nil
   256  func injectNomadInfo(cfg map[string]interface{}, defaultTags map[string]string) {
   257  	const configKey = "envoy_stats_tags"
   258  
   259  	existingTagsI := cfg[configKey]
   260  	switch existingTags := existingTagsI.(type) {
   261  	case []string:
   262  		if len(existingTags) == 0 {
   263  			break
   264  		}
   265  	OUTER:
   266  		for key, value := range defaultTags {
   267  			for _, tag := range existingTags {
   268  				if strings.HasPrefix(tag, key) {
   269  					continue OUTER
   270  				}
   271  			}
   272  			existingTags = append(existingTags, key+value)
   273  		}
   274  		cfg[configKey] = existingTags
   275  		return
   276  	}
   277  
   278  	// common case.
   279  	var tags []string
   280  	for key, value := range defaultTags {
   281  		if value == "" {
   282  			continue
   283  		}
   284  		tag := key + value
   285  		tags = append(tags, tag)
   286  	}
   287  	sort.Strings(tags) // mostly for test stability
   288  	cfg[configKey] = tags
   289  }
   290  
   291  func connectNetworkInvariants(networks structs.Networks) error {
   292  	if n := len(networks); n != 1 {
   293  		return fmt.Errorf("Connect only supported with exactly 1 network (found %d)", n)
   294  	}
   295  	return nil
   296  }
   297  
   298  // connectPort returns the network and port for the Connect proxy sidecar
   299  // defined for this service. An error is returned if the network and port
   300  // cannot be determined.
   301  func connectPort(portLabel string, networks structs.Networks, ports structs.AllocatedPorts) (structs.AllocatedPortMapping, error) {
   302  	if err := connectNetworkInvariants(networks); err != nil {
   303  		return structs.AllocatedPortMapping{}, err
   304  	}
   305  	mapping, ok := ports.Get(portLabel)
   306  	if !ok {
   307  		mapping = networks.Port(portLabel)
   308  		if mapping.Value > 0 {
   309  			return mapping, nil
   310  		}
   311  		return structs.AllocatedPortMapping{}, fmt.Errorf("No port of label %q defined", portLabel)
   312  	}
   313  	return mapping, nil
   314  }
   315  
   316  // connectExposePathPort returns the port for the exposed path for the exposed
   317  // proxy path.
   318  func connectExposePathPort(portLabel string, networks structs.Networks) (string, int, error) {
   319  	if err := connectNetworkInvariants(networks); err != nil {
   320  		return "", 0, err
   321  	}
   322  
   323  	mapping := networks.Port(portLabel)
   324  	if mapping.Value == 0 {
   325  		return "", 0, fmt.Errorf("No port of label %q defined", portLabel)
   326  	}
   327  
   328  	return mapping.HostIP, mapping.Value, nil
   329  }