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 }