Apply task and play environment vars
This commit is contained in:
parent
4245e1e530
commit
f71e8642e9
3 changed files with 133 additions and 0 deletions
64
executor.go
64
executor.go
|
|
@ -34,6 +34,21 @@ type sshExecutorClient interface {
|
|||
Close() error
|
||||
}
|
||||
|
||||
// environmentSSHClient wraps another SSH client and prefixes commands with
|
||||
// shell exports so play/task environment variables reach remote execution.
|
||||
type environmentSSHClient struct {
|
||||
sshExecutorClient
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (c *environmentSSHClient) Run(ctx context.Context, cmd string) (string, string, int, error) {
|
||||
return c.sshExecutorClient.Run(ctx, c.prefix+cmd)
|
||||
}
|
||||
|
||||
func (c *environmentSSHClient) RunScript(ctx context.Context, script string) (string, string, int, error) {
|
||||
return c.sshExecutorClient.RunScript(ctx, c.prefix+script)
|
||||
}
|
||||
|
||||
// Executor runs Ansible playbooks.
|
||||
//
|
||||
// Example:
|
||||
|
|
@ -1498,6 +1513,55 @@ func (e *Executor) resolveExpr(expr string, host string, task *Task) string {
|
|||
return "{{ " + expr + " }}" // Return as-is if unresolved
|
||||
}
|
||||
|
||||
// buildEnvironmentPrefix renders merged play/task environment variables as
|
||||
// shell export statements.
|
||||
func (e *Executor) buildEnvironmentPrefix(host string, task *Task, play *Play) string {
|
||||
env := make(map[string]string)
|
||||
|
||||
if play != nil {
|
||||
for key, value := range play.Environment {
|
||||
env[key] = value
|
||||
}
|
||||
}
|
||||
if task != nil {
|
||||
for key, value := range task.Environment {
|
||||
env[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if len(env) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(env))
|
||||
for key := range env {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
|
||||
parts := make([]string, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
renderedKey := e.templateString(key, host, task)
|
||||
if renderedKey == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
renderedValue := e.templateString(env[key], host, task)
|
||||
parts = append(parts, sprintf("export %s=%s", renderedKey, shellQuote(renderedValue)))
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return join("; ", parts) + "; "
|
||||
}
|
||||
|
||||
// shellQuote wraps a string in single quotes for shell use.
|
||||
func shellQuote(value string) string {
|
||||
return "'" + replaceAll(value, "'", "'\\''") + "'"
|
||||
}
|
||||
|
||||
// lookupExprValue resolves the first segment of an expression against the
|
||||
// executor, task, and inventory scopes.
|
||||
func (e *Executor) lookupExprValue(name string, host string, task *Task) (any, bool) {
|
||||
|
|
|
|||
|
|
@ -231,6 +231,68 @@ func TestExecutor_RunTaskOnHost_Good_DelegateToUsesDelegatedClient(t *testing.T)
|
|||
assert.Equal(t, 1, mock.commandCount())
|
||||
}
|
||||
|
||||
func TestExecutor_RunTaskOnHost_Good_EnvironmentMergesForCommand(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
play := &Play{
|
||||
Environment: map[string]string{
|
||||
"APP_ENV": "play",
|
||||
"PLAY_ONLY": "from-play",
|
||||
},
|
||||
}
|
||||
task := &Task{
|
||||
Name: "Environment command",
|
||||
Module: "command",
|
||||
Args: map[string]any{
|
||||
"cmd": `echo "$APP_ENV:$PLAY_ONLY:$TASK_ONLY"`,
|
||||
},
|
||||
Environment: map[string]string{
|
||||
"APP_ENV": "task",
|
||||
"TASK_ONLY": "from-task",
|
||||
},
|
||||
Register: "env_result",
|
||||
}
|
||||
|
||||
mock.expectCommand(`export APP_ENV='task'; export PLAY_ONLY='from-play'; export TASK_ONLY='from-task'; echo`, "task:from-play:from-task\n", "", 0)
|
||||
|
||||
err := e.runTaskOnHost(context.Background(), "host1", []string{"host1"}, task, play)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, e.results["host1"]["env_result"])
|
||||
assert.Equal(t, "task:from-play:from-task\n", e.results["host1"]["env_result"].Stdout)
|
||||
assert.True(t, mock.hasExecuted(`export APP_ENV='task'; export PLAY_ONLY='from-play'; export TASK_ONLY='from-task'; echo`))
|
||||
}
|
||||
|
||||
func TestExecutor_RunTaskOnHost_Good_EnvironmentAppliesToShellScript(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
play := &Play{
|
||||
Environment: map[string]string{
|
||||
"SHELL_ONLY": "from-play",
|
||||
},
|
||||
}
|
||||
task := &Task{
|
||||
Name: "Environment shell",
|
||||
Module: "shell",
|
||||
Args: map[string]any{
|
||||
"_raw_params": `echo "$SHELL_ONLY"`,
|
||||
},
|
||||
Environment: map[string]string{
|
||||
"SHELL_ONLY": "from-task",
|
||||
},
|
||||
Register: "shell_env_result",
|
||||
}
|
||||
|
||||
mock.expectCommand(`export SHELL_ONLY='from-task'; echo`, "from-task\n", "", 0)
|
||||
|
||||
err := e.runTaskOnHost(context.Background(), "host1", []string{"host1"}, task, play)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, e.results["host1"]["shell_env_result"])
|
||||
assert.Equal(t, "from-task\n", e.results["host1"]["shell_env_result"].Stdout)
|
||||
assert.True(t, mock.hasExecuted(`export SHELL_ONLY='from-task'; echo`))
|
||||
}
|
||||
|
||||
func TestExecutor_RunRole_Good_AppliesRoleTagsToTasks(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
roleTasks := `---
|
||||
|
|
|
|||
|
|
@ -34,6 +34,13 @@ func (e *Executor) executeModule(ctx context.Context, host string, client sshExe
|
|||
defer client.SetBecome(oldBecome, oldUser, oldPass)
|
||||
}
|
||||
|
||||
if prefix := e.buildEnvironmentPrefix(host, task, play); prefix != "" {
|
||||
client = &environmentSSHClient{
|
||||
sshExecutorClient: client,
|
||||
prefix: prefix,
|
||||
}
|
||||
}
|
||||
|
||||
// Template the args
|
||||
args := e.templateArgs(task.Args, host, task)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue