mirror of
https://github.com/kemko/nomad.git
synced 2026-01-04 09:25:46 +03:00
The `consul_hook` in the allocrunner gets a separate Consul token for each task, even if the tasks' identities have the same name, but used the identity name as the key to the alloc hook resources map. This means the last task in the group overwrites the Consul tokens of all other tasks. Fix this by adding the task name to the key in the allocrunner's `consul_hook`. And update the taskrunner's `consul_hook` to expect the task name in the key. Fixes: https://github.com/hashicorp/nomad/issues/20374 Fixes: https://hashicorp.atlassian.net/browse/NOMAD-614
272 lines
6.9 KiB
Go
272 lines
6.9 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/web", 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: "legacy 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{
|
|
Alloc: a,
|
|
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{
|
|
Alloc: alloc,
|
|
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)
|
|
})
|
|
}
|
|
}
|