github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/client/allocrunner/taskrunner/envoybootstrap_hook_test.go (about) 1 // +build !windows 2 // todo(shoenig): Once Connect is supported on Windows, we'll need to make this 3 // set of tests work there too. 4 5 package taskrunner 6 7 import ( 8 "context" 9 "encoding/json" 10 "io/ioutil" 11 "os" 12 "path/filepath" 13 "testing" 14 15 consulapi "github.com/hashicorp/consul/api" 16 consultest "github.com/hashicorp/consul/sdk/testutil" 17 "github.com/hashicorp/nomad/client/allocdir" 18 "github.com/hashicorp/nomad/client/allocrunner/interfaces" 19 "github.com/hashicorp/nomad/client/taskenv" 20 "github.com/hashicorp/nomad/client/testutil" 21 agentconsul "github.com/hashicorp/nomad/command/agent/consul" 22 "github.com/hashicorp/nomad/helper" 23 "github.com/hashicorp/nomad/helper/args" 24 "github.com/hashicorp/nomad/helper/testlog" 25 "github.com/hashicorp/nomad/helper/uuid" 26 "github.com/hashicorp/nomad/nomad/mock" 27 "github.com/hashicorp/nomad/nomad/structs" 28 "github.com/hashicorp/nomad/nomad/structs/config" 29 "github.com/stretchr/testify/require" 30 "golang.org/x/sys/unix" 31 ) 32 33 var _ interfaces.TaskPrestartHook = (*envoyBootstrapHook)(nil) 34 35 func writeTmp(t *testing.T, s string, fm os.FileMode) string { 36 dir, err := ioutil.TempDir("", "envoy-") 37 require.NoError(t, err) 38 39 fPath := filepath.Join(dir, sidsTokenFile) 40 err = ioutil.WriteFile(fPath, []byte(s), fm) 41 require.NoError(t, err) 42 43 return dir 44 } 45 46 func TestEnvoyBootstrapHook_maybeLoadSIToken(t *testing.T) { 47 t.Parallel() 48 49 // This test fails when running as root because the test case for checking 50 // the error condition when the file is unreadable fails (root can read the 51 // file even though the permissions are set to 0200). 52 if unix.Geteuid() == 0 { 53 t.Skip("test only works as non-root") 54 } 55 56 t.Run("file does not exist", func(t *testing.T) { 57 h := newEnvoyBootstrapHook(&envoyBootstrapHookConfig{logger: testlog.HCLogger(t)}) 58 cfg, err := h.maybeLoadSIToken("task1", "/does/not/exist") 59 require.NoError(t, err) // absence of token is not an error 60 require.Equal(t, "", cfg) 61 }) 62 63 t.Run("load token from file", func(t *testing.T) { 64 token := uuid.Generate() 65 f := writeTmp(t, token, 0440) 66 defer cleanupDir(t, f) 67 68 h := newEnvoyBootstrapHook(&envoyBootstrapHookConfig{logger: testlog.HCLogger(t)}) 69 cfg, err := h.maybeLoadSIToken("task1", f) 70 require.NoError(t, err) 71 require.Equal(t, token, cfg) 72 }) 73 74 t.Run("file is unreadable", func(t *testing.T) { 75 token := uuid.Generate() 76 f := writeTmp(t, token, 0200) 77 defer cleanupDir(t, f) 78 79 h := newEnvoyBootstrapHook(&envoyBootstrapHookConfig{logger: testlog.HCLogger(t)}) 80 cfg, err := h.maybeLoadSIToken("task1", f) 81 require.Error(t, err) 82 require.False(t, os.IsNotExist(err)) 83 require.Equal(t, "", cfg) 84 }) 85 } 86 87 func TestEnvoyBootstrapHook_decodeTriState(t *testing.T) { 88 t.Parallel() 89 90 require.Equal(t, "", decodeTriState(nil)) 91 require.Equal(t, "true", decodeTriState(helper.BoolToPtr(true))) 92 require.Equal(t, "false", decodeTriState(helper.BoolToPtr(false))) 93 } 94 95 var ( 96 consulPlainConfig = envoyBootstrapConsulConfig{ 97 HTTPAddr: "2.2.2.2", 98 } 99 100 consulTLSConfig = envoyBootstrapConsulConfig{ 101 HTTPAddr: "2.2.2.2", // arg 102 Auth: "user:password", // env 103 SSL: "true", // env 104 VerifySSL: "true", // env 105 CAFile: "/etc/tls/ca-file", // arg 106 CertFile: "/etc/tls/cert-file", // arg 107 KeyFile: "/etc/tls/key-file", // arg 108 } 109 ) 110 111 func TestEnvoyBootstrapHook_envoyBootstrapArgs(t *testing.T) { 112 t.Parallel() 113 114 t.Run("excluding SI token", func(t *testing.T) { 115 ebArgs := envoyBootstrapArgs{ 116 sidecarFor: "s1", 117 grpcAddr: "1.1.1.1", 118 consulConfig: consulPlainConfig, 119 envoyAdminBind: "localhost:3333", 120 } 121 result := ebArgs.args() 122 require.Equal(t, []string{"connect", "envoy", 123 "-grpc-addr", "1.1.1.1", 124 "-http-addr", "2.2.2.2", 125 "-admin-bind", "localhost:3333", 126 "-bootstrap", 127 "-sidecar-for", "s1", 128 }, result) 129 }) 130 131 t.Run("including SI token", func(t *testing.T) { 132 token := uuid.Generate() 133 ebArgs := envoyBootstrapArgs{ 134 sidecarFor: "s1", 135 grpcAddr: "1.1.1.1", 136 consulConfig: consulPlainConfig, 137 envoyAdminBind: "localhost:3333", 138 siToken: token, 139 } 140 result := ebArgs.args() 141 require.Equal(t, []string{"connect", "envoy", 142 "-grpc-addr", "1.1.1.1", 143 "-http-addr", "2.2.2.2", 144 "-admin-bind", "localhost:3333", 145 "-bootstrap", 146 "-sidecar-for", "s1", 147 "-token", token, 148 }, result) 149 }) 150 151 t.Run("including certificates", func(t *testing.T) { 152 ebArgs := envoyBootstrapArgs{ 153 sidecarFor: "s1", 154 grpcAddr: "1.1.1.1", 155 consulConfig: consulTLSConfig, 156 envoyAdminBind: "localhost:3333", 157 } 158 result := ebArgs.args() 159 require.Equal(t, []string{"connect", "envoy", 160 "-grpc-addr", "1.1.1.1", 161 "-http-addr", "2.2.2.2", 162 "-admin-bind", "localhost:3333", 163 "-bootstrap", 164 "-sidecar-for", "s1", 165 "-ca-file", "/etc/tls/ca-file", 166 "-client-cert", "/etc/tls/cert-file", 167 "-client-key", "/etc/tls/key-file", 168 }, result) 169 }) 170 } 171 172 func TestEnvoyBootstrapHook_envoyBootstrapEnv(t *testing.T) { 173 t.Parallel() 174 175 environment := []string{"foo=bar", "baz=1"} 176 177 t.Run("plain consul config", func(t *testing.T) { 178 require.Equal(t, []string{ 179 "foo=bar", "baz=1", 180 }, envoyBootstrapArgs{ 181 sidecarFor: "s1", 182 grpcAddr: "1.1.1.1", 183 consulConfig: consulPlainConfig, 184 envoyAdminBind: "localhost:3333", 185 }.env(environment)) 186 }) 187 188 t.Run("tls consul config", func(t *testing.T) { 189 require.Equal(t, []string{ 190 "foo=bar", "baz=1", 191 "CONSUL_HTTP_AUTH=user:password", 192 "CONSUL_HTTP_SSL=true", 193 "CONSUL_HTTP_SSL_VERIFY=true", 194 }, envoyBootstrapArgs{ 195 sidecarFor: "s1", 196 grpcAddr: "1.1.1.1", 197 consulConfig: consulTLSConfig, 198 envoyAdminBind: "localhost:3333", 199 }.env(environment)) 200 }) 201 } 202 203 // dig through envoy config to look for consul token 204 type envoyConfig struct { 205 DynamicResources struct { 206 ADSConfig struct { 207 GRPCServices struct { 208 InitialMetadata []struct { 209 Key string `json:"key"` 210 Value string `json:"value"` 211 } `json:"initial_metadata"` 212 } `json:"grpc_services"` 213 } `json:"ads_config"` 214 } `json:"dynamic_resources"` 215 } 216 217 // TestEnvoyBootstrapHook_with_SI_token asserts the bootstrap file written for 218 // Envoy contains a Consul SI token. 219 func TestEnvoyBootstrapHook_with_SI_token(t *testing.T) { 220 t.Parallel() 221 testutil.RequireConsul(t) 222 223 testconsul, err := consultest.NewTestServerConfig(func(c *consultest.TestServerConfig) { 224 // If -v wasn't specified squelch consul logging 225 if !testing.Verbose() { 226 c.Stdout = ioutil.Discard 227 c.Stderr = ioutil.Discard 228 } 229 }) 230 if err != nil { 231 t.Fatalf("error starting test consul server: %v", err) 232 } 233 defer testconsul.Stop() 234 235 alloc := mock.Alloc() 236 alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{ 237 { 238 Mode: "bridge", 239 IP: "10.0.0.1", 240 DynamicPorts: []structs.Port{ 241 { 242 Label: "connect-proxy-foo", 243 Value: 9999, 244 To: 9999, 245 }, 246 }, 247 }, 248 } 249 tg := alloc.Job.TaskGroups[0] 250 tg.Services = []*structs.Service{ 251 { 252 Name: "foo", 253 PortLabel: "9999", // Just need a valid port, nothing will bind to it 254 Connect: &structs.ConsulConnect{ 255 SidecarService: &structs.ConsulSidecarService{}, 256 }, 257 }, 258 } 259 sidecarTask := &structs.Task{ 260 Name: "sidecar", 261 Kind: "connect-proxy:foo", 262 } 263 tg.Tasks = append(tg.Tasks, sidecarTask) 264 265 logger := testlog.HCLogger(t) 266 267 allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap") 268 defer cleanup() 269 270 // Register Group Services 271 consulConfig := consulapi.DefaultConfig() 272 consulConfig.Address = testconsul.HTTPAddr 273 consulAPIClient, err := consulapi.NewClient(consulConfig) 274 require.NoError(t, err) 275 276 consulClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), logger, true) 277 go consulClient.Run() 278 defer consulClient.Shutdown() 279 require.NoError(t, consulClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter()))) 280 281 // Run Connect bootstrap Hook 282 h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{ 283 Addr: consulConfig.Address, 284 }, logger)) 285 req := &interfaces.TaskPrestartRequest{ 286 Task: sidecarTask, 287 TaskDir: allocDir.NewTaskDir(sidecarTask.Name), 288 } 289 require.NoError(t, req.TaskDir.Build(false, nil)) 290 291 // Insert service identity token in the secrets directory 292 token := uuid.Generate() 293 siTokenFile := filepath.Join(req.TaskDir.SecretsDir, sidsTokenFile) 294 err = ioutil.WriteFile(siTokenFile, []byte(token), 0440) 295 require.NoError(t, err) 296 297 resp := &interfaces.TaskPrestartResponse{} 298 299 // Run the hook 300 require.NoError(t, h.Prestart(context.Background(), req, resp)) 301 302 // Assert it is Done 303 require.True(t, resp.Done) 304 305 // Ensure the default path matches 306 env := map[string]string{ 307 taskenv.SecretsDir: req.TaskDir.SecretsDir, 308 } 309 f, err := os.Open(args.ReplaceEnv(structs.EnvoyBootstrapPath, env)) 310 require.NoError(t, err) 311 defer f.Close() 312 313 // Assert bootstrap configuration is valid json 314 var out envoyConfig 315 require.NoError(t, json.NewDecoder(f).Decode(&out)) 316 317 // Assert the SI token got set 318 key := out.DynamicResources.ADSConfig.GRPCServices.InitialMetadata[0].Key 319 value := out.DynamicResources.ADSConfig.GRPCServices.InitialMetadata[0].Value 320 require.Equal(t, "x-consul-token", key) 321 require.Equal(t, token, value) 322 } 323 324 // TestTaskRunner_EnvoyBootstrapHook_Prestart asserts the EnvoyBootstrapHook 325 // creates Envoy's bootstrap.json configuration based on Connect proxy sidecars 326 // registered for the task. 327 func TestTaskRunner_EnvoyBootstrapHook_Ok(t *testing.T) { 328 t.Parallel() 329 testutil.RequireConsul(t) 330 331 testconsul, err := consultest.NewTestServerConfig(func(c *consultest.TestServerConfig) { 332 // If -v wasn't specified squelch consul logging 333 if !testing.Verbose() { 334 c.Stdout = ioutil.Discard 335 c.Stderr = ioutil.Discard 336 } 337 }) 338 if err != nil { 339 t.Fatalf("error starting test consul server: %v", err) 340 } 341 defer testconsul.Stop() 342 343 alloc := mock.Alloc() 344 alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{ 345 { 346 Mode: "bridge", 347 IP: "10.0.0.1", 348 DynamicPorts: []structs.Port{ 349 { 350 Label: "connect-proxy-foo", 351 Value: 9999, 352 To: 9999, 353 }, 354 }, 355 }, 356 } 357 tg := alloc.Job.TaskGroups[0] 358 tg.Services = []*structs.Service{ 359 { 360 Name: "foo", 361 PortLabel: "9999", // Just need a valid port, nothing will bind to it 362 Connect: &structs.ConsulConnect{ 363 SidecarService: &structs.ConsulSidecarService{}, 364 }, 365 }, 366 } 367 sidecarTask := &structs.Task{ 368 Name: "sidecar", 369 Kind: "connect-proxy:foo", 370 } 371 tg.Tasks = append(tg.Tasks, sidecarTask) 372 373 logger := testlog.HCLogger(t) 374 375 allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap") 376 defer cleanup() 377 378 // Register Group Services 379 consulConfig := consulapi.DefaultConfig() 380 consulConfig.Address = testconsul.HTTPAddr 381 consulAPIClient, err := consulapi.NewClient(consulConfig) 382 require.NoError(t, err) 383 384 consulClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), logger, true) 385 go consulClient.Run() 386 defer consulClient.Shutdown() 387 require.NoError(t, consulClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter()))) 388 389 // Run Connect bootstrap Hook 390 h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{ 391 Addr: consulConfig.Address, 392 }, logger)) 393 req := &interfaces.TaskPrestartRequest{ 394 Task: sidecarTask, 395 TaskDir: allocDir.NewTaskDir(sidecarTask.Name), 396 } 397 require.NoError(t, req.TaskDir.Build(false, nil)) 398 399 resp := &interfaces.TaskPrestartResponse{} 400 401 // Run the hook 402 require.NoError(t, h.Prestart(context.Background(), req, resp)) 403 404 // Assert it is Done 405 require.True(t, resp.Done) 406 407 require.NotNil(t, resp.Env) 408 require.Equal(t, "localhost:19001", resp.Env[envoyAdminBindEnvPrefix+"foo"]) 409 410 // Ensure the default path matches 411 env := map[string]string{ 412 taskenv.SecretsDir: req.TaskDir.SecretsDir, 413 } 414 f, err := os.Open(args.ReplaceEnv(structs.EnvoyBootstrapPath, env)) 415 require.NoError(t, err) 416 defer f.Close() 417 418 // Assert bootstrap configuration is valid json 419 var out envoyConfig 420 require.NoError(t, json.NewDecoder(f).Decode(&out)) 421 422 // Assert no SI token got set 423 key := out.DynamicResources.ADSConfig.GRPCServices.InitialMetadata[0].Key 424 value := out.DynamicResources.ADSConfig.GRPCServices.InitialMetadata[0].Value 425 require.Equal(t, "x-consul-token", key) 426 require.Equal(t, "", value) 427 } 428 429 // TestTaskRunner_EnvoyBootstrapHook_Noop asserts that the Envoy bootstrap hook 430 // is a noop for non-Connect proxy sidecar tasks. 431 func TestTaskRunner_EnvoyBootstrapHook_Noop(t *testing.T) { 432 t.Parallel() 433 logger := testlog.HCLogger(t) 434 435 allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap") 436 defer cleanup() 437 438 alloc := mock.Alloc() 439 task := alloc.Job.LookupTaskGroup(alloc.TaskGroup).Tasks[0] 440 441 // Run Envoy bootstrap Hook. Use invalid Consul address as it should 442 // not get hit. 443 h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{ 444 Addr: "http://127.0.0.2:1", 445 }, logger)) 446 req := &interfaces.TaskPrestartRequest{ 447 Task: task, 448 TaskDir: allocDir.NewTaskDir(task.Name), 449 } 450 require.NoError(t, req.TaskDir.Build(false, nil)) 451 452 resp := &interfaces.TaskPrestartResponse{} 453 454 // Run the hook 455 require.NoError(t, h.Prestart(context.Background(), req, resp)) 456 457 // Assert it is Done 458 require.True(t, resp.Done) 459 460 // Assert no file was written 461 _, err := os.Open(filepath.Join(req.TaskDir.SecretsDir, "envoy_bootstrap.json")) 462 require.Error(t, err) 463 require.True(t, os.IsNotExist(err)) 464 } 465 466 // TestTaskRunner_EnvoyBootstrapHook_RecoverableError asserts the Envoy 467 // bootstrap hook returns a Recoverable error if the bootstrap command runs but 468 // fails. 469 func TestTaskRunner_EnvoyBootstrapHook_RecoverableError(t *testing.T) { 470 t.Parallel() 471 testutil.RequireConsul(t) 472 473 testconsul, err := consultest.NewTestServerConfig(func(c *consultest.TestServerConfig) { 474 // If -v wasn't specified squelch consul logging 475 if !testing.Verbose() { 476 c.Stdout = ioutil.Discard 477 c.Stderr = ioutil.Discard 478 } 479 }) 480 if err != nil { 481 t.Fatalf("error starting test consul server: %v", err) 482 } 483 defer testconsul.Stop() 484 485 alloc := mock.Alloc() 486 alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{ 487 { 488 Mode: "bridge", 489 IP: "10.0.0.1", 490 DynamicPorts: []structs.Port{ 491 { 492 Label: "connect-proxy-foo", 493 Value: 9999, 494 To: 9999, 495 }, 496 }, 497 }, 498 } 499 tg := alloc.Job.TaskGroups[0] 500 tg.Services = []*structs.Service{ 501 { 502 Name: "foo", 503 PortLabel: "9999", // Just need a valid port, nothing will bind to it 504 Connect: &structs.ConsulConnect{ 505 SidecarService: &structs.ConsulSidecarService{}, 506 }, 507 }, 508 } 509 sidecarTask := &structs.Task{ 510 Name: "sidecar", 511 Kind: "connect-proxy:foo", 512 } 513 tg.Tasks = append(tg.Tasks, sidecarTask) 514 515 logger := testlog.HCLogger(t) 516 517 allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap") 518 defer cleanup() 519 520 // Unlike the successful test above, do NOT register the group services 521 // yet. This should cause a recoverable error similar to if Consul was 522 // not running. 523 524 // Run Connect bootstrap Hook 525 h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{ 526 Addr: testconsul.HTTPAddr, 527 }, logger)) 528 req := &interfaces.TaskPrestartRequest{ 529 Task: sidecarTask, 530 TaskDir: allocDir.NewTaskDir(sidecarTask.Name), 531 } 532 require.NoError(t, req.TaskDir.Build(false, nil)) 533 534 resp := &interfaces.TaskPrestartResponse{} 535 536 // Run the hook 537 err = h.Prestart(context.Background(), req, resp) 538 require.EqualError(t, err, "error creating bootstrap configuration for Connect proxy sidecar: exit status 1") 539 require.True(t, structs.IsRecoverable(err)) 540 541 // Assert it is not Done 542 require.False(t, resp.Done) 543 544 // Assert no file was written 545 _, err = os.Open(filepath.Join(req.TaskDir.SecretsDir, "envoy_bootstrap.json")) 546 require.Error(t, err) 547 require.True(t, os.IsNotExist(err)) 548 }