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 }