mirror of
https://github.com/kemko/nomad.git
synced 2026-01-04 17:35:43 +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
287 lines
8.0 KiB
Go
287 lines
8.0 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package allocrunner
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"testing"
|
|
|
|
consulapi "github.com/hashicorp/consul/api"
|
|
"github.com/hashicorp/nomad/ci"
|
|
"github.com/hashicorp/nomad/client/allocrunner/interfaces"
|
|
"github.com/hashicorp/nomad/client/consul"
|
|
cstate "github.com/hashicorp/nomad/client/state"
|
|
cstructs "github.com/hashicorp/nomad/client/structs"
|
|
"github.com/hashicorp/nomad/client/taskenv"
|
|
"github.com/hashicorp/nomad/client/widmgr"
|
|
"github.com/hashicorp/nomad/helper/testlog"
|
|
"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"
|
|
)
|
|
|
|
// statically assert consul hook implements the expected interfaces
|
|
var _ interfaces.RunnerPrerunHook = (*consulHook)(nil)
|
|
|
|
func consulHookTestHarness(t *testing.T) *consulHook {
|
|
logger := testlog.HCLogger(t)
|
|
|
|
alloc := mock.Alloc()
|
|
|
|
task1 := alloc.LookupTask("web")
|
|
|
|
task1.Consul = &structs.Consul{
|
|
Cluster: "default",
|
|
}
|
|
task1.Identities = []*structs.WorkloadIdentity{
|
|
{Name: fmt.Sprintf("%s_default", structs.ConsulTaskIdentityNamePrefix)},
|
|
}
|
|
|
|
task2 := task1.Copy()
|
|
task2.Name = "extra"
|
|
task2.Services = nil
|
|
alloc.Job.TaskGroups[0].Tasks = append(alloc.Job.TaskGroups[0].Tasks, task2)
|
|
|
|
task1.Services = []*structs.Service{
|
|
{
|
|
Provider: structs.ServiceProviderConsul,
|
|
Identity: &structs.WorkloadIdentity{Name: "consul-service_webservice", Audience: []string{"consul.io"}},
|
|
Cluster: "default",
|
|
Name: "${NOMAD_TASK_NAME}service",
|
|
TaskName: "web", // note: this doesn't interpolate
|
|
},
|
|
}
|
|
|
|
// setup mock signer but don't sign identities, as we're going to want them
|
|
// interpolated by the WIDMgr
|
|
mockSigner := widmgr.NewMockWIDSigner(nil)
|
|
db := cstate.NewMemDB(logger)
|
|
|
|
// the WIDMgr env builder never has the task available
|
|
envBuilder := taskenv.NewBuilder(mock.Node(), alloc, nil, "global")
|
|
|
|
mockWIDMgr := widmgr.NewWIDMgr(mockSigner, alloc, db, logger, envBuilder)
|
|
mockWIDMgr.SignForTesting()
|
|
|
|
consulConfigs := map[string]*structsc.ConsulConfig{
|
|
"default": structsc.DefaultConsulConfig(),
|
|
}
|
|
|
|
hookResources := cstructs.NewAllocHookResources()
|
|
envBuilderFn := func() *taskenv.Builder {
|
|
return taskenv.NewBuilder(mock.Node(), alloc, nil, "global")
|
|
}
|
|
|
|
consulHookCfg := consulHookConfig{
|
|
alloc: alloc,
|
|
allocdir: nil,
|
|
widmgr: mockWIDMgr,
|
|
consulConfigs: consulConfigs,
|
|
consulClientConstructor: consul.NewMockConsulClient,
|
|
hookResources: hookResources,
|
|
envBuilder: envBuilderFn,
|
|
logger: logger,
|
|
}
|
|
return newConsulHook(consulHookCfg)
|
|
}
|
|
|
|
func Test_consulHook_prepareConsulTokensForTask(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
hook := consulHookTestHarness(t)
|
|
task := hook.alloc.LookupTask("web")
|
|
|
|
wid := task.GetIdentity("consul_default")
|
|
ti := *task.IdentityHandle(wid)
|
|
jwt, err := hook.widmgr.Get(ti)
|
|
must.NoError(t, err)
|
|
hashJWT1 := md5.Sum([]byte(jwt.JWT))
|
|
|
|
task2 := hook.alloc.LookupTask("extra")
|
|
ti2 := *task2.IdentityHandle(wid)
|
|
jwt2, err := hook.widmgr.Get(ti2)
|
|
must.NoError(t, err)
|
|
hashJWT2 := md5.Sum([]byte(jwt2.JWT))
|
|
|
|
tests := []struct {
|
|
name string
|
|
tasks []*structs.Task
|
|
wantErr bool
|
|
errMsg string
|
|
expectedTokens map[string]map[string]*consulapi.ACLToken
|
|
}{
|
|
{
|
|
name: "empty task",
|
|
tasks: nil,
|
|
wantErr: true,
|
|
errMsg: "no task specified",
|
|
expectedTokens: map[string]map[string]*consulapi.ACLToken{},
|
|
},
|
|
{
|
|
name: "task with signed identity",
|
|
tasks: []*structs.Task{task},
|
|
wantErr: false,
|
|
errMsg: "",
|
|
expectedTokens: map[string]map[string]*consulapi.ACLToken{
|
|
"default": {
|
|
"consul_default/web": &consulapi.ACLToken{
|
|
AccessorID: hex.EncodeToString(hashJWT1[:]),
|
|
SecretID: hex.EncodeToString(hashJWT1[:]),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "multiple tasks with signed identities",
|
|
tasks: []*structs.Task{task, task2},
|
|
wantErr: false,
|
|
errMsg: "",
|
|
expectedTokens: map[string]map[string]*consulapi.ACLToken{
|
|
"default": {
|
|
"consul_default/web": &consulapi.ACLToken{
|
|
AccessorID: hex.EncodeToString(hashJWT1[:]),
|
|
SecretID: hex.EncodeToString(hashJWT1[:]),
|
|
},
|
|
"consul_default/extra": &consulapi.ACLToken{
|
|
AccessorID: hex.EncodeToString(hashJWT2[:]),
|
|
SecretID: hex.EncodeToString(hashJWT2[:]),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "task with unknown identity",
|
|
tasks: []*structs.Task{{
|
|
Identities: []*structs.WorkloadIdentity{
|
|
{Name: structs.ConsulTaskIdentityNamePrefix + "_default"}},
|
|
Name: "foo",
|
|
}},
|
|
wantErr: true,
|
|
errMsg: "unable to find token for workload",
|
|
expectedTokens: map[string]map[string]*consulapi.ACLToken{},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tokens := map[string]map[string]*consulapi.ACLToken{}
|
|
for _, task := range tt.tasks {
|
|
err := hook.prepareConsulTokensForTask(task, nil, tokens)
|
|
if tt.wantErr {
|
|
must.Error(t, err)
|
|
must.ErrorContains(t, err, tt.errMsg)
|
|
} else {
|
|
must.NoError(t, err)
|
|
}
|
|
}
|
|
must.Eq(t, tt.expectedTokens, tokens)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_consulHook_prepareConsulTokensForServices(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
hook := consulHookTestHarness(t)
|
|
task := hook.alloc.LookupTask("web")
|
|
services := task.Services
|
|
hook.envBuilder.UpdateTask(hook.alloc, task)
|
|
env := hook.envBuilder.Build()
|
|
hashedJWT := make(map[string]string)
|
|
|
|
for _, s := range services {
|
|
widHandle := *s.IdentityHandle(env.ReplaceEnv)
|
|
jwt, err := hook.widmgr.Get(widHandle)
|
|
must.NoError(t, err)
|
|
|
|
hash := md5.Sum([]byte(jwt.JWT))
|
|
hashedJWT[widHandle.InterpolatedWorkloadIdentifier] = hex.EncodeToString(hash[:])
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
services []*structs.Service
|
|
wantErr bool
|
|
errMsg string
|
|
expectedTokens map[string]map[string]*consulapi.ACLToken
|
|
}{
|
|
{
|
|
name: "empty services",
|
|
services: nil,
|
|
wantErr: false,
|
|
errMsg: "",
|
|
expectedTokens: map[string]map[string]*consulapi.ACLToken{},
|
|
},
|
|
{
|
|
name: "services with signed identity",
|
|
services: services,
|
|
wantErr: false,
|
|
errMsg: "",
|
|
expectedTokens: map[string]map[string]*consulapi.ACLToken{
|
|
"default": {
|
|
"consul-service_webservice": {
|
|
AccessorID: hashedJWT["webservice"],
|
|
SecretID: hashedJWT["webservice"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "services with unknown identity",
|
|
services: []*structs.Service{
|
|
{
|
|
Provider: structs.ServiceProviderConsul,
|
|
Identity: &structs.WorkloadIdentity{
|
|
Name: "consul-service_webservice", Audience: []string{"consul.io"}},
|
|
Cluster: "default",
|
|
Name: "foo",
|
|
TaskName: "web",
|
|
},
|
|
},
|
|
wantErr: true,
|
|
errMsg: "unable to find token for workload",
|
|
expectedTokens: map[string]map[string]*consulapi.ACLToken{},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tokens := map[string]map[string]*consulapi.ACLToken{}
|
|
err := hook.prepareConsulTokensForServices(tt.services, nil, tokens, env)
|
|
if tt.wantErr {
|
|
must.Error(t, err)
|
|
must.ErrorContains(t, err, tt.errMsg)
|
|
} else {
|
|
must.NoError(t, err)
|
|
must.Eq(t, tt.expectedTokens, tokens)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_consulHook_Postrun(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
// no-op must be safe
|
|
hook := consulHookTestHarness(t)
|
|
must.NoError(t, hook.Postrun())
|
|
|
|
task := hook.alloc.LookupTask("web")
|
|
tokens := map[string]map[string]*consulapi.ACLToken{}
|
|
must.NoError(t, hook.prepareConsulTokensForTask(task, nil, tokens))
|
|
hook.hookResources.SetConsulTokens(tokens)
|
|
must.MapLen(t, 1, tokens)
|
|
|
|
// gracefully handle wrong tokens
|
|
otherTokens := map[string]map[string]*consulapi.ACLToken{
|
|
"default": {"foo": &consulapi.ACLToken{AccessorID: "foo", SecretID: "foo"}}}
|
|
must.NoError(t, hook.revokeTokens(otherTokens))
|
|
|
|
// hook resources should be cleared
|
|
must.NoError(t, hook.Postrun())
|
|
tokens = hook.hookResources.GetConsulTokens()
|
|
must.MapEmpty(t, tokens["default"])
|
|
}
|