github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/client/allocrunner/taskrunner/envoy_bootstrap_hook_test.go (about) 1 //go:build !windows 2 // +build !windows 3 4 // todo(shoenig): Once Connect is supported on Windows, we'll need to make this 5 // set of tests work there too. 6 7 package taskrunner 8 9 import ( 10 "context" 11 "encoding/json" 12 "fmt" 13 "io/ioutil" 14 "os" 15 "path/filepath" 16 "testing" 17 "time" 18 19 consulapi "github.com/hashicorp/consul/api" 20 "github.com/hashicorp/nomad/ci" 21 "github.com/hashicorp/nomad/client/allocdir" 22 "github.com/hashicorp/nomad/client/allocrunner/interfaces" 23 "github.com/hashicorp/nomad/client/taskenv" 24 "github.com/hashicorp/nomad/client/testutil" 25 agentconsul "github.com/hashicorp/nomad/command/agent/consul" 26 "github.com/hashicorp/nomad/helper/args" 27 "github.com/hashicorp/nomad/helper/pointer" 28 "github.com/hashicorp/nomad/helper/testlog" 29 "github.com/hashicorp/nomad/helper/uuid" 30 "github.com/hashicorp/nomad/nomad/mock" 31 "github.com/hashicorp/nomad/nomad/structs" 32 "github.com/hashicorp/nomad/nomad/structs/config" 33 "github.com/stretchr/testify/require" 34 "golang.org/x/sys/unix" 35 ) 36 37 var _ interfaces.TaskPrestartHook = (*envoyBootstrapHook)(nil) 38 39 const ( 40 // consulNamespace is empty string in OSS, because Consul OSS does not like 41 // having even the default namespace set. 42 consulNamespace = "" 43 ) 44 45 func writeTmp(t *testing.T, s string, fm os.FileMode) string { 46 dir := t.TempDir() 47 48 fPath := filepath.Join(dir, sidsTokenFile) 49 err := ioutil.WriteFile(fPath, []byte(s), fm) 50 require.NoError(t, err) 51 52 return dir 53 } 54 55 func TestEnvoyBootstrapHook_maybeLoadSIToken(t *testing.T) { 56 ci.Parallel(t) 57 58 // This test fails when running as root because the test case for checking 59 // the error condition when the file is unreadable fails (root can read the 60 // file even though the permissions are set to 0200). 61 if unix.Geteuid() == 0 { 62 t.Skip("test only works as non-root") 63 } 64 65 t.Run("file does not exist", func(t *testing.T) { 66 h := newEnvoyBootstrapHook(&envoyBootstrapHookConfig{logger: testlog.HCLogger(t)}) 67 cfg, err := h.maybeLoadSIToken("task1", "/does/not/exist") 68 require.NoError(t, err) // absence of token is not an error 69 require.Equal(t, "", cfg) 70 }) 71 72 t.Run("load token from file", func(t *testing.T) { 73 token := uuid.Generate() 74 f := writeTmp(t, token, 0440) 75 76 h := newEnvoyBootstrapHook(&envoyBootstrapHookConfig{logger: testlog.HCLogger(t)}) 77 cfg, err := h.maybeLoadSIToken("task1", f) 78 require.NoError(t, err) 79 require.Equal(t, token, cfg) 80 }) 81 82 t.Run("file is unreadable", func(t *testing.T) { 83 token := uuid.Generate() 84 f := writeTmp(t, token, 0200) 85 86 h := newEnvoyBootstrapHook(&envoyBootstrapHookConfig{logger: testlog.HCLogger(t)}) 87 cfg, err := h.maybeLoadSIToken("task1", f) 88 require.Error(t, err) 89 require.False(t, os.IsNotExist(err)) 90 require.Equal(t, "", cfg) 91 }) 92 } 93 94 func TestEnvoyBootstrapHook_decodeTriState(t *testing.T) { 95 ci.Parallel(t) 96 97 require.Equal(t, "", decodeTriState(nil)) 98 require.Equal(t, "true", decodeTriState(pointer.Of(true))) 99 require.Equal(t, "false", decodeTriState(pointer.Of(false))) 100 } 101 102 var ( 103 consulPlainConfig = consulTransportConfig{ 104 HTTPAddr: "2.2.2.2", 105 } 106 107 consulTLSConfig = consulTransportConfig{ 108 HTTPAddr: "2.2.2.2", // arg 109 Auth: "user:password", // env 110 SSL: "true", // env 111 VerifySSL: "true", // env 112 CAFile: "/etc/tls/ca-file", // arg 113 CertFile: "/etc/tls/cert-file", // arg 114 KeyFile: "/etc/tls/key-file", // arg 115 } 116 ) 117 118 func TestEnvoyBootstrapHook_envoyBootstrapArgs(t *testing.T) { 119 ci.Parallel(t) 120 121 t.Run("excluding SI token", func(t *testing.T) { 122 ebArgs := envoyBootstrapArgs{ 123 proxyID: "s1-sidecar-proxy", 124 grpcAddr: "1.1.1.1", 125 consulConfig: consulPlainConfig, 126 envoyAdminBind: "127.0.0.2:19000", 127 envoyReadyBind: "127.0.0.1:19100", 128 } 129 result := ebArgs.args() 130 require.Equal(t, []string{"connect", "envoy", 131 "-grpc-addr", "1.1.1.1", 132 "-http-addr", "2.2.2.2", 133 "-admin-bind", "127.0.0.2:19000", 134 "-address", "127.0.0.1:19100", 135 "-proxy-id", "s1-sidecar-proxy", 136 "-bootstrap", 137 }, result) 138 }) 139 140 t.Run("including SI token", func(t *testing.T) { 141 token := uuid.Generate() 142 ebArgs := envoyBootstrapArgs{ 143 proxyID: "s1-sidecar-proxy", 144 grpcAddr: "1.1.1.1", 145 consulConfig: consulPlainConfig, 146 envoyAdminBind: "127.0.0.2:19000", 147 envoyReadyBind: "127.0.0.1:19100", 148 siToken: token, 149 } 150 result := ebArgs.args() 151 require.Equal(t, []string{"connect", "envoy", 152 "-grpc-addr", "1.1.1.1", 153 "-http-addr", "2.2.2.2", 154 "-admin-bind", "127.0.0.2:19000", 155 "-address", "127.0.0.1:19100", 156 "-proxy-id", "s1-sidecar-proxy", 157 "-bootstrap", 158 "-token", token, 159 }, result) 160 }) 161 162 t.Run("including certificates", func(t *testing.T) { 163 ebArgs := envoyBootstrapArgs{ 164 proxyID: "s1-sidecar-proxy", 165 grpcAddr: "1.1.1.1", 166 consulConfig: consulTLSConfig, 167 envoyAdminBind: "127.0.0.2:19000", 168 envoyReadyBind: "127.0.0.1:19100", 169 } 170 result := ebArgs.args() 171 require.Equal(t, []string{"connect", "envoy", 172 "-grpc-addr", "1.1.1.1", 173 "-http-addr", "2.2.2.2", 174 "-admin-bind", "127.0.0.2:19000", 175 "-address", "127.0.0.1:19100", 176 "-proxy-id", "s1-sidecar-proxy", 177 "-bootstrap", 178 "-ca-file", "/etc/tls/ca-file", 179 "-client-cert", "/etc/tls/cert-file", 180 "-client-key", "/etc/tls/key-file", 181 }, result) 182 }) 183 184 t.Run("ingress gateway", func(t *testing.T) { 185 ebArgs := envoyBootstrapArgs{ 186 consulConfig: consulPlainConfig, 187 grpcAddr: "1.1.1.1", 188 envoyAdminBind: "127.0.0.2:19000", 189 envoyReadyBind: "127.0.0.1:19100", 190 gateway: "my-ingress-gateway", 191 proxyID: "_nomad-task-803cb569-881c-b0d8-9222-360bcc33157e-group-ig-ig-8080", 192 } 193 result := ebArgs.args() 194 require.Equal(t, []string{"connect", "envoy", 195 "-grpc-addr", "1.1.1.1", 196 "-http-addr", "2.2.2.2", 197 "-admin-bind", "127.0.0.2:19000", 198 "-address", "127.0.0.1:19100", 199 "-proxy-id", "_nomad-task-803cb569-881c-b0d8-9222-360bcc33157e-group-ig-ig-8080", 200 "-bootstrap", 201 "-gateway", "my-ingress-gateway", 202 }, result) 203 }) 204 205 t.Run("mesh gateway", func(t *testing.T) { 206 ebArgs := envoyBootstrapArgs{ 207 consulConfig: consulPlainConfig, 208 grpcAddr: "1.1.1.1", 209 envoyAdminBind: "127.0.0.2:19000", 210 envoyReadyBind: "127.0.0.1:19100", 211 gateway: "my-mesh-gateway", 212 proxyID: "_nomad-task-803cb569-881c-b0d8-9222-360bcc33157e-group-mesh-mesh-8080", 213 } 214 result := ebArgs.args() 215 require.Equal(t, []string{"connect", "envoy", 216 "-grpc-addr", "1.1.1.1", 217 "-http-addr", "2.2.2.2", 218 "-admin-bind", "127.0.0.2:19000", 219 "-address", "127.0.0.1:19100", 220 "-proxy-id", "_nomad-task-803cb569-881c-b0d8-9222-360bcc33157e-group-mesh-mesh-8080", 221 "-bootstrap", 222 "-gateway", "my-mesh-gateway", 223 }, result) 224 }) 225 } 226 227 func TestEnvoyBootstrapHook_envoyBootstrapEnv(t *testing.T) { 228 ci.Parallel(t) 229 230 environment := []string{"foo=bar", "baz=1"} 231 232 t.Run("plain consul config", func(t *testing.T) { 233 require.Equal(t, []string{ 234 "foo=bar", "baz=1", 235 }, envoyBootstrapArgs{ 236 proxyID: "s1-sidecar-proxy", 237 grpcAddr: "1.1.1.1", 238 consulConfig: consulPlainConfig, 239 envoyAdminBind: "localhost:3333", 240 }.env(environment)) 241 }) 242 243 t.Run("tls consul config", func(t *testing.T) { 244 require.Equal(t, []string{ 245 "foo=bar", "baz=1", 246 "CONSUL_HTTP_AUTH=user:password", 247 "CONSUL_HTTP_SSL=true", 248 "CONSUL_HTTP_SSL_VERIFY=true", 249 }, envoyBootstrapArgs{ 250 proxyID: "s1-sidecar-proxy", 251 grpcAddr: "1.1.1.1", 252 consulConfig: consulTLSConfig, 253 envoyAdminBind: "localhost:3333", 254 }.env(environment)) 255 }) 256 } 257 258 // envoyConfig is used to unmarshal an envoy bootstrap configuration file, so that 259 // we can inspect the contents in tests. 260 type envoyConfig struct { 261 Admin struct { 262 Address struct { 263 SocketAddress struct { 264 Address string `json:"address"` 265 Port int `json:"port_value"` 266 } `json:"socket_address"` 267 } `json:"address"` 268 } `json:"admin"` 269 Node struct { 270 Cluster string `json:"cluster"` 271 ID string `json:"id"` 272 Metadata struct { 273 Namespace string `json:"namespace"` 274 Version string `json:"envoy_version"` 275 } 276 } 277 DynamicResources struct { 278 ADSConfig struct { 279 GRPCServices struct { 280 InitialMetadata []struct { 281 Key string `json:"key"` 282 Value string `json:"value"` 283 } `json:"initial_metadata"` 284 } `json:"grpc_services"` 285 } `json:"ads_config"` 286 } `json:"dynamic_resources"` 287 } 288 289 // TestEnvoyBootstrapHook_with_SI_token asserts the bootstrap file written for 290 // Envoy contains a Consul SI token. 291 func TestEnvoyBootstrapHook_with_SI_token(t *testing.T) { 292 ci.Parallel(t) 293 testutil.RequireConsul(t) 294 295 testConsul := getTestConsul(t) 296 defer testConsul.Stop() 297 298 alloc := mock.ConnectAlloc() 299 alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{ 300 { 301 Mode: "bridge", 302 IP: "10.0.0.1", 303 DynamicPorts: []structs.Port{ 304 { 305 Label: "connect-proxy-foo", 306 Value: 9999, 307 To: 9999, 308 }, 309 }, 310 }, 311 } 312 tg := alloc.Job.TaskGroups[0] 313 tg.Services = []*structs.Service{ 314 { 315 Name: "foo", 316 PortLabel: "9999", // Just need a valid port, nothing will bind to it 317 Connect: &structs.ConsulConnect{ 318 SidecarService: &structs.ConsulSidecarService{}, 319 }, 320 }, 321 } 322 sidecarTask := &structs.Task{ 323 Name: "sidecar", 324 Kind: "connect-proxy:foo", 325 } 326 tg.Tasks = append(tg.Tasks, sidecarTask) 327 328 logger := testlog.HCLogger(t) 329 330 allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap", alloc.ID) 331 defer cleanup() 332 333 // Register Group Services 334 consulConfig := consulapi.DefaultConfig() 335 consulConfig.Address = testConsul.HTTPAddr 336 consulAPIClient, err := consulapi.NewClient(consulConfig) 337 require.NoError(t, err) 338 namespacesClient := agentconsul.NewNamespacesClient(consulAPIClient.Namespaces(), consulAPIClient.Agent()) 339 340 consulClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), namespacesClient, logger, true) 341 go consulClient.Run() 342 defer consulClient.Shutdown() 343 require.NoError(t, consulClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter()))) 344 345 // Run Connect bootstrap Hook 346 h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{ 347 Addr: consulConfig.Address, 348 }, consulNamespace, logger)) 349 req := &interfaces.TaskPrestartRequest{ 350 Task: sidecarTask, 351 TaskDir: allocDir.NewTaskDir(sidecarTask.Name), 352 TaskEnv: taskenv.NewEmptyTaskEnv(), 353 } 354 require.NoError(t, req.TaskDir.Build(false, nil)) 355 356 // Insert service identity token in the secrets directory 357 token := uuid.Generate() 358 siTokenFile := filepath.Join(req.TaskDir.SecretsDir, sidsTokenFile) 359 err = ioutil.WriteFile(siTokenFile, []byte(token), 0440) 360 require.NoError(t, err) 361 362 resp := &interfaces.TaskPrestartResponse{} 363 364 // Run the hook 365 require.NoError(t, h.Prestart(context.Background(), req, resp)) 366 367 // Assert it is Done 368 require.True(t, resp.Done) 369 370 // Ensure the default path matches 371 env := map[string]string{ 372 taskenv.SecretsDir: req.TaskDir.SecretsDir, 373 } 374 f, err := os.Open(args.ReplaceEnv(structs.EnvoyBootstrapPath, env)) 375 require.NoError(t, err) 376 defer f.Close() 377 378 // Assert bootstrap configuration is valid json 379 var out envoyConfig 380 require.NoError(t, json.NewDecoder(f).Decode(&out)) 381 382 // Assert the SI token got set 383 key := out.DynamicResources.ADSConfig.GRPCServices.InitialMetadata[0].Key 384 value := out.DynamicResources.ADSConfig.GRPCServices.InitialMetadata[0].Value 385 require.Equal(t, "x-consul-token", key) 386 require.Equal(t, token, value) 387 } 388 389 // TestTaskRunner_EnvoyBootstrapHook_sidecar_ok asserts the EnvoyBootstrapHook 390 // creates Envoy's bootstrap.json configuration based on Connect proxy sidecars 391 // registered for the task. 392 func TestTaskRunner_EnvoyBootstrapHook_sidecar_ok(t *testing.T) { 393 ci.Parallel(t) 394 testutil.RequireConsul(t) 395 396 testConsul := getTestConsul(t) 397 defer testConsul.Stop() 398 399 alloc := mock.ConnectAlloc() 400 alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{ 401 { 402 Mode: "bridge", 403 IP: "10.0.0.1", 404 DynamicPorts: []structs.Port{ 405 { 406 Label: "connect-proxy-foo", 407 Value: 9999, 408 To: 9999, 409 }, 410 }, 411 }, 412 } 413 tg := alloc.Job.TaskGroups[0] 414 tg.Services = []*structs.Service{ 415 { 416 Name: "foo", 417 PortLabel: "9999", // Just need a valid port, nothing will bind to it 418 Connect: &structs.ConsulConnect{ 419 SidecarService: &structs.ConsulSidecarService{}, 420 }, 421 }, 422 } 423 sidecarTask := &structs.Task{ 424 Name: "sidecar", 425 Kind: structs.NewTaskKind(structs.ConnectProxyPrefix, "foo"), 426 } 427 tg.Tasks = append(tg.Tasks, sidecarTask) 428 429 logger := testlog.HCLogger(t) 430 431 allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap", alloc.ID) 432 defer cleanup() 433 434 // Register Group Services 435 consulConfig := consulapi.DefaultConfig() 436 consulConfig.Address = testConsul.HTTPAddr 437 consulAPIClient, err := consulapi.NewClient(consulConfig) 438 require.NoError(t, err) 439 namespacesClient := agentconsul.NewNamespacesClient(consulAPIClient.Namespaces(), consulAPIClient.Agent()) 440 441 consulClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), namespacesClient, logger, true) 442 go consulClient.Run() 443 defer consulClient.Shutdown() 444 require.NoError(t, consulClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter()))) 445 446 // Run Connect bootstrap Hook 447 h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{ 448 Addr: consulConfig.Address, 449 }, consulNamespace, logger)) 450 req := &interfaces.TaskPrestartRequest{ 451 Task: sidecarTask, 452 TaskDir: allocDir.NewTaskDir(sidecarTask.Name), 453 TaskEnv: taskenv.NewEmptyTaskEnv(), 454 } 455 require.NoError(t, req.TaskDir.Build(false, nil)) 456 457 resp := &interfaces.TaskPrestartResponse{} 458 459 // Run the hook 460 require.NoError(t, h.Prestart(context.Background(), req, resp)) 461 462 // Assert it is Done 463 require.True(t, resp.Done) 464 465 require.NotNil(t, resp.Env) 466 require.Equal(t, "127.0.0.2:19001", resp.Env[envoyAdminBindEnvPrefix+"foo"]) 467 468 // Ensure the default path matches 469 env := map[string]string{ 470 taskenv.SecretsDir: req.TaskDir.SecretsDir, 471 } 472 f, err := os.Open(args.ReplaceEnv(structs.EnvoyBootstrapPath, env)) 473 require.NoError(t, err) 474 defer f.Close() 475 476 // Assert bootstrap configuration is valid json 477 var out envoyConfig 478 require.NoError(t, json.NewDecoder(f).Decode(&out)) 479 480 // Assert no SI token got set 481 key := out.DynamicResources.ADSConfig.GRPCServices.InitialMetadata[0].Key 482 value := out.DynamicResources.ADSConfig.GRPCServices.InitialMetadata[0].Value 483 require.Equal(t, "x-consul-token", key) 484 require.Equal(t, "", value) 485 } 486 487 func TestTaskRunner_EnvoyBootstrapHook_gateway_ok(t *testing.T) { 488 ci.Parallel(t) 489 logger := testlog.HCLogger(t) 490 491 testConsul := getTestConsul(t) 492 defer testConsul.Stop() 493 494 // Setup an Allocation 495 alloc := mock.ConnectIngressGatewayAlloc("bridge") 496 allocDir, cleanupDir := allocdir.TestAllocDir(t, logger, "EnvoyBootstrapIngressGateway", alloc.ID) 497 defer cleanupDir() 498 499 // Get a Consul client 500 consulConfig := consulapi.DefaultConfig() 501 consulConfig.Address = testConsul.HTTPAddr 502 consulAPIClient, err := consulapi.NewClient(consulConfig) 503 require.NoError(t, err) 504 namespacesClient := agentconsul.NewNamespacesClient(consulAPIClient.Namespaces(), consulAPIClient.Agent()) 505 506 // Register Group Services 507 serviceClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), namespacesClient, logger, true) 508 go serviceClient.Run() 509 defer serviceClient.Shutdown() 510 require.NoError(t, serviceClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter()))) 511 512 // Register Configuration Entry 513 ceClient := consulAPIClient.ConfigEntries() 514 set, _, err := ceClient.Set(&consulapi.IngressGatewayConfigEntry{ 515 Kind: consulapi.IngressGateway, 516 Name: "gateway-service", // matches job 517 Listeners: []consulapi.IngressListener{{ 518 Port: 2000, 519 Protocol: "tcp", 520 Services: []consulapi.IngressService{{ 521 Name: "service1", 522 }}, 523 }}, 524 }, nil) 525 require.NoError(t, err) 526 require.True(t, set) 527 528 // Run Connect bootstrap hook 529 h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{ 530 Addr: consulConfig.Address, 531 }, consulNamespace, logger)) 532 533 req := &interfaces.TaskPrestartRequest{ 534 Task: alloc.Job.TaskGroups[0].Tasks[0], 535 TaskDir: allocDir.NewTaskDir(alloc.Job.TaskGroups[0].Tasks[0].Name), 536 TaskEnv: taskenv.NewEmptyTaskEnv(), 537 } 538 require.NoError(t, req.TaskDir.Build(false, nil)) 539 540 var resp interfaces.TaskPrestartResponse 541 542 // Run the hook 543 require.NoError(t, h.Prestart(context.Background(), req, &resp)) 544 545 // Assert the hook is Done 546 require.True(t, resp.Done) 547 require.NotNil(t, resp.Env) 548 549 // Read the Envoy Config file 550 env := map[string]string{ 551 taskenv.SecretsDir: req.TaskDir.SecretsDir, 552 } 553 f, err := os.Open(args.ReplaceEnv(structs.EnvoyBootstrapPath, env)) 554 require.NoError(t, err) 555 defer f.Close() 556 557 var out envoyConfig 558 require.NoError(t, json.NewDecoder(f).Decode(&out)) 559 560 // The only interesting thing on bootstrap is the presence of the cluster, 561 // and its associated ID that Nomad sets. Everything is configured at runtime 562 // through xDS. 563 expID := fmt.Sprintf("_nomad-task-%s-group-web-my-ingress-service-9999", alloc.ID) 564 require.Equal(t, expID, out.Node.ID) 565 require.Equal(t, "ingress-gateway", out.Node.Cluster) 566 } 567 568 // TestTaskRunner_EnvoyBootstrapHook_Noop asserts that the Envoy bootstrap hook 569 // is a noop for non-Connect proxy sidecar / gateway tasks. 570 func TestTaskRunner_EnvoyBootstrapHook_Noop(t *testing.T) { 571 ci.Parallel(t) 572 logger := testlog.HCLogger(t) 573 574 alloc := mock.Alloc() 575 task := alloc.Job.LookupTaskGroup(alloc.TaskGroup).Tasks[0] 576 allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap", alloc.ID) 577 defer cleanup() 578 579 // Run Envoy bootstrap Hook. Use invalid Consul address as it should 580 // not get hit. 581 h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{ 582 Addr: "http://127.0.0.2:1", 583 }, consulNamespace, logger)) 584 req := &interfaces.TaskPrestartRequest{ 585 Task: task, 586 TaskDir: allocDir.NewTaskDir(task.Name), 587 } 588 require.NoError(t, req.TaskDir.Build(false, nil)) 589 590 resp := &interfaces.TaskPrestartResponse{} 591 592 // Run the hook 593 require.NoError(t, h.Prestart(context.Background(), req, resp)) 594 595 // Assert it is Done 596 require.True(t, resp.Done) 597 598 // Assert no file was written 599 _, err := os.Open(filepath.Join(req.TaskDir.SecretsDir, "envoy_bootstrap.json")) 600 require.Error(t, err) 601 require.True(t, os.IsNotExist(err)) 602 } 603 604 // TestTaskRunner_EnvoyBootstrapHook_RecoverableError asserts the Envoy 605 // bootstrap hook returns a Recoverable error if the bootstrap command runs but 606 // fails. 607 func TestTaskRunner_EnvoyBootstrapHook_RecoverableError(t *testing.T) { 608 ci.Parallel(t) 609 testutil.RequireConsul(t) 610 611 testConsul := getTestConsul(t) 612 defer testConsul.Stop() 613 614 alloc := mock.ConnectAlloc() 615 alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{ 616 { 617 Mode: "bridge", 618 IP: "10.0.0.1", 619 DynamicPorts: []structs.Port{ 620 { 621 Label: "connect-proxy-foo", 622 Value: 9999, 623 To: 9999, 624 }, 625 }, 626 }, 627 } 628 tg := alloc.Job.TaskGroups[0] 629 tg.Services = []*structs.Service{ 630 { 631 Name: "foo", 632 PortLabel: "9999", // Just need a valid port, nothing will bind to it 633 Connect: &structs.ConsulConnect{ 634 SidecarService: &structs.ConsulSidecarService{}, 635 }, 636 }, 637 } 638 sidecarTask := &structs.Task{ 639 Name: "sidecar", 640 Kind: "connect-proxy:foo", 641 } 642 tg.Tasks = append(tg.Tasks, sidecarTask) 643 644 logger := testlog.HCLogger(t) 645 646 allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap", alloc.ID) 647 defer cleanup() 648 649 // Unlike the successful test above, do NOT register the group services 650 // yet. This should cause a recoverable error similar to if Consul was 651 // not running. 652 653 // Run Connect bootstrap Hook 654 h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{ 655 Addr: testConsul.HTTPAddr, 656 }, consulNamespace, logger)) 657 658 // Lower the allowable wait time for testing 659 h.envoyBootstrapWaitTime = 1 * time.Second 660 h.envoyBoostrapInitialGap = 100 * time.Millisecond 661 662 req := &interfaces.TaskPrestartRequest{ 663 Task: sidecarTask, 664 TaskDir: allocDir.NewTaskDir(sidecarTask.Name), 665 TaskEnv: taskenv.NewEmptyTaskEnv(), 666 } 667 require.NoError(t, req.TaskDir.Build(false, nil)) 668 669 resp := &interfaces.TaskPrestartResponse{} 670 671 // Run the hook 672 err := h.Prestart(context.Background(), req, resp) 673 require.EqualError(t, err, "error creating bootstrap configuration for Connect proxy sidecar: exit status 1") 674 require.True(t, structs.IsRecoverable(err)) 675 676 // Assert it is not Done 677 require.False(t, resp.Done) 678 679 // Assert no file was written 680 _, err = os.Open(filepath.Join(req.TaskDir.SecretsDir, "envoy_bootstrap.json")) 681 require.Error(t, err) 682 require.True(t, os.IsNotExist(err)) 683 } 684 685 func TestTaskRunner_EnvoyBootstrapHook_retryTimeout(t *testing.T) { 686 ci.Parallel(t) 687 logger := testlog.HCLogger(t) 688 689 testConsul := getTestConsul(t) 690 defer testConsul.Stop() 691 692 begin := time.Now() 693 694 // Setup an Allocation 695 alloc := mock.ConnectAlloc() 696 alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{ 697 { 698 Mode: "bridge", 699 IP: "10.0.0.1", 700 DynamicPorts: []structs.Port{ 701 { 702 Label: "connect-proxy-foo", 703 Value: 9999, 704 To: 9999, 705 }, 706 }, 707 }, 708 } 709 tg := alloc.Job.TaskGroups[0] 710 tg.Services = []*structs.Service{ 711 { 712 Name: "foo", 713 PortLabel: "9999", // Just need a valid port, nothing will bind to it 714 Connect: &structs.ConsulConnect{ 715 SidecarService: &structs.ConsulSidecarService{}, 716 }, 717 }, 718 } 719 sidecarTask := &structs.Task{ 720 Name: "sidecar", 721 Kind: structs.NewTaskKind(structs.ConnectProxyPrefix, "foo"), 722 } 723 tg.Tasks = append(tg.Tasks, sidecarTask) 724 allocDir, cleanupAlloc := allocdir.TestAllocDir(t, logger, "EnvoyBootstrapRetryTimeout", alloc.ID) 725 defer cleanupAlloc() 726 727 // Get a Consul client 728 consulConfig := consulapi.DefaultConfig() 729 consulConfig.Address = testConsul.HTTPAddr 730 731 // Do NOT register group services, causing the hook to retry until timeout 732 733 // Run Connect bootstrap hook 734 h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{ 735 Addr: consulConfig.Address, 736 }, consulNamespace, logger)) 737 738 // Keep track of the retry backoff iterations 739 iterations := 0 740 741 // Lower the allowable wait time for testing 742 h.envoyBootstrapWaitTime = 3 * time.Second 743 h.envoyBoostrapInitialGap = 1 * time.Second 744 h.envoyBootstrapExpSleep = func(d time.Duration) { 745 iterations++ 746 time.Sleep(d) 747 } 748 749 // Create the prestart request 750 req := &interfaces.TaskPrestartRequest{ 751 Task: sidecarTask, 752 TaskDir: allocDir.NewTaskDir(sidecarTask.Name), 753 TaskEnv: taskenv.NewEmptyTaskEnv(), 754 } 755 require.NoError(t, req.TaskDir.Build(false, nil)) 756 757 var resp interfaces.TaskPrestartResponse 758 759 // Run the hook and get the error 760 err := h.Prestart(context.Background(), req, &resp) 761 require.EqualError(t, err, "error creating bootstrap configuration for Connect proxy sidecar: exit status 1") 762 763 // Current time should be at least start time + total wait time 764 minimum := begin.Add(h.envoyBootstrapWaitTime) 765 require.True(t, time.Now().After(minimum)) 766 767 // Should hit at least 2 iterations 768 require.Greater(t, 2, iterations) 769 770 // Make sure we captured the recoverable-ness of the error 771 _, ok := err.(*structs.RecoverableError) 772 require.True(t, ok) 773 774 // Assert the hook is not done (it failed) 775 require.False(t, resp.Done) 776 } 777 778 func TestTaskRunner_EnvoyBootstrapHook_extractNameAndKind(t *testing.T) { 779 t.Run("connect sidecar", func(t *testing.T) { 780 kind, name, err := (*envoyBootstrapHook)(nil).extractNameAndKind( 781 structs.NewTaskKind(structs.ConnectProxyPrefix, "foo"), 782 ) 783 require.Nil(t, err) 784 require.Equal(t, "connect-proxy", kind) 785 require.Equal(t, "foo", name) 786 }) 787 788 t.Run("connect gateway", func(t *testing.T) { 789 kind, name, err := (*envoyBootstrapHook)(nil).extractNameAndKind( 790 structs.NewTaskKind(structs.ConnectIngressPrefix, "foo"), 791 ) 792 require.Nil(t, err) 793 require.Equal(t, "connect-ingress", kind) 794 require.Equal(t, "foo", name) 795 }) 796 797 t.Run("connect native", func(t *testing.T) { 798 _, _, err := (*envoyBootstrapHook)(nil).extractNameAndKind( 799 structs.NewTaskKind(structs.ConnectNativePrefix, "foo"), 800 ) 801 require.EqualError(t, err, "envoy must be used as connect sidecar or gateway") 802 }) 803 804 t.Run("normal task", func(t *testing.T) { 805 _, _, err := (*envoyBootstrapHook)(nil).extractNameAndKind( 806 structs.TaskKind(""), 807 ) 808 require.EqualError(t, err, "envoy must be used as connect sidecar or gateway") 809 }) 810 } 811 812 func TestTaskRunner_EnvoyBootstrapHook_grpcAddress(t *testing.T) { 813 ci.Parallel(t) 814 815 bridgeH := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig( 816 mock.ConnectIngressGatewayAlloc("bridge"), 817 new(config.ConsulConfig), 818 consulNamespace, 819 testlog.HCLogger(t), 820 )) 821 822 hostH := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig( 823 mock.ConnectIngressGatewayAlloc("host"), 824 new(config.ConsulConfig), 825 consulNamespace, 826 testlog.HCLogger(t), 827 )) 828 829 t.Run("environment", func(t *testing.T) { 830 env := map[string]string{ 831 grpcConsulVariable: "1.2.3.4:9000", 832 } 833 require.Equal(t, "1.2.3.4:9000", bridgeH.grpcAddress(env)) 834 require.Equal(t, "1.2.3.4:9000", hostH.grpcAddress(env)) 835 }) 836 837 t.Run("defaults", func(t *testing.T) { 838 require.Equal(t, "unix://alloc/tmp/consul_grpc.sock", bridgeH.grpcAddress(nil)) 839 require.Equal(t, "127.0.0.1:8502", hostH.grpcAddress(nil)) 840 }) 841 } 842 843 func TestTaskRunner_EnvoyBootstrapHook_isConnectKind(t *testing.T) { 844 ci.Parallel(t) 845 846 require.True(t, isConnectKind(structs.ConnectProxyPrefix)) 847 require.True(t, isConnectKind(structs.ConnectIngressPrefix)) 848 require.True(t, isConnectKind(structs.ConnectTerminatingPrefix)) 849 require.True(t, isConnectKind(structs.ConnectMeshPrefix)) 850 require.False(t, isConnectKind("")) 851 require.False(t, isConnectKind("something")) 852 }