Skip to navigation

nemo_gym.data.tasks.harbor.compose_variables

View as Markdown

Compose interpolation for Harbor tasks.

The interpolation engine with Compose’s operators, the dotenv reader with Compose’s quoting rules, the inlining of env_file entries into each service, the type restoration healthcheck fields need after interpolation, the residual interpolation check, and Harbor’s variable precedence for a task: infrastructure variables, then the task’s environment table, then the process environment, then environment/.env, with Compose’s empty-string rule and one warning per unset name. It lives under tasks/harbor because it reads HarborTask and the task digest’s ignore rules. The error type and the two Compose shape helpers it raises and reads live in nemo_gym.sandbox.compose_fields, which the adapter-side field checks share. The loader and the build modules both resolve a task’s Compose file through it.

Module Contents

Functions

NameDescription
_closing_braceIndex of the } that closes the ${ opening at start, nested ${...} included.
_dotenv_variablesenvironment/.env read as Compose’s dotenv reader reads it (compose-go/dotenv): an unquoted or
_env_file_entriesA service’s env_file entries (a path, a list of paths or long-syntax mappings) as (path, required)
_env_file_pathpath resolved under base_dir (Harbor’s ${CONTEXT_DIR}/x.env is absolute and lands there too).
_literalvalue as the document’s interpolation and literal checks read a literal: every $ doubled.
_quoted_env_valueThe quoted env_file value opening at text[0]: (value, quote, characters consumed). The value runs
_resolve-
_resolved_alreadyThe resolver of entries resolve_env_files wrote: absolute paths already checked by their own rule.
_substitute_textCompose interpolation: $$ (a literal dollar, kept doubled for _compose_literals), ${NAME},
_task_env[environment].env as Harbor’s resolve_env_vars hands it to docker compose (see
_warn_unset-
compose_variablesThe variables a task’s Compose file may interpolate, resolved in the order Harbor’s docker compose sees.
env_file_entriesThe KEY=value lines of a Compose env file in file order, as (key, value, quote). quote is
env_file_pathsA service’s env_file entries (a path, a list of paths or long-syntax mappings) as the absolute paths of the
infrastructure_variablesGym’s infrastructure set for task (source 1 of compose_variables, the one Harbor’s
inline_env_filesRead each service’s env_file entries into its environment mapping: relative to base_dir under the
literal_variablesA deployment’s compose_variables values as the document reads literals (_literal on each), the way
parse_env_fileKEY=value lines of a Compose env_file, taken literally.
residual_interpolationThe key path (services.api.environment.DB_PASS, services.a.command[1]) of the first string still
resolve_env_file_entriesA service’s env_file entries with each path resolved by resolve to the absolute path of the file, the
resolve_env_filesEach service’s env_file entries replaced by what resolve_env_file_entries resolves them to (absolute
restore_compose_typesGive the typed healthcheck fields their types back after interpolation, which turns every scalar it
substitute_composeInterpolate ${VAR} references in every string of a Compose document from variables alone.
substitute_with_emptysubstitute_compose with Compose’s rule for a bare ${VAR} or $VAR nobody sets: an empty
under_task_folderThe task file’s resolver: entries relative to base_dir (the task’s environment folder) and kept under

Data

COMPOSE_DOTENV

EnvFileResolver

HARBOR_ENV_TEMPLATE

LOGGER

LOG_MOUNT_PATHS

TEST_DIR

_DOUBLE_QUOTE_ESCAPES

_HEALTHCHECK_BOOLEANS

_NAME

_WARNED_UNSET

API

nemo_gym.data.tasks.harbor.compose_variables._closing_brace(
text: str,
start: int
) -> int

Index of the } that closes the ${ opening at start, nested ${...} included.

nemo_gym.data.tasks.harbor.compose_variables._dotenv_variables(
context: pathlib.Path,
above: collections.abc.Mapping[str, str],
unset: set[str]
) -> dict[str, str]

environment/.env read as Compose’s dotenv reader reads it (compose-go/dotenv): an unquoted or double-quoted value is interpolated against above and then the lines before it, a single-quoted value is literal. A dollar that stays literal is kept doubled, as parse_env_file keeps it, so the document’s literal check leaves it alone.

nemo_gym.data.tasks.harbor.compose_variables._env_file_entries(
service: str,
entries: typing.Any
) -> list[tuple[str, bool]]

A service’s env_file entries (a path, a list of paths or long-syntax mappings) as (path, required) pairs, in order. An entry with no path raises ComposeSubstitutionError; so does one with a format, as only the default dotenv format is read.

nemo_gym.data.tasks.harbor.compose_variables._env_file_path(
base_dir: pathlib.Path,
path: str,
service: str
) -> pathlib.Path

path resolved under base_dir (Harbor’s ${CONTEXT_DIR}/x.env is absolute and lands there too). A path that resolves outside, or a symlink, is refused: task folders are third-party content and the file must be part of what prepare digested. So is a file the task digest skips (*~, *.swp, __pycache__ and the like, see nemo_gym.data.tasks.harbor.digest.is_ignored): it would change the document without changing the identity the server checks at seed.

nemo_gym.data.tasks.harbor.compose_variables._literal(
value: str
) -> str

value as the document’s interpolation and literal checks read a literal: every $ doubled.

nemo_gym.data.tasks.harbor.compose_variables._quoted_env_value(
text: str,
line: int,
escaped_dollar: str
) -> tuple[str, str, int]

The quoted env_file value opening at text[0]: (value, quote, characters consumed). The value runs to its closing quote, across newlines if need be. Double quotes decode \n, \r, \t, \\, \" and \$ (the last becoming escaped_dollar) and keep any other \X as written; single quotes are literal except for \', which is an escaped quote in either style.

nemo_gym.data.tasks.harbor.compose_variables._resolve(
name: str,
operator: str,
argument: str,
variables: dict[str, str]
) -> str
nemo_gym.data.tasks.harbor.compose_variables._resolved_already(
path: str,
service: str
) -> pathlib.Path

The resolver of entries resolve_env_files wrote: absolute paths already checked by their own rule.

nemo_gym.data.tasks.harbor.compose_variables._substitute_text(
text: str,
variables: dict[str, str]
) -> str

Compose interpolation: $$ (a literal dollar, kept doubled for _compose_literals), ${NAME}, ${NAME:-default}, ${NAME-default}, ${NAME:?error}, ${NAME?error}, ${NAME:+alternative}, ${NAME+alternative} and bare $NAME. Defaults, alternatives and error texts are interpolated too, so ${A:-${B:-x}} nests the way Compose documents.

nemo_gym.data.tasks.harbor.compose_variables._task_env(
env: collections.abc.Mapping[str, typing.Any]
) -> dict[str, str]

[environment].env as Harbor’s resolve_env_vars hands it to docker compose (see compose_variables), each value literal to the document (_literal).

nemo_gym.data.tasks.harbor.compose_variables._warn_unset(
unset: set[str]
) -> None
nemo_gym.data.tasks.harbor.compose_variables.compose_variables(
unset: set[str] | None = None
) -> dict[str, str]

The variables a task’s Compose file may interpolate, resolved in the order Harbor’s docker compose sees.

Harbor runs docker compose with the process environment, the task’s [environment].env over it and its own infrastructure variables over both (merge_compose_env in harbor/environments/docker/compose_env.py and _compose_env_vars in docker.py at v0.22.0, called with include_os_env=True), and Compose reads the project directory’s .env below everything the command’s environment sets. Highest first:

  1. Gym’s infrastructure set: MAIN_IMAGE_NAME (the resolved task image), CONTEXT_DIR (the task’s environment/ folder), TEST_DIR (TEST_DIR), COMPOSE_PROJECT_NAME (hb__<hash>, Harbor’s environment id of the context and the main image name, a stable placeholder: compose-go injects the real value from --project-name, the trial session id under Harbor and compose-<uuid> per episode in Gym’s adapter, which has no Compose-generated names for it to match), PREBUILT_IMAGE_NAME when docker_image is declared, CPUS and MEMORY (as <n>M, Harbor’s spelling) from the task’s resources, and the HOST_*_PATH / ENV_*_PATH log mounts as the /logs tree;
  2. the task’s [environment].env, each value read as Harbor’s resolve_env_vars reads it (harbor/utils/env.py): a whole value ${VAR} or ${VAR:-default} comes from the process environment, the default standing in when the variable is unset, and one with neither raises ImageConfigError naming the task and the variable, as Harbor fails the trial; any other value is literal;
  3. the process environment;
  4. environment/.env, its unquoted and double-quoted values interpolated against the levels above and the lines before them, as Compose’s dotenv reader does; single-quoted values are literal.

A reference none of them sets takes its default (${VAR:-x}, ${VAR-x}), else it is an empty string, as docker compose makes it, with one warning naming the task and the variable (added to unset when given, so the caller warns once for the whole document, else logged here). ${VAR:?msg} and ${VAR?msg} raise ImageConfigError with the message. The process environment is not part of the task’s digest: prepare and the environment server must run with the same variables, as Harbor’s one process does.

Every value is literal to the document, as it is to Compose (which splices a variable’s value in verbatim and never re-scans it): a $ in a host, task or infrastructure value is doubled here, the convention parse_env_file and _dotenv_variables keep for a literal dollar, so a password such as $2b$12$... neither trips the document’s residual check nor loses a $$ when the sandbox starts.

nemo_gym.data.tasks.harbor.compose_variables.env_file_entries(
text: str,
escaped_dollar: str = '$'
) -> list[tuple[str, str, str]]

The KEY=value lines of a Compose env file in file order, as (key, value, quote). quote is ', " or "" for an unquoted value, which is how Compose’s dotenv reader decides what to do with it: a single-quoted value is literal, the other two it interpolates. Blank lines and # comments are skipped and an optional export prefix dropped. A quoted value ends at its closing quote, which may be on a later line, and the rest of that line is a comment; its escapes are decoded as _quoted_env_value says, a double-quoted \$ becoming escaped_dollar. An unquoted value ends at the first # that follows whitespace and is returned as written, $ included. A line that is not KEY=value or a quote that never closes raises ComposeSubstitutionError naming the line number only, as env files hold secrets.

nemo_gym.data.tasks.harbor.compose_variables.env_file_paths(
service: str,
entries: typing.Any,
) -> list[str]

A service’s env_file entries (a path, a list of paths or long-syntax mappings) as the absolute paths of the files to read, in order, each resolved by resolve and existing. A missing required file raises ComposeSubstitutionError naming the entry as written and, when it differs, where it resolved to; an entry with required: false is dropped when absent, as Compose skips it; an entry with a format is refused, as only the default dotenv format is read.

nemo_gym.data.tasks.harbor.compose_variables.infrastructure_variables(
) -> dict[str, str]

Gym’s infrastructure set for task (source 1 of compose_variables, the one Harbor’s merge_compose_env puts above every other), as raw values: the caller escapes them for the document.

nemo_gym.data.tasks.harbor.compose_variables.inline_env_files(
document: dict,
base_dir: pathlib.Path | None,
variables: dict[str, str] | None = None
) -> dict

Read each service’s env_file entries into its environment mapping: relative to base_dir under the task’s rule (under_task_folder), or, with base_dir=None, the absolute paths resolve_env_files wrote, each file’s own rule applied already.

Every service’s environment becomes a mapping of strings. A service’s own entries win over its files, as in Compose. A bare NAME entry in a list-form environment (or NAME: with no value in the mapping form) takes the task variable of that name; when there is none the variable is left unset, as Compose leaves it, so it also removes the env_file value of that name. A missing required file raises ComposeSubstitutionError naming it; an entry with required: false is skipped when absent; an entry with a format is refused, as only the default dotenv format is read. A file that does not parse raises with the service, the file and the line number, never the line.

nemo_gym.data.tasks.harbor.compose_variables.literal_variables(
variables: collections.abc.Mapping[str, str] | None
) -> dict[str, str] | None

A deployment’s compose_variables values as the document reads literals (_literal on each), the way every source of compose_variables is escaped; None (no deployment values, as at prepare) stays None.

nemo_gym.data.tasks.harbor.compose_variables.parse_env_file(
text: str
) -> dict[str, str]

KEY=value lines of a Compose env_file, taken literally.

Lines are read as env_file_entries reads them. Values are not interpolated: a $ in a value is escaped as $$ so the document’s later interpolation and literal checks leave it as one $.

nemo_gym.data.tasks.harbor.compose_variables.residual_interpolation(
value: typing.Any,
path: str = ''
) -> str | None

The key path (services.api.environment.DB_PASS, services.a.command[1]) of the first string still carrying a $NAME or ${...} after substitution, or None. The path, not the value: the value next to a residual may be a secret the host supplied, and the caller reports this in an error message.

nemo_gym.data.tasks.harbor.compose_variables.resolve_env_file_entries(
service: str,
entries: typing.Any,
) -> list[str | dict[str, typing.Any]]

A service’s env_file entries with each path resolved by resolve to the absolute path of the file, the file not checked to exist: a required entry becomes its absolute path, an optional one (required: false) {"path": ..., "required": False}, so the reading (env_file_paths) can skip it when it is absent. Compose resolves each -f file’s paths before the merge and reads the env files of the merged document after it, so a file an earlier document names and a later file drops (env_file: !reset []) is never required to exist; what resolve refuses (a task’s entry outside its folder) is refused here, at the file’s own rule.

nemo_gym.data.tasks.harbor.compose_variables.resolve_env_files(
document: dict,
) -> dict

Each service’s env_file entries replaced by what resolve_env_file_entries resolves them to (absolute paths, an optional entry keeping its required: false), the list’s type kept, the files not read or checked to exist yet. Compose resolves a file’s relative paths against that file before it merges the -f files and reads the env files once after the merge, so a missing file is reported then, if the merged document still names it; inline_env_files with base_dir=None is that reading. Mutates and returns document.

nemo_gym.data.tasks.harbor.compose_variables.restore_compose_types(
document: dict
) -> dict

Give the typed healthcheck fields their types back after interpolation, which turns every scalar it touches into a string: disable becomes a boolean (${DISABLE:-false} must not disable the check by being a non-empty string) and retries an integer. Any other string value raises ComposeSubstitutionError naming the field. Mutates and returns document.

nemo_gym.data.tasks.harbor.compose_variables.substitute_compose(
value: typing.Any,
variables: dict[str, str]
) -> typing.Any

Interpolate ${VAR} references in every string of a Compose document from variables alone.

The process environment is never consulted, so a dataset resolves the same way on every deployment. A variable that is neither defined nor given a default raises ComposeSubstitutionError naming it.

nemo_gym.data.tasks.harbor.compose_variables.substitute_with_empty(
value: typing.Any,
variables: collections.abc.Mapping[str, str],
unset: set[str]
) -> typing.Any

substitute_compose with Compose’s rule for a bare ${VAR} or $VAR nobody sets: an empty string, the name added to unset for the caller to warn about once. Only the bare form raises the resolver’s default message; ${VAR:-x} keeps its default, ${VAR:?msg} its refusal, and every other failure passes through unchanged. The loader and the Dockerfile build (nemo_gym.data.tasks.harbor.image_build) interpolate a task’s Compose file through this one rule, so the two never disagree on the same file.

nemo_gym.data.tasks.harbor.compose_variables.under_task_folder(
base_dir: pathlib.Path

The task file’s resolver: entries relative to base_dir (the task’s environment folder) and kept under it, see _env_file_path.

nemo_gym.data.tasks.harbor.compose_variables.COMPOSE_DOTENV = '.env'
nemo_gym.data.tasks.harbor.compose_variables.EnvFileResolver = Callable[[str, str], Path]
nemo_gym.data.tasks.harbor.compose_variables.HARBOR_ENV_TEMPLATE = re.compile('\\$\\{([^}:]+)(?::-(.*))?\\}')
nemo_gym.data.tasks.harbor.compose_variables.LOGGER = logging.getLogger(__name__)
nemo_gym.data.tasks.harbor.compose_variables.LOG_MOUNT_PATHS = {'VERIFIER_LOGS': '/logs/verifier', 'AGENT_LOGS': '/logs/agent', 'ARTIFACTS': '/...
nemo_gym.data.tasks.harbor.compose_variables.TEST_DIR = '/tests'
nemo_gym.data.tasks.harbor.compose_variables._DOUBLE_QUOTE_ESCAPES = {'n': '\n', 'r': '\r', 't': '\t', '\\': '\\', '"': '"'}
nemo_gym.data.tasks.harbor.compose_variables._HEALTHCHECK_BOOLEANS = {'true': True, 'false': False}
nemo_gym.data.tasks.harbor.compose_variables._NAME = re.compile('[A-Za-z_][A-Za-z0-9_]*')
nemo_gym.data.tasks.harbor.compose_variables._WARNED_UNSET: set[tuple[str, str]] = set()