Skip to content

Commit

Permalink
feat!: Add PATH parameter handling logic to preprocess_job_parameters…
Browse files Browse the repository at this point in the history
…() (#39)

* Add parameters job_template_dir, current_working_dir, and
  allow_job_template_dir_walk_up.
* When resolving parameters:
    * PATH parameter defaults that are relative paths are joined
      with job_template_dir.
    * PATH parameter values that are relative paths are joined
      with current_working_dir.
    * Empty ("") PATH parameter values are passed through, so they can
      be used as a "not providing any value" signal. The value "." is
      what to use where a path is intended.
* If allow_job_template_dir_walk_up is False, then:
    * PATH parameter defaults cannot be absolute paths.
    * Relative PATH parameter defaults cannot walk up out of job_template_dir.
    * The provided job_template_dir must be an absolute path.

Signed-off-by: Mark Wiebe <399551+mwiebe@users.noreply.github.com>
  • Loading branch information
mwiebe authored Jan 18, 2024
1 parent ad944eb commit 9d8d08c
Show file tree
Hide file tree
Showing 3 changed files with 392 additions and 21 deletions.
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ job_template = JobTemplate(
script=StepScript(
actions=StepActions(
onRun=Action(
command="echo",
args=["Hello world"]
command="python",
args=["-c", "print('Hello world!')"]
)
)
)
Expand Down Expand Up @@ -109,13 +109,16 @@ print(json.dumps(obj))
### Creating a Job from a Job Template

```python
import os
from pathlib import Path
from openjd.model import (
DecodeValidationError,
create_job,
decode_job_template,
preprocess_job_parameters
)

job_template_path = Path("/absolute/path/to/job/template.json")
job_template = decode_job_template(
template={
"name": "DemoJob",
Expand All @@ -128,7 +131,7 @@ job_template = decode_job_template(
"name": "DemoStep",
"script": {
"actions": {
"onRun": { "command": "echo", "args": [ "Foo={{Param.Foo}}" ] }
"onRun": { "command": "python", "args": [ "-c", "print(r'Foo={{Param.Foo}}')" ] }
}
}
}
Expand All @@ -140,13 +143,15 @@ try:
job_template=job_template,
job_parameter_values={
"Foo": "12"
}
},
job_template_dir=job_template_path.parent,
current_working_dir=Path(os.getcwd())
)
job = create_job(
job_template=job_template,
job_parameter_values=parameters
)
except DecodeValidationError as e:
except (DecodeValidationError, RuntimeError) as e:
print(str(e))
```

Expand All @@ -168,7 +173,7 @@ job_template = decode_job_template(
"name": "Step1",
"script": {
"actions": {
"onRun": { "command": "echo", "args": [ "Step1" ] }
"onRun": { "command": "python", "args": [ "-c", "print('Step1')" ] }
}
}
},
Expand All @@ -177,7 +182,7 @@ job_template = decode_job_template(
"dependencies": [ { "dependsOn": "Step1" }],
"script": {
"actions": {
"onRun": { "command": "echo", "args": [ "Step2" ] }
"onRun": { "command": "python", "args": [ "-c", "print('Step2')" ] }
}
}
}
Expand Down Expand Up @@ -225,8 +230,8 @@ job_template = decode_job_template(
"script": {
"actions": {
"onRun": {
"command": "echo",
"args": [ "Foo={{Task.Param.Foo}}", "Bar={{Task.Param.Bar}}"]
"command": "python",
"args": [ "-c", "print(f'Foo={{Task.Param.Foo}}, Bar={{Task.Param.Bar}}"]
}
}
}
Expand Down
89 changes: 86 additions & 3 deletions src/openjd/model/_create_job.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

from os.path import normpath
from pathlib import Path
from typing import Optional, cast

from pydantic import ValidationError
Expand Down Expand Up @@ -55,18 +57,55 @@ def _collect_missing_job_parameter_names(
def _collect_defaults_2023_09(
job_parameter_definitions: list[JobParameterDefinition],
job_parameter_values: JobParameterInputValues,
job_template_dir: Path,
current_working_dir: Path,
allow_job_template_dir_walk_up: bool,
) -> JobParameterValues:
if not allow_job_template_dir_walk_up and not job_template_dir.is_absolute():
raise ValueError(
f"The value supplied for the job template dir, {job_template_dir}, is not an absolute path. It must be absolute to enforce that PATH parameter defaults are always inside the job template dir."
)

return_value: JobParameterValues = dict[str, ParameterValue]()
# Collect defaults
for param in job_parameter_definitions:
if param.name not in job_parameter_values:
if param.default is not None:
default = str(param.default)
# Make PATH defaults relative to job_template_dir, and
# enforce the `allow_job_template_dir_walk_up` parameter request.
if param.type.name == "PATH" and default != "":
default_path = Path(default)
if default_path.is_absolute():
# While we could permit absolute paths within the job template dir,
# we choose not to do so. A job template using absolute paths as path defaults
# within the template's directory isn't portable and it's easier to make
# them relative early in the creating a job.
if not allow_job_template_dir_walk_up:
raise ValueError(
f"The default value of PATH parameter {param.name} is an absolute path. Default paths must be relative, and are joined to the job template's directory."
)
elif job_template_dir.is_absolute():
# Note: Using os.path.normpath instead of Path.resolve, since
# Path.resolve makes changes to the path unexpected by users,
# like switching Windows drive letters to UNC paths.
default_path = Path(normpath(job_template_dir / default_path))
if not allow_job_template_dir_walk_up and not default_path.is_relative_to(
job_template_dir
):
raise ValueError(
f"The default value of PATH parameter {param.name} references a path outside of the template directory. Walking up from the template directory is not permitted."
)
default = str(default_path)
return_value[param.name] = ParameterValue(
type=ParameterValueType(param.type), value=str(param.default)
type=ParameterValueType(param.type), value=default
)
else:
# Check the parameter against the constraints
value = job_parameter_values[param.name]
# Join any provided relative PATH parameter value with the current_working_directory (except the empty value "")
if param.type.name == "PATH" and value != "" and not Path(value).is_absolute():
value = str(current_working_dir / value)
return_value[param.name] = ParameterValue(
type=ParameterValueType(param.type), value=str(value)
)
Expand Down Expand Up @@ -96,23 +135,54 @@ def preprocess_job_parameters(
*,
job_template: JobTemplate,
job_parameter_values: JobParameterInputValues,
job_template_dir: Path,
current_working_dir: Path,
allow_job_template_dir_walk_up: bool = False,
environment_templates: Optional[list[EnvironmentTemplate]] = None,
) -> JobParameterValues:
"""Preprocess a collection of job parameter values. Must be used prior to
instantiating a Job Template into a Job.
This:
By default, this function performs client-side validation of PATH parameters to
ensure that path references in the job template, either relative or absolute, cannot
escape the directory the template is in. While doing so, it transforms relative paths
into absolute paths. This is the right default for use in a client job submission context,
for example with access to the workstation's file system.
In a server context that no longer can access the workstation's file system, you
can pass Path() as the job template and current working directories and True
as allow_job_template_dir_walk_up. With these options, the PATH parameter values will
remain untouched, and no validation of paths escaping the job template directory will
be performed.
This function does the following:
1. Errors if job parameter values are defined that are not defined in the template.
2. Errors if there are job parameters defined in the job template that do not have default
values, and do not have defined job parameter values.
3. Adds values to the job parameter values for any missing job parameters for which
the job template defines default values.
4. Errors if any of the provided job parameter values do not meet the constraints
for the parameter defined in the job template.
5. For any PATH parameter from the job template with a default value that is relative,
makes it absolute by joining with `job_template_dir`.
6. Errors if `allow_job_template_dir_walk_up` is False, and any PATH parameter default
is an absolute path or resolves to a path outside of `job_template_dir`.
7. For any PATH parameter from the `job_parameter_values` with a value that is relative,
makes it absolute by joining with `current_working_dir`.
Arguments:
job_template (JobTemplate) -- A Job Template to check the job parameter values against.
job_parameter_values (JobParameterValues) -- Mapping of Job Parameter names to values.
e.g. { "Foo": 12 } if you have a Job Parameter named "Foo"
job_template_dir (Path) -- The path, on the local file system, where the job template
lives. Any PATH parameter's default with a relative path value
is joined to this path.
current_working_dir (Path) -- The current working directory to use. Any input
PATH job parameter with a relative path value is joined to this path. These are input
from the user submitting the job, and any absolute or relative paths are permitted.
allow_job_template_dir_walk_up (bool) -- Affects the validation of PATH parameter defaults.
If True, allows absolute paths and relative paths with ".." that walk up outside
the job template dir. If False, disallows these cases.
environment_templates (Optional[list[EnvironmentTemplate]]) -- An ordered list of the
externally defined Environment Templates that are applied to the Job.
Expand Down Expand Up @@ -156,7 +226,13 @@ def preprocess_job_parameters(
# Set of all required, but undefined, job parameter values
try:
if job_template.version == SchemaVersion.v2023_09:
return_value = _collect_defaults_2023_09(parameterDefinitions, job_parameter_values)
return_value = _collect_defaults_2023_09(
parameterDefinitions,
job_parameter_values,
job_template_dir,
current_working_dir,
allow_job_template_dir_walk_up,
)
_check_2023_09(parameterDefinitions, return_value)
else:
raise NotImplementedError(
Expand Down Expand Up @@ -210,11 +286,18 @@ def create_job(
# Raises: ValueError
try:
# Raises: ValueError

# Because this is validating the parameter values without the original job template
# dir and current working dir, this call passes Path() for job_template_dir
# and current_working_dir, and True for allow_job_template_dir_walkup.
all_job_parameter_values = preprocess_job_parameters(
job_template=job_template,
job_parameter_values={
name: param.value for name, param in job_parameter_values.items()
},
job_template_dir=Path(),
current_working_dir=Path(),
allow_job_template_dir_walk_up=True,
environment_templates=environment_templates,
)
except ValueError as exc:
Expand Down
Loading

0 comments on commit 9d8d08c

Please sign in to comment.