github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/client/allocrunner/taskrunner/connect_native_hook_test.go (about) 1 package taskrunner 2 3 import ( 4 "context" 5 "io/ioutil" 6 "os" 7 "path/filepath" 8 "testing" 9 10 consulapi "github.com/hashicorp/consul/api" 11 consultest "github.com/hashicorp/consul/sdk/testutil" 12 "github.com/hashicorp/nomad/client/allocdir" 13 "github.com/hashicorp/nomad/client/allocrunner/interfaces" 14 "github.com/hashicorp/nomad/client/taskenv" 15 "github.com/hashicorp/nomad/client/testutil" 16 agentconsul "github.com/hashicorp/nomad/command/agent/consul" 17 "github.com/hashicorp/nomad/helper" 18 "github.com/hashicorp/nomad/helper/testlog" 19 "github.com/hashicorp/nomad/helper/uuid" 20 "github.com/hashicorp/nomad/nomad/mock" 21 "github.com/hashicorp/nomad/nomad/structs" 22 "github.com/hashicorp/nomad/nomad/structs/config" 23 "github.com/stretchr/testify/require" 24 ) 25 26 func getTestConsul(t *testing.T) *consultest.TestServer { 27 testConsul, err := consultest.NewTestServerConfigT(t, func(c *consultest.TestServerConfig) { 28 if !testing.Verbose() { // disable consul logging if -v not set 29 c.Stdout = ioutil.Discard 30 c.Stderr = ioutil.Discard 31 } 32 }) 33 require.NoError(t, err, "failed to start test consul server") 34 return testConsul 35 } 36 37 func TestConnectNativeHook_Name(t *testing.T) { 38 t.Parallel() 39 name := new(connectNativeHook).Name() 40 require.Equal(t, "connect_native", name) 41 } 42 43 func setupCertDirs(t *testing.T) (string, string) { 44 fd, err := ioutil.TempFile("", "connect_native_testcert") 45 require.NoError(t, err) 46 _, err = fd.WriteString("ABCDEF") 47 require.NoError(t, err) 48 err = fd.Close() 49 require.NoError(t, err) 50 51 d, err := ioutil.TempDir("", "connect_native_testsecrets") 52 require.NoError(t, err) 53 return fd.Name(), d 54 } 55 56 func cleanupCertDirs(t *testing.T, original, secrets string) { 57 err := os.Remove(original) 58 require.NoError(t, err) 59 err = os.RemoveAll(secrets) 60 require.NoError(t, err) 61 } 62 63 func TestConnectNativeHook_copyCertificate(t *testing.T) { 64 t.Parallel() 65 66 f, d := setupCertDirs(t) 67 defer cleanupCertDirs(t, f, d) 68 69 t.Run("no source", func(t *testing.T) { 70 err := new(connectNativeHook).copyCertificate("", d, "out.pem") 71 require.NoError(t, err) 72 }) 73 74 t.Run("normal", func(t *testing.T) { 75 err := new(connectNativeHook).copyCertificate(f, d, "out.pem") 76 require.NoError(t, err) 77 b, err := ioutil.ReadFile(filepath.Join(d, "out.pem")) 78 require.NoError(t, err) 79 require.Equal(t, "ABCDEF", string(b)) 80 }) 81 } 82 83 func TestConnectNativeHook_copyCertificates(t *testing.T) { 84 t.Parallel() 85 86 f, d := setupCertDirs(t) 87 defer cleanupCertDirs(t, f, d) 88 89 t.Run("normal", func(t *testing.T) { 90 err := new(connectNativeHook).copyCertificates(consulTransportConfig{ 91 CAFile: f, 92 CertFile: f, 93 KeyFile: f, 94 }, d) 95 require.NoError(t, err) 96 ls, err := ioutil.ReadDir(d) 97 require.NoError(t, err) 98 require.Equal(t, 3, len(ls)) 99 }) 100 101 t.Run("no source", func(t *testing.T) { 102 err := new(connectNativeHook).copyCertificates(consulTransportConfig{ 103 CAFile: "/does/not/exist.pem", 104 CertFile: "/does/not/exist.pem", 105 KeyFile: "/does/not/exist.pem", 106 }, d) 107 require.EqualError(t, err, "failed to open consul TLS certificate: open /does/not/exist.pem: no such file or directory") 108 }) 109 } 110 111 func TestConnectNativeHook_tlsEnv(t *testing.T) { 112 t.Parallel() 113 114 // the hook config comes from client config 115 emptyHook := new(connectNativeHook) 116 fullHook := &connectNativeHook{ 117 consulConfig: consulTransportConfig{ 118 Auth: "user:password", 119 SSL: "true", 120 VerifySSL: "true", 121 CAFile: "/not/real/ca.pem", 122 CertFile: "/not/real/cert.pem", 123 KeyFile: "/not/real/key.pem", 124 }, 125 } 126 127 // existing config from task env stanza 128 taskEnv := map[string]string{ 129 "CONSUL_CACERT": "fakeCA.pem", 130 "CONSUL_CLIENT_CERT": "fakeCert.pem", 131 "CONSUL_CLIENT_KEY": "fakeKey.pem", 132 "CONSUL_HTTP_AUTH": "foo:bar", 133 "CONSUL_HTTP_SSL": "false", 134 "CONSUL_HTTP_SSL_VERIFY": "false", 135 } 136 137 t.Run("empty hook and empty task", func(t *testing.T) { 138 result := emptyHook.tlsEnv(nil) 139 require.Empty(t, result) 140 }) 141 142 t.Run("empty hook and non-empty task", func(t *testing.T) { 143 result := emptyHook.tlsEnv(taskEnv) 144 require.Empty(t, result) // tlsEnv only overrides; task env is actually set elsewhere 145 }) 146 147 t.Run("non-empty hook and empty task", func(t *testing.T) { 148 result := fullHook.tlsEnv(nil) 149 require.Equal(t, map[string]string{ 150 // ca files are specifically copied into FS namespace 151 "CONSUL_CACERT": "/secrets/consul_ca_file.pem", 152 "CONSUL_CLIENT_CERT": "/secrets/consul_cert_file.pem", 153 "CONSUL_CLIENT_KEY": "/secrets/consul_key_file.pem", 154 "CONSUL_HTTP_SSL": "true", 155 "CONSUL_HTTP_SSL_VERIFY": "true", 156 }, result) 157 }) 158 159 t.Run("non-empty hook and non-empty task", func(t *testing.T) { 160 result := fullHook.tlsEnv(taskEnv) // task env takes precedence, nothing gets set here 161 require.Empty(t, result) 162 }) 163 } 164 165 func TestConnectNativeHook_bridgeEnv_bridge(t *testing.T) { 166 t.Parallel() 167 168 hook := new(connectNativeHook) 169 hook.alloc = mock.ConnectNativeAlloc("bridge") 170 171 t.Run("consul address env not preconfigured", func(t *testing.T) { 172 result := hook.bridgeEnv(nil) 173 require.Equal(t, map[string]string{ 174 "CONSUL_HTTP_ADDR": "unix:///alloc/tmp/consul_http.sock", 175 }, result) 176 }) 177 178 t.Run("consul address env is preconfigured", func(t *testing.T) { 179 result := hook.bridgeEnv(map[string]string{ 180 "CONSUL_HTTP_ADDR": "10.1.1.1", 181 }) 182 require.Empty(t, result) 183 }) 184 } 185 186 func TestConnectNativeHook_bridgeEnv_host(t *testing.T) { 187 t.Parallel() 188 189 hook := new(connectNativeHook) 190 hook.alloc = mock.ConnectNativeAlloc("host") 191 192 t.Run("consul address env not preconfigured", func(t *testing.T) { 193 result := hook.bridgeEnv(nil) 194 require.Empty(t, result) 195 }) 196 197 t.Run("consul address env is preconfigured", func(t *testing.T) { 198 result := hook.bridgeEnv(map[string]string{ 199 "CONSUL_HTTP_ADDR": "10.1.1.1", 200 }) 201 require.Empty(t, result) 202 }) 203 } 204 205 func TestTaskRunner_ConnectNativeHook_Noop(t *testing.T) { 206 t.Parallel() 207 logger := testlog.HCLogger(t) 208 209 allocDir, cleanup := allocdir.TestAllocDir(t, logger, "ConnectNative") 210 defer cleanup() 211 212 alloc := mock.Alloc() 213 task := alloc.Job.LookupTaskGroup(alloc.TaskGroup).Tasks[0] 214 215 // run the connect native hook. use invalid consul address as it should not get hit 216 h := newConnectNativeHook(newConnectNativeHookConfig(alloc, &config.ConsulConfig{ 217 Addr: "http://127.0.0.2:1", 218 }, logger)) 219 220 request := &interfaces.TaskPrestartRequest{ 221 Task: task, 222 TaskDir: allocDir.NewTaskDir(task.Name), 223 } 224 require.NoError(t, request.TaskDir.Build(false, nil)) 225 226 response := new(interfaces.TaskPrestartResponse) 227 228 // Run the hook 229 require.NoError(t, h.Prestart(context.Background(), request, response)) 230 231 // Assert the hook is Done 232 require.True(t, response.Done) 233 234 // Assert secrets dir is empty (no TLS config set) 235 checkFilesInDir(t, request.TaskDir.SecretsDir, 236 nil, 237 []string{sidsTokenFile, secretCAFilename, secretCertfileFilename, secretKeyfileFilename}, 238 ) 239 } 240 241 func TestTaskRunner_ConnectNativeHook_Ok(t *testing.T) { 242 t.Parallel() 243 testutil.RequireConsul(t) 244 245 testConsul := getTestConsul(t) 246 defer testConsul.Stop() 247 248 alloc := mock.Alloc() 249 alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{{Mode: "host", IP: "1.1.1.1"}} 250 tg := alloc.Job.TaskGroups[0] 251 tg.Services = []*structs.Service{{ 252 Name: "cn-service", 253 TaskName: tg.Tasks[0].Name, 254 Connect: &structs.ConsulConnect{ 255 Native: true, 256 }}, 257 } 258 tg.Tasks[0].Kind = structs.NewTaskKind("connect-native", "cn-service") 259 260 logger := testlog.HCLogger(t) 261 262 allocDir, cleanup := allocdir.TestAllocDir(t, logger, "ConnectNative") 263 defer cleanup() 264 265 // register group services 266 consulConfig := consulapi.DefaultConfig() 267 consulConfig.Address = testConsul.HTTPAddr 268 consulAPIClient, err := consulapi.NewClient(consulConfig) 269 require.NoError(t, err) 270 271 consulClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), logger, true) 272 go consulClient.Run() 273 defer consulClient.Shutdown() 274 require.NoError(t, consulClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter()))) 275 276 // Run Connect Native hook 277 h := newConnectNativeHook(newConnectNativeHookConfig(alloc, &config.ConsulConfig{ 278 Addr: consulConfig.Address, 279 }, logger)) 280 request := &interfaces.TaskPrestartRequest{ 281 Task: tg.Tasks[0], 282 TaskDir: allocDir.NewTaskDir(tg.Tasks[0].Name), 283 TaskEnv: taskenv.NewEmptyTaskEnv(), 284 } 285 require.NoError(t, request.TaskDir.Build(false, nil)) 286 287 response := new(interfaces.TaskPrestartResponse) 288 289 // Run the Connect Native hook 290 require.NoError(t, h.Prestart(context.Background(), request, response)) 291 292 // Assert the hook is Done 293 require.True(t, response.Done) 294 295 // Assert no environment variables configured to be set 296 require.Empty(t, response.Env) 297 298 // Assert no secrets were written 299 checkFilesInDir(t, request.TaskDir.SecretsDir, 300 nil, 301 []string{sidsTokenFile, secretCAFilename, secretCertfileFilename, secretKeyfileFilename}, 302 ) 303 } 304 305 func TestTaskRunner_ConnectNativeHook_with_SI_token(t *testing.T) { 306 t.Parallel() 307 testutil.RequireConsul(t) 308 309 testConsul := getTestConsul(t) 310 defer testConsul.Stop() 311 312 alloc := mock.Alloc() 313 alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{{Mode: "host", IP: "1.1.1.1"}} 314 tg := alloc.Job.TaskGroups[0] 315 tg.Services = []*structs.Service{{ 316 Name: "cn-service", 317 TaskName: tg.Tasks[0].Name, 318 Connect: &structs.ConsulConnect{ 319 Native: true, 320 }}, 321 } 322 tg.Tasks[0].Kind = structs.NewTaskKind("connect-native", "cn-service") 323 324 logger := testlog.HCLogger(t) 325 326 allocDir, cleanup := allocdir.TestAllocDir(t, logger, "ConnectNative") 327 defer cleanup() 328 329 // register group services 330 consulConfig := consulapi.DefaultConfig() 331 consulConfig.Address = testConsul.HTTPAddr 332 consulAPIClient, err := consulapi.NewClient(consulConfig) 333 require.NoError(t, err) 334 335 consulClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), logger, true) 336 go consulClient.Run() 337 defer consulClient.Shutdown() 338 require.NoError(t, consulClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter()))) 339 340 // Run Connect Native hook 341 h := newConnectNativeHook(newConnectNativeHookConfig(alloc, &config.ConsulConfig{ 342 Addr: consulConfig.Address, 343 }, logger)) 344 request := &interfaces.TaskPrestartRequest{ 345 Task: tg.Tasks[0], 346 TaskDir: allocDir.NewTaskDir(tg.Tasks[0].Name), 347 TaskEnv: taskenv.NewEmptyTaskEnv(), 348 } 349 require.NoError(t, request.TaskDir.Build(false, nil)) 350 351 // Insert service identity token in the secrets directory 352 token := uuid.Generate() 353 siTokenFile := filepath.Join(request.TaskDir.SecretsDir, sidsTokenFile) 354 err = ioutil.WriteFile(siTokenFile, []byte(token), 0440) 355 require.NoError(t, err) 356 357 response := new(interfaces.TaskPrestartResponse) 358 response.Env = make(map[string]string) 359 360 // Run the Connect Native hook 361 require.NoError(t, h.Prestart(context.Background(), request, response)) 362 363 // Assert the hook is Done 364 require.True(t, response.Done) 365 366 // Assert environment variable for token is set 367 require.NotEmpty(t, response.Env) 368 require.Equal(t, token, response.Env["CONSUL_HTTP_TOKEN"]) 369 370 // Assert no additional secrets were written 371 checkFilesInDir(t, request.TaskDir.SecretsDir, 372 []string{sidsTokenFile}, 373 []string{secretCAFilename, secretCertfileFilename, secretKeyfileFilename}, 374 ) 375 } 376 377 func TestTaskRunner_ConnectNativeHook_shareTLS(t *testing.T) { 378 t.Parallel() 379 testutil.RequireConsul(t) 380 381 try := func(t *testing.T, shareSSL *bool) { 382 fakeCert, fakeCertDir := setupCertDirs(t) 383 defer cleanupCertDirs(t, fakeCert, fakeCertDir) 384 385 testConsul := getTestConsul(t) 386 defer testConsul.Stop() 387 388 alloc := mock.Alloc() 389 alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{{Mode: "host", IP: "1.1.1.1"}} 390 tg := alloc.Job.TaskGroups[0] 391 tg.Services = []*structs.Service{{ 392 Name: "cn-service", 393 TaskName: tg.Tasks[0].Name, 394 Connect: &structs.ConsulConnect{ 395 Native: true, 396 }}, 397 } 398 tg.Tasks[0].Kind = structs.NewTaskKind("connect-native", "cn-service") 399 400 logger := testlog.HCLogger(t) 401 402 allocDir, cleanup := allocdir.TestAllocDir(t, logger, "ConnectNative") 403 defer cleanup() 404 405 // register group services 406 consulConfig := consulapi.DefaultConfig() 407 consulConfig.Address = testConsul.HTTPAddr 408 consulAPIClient, err := consulapi.NewClient(consulConfig) 409 require.NoError(t, err) 410 411 consulClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), logger, true) 412 go consulClient.Run() 413 defer consulClient.Shutdown() 414 require.NoError(t, consulClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter()))) 415 416 // Run Connect Native hook 417 h := newConnectNativeHook(newConnectNativeHookConfig(alloc, &config.ConsulConfig{ 418 Addr: consulConfig.Address, 419 420 // TLS config consumed by native application 421 ShareSSL: shareSSL, 422 EnableSSL: helper.BoolToPtr(true), 423 VerifySSL: helper.BoolToPtr(true), 424 CAFile: fakeCert, 425 CertFile: fakeCert, 426 KeyFile: fakeCert, 427 Auth: "user:password", 428 Token: uuid.Generate(), 429 }, logger)) 430 request := &interfaces.TaskPrestartRequest{ 431 Task: tg.Tasks[0], 432 TaskDir: allocDir.NewTaskDir(tg.Tasks[0].Name), 433 TaskEnv: taskenv.NewEmptyTaskEnv(), // nothing set in env stanza 434 } 435 require.NoError(t, request.TaskDir.Build(false, nil)) 436 437 response := new(interfaces.TaskPrestartResponse) 438 response.Env = make(map[string]string) 439 440 // Run the Connect Native hook 441 require.NoError(t, h.Prestart(context.Background(), request, response)) 442 443 // Assert the hook is Done 444 require.True(t, response.Done) 445 446 // Assert environment variable for token is set 447 require.NotEmpty(t, response.Env) 448 require.Equal(t, map[string]string{ 449 "CONSUL_CACERT": "/secrets/consul_ca_file.pem", 450 "CONSUL_CLIENT_CERT": "/secrets/consul_cert_file.pem", 451 "CONSUL_CLIENT_KEY": "/secrets/consul_key_file.pem", 452 "CONSUL_HTTP_SSL": "true", 453 "CONSUL_HTTP_SSL_VERIFY": "true", 454 }, response.Env) 455 require.NotContains(t, response.Env, "CONSUL_HTTP_AUTH") // explicitly not shared 456 require.NotContains(t, response.Env, "CONSUL_HTTP_TOKEN") // explicitly not shared 457 458 // Assert 3 pem files were written 459 checkFilesInDir(t, request.TaskDir.SecretsDir, 460 []string{secretCAFilename, secretCertfileFilename, secretKeyfileFilename}, 461 []string{sidsTokenFile}, 462 ) 463 } 464 465 // The default behavior is that share_ssl is true (similar to allow_unauthenticated) 466 // so make sure an unset value turns the feature on. 467 468 t.Run("share_ssl is true", func(t *testing.T) { 469 try(t, helper.BoolToPtr(true)) 470 }) 471 472 t.Run("share_ssl is nil", func(t *testing.T) { 473 try(t, nil) 474 }) 475 } 476 477 func checkFilesInDir(t *testing.T, dir string, includes, excludes []string) { 478 ls, err := ioutil.ReadDir(dir) 479 require.NoError(t, err) 480 481 var present []string 482 for _, fInfo := range ls { 483 present = append(present, fInfo.Name()) 484 } 485 486 for _, filename := range includes { 487 require.Contains(t, present, filename) 488 } 489 for _, filename := range excludes { 490 require.NotContains(t, present, filename) 491 } 492 } 493 494 func TestTaskRunner_ConnectNativeHook_shareTLS_override(t *testing.T) { 495 t.Parallel() 496 testutil.RequireConsul(t) 497 498 fakeCert, fakeCertDir := setupCertDirs(t) 499 defer cleanupCertDirs(t, fakeCert, fakeCertDir) 500 501 testConsul := getTestConsul(t) 502 defer testConsul.Stop() 503 504 alloc := mock.Alloc() 505 alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{{Mode: "host", IP: "1.1.1.1"}} 506 tg := alloc.Job.TaskGroups[0] 507 tg.Services = []*structs.Service{{ 508 Name: "cn-service", 509 TaskName: tg.Tasks[0].Name, 510 Connect: &structs.ConsulConnect{ 511 Native: true, 512 }}, 513 } 514 tg.Tasks[0].Kind = structs.NewTaskKind("connect-native", "cn-service") 515 516 logger := testlog.HCLogger(t) 517 518 allocDir, cleanup := allocdir.TestAllocDir(t, logger, "ConnectNative") 519 defer cleanup() 520 521 // register group services 522 consulConfig := consulapi.DefaultConfig() 523 consulConfig.Address = testConsul.HTTPAddr 524 consulAPIClient, err := consulapi.NewClient(consulConfig) 525 require.NoError(t, err) 526 527 consulClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), logger, true) 528 go consulClient.Run() 529 defer consulClient.Shutdown() 530 require.NoError(t, consulClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter()))) 531 532 // Run Connect Native hook 533 h := newConnectNativeHook(newConnectNativeHookConfig(alloc, &config.ConsulConfig{ 534 Addr: consulConfig.Address, 535 536 // TLS config consumed by native application 537 ShareSSL: helper.BoolToPtr(true), 538 EnableSSL: helper.BoolToPtr(true), 539 VerifySSL: helper.BoolToPtr(true), 540 CAFile: fakeCert, 541 CertFile: fakeCert, 542 KeyFile: fakeCert, 543 Auth: "user:password", 544 }, logger)) 545 546 taskEnv := taskenv.NewEmptyTaskEnv() 547 taskEnv.EnvMap = map[string]string{ 548 "CONSUL_CACERT": "/foo/ca.pem", 549 "CONSUL_CLIENT_CERT": "/foo/cert.pem", 550 "CONSUL_CLIENT_KEY": "/foo/key.pem", 551 "CONSUL_HTTP_AUTH": "foo:bar", 552 "CONSUL_HTTP_SSL_VERIFY": "false", 553 // CONSUL_HTTP_SSL (check the default value is assumed from client config) 554 } 555 556 request := &interfaces.TaskPrestartRequest{ 557 Task: tg.Tasks[0], 558 TaskDir: allocDir.NewTaskDir(tg.Tasks[0].Name), 559 TaskEnv: taskEnv, // env stanza is configured w/ non-default tls configs 560 } 561 require.NoError(t, request.TaskDir.Build(false, nil)) 562 563 response := new(interfaces.TaskPrestartResponse) 564 response.Env = make(map[string]string) 565 566 // Run the Connect Native hook 567 require.NoError(t, h.Prestart(context.Background(), request, response)) 568 569 // Assert the hook is Done 570 require.True(t, response.Done) 571 572 // Assert environment variable for CONSUL_HTTP_SSL is set, because it was 573 // the only one not overridden by task env stanza config 574 require.NotEmpty(t, response.Env) 575 require.Equal(t, map[string]string{ 576 "CONSUL_HTTP_SSL": "true", 577 }, response.Env) 578 579 // Assert 3 pem files were written (even though they will be ignored) 580 // as this is gated by share_tls, not the presense of ca environment variables. 581 checkFilesInDir(t, request.TaskDir.SecretsDir, 582 []string{secretCAFilename, secretCertfileFilename, secretKeyfileFilename}, 583 []string{sidsTokenFile}, 584 ) 585 }