Skip to content

Commit ce00cca

Browse files
authored
Merge pull request #6506 from grondo/mustache-env
allow mustache templates in job environment variables
2 parents b947aa6 + 9cb6099 commit ce00cca

File tree

6 files changed

+154
-7
lines changed

6 files changed

+154
-7
lines changed

doc/man1/common/job-env-rules.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,13 @@ via a set of *RULE* expressions. The currently supported rules are
3131
* Advanced parameter substitution is not allowed, e.g. ``${var:-foo}``
3232
will raise an error.
3333

34+
``VAL`` may also contain a mustache template, in which case the template
35+
will be substituted in the job shell with the corresponding value before
36+
launching job tasks. See `MUSTACHE TEMPLATES`_ for more information.
37+
3438
Examples:
35-
``PATH=/bin``, ``PATH=$PATH:/bin``, ``FOO=${BAR}something``
39+
``PATH=/bin``, ``PATH=$PATH:/bin``, ``FOO=${BAR}something``,
40+
``PATH=${PATH}:/{{tmpdir}}/bin``
3641

3742
* Otherwise, the rule is considered a pattern from which to match
3843
variables from the process environment if they do not exist in

src/bindings/python/flux/cli/base.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -288,10 +288,11 @@ def get_filtered_environment(rules, environ=None):
288288
Filter environment dictionary 'environ' given a list of rules.
289289
Each rule can filter, set, or modify the existing environment.
290290
"""
291+
env_expand = {}
291292
if environ is None:
292293
environ = dict(os.environ)
293294
if rules is None:
294-
return environ
295+
return environ, env_expand
295296
for rule in rules:
296297
#
297298
# If rule starts with '-' then the rest of the rule is a pattern
@@ -308,7 +309,8 @@ def get_filtered_environment(rules, environ=None):
308309
filename = os.path.expanduser(rule[1::])
309310
with open(filename) as envfile:
310311
lines = [line.strip() for line in envfile]
311-
environ = get_filtered_environment(lines, environ=environ)
312+
environ, envx = get_filtered_environment(lines, environ=environ)
313+
env_expand.update(envx)
312314
#
313315
# Otherwise, the rule is an explicit variable assignment
314316
# VAR=VAL. If =VAL is not provided then VAL refers to the
@@ -330,6 +332,11 @@ def get_filtered_environment(rules, environ=None):
330332
for key, value in env.items():
331333
if key not in environ:
332334
environ[key] = value
335+
elif "{{" in rest[0]:
336+
#
337+
# Mustache template which should be expanded by job shell.
338+
# Place result in env_expand instead of environ:
339+
env_expand[var] = rest[0]
333340
else:
334341
#
335342
# Template lookup: use jobspec environment first, fallback
@@ -342,7 +349,7 @@ def get_filtered_environment(rules, environ=None):
342349
raise
343350
except KeyError as ex:
344351
raise Exception(f"--env: Variable {ex} not found in {rule}")
345-
return environ
352+
return environ, env_expand
346353

347354

348355
class EnvFileAction(argparse.Action):
@@ -1001,7 +1008,13 @@ def jobspec_create(self, args):
10011008
Create a jobspec from args and return it to caller
10021009
"""
10031010
jobspec = self.init_jobspec(args)
1004-
jobspec.environment = get_filtered_environment(args.env)
1011+
1012+
jobspec.environment, env_expand = get_filtered_environment(args.env)
1013+
if env_expand:
1014+
# "expanded" environment variables are set in env-expand
1015+
# shell options dict and will be processed by the shell.
1016+
jobspec.setattr_shell_option("env-expand", env_expand)
1017+
10051018
jobspec.cwd = args.cwd if args.cwd is not None else os.getcwd()
10061019
rlimits = get_filtered_rlimits(args.rlimit)
10071020
if rlimits:

src/shell/Makefile.am

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ flux_shell_SOURCES = \
9999
signal.c \
100100
files.c \
101101
hwloc.c \
102-
rexec.c
102+
rexec.c \
103+
env-expand.c
103104

104105
if HAVE_INOTIFY
105106
flux_shell_SOURCES += oom.c

src/shell/builtins.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ extern struct shell_builtin builtin_signal;
5656
extern struct shell_builtin builtin_oom;
5757
extern struct shell_builtin builtin_hwloc;
5858
extern struct shell_builtin builtin_rexec;
59+
extern struct shell_builtin builtin_env_expand;
5960

6061
static struct shell_builtin * builtins [] = {
6162
&builtin_tmpdir,
@@ -86,6 +87,7 @@ static struct shell_builtin * builtins [] = {
8687
#endif
8788
&builtin_hwloc,
8889
&builtin_rexec,
90+
&builtin_env_expand,
8991
&builtin_list_end,
9092
};
9193

src/shell/env-expand.c

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/************************************************************\
2+
* Copyright 2024 Lawrence Livermore National Security, LLC
3+
* (c.f. AUTHORS, NOTICE.LLNS, COPYING)
4+
*
5+
* This file is part of the Flux resource manager framework.
6+
* For details, see https://github.com/flux-framework.
7+
*
8+
* SPDX-License-Identifier: LGPL-3.0
9+
\************************************************************/
10+
11+
#define FLUX_SHELL_PLUGIN_NAME "env-expand"
12+
13+
#if HAVE_CONFIG_H
14+
#include "config.h"
15+
#endif
16+
17+
#include <jansson.h>
18+
19+
#include <flux/shell.h>
20+
21+
#include "builtins.h"
22+
23+
static int env_expand (flux_plugin_t *p,
24+
const char *topic,
25+
flux_plugin_arg_t *args,
26+
void *data)
27+
{
28+
json_t *to_expand = NULL;
29+
json_t *value;
30+
void *tmp;
31+
const char *key;
32+
flux_shell_t *shell = flux_plugin_get_shell (p);
33+
34+
if (!(shell = flux_plugin_get_shell (p)))
35+
return shell_log_errno ("unable to get shell handle");
36+
if (flux_shell_getopt_unpack (shell, "env-expand", "o", &to_expand) != 1)
37+
return 0;
38+
json_object_foreach_safe (to_expand, tmp, key, value) {
39+
const char *s = json_string_value (value);
40+
char *result;
41+
if (s == NULL) {
42+
shell_log_error ("invalid value for env var %s", key);
43+
continue;
44+
}
45+
result = flux_shell_mustache_render (shell, s);
46+
47+
/* If mustache render was successful, then set it for the job and
48+
* remove the key from the env-expand object internally, so it isn't
49+
* expanded again in task_env_expand():
50+
*/
51+
if (result && !strstr (result, "{{")) {
52+
if (flux_shell_setenvf (shell, 1, key, "%s", result) < 0)
53+
shell_log_errno ("failed to set %s=%s", key, result);
54+
else
55+
(void) json_object_del (to_expand, key);
56+
}
57+
free (result);
58+
}
59+
return 0;
60+
}
61+
62+
/* Per-task environment variable mustache substitution.
63+
* N.B.: Only templates that were not fully rendered by env_expand() above
64+
* should remain in the `env-expand` shell options object.
65+
*/
66+
static int task_env_expand (flux_plugin_t *p,
67+
const char *topic,
68+
flux_plugin_arg_t *args,
69+
void *data)
70+
{
71+
json_t *to_expand = NULL;
72+
json_t *value;
73+
const char *key;
74+
flux_shell_t *shell;
75+
flux_shell_task_t *task;
76+
flux_cmd_t *cmd;
77+
78+
if (!(shell = flux_plugin_get_shell (p)))
79+
return shell_log_errno ("unable to get shell handle");
80+
if (flux_shell_getopt_unpack (shell, "env-expand", "o", &to_expand) != 1)
81+
return 0;
82+
83+
if (!(task = flux_shell_current_task (shell))
84+
|| !(cmd = flux_shell_task_cmd (task)))
85+
return -1;
86+
87+
json_object_foreach (to_expand, key, value) {
88+
const char *s = json_string_value (value);
89+
char *result;
90+
if (s == NULL) {
91+
shell_log_error ("invalid value for env var %s", key);
92+
continue;
93+
}
94+
if (!(result = flux_shell_mustache_render (shell, s))) {
95+
shell_log_errno ("failed to expand env var %s=%s", key, s);
96+
continue;
97+
}
98+
if (flux_cmd_setenvf (cmd, 1, key, "%s", result) < 0)
99+
shell_log_errno ("failed to set %s=%s", key, result);
100+
free (result);
101+
}
102+
return 0;
103+
}
104+
105+
struct shell_builtin builtin_env_expand = {
106+
.name = FLUX_SHELL_PLUGIN_NAME,
107+
.init = env_expand,
108+
.task_init = task_env_expand,
109+
};
110+
111+
/*
112+
* vi:tabstop=4 shiftwidth=4 expandtab
113+
*/

t/t2620-job-shell-mustache.t

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,18 @@ test_expect_success 'mustache: unsupported tag is left alone' '
4545
test_debug "cat output.4" &&
4646
test "$(cat output.4)" = "{{foo}} {{node.foo}} {{task.foo}}"
4747
'
48-
48+
test_expect_success 'mustache: mustache templates can be rendered in env' '
49+
flux run --env=TEST={{tmpdir}} -N2 \
50+
sh -c "test \$TEST = \$FLUX_JOB_TMPDIR"
51+
'
52+
test_expect_success 'mustache: env variables can have per-task tags' '
53+
flux run --env=TEST={{taskid}} -N2 -n4 \
54+
sh -c "test \$TEST = \$FLUX_TASK_RANK" &&
55+
flux run --env=T1={{size}} --env=T2={{taskid}} -N2 -n4 \
56+
sh -c "test \$T1 -eq 4 -a \$T2 = \$FLUX_TASK_RANK"
57+
'
58+
test_expect_success 'mustache: invalid tags in env vars are left unexpanded' '
59+
flux run --env=TEST={{task.foo}} \
60+
sh -c "test \$TEST = {{task.foo}}"
61+
'
4962
test_done

0 commit comments

Comments
 (0)