Skip to content

Commit 630362f

Browse files
committed
feat: Adding support for redacted environment variable values through openjd_redacted_env
Signed-off-by: Brian Axelson <86568017+baxeaz@users.noreply.github.com>
1 parent 63e0f3b commit 630362f

File tree

5 files changed

+149
-20
lines changed

5 files changed

+149
-20
lines changed

requirements-testing.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
coverage[toml] == 7.*
22
pytest == 8.3.*
33
pytest-cov == 6.1.*
4-
pytest-timeout == 2.3.*
4+
pytest-timeout == 2.4.*
55
pytest-xdist == 3.6.*
66
types-PyYAML ~= 6.0
77
black == 25.*

src/openjd/model/v2023_09/_model.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ class ExtensionName(str, Enum):
8989

9090
# # https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/rfcs/0001-task-chunking.md
9191
TASK_CHUNKING = "TASK_CHUNKING"
92+
# Extension that enables the use of openjd_redacted_env for setting environment variables with redacted values in logs
93+
REDACTED_ENV_VARS = "REDACTED_ENV_VARS"
9294

9395

9496
ExtensionNameList = Annotated[list[str], Field(min_length=1)]
@@ -512,24 +514,6 @@ class RangeString(FormatString):
512514
TaskRangeList = list[Union[TaskParameterStringValueAsJob, int, float, Decimal]]
513515

514516

515-
# Target model for task parameters when instantiating a job.
516-
class RangeListTaskParameterDefinition(OpenJDModel_v2023_09):
517-
# element type of items in the range
518-
type: TaskParameterType
519-
# NOTE: Pydantic V1 was allowing non-string values in this range, V2 is enforcing that type.
520-
range: TaskRangeList
521-
# has a value when type is CHUNK[INT], which is only possible from the TASK_CHUNKING extension
522-
chunks: Optional[TaskChunksDefinition] = None
523-
524-
525-
class RangeExpressionTaskParameterDefinition(OpenJDModel_v2023_09):
526-
# element type of items in the range
527-
type: TaskParameterType
528-
range: IntRangeExpr
529-
# has a value when type is CHUNK[INT], which is only possible from the TASK_CHUNKING extension
530-
chunks: Optional[TaskChunksDefinition] = None
531-
532-
533517
class TaskChunksRangeConstraint(str, Enum):
534518
CONTIGUOUS = "CONTIGUOUS"
535519
NONCONTIGUOUS = "NONCONTIGUOUS"
@@ -559,6 +543,24 @@ def _validate_target_runtime_seconds(cls, value: Any, info: ValidationInfo) -> A
559543
return validate_int_fmtstring_field(value, ge=0, context=context)
560544

561545

546+
# Target model for task parameters when instantiating a job.
547+
class RangeListTaskParameterDefinition(OpenJDModel_v2023_09):
548+
# element type of items in the range
549+
type: TaskParameterType
550+
# NOTE: Pydantic V1 was allowing non-string values in this range, V2 is enforcing that type.
551+
range: TaskRangeList
552+
# has a value when type is CHUNK[INT], which is only possible from the TASK_CHUNKING extension
553+
chunks: Optional[TaskChunksDefinition] = None
554+
555+
556+
class RangeExpressionTaskParameterDefinition(OpenJDModel_v2023_09):
557+
# element type of items in the range
558+
type: TaskParameterType
559+
range: IntRangeExpr
560+
# has a value when type is CHUNK[INT], which is only possible from the TASK_CHUNKING extension
561+
chunks: Optional[TaskChunksDefinition] = None
562+
563+
562564
class IntTaskParameterDefinition(OpenJDModel_v2023_09):
563565
"""Definition of an integer-typed Task Parameter and its value range.
564566

test/openjd/model/v2023_09/test_definitions.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,37 @@
22

33
import pytest
44
from pydantic import BaseModel
5-
from typing import Type
5+
from typing import Type, ForwardRef
66
import openjd.model.v2023_09 as mod
77
from inspect import getmembers, getmodule, isclass
88

99

10+
from .test_module import ClassWithForwardRef, ClassWithoutForwardRef
11+
12+
1013
ALL_MODELS = sorted(
1114
[obj for name, obj in getmembers(mod) if isclass(obj) and issubclass(obj, BaseModel)],
1215
key=lambda o: o.__name__,
1316
)
1417

1518

19+
def test_forward_ref_detection():
20+
"""Test that our ForwardRef detection works by checking classes that use ForwardRefs vs direct references."""
21+
# When referencing a class that's already defined, no ForwardRef is created
22+
field = ClassWithoutForwardRef.model_fields["ref"]
23+
assert not isinstance(field.annotation, ForwardRef), (
24+
"Expected ClassWithoutForwardRef.ref to NOT be a ForwardRef since ReferencedClass "
25+
"is defined before it's used"
26+
)
27+
28+
# When referencing a class that's defined later, a ForwardRef is created
29+
field = ClassWithForwardRef.model_fields["ref"]
30+
assert isinstance(field.annotation, ForwardRef), (
31+
"Expected ClassWithForwardRef.ref to be a ForwardRef since SecondReferencedClass "
32+
"is defined after it's used"
33+
)
34+
35+
1636
@pytest.mark.parametrize("model", ALL_MODELS)
1737
def test_models_in_same_module(model: Type[BaseModel]) -> None:
1838
# For our error reporting of discriminated union fields to be correctly reported
@@ -21,3 +41,18 @@ def test_models_in_same_module(model: Type[BaseModel]) -> None:
2141
# This is to identify when a name in an error location is actually a class name from
2242
# a typed union.
2343
assert getmodule(mod.JobTemplate) == getmodule(model)
44+
45+
46+
@pytest.mark.parametrize("model", ALL_MODELS)
47+
def test_no_forward_refs_in_models(model: Type[BaseModel]) -> None:
48+
"""Test that no models in _model.py use ForwardRefs in their field annotations.
49+
50+
ForwardRefs indicate that a type is being referenced before it's defined, which can lead
51+
to issues in pydantic validation. This test ensures all types are properly defined before
52+
they're used.
53+
"""
54+
for field_name, field in model.model_fields.items():
55+
assert not isinstance(field.annotation, ForwardRef), (
56+
f"Field '{field_name}' in model '{model.__name__}' uses a ForwardRef. "
57+
"The referenced type should be defined before it's used."
58+
)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
3+
from __future__ import annotations
4+
5+
from typing import Optional
6+
from openjd.model.v2023_09._model import OpenJDModel_v2023_09
7+
8+
9+
# First define a class we'll reference properly
10+
class ReferencedClass(OpenJDModel_v2023_09):
11+
value: str
12+
13+
14+
class ClassWithoutForwardRef(OpenJDModel_v2023_09):
15+
# This won't create a ForwardRef since ReferencedClass is already defined
16+
ref: ReferencedClass
17+
18+
19+
# Now try to reference a class before it's defined, like we did in _model.py
20+
class ClassWithForwardRef(OpenJDModel_v2023_09):
21+
# This should create a ForwardRef since SecondReferencedClass isn't defined yet
22+
ref: Optional[SecondReferencedClass] = None
23+
24+
25+
# Define the class after it's referenced
26+
class SecondReferencedClass(OpenJDModel_v2023_09):
27+
value: str
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
3+
import pytest
4+
from pydantic import ValidationError
5+
6+
from openjd.model._parse import _parse_model
7+
from openjd.model.v2023_09 import (
8+
JobTemplate,
9+
ModelParsingContext,
10+
)
11+
12+
13+
def test_redacted_env_vars_extension_supported() -> None:
14+
"""Test that the REDACTED_ENV_VARS extension can be used in a job template."""
15+
data = {
16+
"specificationVersion": "jobtemplate-2023-09",
17+
"extensions": ["REDACTED_ENV_VARS"],
18+
"name": "Test Job",
19+
"steps": [
20+
{
21+
"name": "step1",
22+
"script": {
23+
"actions": {"onRun": {"command": "python", "args": ["{{Task.File.Run}}"]}},
24+
"embeddedFiles": [
25+
{
26+
"name": "Run",
27+
"type": "TEXT",
28+
"data": 'print("openjd_redacted_env: SECRETVAR=SECRETVAL")',
29+
}
30+
],
31+
},
32+
}
33+
],
34+
}
35+
36+
# It parses successfully when the REDACTED_ENV_VARS extension is requested
37+
_parse_model(
38+
model=JobTemplate,
39+
obj=data,
40+
context=ModelParsingContext(supported_extensions=["REDACTED_ENV_VARS"]),
41+
)
42+
43+
44+
def test_redacted_env_vars_extension_not_supported() -> None:
45+
"""Test that using REDACTED_ENV_VARS extension fails when not supported."""
46+
data = {
47+
"specificationVersion": "jobtemplate-2023-09",
48+
"extensions": ["REDACTED_ENV_VARS"],
49+
"name": "Test Job",
50+
"steps": [
51+
{
52+
"name": "step1",
53+
"script": {"actions": {"onRun": {"command": "echo", "args": ["test"]}}},
54+
}
55+
],
56+
}
57+
58+
# It fails to parse when REDACTED_ENV_VARS extension is not supported
59+
with pytest.raises(ValidationError) as excinfo:
60+
_parse_model(
61+
model=JobTemplate,
62+
obj=data,
63+
context=ModelParsingContext(),
64+
)
65+
assert "Unsupported extension names: REDACTED_ENV_VARS" in str(excinfo.value)

0 commit comments

Comments
 (0)