Files
nomad/client/allocrunner/taskrunner/template_hook_test.go
Luiz Aoqui 0bc822db40 vault: load default config for tasks without vault (#19439)
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.
2023-12-12 14:06:55 -05:00

270 lines
6.8 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
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"
"github.com/hashicorp/nomad/client/allocdir"
"github.com/hashicorp/nomad/client/allocrunner/interfaces"
trtesting "github.com/hashicorp/nomad/client/allocrunner/taskrunner/testing"
"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"
)
func Test_templateHook_Prestart_ConsulWI(t *testing.T) {
ci.Parallel(t)
logger := testlog.HCLogger(t)
// Create some alloc hook resources, one with tokens and an empty one.
defaultToken := uuid.Generate()
hrTokens := cstructs.NewAllocHookResources()
hrTokens.SetConsulTokens(
map[string]map[string]*consulapi.ACLToken{
structs.ConsulDefaultCluster: {
fmt.Sprintf("consul_%s", structs.ConsulDefaultCluster): &consulapi.ACLToken{
SecretID: defaultToken,
},
},
},
)
hrEmpty := cstructs.NewAllocHookResources()
tests := []struct {
name string
taskConsul *structs.Consul
groupConsul *structs.Consul
hr *cstructs.AllocHookResources
wantErrMsg string
wantConsulToken string
legacyFlow bool
}{
{
// COMPAT remove in 1.9+
name: "legecy flow",
hr: hrEmpty,
legacyFlow: true,
wantConsulToken: "",
},
{
name: "task missing Consul token",
hr: hrEmpty,
wantErrMsg: "not found",
},
{
name: "task without consul blocks uses default cluster",
hr: hrTokens,
wantConsulToken: defaultToken,
},
{
name: "task with consul block at task level",
hr: hrTokens,
taskConsul: &structs.Consul{
Cluster: structs.ConsulDefaultCluster,
},
wantConsulToken: defaultToken,
},
{
name: "task with consul block at group level",
hr: hrTokens,
groupConsul: &structs.Consul{
Cluster: structs.ConsulDefaultCluster,
},
wantConsulToken: defaultToken,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := mock.Alloc()
task := a.Job.TaskGroups[0].Tasks[0]
if !tt.legacyFlow {
task.Identities = []*structs.WorkloadIdentity{
{Name: fmt.Sprintf("%s_%s",
structs.ConsulTaskIdentityNamePrefix,
structs.ConsulDefaultCluster,
)},
}
}
clientConfig := &config.Config{Region: "global"}
envBuilder := taskenv.NewBuilder(mock.Node(), a, task, clientConfig.Region)
taskHooks := trtesting.NewMockTaskHooks()
conf := &templateHookConfig{
alloc: a,
logger: logger,
lifecycle: taskHooks,
events: &trtesting.MockEmitter{},
clientConfig: clientConfig,
envBuilder: envBuilder,
hookResources: tt.hr,
}
h := &templateHook{
config: conf,
logger: logger,
managerLock: sync.Mutex{},
driverHandle: nil,
}
req := &interfaces.TaskPrestartRequest{
Task: a.Job.TaskGroups[0].Tasks[0],
TaskDir: &allocdir.TaskDir{Dir: "foo"},
}
err := h.Prestart(context.Background(), req, nil)
if tt.wantErrMsg != "" {
must.Error(t, err)
must.ErrorContains(t, err, tt.wantErrMsg)
} else {
must.NoError(t, err)
}
must.Eq(t, tt.wantConsulToken, h.consulToken)
})
}
}
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)
})
}
}