github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/command/agent/consul/connect.go (about)

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