task: adds ability to interpret values from secrets hook (#26261)

This commit is contained in:
Michael Smithhisler
2025-07-14 12:54:05 -04:00
parent 2d0ce43c47
commit 85a2875183
8 changed files with 74 additions and 43 deletions

View File

@@ -88,7 +88,7 @@ func TestTaskRunner_ArtifactHook_PartialDone(t *testing.T) {
_, destdir := getter.SetupDir(t) _, destdir := getter.SetupDir(t)
req := &interfaces.TaskPrestartRequest{ req := &interfaces.TaskPrestartRequest{
TaskEnv: taskenv.NewTaskEnv(nil, nil, nil, nil, destdir, ""), TaskEnv: taskenv.NewTaskEnv(nil, nil, nil, nil, nil, destdir, ""),
TaskDir: &allocdir.TaskDir{Dir: destdir}, TaskDir: &allocdir.TaskDir{Dir: destdir},
Task: &structs.Task{ Task: &structs.Task{
Artifacts: []*structs.TaskArtifact{ Artifacts: []*structs.TaskArtifact{
@@ -180,7 +180,7 @@ func TestTaskRunner_ArtifactHook_ConcurrentDownloadSuccess(t *testing.T) {
_, destdir := getter.SetupDir(t) _, destdir := getter.SetupDir(t)
req := &interfaces.TaskPrestartRequest{ req := &interfaces.TaskPrestartRequest{
TaskEnv: taskenv.NewTaskEnv(nil, nil, nil, nil, destdir, ""), TaskEnv: taskenv.NewTaskEnv(nil, nil, nil, nil, nil, destdir, ""),
TaskDir: &allocdir.TaskDir{Dir: destdir}, TaskDir: &allocdir.TaskDir{Dir: destdir},
Task: &structs.Task{ Task: &structs.Task{
Artifacts: []*structs.TaskArtifact{ Artifacts: []*structs.TaskArtifact{
@@ -271,7 +271,7 @@ func TestTaskRunner_ArtifactHook_ConcurrentDownloadFailure(t *testing.T) {
_, destdir := getter.SetupDir(t) _, destdir := getter.SetupDir(t)
req := &interfaces.TaskPrestartRequest{ req := &interfaces.TaskPrestartRequest{
TaskEnv: taskenv.NewTaskEnv(nil, nil, nil, nil, destdir, ""), TaskEnv: taskenv.NewTaskEnv(nil, nil, nil, nil, nil, destdir, ""),
TaskDir: &allocdir.TaskDir{Dir: destdir}, TaskDir: &allocdir.TaskDir{Dir: destdir},
Task: &structs.Task{ Task: &structs.Task{
Artifacts: []*structs.TaskArtifact{ Artifacts: []*structs.TaskArtifact{

View File

@@ -26,7 +26,7 @@ var (
taskEnvDefault = taskenv.NewTaskEnv(nil, nil, nil, map[string]string{ taskEnvDefault = taskenv.NewTaskEnv(nil, nil, nil, map[string]string{
"meta.connect.sidecar_image": envoy.ImageFormat, "meta.connect.sidecar_image": envoy.ImageFormat,
"meta.connect.gateway_image": envoy.ImageFormat, "meta.connect.gateway_image": envoy.ImageFormat,
}, "", "") }, nil, "", "")
) )
func TestEnvoyVersionHook_semver(t *testing.T) { func TestEnvoyVersionHook_semver(t *testing.T) {
@@ -147,7 +147,7 @@ func TestEnvoyVersionHook_interpolateImage(t *testing.T) {
"MY_ENVOY": "my/envoy", "MY_ENVOY": "my/envoy",
}, map[string]string{ }, map[string]string{
"MY_ENVOY": "my/envoy", "MY_ENVOY": "my/envoy",
}, nil, nil, "", "")) }, nil, nil, nil, "", ""))
must.Eq(t, "my/envoy", task.Config["image"]) must.Eq(t, "my/envoy", task.Config["image"])
}) })

View File

@@ -6,7 +6,6 @@ package taskrunner
import ( import (
"context" "context"
"fmt" "fmt"
"maps"
"path/filepath" "path/filepath"
log "github.com/hashicorp/go-hclog" log "github.com/hashicorp/go-hclog"
@@ -73,9 +72,6 @@ type secretsHook struct {
// secrets to be fetched and populated for interpolation // secrets to be fetched and populated for interpolation
secrets []*structs.Secret secrets []*structs.Secret
// taskrunner secrets map
taskSecrets map[string]string
} }
func newSecretsHook(conf *secretsHookConfig, secrets []*structs.Secret) *secretsHook { func newSecretsHook(conf *secretsHookConfig, secrets []*structs.Secret) *secretsHook {
@@ -87,9 +83,6 @@ func newSecretsHook(conf *secretsHookConfig, secrets []*structs.Secret) *secrets
envBuilder: conf.envBuilder, envBuilder: conf.envBuilder,
nomadNamespace: conf.nomadNamespace, nomadNamespace: conf.nomadNamespace,
secrets: secrets, secrets: secrets,
// Future work will inject taskSecrets from the taskRunner, so that the taskrunner
// can make these secrets available to other hooks.
taskSecrets: make(map[string]string),
} }
} }
@@ -146,13 +139,13 @@ func (h *secretsHook) Prestart(ctx context.Context, req *interfaces.TaskPrestart
case <-unblock: case <-unblock:
} }
// parse and copy variables to taskSecrets // parse and copy variables to envBuilder secrets
for _, p := range providers { for _, p := range providers {
vars, err := p.Parse() vars, err := p.Parse()
if err != nil { if err != nil {
return err return err
} }
maps.Copy(h.taskSecrets, vars) h.envBuilder.SetSecrets(vars)
} }
return nil return nil

View File

@@ -66,12 +66,13 @@ func TestSecretsHook_Prestart_Nomad(t *testing.T) {
alloc := mock.MinAlloc() alloc := mock.MinAlloc()
task := alloc.Job.TaskGroups[0].Tasks[0] task := alloc.Job.TaskGroups[0].Tasks[0]
taskEnv := taskenv.NewBuilder(mock.Node(), alloc, task, clientConfig.Region)
conf := &secretsHookConfig{ conf := &secretsHookConfig{
logger: testlog.HCLogger(t), logger: testlog.HCLogger(t),
lifecycle: trtesting.NewMockTaskHooks(), lifecycle: trtesting.NewMockTaskHooks(),
events: &trtesting.MockEmitter{}, events: &trtesting.MockEmitter{},
clientConfig: clientConfig, clientConfig: clientConfig,
envBuilder: taskenv.NewBuilder(mock.Node(), alloc, task, clientConfig.Region), envBuilder: taskEnv,
} }
secretHook := newSecretsHook(conf, []*structs.Secret{ secretHook := newSecretsHook(conf, []*structs.Secret{
{ {
@@ -100,7 +101,7 @@ func TestSecretsHook_Prestart_Nomad(t *testing.T) {
"secret.test_secret.key1": "value1", "secret.test_secret.key1": "value1",
"secret.test_secret.key2": "value2", "secret.test_secret.key2": "value2",
} }
must.Eq(t, expected, secretHook.taskSecrets) must.Eq(t, expected, taskEnv.Build().TaskSecrets)
}) })
t.Run("returns early if context is cancelled", func(t *testing.T) { t.Run("returns early if context is cancelled", func(t *testing.T) {
@@ -140,13 +141,13 @@ func TestSecretsHook_Prestart_Nomad(t *testing.T) {
alloc := mock.MinAlloc() alloc := mock.MinAlloc()
task := alloc.Job.TaskGroups[0].Tasks[0] task := alloc.Job.TaskGroups[0].Tasks[0]
taskEnv := taskenv.NewBuilder(mock.Node(), alloc, task, clientConfig.Region)
conf := &secretsHookConfig{ conf := &secretsHookConfig{
logger: testlog.HCLogger(t), logger: testlog.HCLogger(t),
lifecycle: trtesting.NewMockTaskHooks(), lifecycle: trtesting.NewMockTaskHooks(),
events: &trtesting.MockEmitter{}, events: &trtesting.MockEmitter{},
clientConfig: clientConfig, clientConfig: clientConfig,
envBuilder: taskenv.NewBuilder(mock.Node(), alloc, task, clientConfig.Region), envBuilder: taskEnv,
} }
secretHook := newSecretsHook(conf, []*structs.Secret{ secretHook := newSecretsHook(conf, []*structs.Secret{
{ {
@@ -172,7 +173,7 @@ func TestSecretsHook_Prestart_Nomad(t *testing.T) {
must.NoError(t, err) must.NoError(t, err)
expected := map[string]string{} expected := map[string]string{}
must.Eq(t, expected, secretHook.taskSecrets) must.Eq(t, expected, taskEnv.Build().TaskSecrets)
}) })
t.Run("errors when failure building secret providers", func(t *testing.T) { t.Run("errors when failure building secret providers", func(t *testing.T) {
@@ -182,13 +183,13 @@ func TestSecretsHook_Prestart_Nomad(t *testing.T) {
alloc := mock.MinAlloc() alloc := mock.MinAlloc()
task := alloc.Job.TaskGroups[0].Tasks[0] task := alloc.Job.TaskGroups[0].Tasks[0]
taskEnv := taskenv.NewBuilder(mock.Node(), alloc, task, clientConfig.Region)
conf := &secretsHookConfig{ conf := &secretsHookConfig{
logger: testlog.HCLogger(t), logger: testlog.HCLogger(t),
lifecycle: trtesting.NewMockTaskHooks(), lifecycle: trtesting.NewMockTaskHooks(),
events: &trtesting.MockEmitter{}, events: &trtesting.MockEmitter{},
clientConfig: clientConfig, clientConfig: clientConfig,
envBuilder: taskenv.NewBuilder(mock.Node(), alloc, task, clientConfig.Region), envBuilder: taskEnv,
} }
// give an invalid secret, in this case a nomad secret with bad namespace // give an invalid secret, in this case a nomad secret with bad namespace
@@ -214,7 +215,7 @@ func TestSecretsHook_Prestart_Nomad(t *testing.T) {
must.Error(t, err) must.Error(t, err)
expected := map[string]string{} expected := map[string]string{}
must.Eq(t, expected, secretHook.taskSecrets) must.Eq(t, expected, taskEnv.Build().TaskSecrets)
}) })
} }
@@ -259,14 +260,13 @@ func TestSecretsHook_Prestart_Vault(t *testing.T) {
alloc := mock.MinAlloc() alloc := mock.MinAlloc()
task := alloc.Job.TaskGroups[0].Tasks[0] task := alloc.Job.TaskGroups[0].Tasks[0]
taskEnv := taskenv.NewBuilder(mock.Node(), alloc, task, clientConfig.Region)
conf := &secretsHookConfig{ conf := &secretsHookConfig{
// alloc: alloc,
logger: testlog.HCLogger(t), logger: testlog.HCLogger(t),
lifecycle: trtesting.NewMockTaskHooks(), lifecycle: trtesting.NewMockTaskHooks(),
events: &trtesting.MockEmitter{}, events: &trtesting.MockEmitter{},
clientConfig: clientConfig, clientConfig: clientConfig,
envBuilder: taskenv.NewBuilder(mock.Node(), alloc, task, clientConfig.Region), envBuilder: taskEnv,
} }
secretHook := newSecretsHook(conf, []*structs.Secret{ secretHook := newSecretsHook(conf, []*structs.Secret{
{ {
@@ -296,5 +296,5 @@ func TestSecretsHook_Prestart_Vault(t *testing.T) {
"secret.test_secret.secret": "secret", "secret.test_secret.secret": "secret",
} }
must.Eq(t, exp, secretHook.taskSecrets) must.Eq(t, exp, taskEnv.Build().TaskSecrets)
} }

View File

@@ -1699,6 +1699,7 @@ func TestTaskTemplateManager_Env_InterpolatedDest(t *testing.T) {
map[string]string{"NOMAD_META_path": "exists"}, map[string]string{"NOMAD_META_path": "exists"},
map[string]string{}, map[string]string{},
map[string]string{}, map[string]string{},
map[string]string{},
d, "") d, "")
vars, err := loadTemplateEnv(templates, taskEnv) vars, err := loadTemplateEnv(templates, taskEnv)

View File

@@ -154,8 +154,8 @@ const (
nodeMetaPrefix = "meta." nodeMetaPrefix = "meta."
) )
// TaskEnv is a task's environment as well as node attribute's for // TaskEnv is a task's environment as well as node attribute's and
// interpolation. // task secrets for interpolation.
type TaskEnv struct { type TaskEnv struct {
// NodeAttrs is the map of node attributes for interpolation // NodeAttrs is the map of node attributes for interpolation
NodeAttrs map[string]string NodeAttrs map[string]string
@@ -163,6 +163,9 @@ type TaskEnv struct {
// EnvMap is the map of environment variables // EnvMap is the map of environment variables
EnvMap map[string]string EnvMap map[string]string
// TaskSecrets is the map of secrets populated from the secrets hook
TaskSecrets map[string]string
// deviceEnv is the environment variables populated from the device hooks. // deviceEnv is the environment variables populated from the device hooks.
deviceEnv map[string]string deviceEnv map[string]string
@@ -185,11 +188,12 @@ type TaskEnv struct {
// NewTaskEnv creates a new task environment with the given environment, device // NewTaskEnv creates a new task environment with the given environment, device
// environment and node attribute maps. // environment and node attribute maps.
func NewTaskEnv(env, envClient, deviceEnv, node map[string]string, clientTaskDir, clientAllocDir string) *TaskEnv { func NewTaskEnv(env, envClient, deviceEnv, node map[string]string, secrets map[string]string, clientTaskDir, clientAllocDir string) *TaskEnv {
return &TaskEnv{ return &TaskEnv{
NodeAttrs: node, NodeAttrs: node,
deviceEnv: deviceEnv, deviceEnv: deviceEnv,
EnvMap: env, EnvMap: env,
TaskSecrets: secrets,
EnvMapClient: envClient, EnvMapClient: envClient,
clientTaskDir: clientTaskDir, clientTaskDir: clientTaskDir,
clientSharedAllocDir: clientAllocDir, clientSharedAllocDir: clientAllocDir,
@@ -311,6 +315,13 @@ func (t *TaskEnv) AllValues() (map[string]cty.Value, map[string]error, error) {
} }
} }
// Prepare task-based secrets for use in interpolation
for k, v := range t.TaskSecrets {
if err := addNestedKey(allMap, k, v); err != nil {
errs[k] = err
}
}
// Add flat envMap as a Map to allMap so users can access any key via // Add flat envMap as a Map to allMap so users can access any key via
// HCL2's indexing syntax: ${env["foo...bar"]} // HCL2's indexing syntax: ${env["foo...bar"]}
allMap["env"] = cty.MapVal(envMap) allMap["env"] = cty.MapVal(envMap)
@@ -359,10 +370,10 @@ func (t *TaskEnv) ParseAndReplace(args []string) []string {
} }
// ReplaceEnv takes an arg and replaces all occurrences of environment variables // ReplaceEnv takes an arg and replaces all occurrences of environment variables
// and Nomad variables. If the variable is found in the passed map it is // and Node attributes, and task secrets. If the variable is found in the passed map
// replaced, otherwise the original string is returned. // it is replaced, otherwise the original string is returned.
func (t *TaskEnv) ReplaceEnv(arg string) string { func (t *TaskEnv) ReplaceEnv(arg string) string {
return hargs.ReplaceEnv(arg, t.EnvMap, t.NodeAttrs) return hargs.ReplaceEnv(arg, t.EnvMap, t.NodeAttrs, t.TaskSecrets)
} }
// replaceEnvClient takes an arg and replaces all occurrences of client-specific // replaceEnvClient takes an arg and replaces all occurrences of client-specific
@@ -425,6 +436,9 @@ type Builder struct {
// nodeAttrs are Node attributes and metadata // nodeAttrs are Node attributes and metadata
nodeAttrs map[string]string nodeAttrs map[string]string
// taskSecrets are secrets populated from the secrets hook
taskSecrets map[string]string
// taskMeta are the meta attributes on the task // taskMeta are the meta attributes on the task
taskMeta map[string]string taskMeta map[string]string
@@ -516,9 +530,10 @@ func NewBuilder(node *structs.Node, alloc *structs.Allocation, task *structs.Tas
// NewEmptyBuilder creates a new environment builder. // NewEmptyBuilder creates a new environment builder.
func NewEmptyBuilder() *Builder { func NewEmptyBuilder() *Builder {
return &Builder{ return &Builder{
mu: &sync.RWMutex{}, mu: &sync.RWMutex{},
hookEnvs: map[string]map[string]string{}, hookEnvs: map[string]map[string]string{},
envvars: make(map[string]string), envvars: make(map[string]string),
taskSecrets: make(map[string]string),
} }
} }
@@ -649,7 +664,7 @@ func (b *Builder) buildEnv(allocDir, localDir, secretsDir string,
// Copy interpolated task env vars second as they override host env vars // Copy interpolated task env vars second as they override host env vars
for k, v := range b.envvars { for k, v := range b.envvars {
envMap[k] = hargs.ReplaceEnv(v, nodeAttrs, envMap) envMap[k] = hargs.ReplaceEnv(v, nodeAttrs, envMap, b.taskSecrets)
} }
// Copy hook env vars in the order the hooks were run // Copy hook env vars in the order the hooks were run
@@ -709,7 +724,13 @@ func (b *Builder) Build() *TaskEnv {
envMap, deviceEnvs := b.buildEnv(b.allocDir, b.localDir, b.secretsDir, nodeAttrs) envMap, deviceEnvs := b.buildEnv(b.allocDir, b.localDir, b.secretsDir, nodeAttrs)
envMapClient, _ := b.buildEnv(b.clientSharedAllocDir, b.clientTaskLocalDir, b.clientTaskSecretsDir, nodeAttrs) envMapClient, _ := b.buildEnv(b.clientSharedAllocDir, b.clientTaskLocalDir, b.clientTaskSecretsDir, nodeAttrs)
return NewTaskEnv(envMap, envMapClient, deviceEnvs, nodeAttrs, b.clientTaskRoot, b.clientSharedAllocDir) return NewTaskEnv(envMap, envMapClient, deviceEnvs, nodeAttrs, b.taskSecrets, b.clientTaskRoot, b.clientSharedAllocDir)
}
func (b *Builder) SetSecrets(secrets map[string]string) {
b.mu.Lock()
defer b.mu.Unlock()
maps.Copy(b.taskSecrets, secrets)
} }
// SetHookEnv sets environment variables from a hook. Variables are // SetHookEnv sets environment variables from a hook. Variables are

View File

@@ -39,6 +39,9 @@ const (
envOneVal = "127.0.0.1" envOneVal = "127.0.0.1"
envTwoKey = "NOMAD_PORT_WEB" envTwoKey = "NOMAD_PORT_WEB"
envTwoVal = ":80" envTwoVal = ":80"
// Secrets populated from secrets hook
testSecret = "foo"
) )
var ( var (
@@ -65,7 +68,13 @@ func testEnvBuilder() *Builder {
envOneKey: envOneVal, envOneKey: envOneVal,
envTwoKey: envTwoVal, envTwoKey: envTwoVal,
} }
return NewBuilder(n, mock.Alloc(), task, "global")
b := NewBuilder(n, mock.Alloc(), task, "global")
b.taskSecrets = map[string]string{
testSecret: testSecret,
}
return b
} }
func TestEnvironment_ParseAndReplace_Env(t *testing.T) { func TestEnvironment_ParseAndReplace_Env(t *testing.T) {
@@ -145,13 +154,13 @@ func TestEnvironment_ParseAndReplace_Mixed(t *testing.T) {
func TestEnvironment_ReplaceEnv_Mixed(t *testing.T) { func TestEnvironment_ReplaceEnv_Mixed(t *testing.T) {
ci.Parallel(t) ci.Parallel(t)
input := fmt.Sprintf("${%v}${%v%v}", nodeNameKey, nodeAttributePrefix, attrKey) input := fmt.Sprintf("${%v}${%v%v}${%v}", nodeNameKey, nodeAttributePrefix, attrKey, testSecret)
exp := fmt.Sprintf("%v%v", nodeName, attrVal) exp := fmt.Sprintf("%v%v%v", nodeName, attrVal, testSecret)
env := testEnvBuilder() env := testEnvBuilder()
act := env.Build().ReplaceEnv(input) act := env.Build().ReplaceEnv(input)
if act != exp { if act != exp {
t.Fatalf("ParseAndReplace(%v) returned %#v; want %#v", input, act, exp) t.Fatalf("ReplaceEnv(%v) returned %#v; want %#v", input, act, exp)
} }
} }
@@ -364,6 +373,10 @@ func TestEnvironment_AllValues(t *testing.T) {
} }
env.mu.Unlock() env.mu.Unlock()
env.taskSecrets = map[string]string{
"secret.testsecret.test": "foo",
}
values, errs, err := env.Build().AllValues() values, errs, err := env.Build().AllValues()
require.NoError(t, err) require.NoError(t, err)
@@ -397,6 +410,9 @@ func TestEnvironment_AllValues(t *testing.T) {
"node.attr.kernel.name": "linux", "node.attr.kernel.name": "linux",
"node.attr.nomad.version": "0.5.0", "node.attr.nomad.version": "0.5.0",
// Task Secrets for interpreting task config
`secret.testsecret.test`: "foo",
// Env // Env
"taskEnvKey": "taskEnvVal", "taskEnvKey": "taskEnvVal",
"NOMAD_ADDR_http": "127.0.0.1:80", "NOMAD_ADDR_http": "127.0.0.1:80",

View File

@@ -122,7 +122,7 @@ func TestInterpolateServices(t *testing.T) {
var testEnv = NewTaskEnv( var testEnv = NewTaskEnv(
map[string]string{"foo": "bar", "baz": "blah"}, map[string]string{"foo": "bar", "baz": "blah"},
map[string]string{"foo": "bar", "baz": "blah"}, map[string]string{"foo": "bar", "baz": "blah"},
nil, nil, "", "") nil, nil, nil, "", "")
func TestInterpolate_interpolateMapStringSliceString(t *testing.T) { func TestInterpolate_interpolateMapStringSliceString(t *testing.T) {
ci.Parallel(t) ci.Parallel(t)
@@ -219,7 +219,7 @@ func TestInterpolate_interpolateConnect(t *testing.T) {
"service1": "_service1", "service1": "_service1",
"host1": "_host1", "host1": "_host1",
} }
env := NewTaskEnv(e, e, nil, nil, "", "") env := NewTaskEnv(e, e, nil, nil, nil, "", "")
connect := &structs.ConsulConnect{ connect := &structs.ConsulConnect{
Native: false, Native: false,