From bd0626ef3f9e0c6b83a18645b7aefd352e88ed8e Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Mon, 11 Dec 2023 17:53:22 -0500 Subject: [PATCH] vault: load default config for tasks without vault It is often expected that a task that needs access to Vault defines a `vault` block to specify the Vault policy to use to derive a token. But in some scenarios, like when the Nomad client is connected to a local Vault agent that is responsible for authn/authz, the task is not required to defined a `vault` block. In these situations, the `default` Vault cluster should be used to render the template. --- .../allocrunner/taskrunner/template_hook.go | 13 +- .../taskrunner/template_hook_test.go | 132 ++++++++++++++++++ 2 files changed, 137 insertions(+), 8 deletions(-) diff --git a/client/allocrunner/taskrunner/template_hook.go b/client/allocrunner/taskrunner/template_hook.go index 3824dd02a8b..3cd8c412deb 100644 --- a/client/allocrunner/taskrunner/template_hook.go +++ b/client/allocrunner/taskrunner/template_hook.go @@ -16,7 +16,6 @@ import ( cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/client/taskenv" "github.com/hashicorp/nomad/nomad/structs" - structsc "github.com/hashicorp/nomad/nomad/structs/config" ) const ( @@ -212,14 +211,12 @@ func (h *templateHook) Poststart(ctx context.Context, req *interfaces.TaskPostst func (h *templateHook) newManager() (unblock chan struct{}, err error) { unblock = make(chan struct{}) - var vaultConfig *structsc.VaultConfig - if h.task.Vault != nil { - vaultCluster := h.task.GetVaultClusterName() - vaultConfig = h.config.clientConfig.GetVaultConfigs(h.logger)[vaultCluster] + vaultCluster := h.task.GetVaultClusterName() + vaultConfig := h.config.clientConfig.GetVaultConfigs(h.logger)[vaultCluster] - if vaultConfig == nil { - return nil, fmt.Errorf("Vault cluster %q is disabled or not configured", vaultCluster) - } + // Fail if task has a vault block but not client config was found. + if h.task.Vault != nil && vaultConfig == nil { + return nil, fmt.Errorf("Vault cluster %q is disabled or not configured", vaultCluster) } tg := h.config.alloc.Job.LookupTaskGroup(h.config.alloc.TaskGroup) diff --git a/client/allocrunner/taskrunner/template_hook_test.go b/client/allocrunner/taskrunner/template_hook_test.go index 2b09513ebea..91566a3a17b 100644 --- a/client/allocrunner/taskrunner/template_hook_test.go +++ b/client/allocrunner/taskrunner/template_hook_test.go @@ -6,8 +6,12 @@ package taskrunner import ( "context" "fmt" + "net/http" + "net/http/httptest" + "path" "sync" "testing" + "time" consulapi "github.com/hashicorp/consul/api" "github.com/hashicorp/nomad/ci" @@ -17,10 +21,12 @@ import ( "github.com/hashicorp/nomad/client/config" cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/client/taskenv" + "github.com/hashicorp/nomad/helper/pointer" "github.com/hashicorp/nomad/helper/testlog" "github.com/hashicorp/nomad/helper/uuid" "github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/structs" + structsc "github.com/hashicorp/nomad/nomad/structs/config" "github.com/shoenig/test/must" ) @@ -135,3 +141,129 @@ func Test_templateHook_Prestart_ConsulWI(t *testing.T) { }) } } + +func Test_templateHook_Prestart_Vault(t *testing.T) { + ci.Parallel(t) + + secretsResp := ` +{ + "data": { + "data": { + "secret": "secret" + }, + "metadata": { + "created_time": "2023-10-18T15:58:29.65137Z", + "custom_metadata": null, + "deletion_time": "", + "destroyed": false, + "version": 1 + } + } +}` + + // Start test server to simulate Vault cluster responses. + reqCh := make(chan any) + defaultVaultServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + reqCh <- struct{}{} + fmt.Fprintln(w, secretsResp) + })) + t.Cleanup(defaultVaultServer.Close) + + // Setup client with Vault config. + clientConfig := config.DefaultConfig() + clientConfig.TemplateConfig.DisableSandbox = true + clientConfig.VaultConfigs = map[string]*structsc.VaultConfig{ + structs.VaultDefaultCluster: { + Name: structs.VaultDefaultCluster, + Enabled: pointer.Of(true), + Addr: defaultVaultServer.URL, + }, + } + + testCases := []struct { + name string + vault *structs.Vault + expectedCluster string + }{ + { + name: "use default cluster", + vault: &structs.Vault{ + Cluster: structs.VaultDefaultCluster, + }, + expectedCluster: structs.VaultDefaultCluster, + }, + { + name: "use default cluster if no vault block is provided", + vault: nil, + expectedCluster: structs.VaultDefaultCluster, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Setup alloc and task to connect to Vault cluster. + alloc := mock.MinAlloc() + task := alloc.Job.TaskGroups[0].Tasks[0] + task.Vault = tc.vault + + // Setup template hook. + taskDir := t.TempDir() + hookConfig := &templateHookConfig{ + alloc: alloc, + logger: testlog.HCLogger(t), + lifecycle: trtesting.NewMockTaskHooks(), + events: &trtesting.MockEmitter{}, + clientConfig: clientConfig, + envBuilder: taskenv.NewBuilder(mock.Node(), alloc, task, clientConfig.Region), + templates: []*structs.Template{ + { + EmbeddedTmpl: `{{with secret "secret/data/test"}}{{.Data.data.secret}}{{end}}`, + ChangeMode: structs.TemplateChangeModeNoop, + DestPath: path.Join(taskDir, "out.txt"), + }, + }, + } + hook := newTemplateHook(hookConfig) + + // Start template hook with a timeout context to ensure it exists. + req := &interfaces.TaskPrestartRequest{ + Task: task, + TaskDir: &allocdir.TaskDir{Dir: taskDir}, + } + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + t.Cleanup(cancel) + + // Start in a goroutine because Prestart() blocks until first + // render. + hookErrCh := make(chan error) + go func() { + err := hook.Prestart(ctx, req, nil) + hookErrCh <- err + }() + + var gotRequest bool + LOOP: + for { + select { + // Register mock Vault server received a request. + case <-reqCh: + gotRequest = true + + // Verify test doesn't timeout. + case <-ctx.Done(): + must.NoError(t, ctx.Err()) + return + + // Verify hook.Prestart() doesn't errors. + case err := <-hookErrCh: + must.NoError(t, err) + break LOOP + } + } + + // Verify mock Vault server received a request. + must.True(t, gotRequest) + }) + } +}