Skip to content

Commit 65a15b4

Browse files
committed
fix!: Running jobs with env templates does not support parameters
The Open Job Description specification https://github.com/OpenJobDescription/openjd-specifications/wiki/2023-09-Template-Schemas includes both job and environment templates. The openjd CLI lets you run a job in the context of one or more externally defined templates via `openjd run job.yaml --environment env.yaml`. Environment templates can define parameters just like jobs, but when they did this the CLI tool failed to run them. This was because the parameter values were not being processed and forwarded based on the full environment context, they were being regenerated later without the environments. This change fixes that to forward the parameters. BREAKING CHANGE: The functions generate_jobs and job_from_template have changed to accept the environments and return the job parameters alongside the job. Constructing the LocalSession class now requires job parameters. Signed-off-by: Mark Wiebe <[email protected]>
1 parent 05b82c2 commit 65a15b4

File tree

11 files changed

+148
-37
lines changed

11 files changed

+148
-37
lines changed

src/openjd/cli/_common/__init__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
read_job_template,
2222
read_environment_template,
2323
)
24-
from openjd.model import DecodeValidationError, Job
24+
from openjd.model import DecodeValidationError, Job, JobParameterValues, EnvironmentTemplate
2525

2626
__all__ = [
2727
"add_extensions_argument",
@@ -112,13 +112,20 @@ def add(self, name: str, description: str, **kwargs) -> ArgumentParser:
112112
return self.group.add_parser(name, **kwargs)
113113

114114

115-
def generate_job(args: Namespace, *, supported_extensions: list[str]) -> Job:
115+
def generate_job(
116+
args: Namespace,
117+
environments: list[EnvironmentTemplate] = [],
118+
*,
119+
supported_extensions: list[str],
120+
) -> tuple[Job, JobParameterValues]:
116121
try:
117122
# Raises: RuntimeError, DecodeValidationError
118123
template = read_job_template(args.path, supported_extensions=supported_extensions)
124+
119125
# Raises: RuntimeError
120126
return job_from_template(
121127
template,
128+
environments,
122129
args.job_params if args.job_params else None,
123130
Path(os.path.abspath(args.path.parent)),
124131
Path(os.getcwd()),

src/openjd/cli/_common/_job_from_template.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44
import json
55
from pathlib import Path
66
import re
7-
from typing import Union
7+
from typing import Optional, Union
88
import yaml
99

1010
from ._validation_utils import get_doc_type
1111
from openjd.model import (
1212
DecodeValidationError,
1313
DocumentType,
14+
EnvironmentTemplate,
1415
Job,
16+
JobParameterValues,
1517
JobTemplate,
1618
create_job,
1719
preprocess_job_parameters,
@@ -54,15 +56,16 @@ def get_params_from_file(parameter_string: str) -> Union[dict, list]:
5456
return parameters
5557

5658

57-
def get_job_params(parameter_args: list[str]) -> dict:
59+
def get_job_params(parameter_args: Optional[list[str]]) -> dict:
5860
"""
5961
Resolves Job Parameters from a list of command-line arguments.
6062
Arguments may be a filepath or a string with format 'Key=Value'.
6163
6264
Raises: RuntimeError if the provided Parameters are formatted incorrectly or can't be opened
6365
"""
6466
parameter_dict: dict = {}
65-
for arg in parameter_args:
67+
68+
for arg in parameter_args or []:
6669
arg = arg.strip()
6770
# Case 1: Provided argument is a filepath
6871
if arg.startswith("file://"):
@@ -104,29 +107,36 @@ def get_job_params(parameter_args: list[str]) -> dict:
104107

105108
def job_from_template(
106109
template: JobTemplate,
110+
environments: list[EnvironmentTemplate],
107111
parameter_args: list[str] | None,
108112
job_template_dir: Path,
109113
current_working_dir: Path,
110-
) -> Job:
114+
) -> tuple[Job, JobParameterValues]:
111115
"""
112-
Given a decoded Job Template and a user-inputted parameter dictionary,
113-
generates a Job object.
116+
Given a decoded Job Template and a user-input parameter dictionary,
117+
generates a Job object and the parameter values for running the job.
114118
115119
Raises: RuntimeError if parameters are an unsupported type or don't correspond to the template
116120
"""
117-
parameter_dict = get_job_params(parameter_args) if parameter_args else {}
121+
parameter_dict = get_job_params(parameter_args)
118122

119123
try:
120124
parameter_values = preprocess_job_parameters(
121125
job_template=template,
122126
job_parameter_values=parameter_dict,
123127
job_template_dir=job_template_dir,
124128
current_working_dir=current_working_dir,
129+
environment_templates=environments,
125130
)
126131
except ValueError as ve:
127132
raise RuntimeError(str(ve))
128133

129134
try:
130-
return create_job(job_template=template, job_parameter_values=parameter_values)
135+
job = create_job(
136+
job_template=template,
137+
job_parameter_values=parameter_values,
138+
environment_templates=environments,
139+
)
140+
return (job, parameter_values)
131141
except DecodeValidationError as dve:
132142
raise RuntimeError(f"Could not generate Job from template and parameters: {str(dve)}")

src/openjd/cli/_run/_local_session/_session_manager.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
IntRangeExpr,
2222
Job,
2323
JobParameterValues,
24-
ParameterValue,
25-
ParameterValueType,
2624
Step,
2725
StepParameterSpaceIterator,
2826
TaskParameterSet,
@@ -78,6 +76,7 @@ def __init__(
7876
self,
7977
*,
8078
job: Job,
79+
job_parameter_values: JobParameterValues,
8180
session_id: str,
8281
timestamp_format: LoggingTimestampFormat = LoggingTimestampFormat.RELATIVE,
8382
path_mapping_rules: Optional[list[PathMappingRule]] = None,
@@ -93,18 +92,9 @@ def __init__(
9392
self._environments = environments
9493

9594
# Create an OpenJD Session
96-
job_parameters: JobParameterValues
97-
if job.parameters:
98-
job_parameters = {
99-
name: ParameterValue(type=ParameterValueType(param.type.value), value=param.value)
100-
for name, param in job.parameters.items()
101-
}
102-
else:
103-
job_parameters = dict[str, ParameterValue]()
104-
10595
self._openjd_session = Session(
10696
session_id=self.session_id,
107-
job_parameter_values=job_parameters,
97+
job_parameter_values=job_parameter_values,
10898
path_mapping_rules=self._path_mapping_rules,
10999
callback=self._action_callback,
110100
retain_working_dir=retain_working_dir,
@@ -131,6 +121,11 @@ def _context_manager_cleanup(self):
131121
signal(SIGTERM, SIG_DFL)
132122
self._started = False
133123

124+
# A blank line to separate the job log output from this status message
125+
LOG.info(
126+
msg="",
127+
extra={"session_id": self.session_id},
128+
)
134129
if self.failed:
135130
LOG.info(
136131
msg=f"Open Job Description CLI: ERROR executing action: '{self.failed_action}' (see Task logs for details)",

src/openjd/cli/_run/_run_command.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
DecodeValidationError,
2929
EnvironmentTemplate,
3030
Job,
31+
JobParameterValues,
3132
Step,
3233
StepDependencyGraph,
3334
StepParameterSpaceIterator,
@@ -317,6 +318,7 @@ def _validate_task_params(step: Step, task_params: list[dict[str, str]]) -> None
317318
def _run_local_session(
318319
*,
319320
job: Job,
321+
job_parameter_values: JobParameterValues,
320322
step_list: list[Step],
321323
selected_step: Optional[Step],
322324
timestamp_format: LoggingTimestampFormat,
@@ -337,6 +339,7 @@ def _run_local_session(
337339
step_name = ""
338340
with LocalSession(
339341
job=job,
342+
job_parameter_values=job_parameter_values,
340343
timestamp_format=timestamp_format,
341344
session_id="CLI-session",
342345
path_mapping_rules=path_mapping_rules,
@@ -442,7 +445,9 @@ def do_run(args: Namespace) -> OpenJDCliResult:
442445

443446
try:
444447
# Raises: RuntimeError
445-
the_job = generate_job(args, supported_extensions=extensions)
448+
the_job, job_parameter_values = generate_job(
449+
args, environments, supported_extensions=extensions
450+
)
446451

447452
# Map Step names to Step objects so they can be easily accessed
448453
step_map = {step.name: step for step in the_job.steps}
@@ -509,6 +514,7 @@ def do_run(args: Namespace) -> OpenJDCliResult:
509514

510515
return _run_local_session(
511516
job=the_job,
517+
job_parameter_values=job_parameter_values,
512518
step_list=step_list,
513519
selected_step=selected_step,
514520
task_parameter_values=task_parameter_values,

src/openjd/cli/_summary/_summary_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def do_summary(args: Namespace) -> OpenJDCliResult:
3535

3636
try:
3737
# Raises: RuntimeError
38-
sample_job = generate_job(args, supported_extensions=extensions)
38+
sample_job, _ = generate_job(args, supported_extensions=extensions)
3939
except RuntimeError as rte:
4040
return OpenJDCliResult(status="error", message=str(rte))
4141

test/openjd/cli/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ def sample_job_and_dirs(request):
2929

3030
template = decode_job_template(template=MOCK_TEMPLATE)
3131
yield (
32-
job_from_template(
32+
*job_from_template(
3333
template=template,
34+
environments=[],
3435
parameter_args=request.param,
3536
job_template_dir=template_dir,
3637
current_working_dir=current_working_dir,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
specificationVersion: environment-2023-09
2+
parameterDefinitions:
3+
- name: EnvParam
4+
type: STRING
5+
default: DefaultForEnvParam
6+
environment:
7+
name: EnvWithParam
8+
script:
9+
actions:
10+
onEnter:
11+
command: python
12+
args:
13+
- -c
14+
- print('EnvWithParam Enter {{Param.EnvParam}}')
15+
onExit:
16+
command: python
17+
args:
18+
- -c
19+
- print('EnvWithParam Exit {{Param.EnvParam}}')

test/openjd/cli/templates/simple_with_j_param.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"actions": {
1010
"onRun": {
1111
"command": "python",
12-
"args": ["-c", "print('DoTask')"],
12+
"args": ["-c", "print('DoTask {{Param.J}}')"],
1313
}
1414
}
1515
},

test/openjd/cli/test_common.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ def test_job_from_template_success(
366366
template_dir, current_working_dir = template_dir_and_cwd
367367
template = decode_job_template(template=template_dict)
368368

369-
result = job_from_template(template, mock_params, template_dir, current_working_dir)
369+
result, _ = job_from_template(template, [], mock_params, template_dir, current_working_dir)
370370
assert result.name == expected_job_name
371371
assert [step.model_dump(exclude_none=True) for step in result.steps] == [
372372
step.model_dump(exclude_none=True) for step in template.steps
@@ -415,7 +415,7 @@ def test_job_from_template_error(
415415
template = decode_job_template(template=template_dict)
416416

417417
with pytest.raises(RuntimeError) as rte:
418-
job_from_template(template, mock_params, template_dir, current_working_dir)
418+
job_from_template(template, [], mock_params, template_dir, current_working_dir)
419419

420420
assert expected_error in str(rte.value)
421421

@@ -455,8 +455,9 @@ def test_generate_job_success(
455455
new=Mock(side_effect=job_from_template),
456456
) as patched_job_from_template:
457457
generate_job(mock_args, supported_extensions=[])
458+
print(patched_job_from_template.call_args_list)
458459
patched_job_from_template.assert_called_once_with(
459-
ANY, expected_param_list, Path(temp_template.name).parent, Path(os.getcwd())
460+
ANY, [], expected_param_list, Path(temp_template.name).parent, Path(os.getcwd())
460461
)
461462

462463
Path(temp_template.name).unlink()

test/openjd/cli/test_local_session.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def test_localsession_initialize(
6060
"""
6161
Test that initializing the local Session enters external and job environments, and is ready to run tasks.
6262
"""
63-
sample_job, template_dir, current_working_dir = sample_job_and_dirs
63+
sample_job, sample_job_parameters, template_dir, current_working_dir = sample_job_and_dirs
6464
with (
6565
patch.object(
6666
LocalSession,
@@ -72,7 +72,9 @@ def test_localsession_initialize(
7272
LocalSession, "run_step", autospec=True, side_effect=LocalSession.run_step
7373
) as patched_run_step,
7474
):
75-
with LocalSession(job=sample_job, session_id="my-session") as session:
75+
with LocalSession(
76+
job=sample_job, job_parameter_values=sample_job_parameters, session_id="my-session"
77+
) as session:
7678
assert session._openjd_session.state == SessionState.READY
7779

7880
# It should have entered the external and job environments in order
@@ -91,12 +93,14 @@ def test_localsession_initialize(
9193
@pytest.mark.usefixtures("sample_job_and_dirs")
9294
def test_localsession_traps_sigint(sample_job_and_dirs: tuple):
9395
# Make sure that we hook up, and remove the signal handler when using the local session
94-
sample_job, template_dir, current_working_dir = sample_job_and_dirs
96+
sample_job, sample_job_parameters, template_dir, current_working_dir = sample_job_and_dirs
9597

9698
# GIVEN
9799
with patch.object(local_session_mod, "signal") as signal_mod:
98100
# WHEN
99-
with LocalSession(job=sample_job, session_id="test-id") as localsession:
101+
with LocalSession(
102+
job=sample_job, job_parameter_values=sample_job_parameters, session_id="test-id"
103+
) as localsession:
100104
pass
101105

102106
# THEN
@@ -124,7 +128,7 @@ def test_localsession_run_success(
124128
"""
125129
Test that calling `run_step` causes the local Session to run the tasks requested in that step.
126130
"""
127-
sample_job, template_dir, current_working_dir = sample_job_and_dirs
131+
sample_job, sample_job_parameters, template_dir, current_working_dir = sample_job_and_dirs
128132

129133
if parameter_sets is None:
130134
parameter_sets = StepParameterSpaceIterator(
@@ -151,7 +155,9 @@ def test_localsession_run_success(
151155
LocalSession, "run_task", autospec=True, side_effect=LocalSession.run_task
152156
) as patched_run_task,
153157
):
154-
with LocalSession(job=sample_job, session_id="my-session") as session:
158+
with LocalSession(
159+
job=sample_job, job_parameter_values=sample_job_parameters, session_id="my-session"
160+
) as session:
155161
session.run_step(
156162
sample_job.steps[step_index],
157163
task_parameters=parameter_sets,
@@ -192,7 +198,7 @@ def test_localsession_run_failed(sample_job_and_dirs: tuple, capsys: pytest.Capt
192198
"""
193199
Test that a LocalSession can gracefully handle an error in its inner Session.
194200
"""
195-
sample_job, template_dir, current_working_dir = sample_job_and_dirs
201+
sample_job, sample_job_parameters, template_dir, current_working_dir = sample_job_and_dirs
196202
with (
197203
patch.object(
198204
LocalSession,
@@ -201,7 +207,9 @@ def test_localsession_run_failed(sample_job_and_dirs: tuple, capsys: pytest.Capt
201207
side_effect=LocalSession.run_environment_enters,
202208
) as patched_run_environment_enters,
203209
):
204-
with LocalSession(job=sample_job, session_id="bad-session") as session:
210+
with LocalSession(
211+
job=sample_job, job_parameter_values=sample_job_parameters, session_id="bad-session"
212+
) as session:
205213
with pytest.raises(LocalSessionFailed):
206214
session.run_step(sample_job.steps[SampleSteps.BadCommand])
207215

0 commit comments

Comments
 (0)