Skip to navigation

nemo_gym.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_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-
_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
inline_env_filesRead each service’s env_file entries (relative to base_dir) into its environment mapping.
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
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

Data

COMPOSE_DOTENV

HARBOR_ENV_TEMPLATE

LOGGER

LOG_MOUNT_PATHS

TEST_DIR

_DOUBLE_QUOTE_ESCAPES

_HEALTHCHECK_BOOLEANS

_NAME

_WARNED_UNSET

API

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

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

nemo_gym.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.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.tasks.harbor.digest.is_ignored): it would change the document without changing the identity the server checks at seed.

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

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

nemo_gym.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.tasks.harbor.compose_variables._resolve(
name: str,
operator: str,
argument: str,
variables: dict[str, str]
) -> str
nemo_gym.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.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.tasks.harbor.compose_variables._warn_unset(
unset: set[str]
) -> None
nemo_gym.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.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.tasks.harbor.compose_variables.inline_env_files(
document: dict,
base_dir: pathlib.Path,
variables: dict[str, str] | None = None
) -> dict

Read each service’s env_file entries (relative to base_dir) into its environment mapping.

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