Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions architecture/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,18 @@ A supported service key handled *outside* the service-key registry because the
(`env_file`, `volumes`), spans keys, or occupies the image/command slot
(`entrypoint`). Structural keys keep their own validate/emit machinery.
_Avoid_: special key, bespoke key (bespoke describes the spec body, not the key).

**Store kind**:
One flavor of podman-secret-backed store — a Compose `secret` or `config` — with
its own namespacing prefix, allowed sources, and default mount target
(`StoreKind` in `stores.py`). Both kinds render as podman secrets (podman has no
config primitive), so they differ in namespacing and mount, never in the podman
noun; the noun lives in `stores.py` alone.
_Avoid_: secret type, store type, backend.

**Store registry**:
The tuple of every store kind (`_STORE_KINDS = (SECRET, CONFIG)` in `stores.py`),
module-private so the store interface (`validate`, `flags`, `create_lines`,
`teardown_line`, `referenced_variables`) hides the kinds from callers — the same
single-source shape as the service-key registry.
_Avoid_: store list, kinds table.
64 changes: 33 additions & 31 deletions architecture/supported-subset.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,51 +305,52 @@ colon-less entry that is not absolute (e.g. `./cache`) is malformed and raises.
relative) or `environment:` (a host environment variable name), both as a
plain string. `external: true` gets its own rejection message (compose's
"must already exist" secrets have no analogue here); any other unrecognized
key raises generically (`_validate_def`, `compose2pod/store.py`, via the
`SECRET` `StoreKind` in `compose2pod/secrets.py`).
key raises generically (`_validate_def`, `compose2pod/stores.py`, via the
`SECRET` `StoreKind` in `compose2pod/stores.py`).
- **Per-service `secrets:` references:** short form (`- name`) or long form
(a mapping with `source` and optionally `target`, `uid`, `gid`, `mode`).
`source` must name a top-level secret; an unknown `source` raises at
`validate()` time (`_ref_source`/`validate_all`, `compose2pod/store.py` --
`validate_all` dispatches per store kind through the internal
`validate()` time (`_ref_source`/`stores.validate`, `compose2pod/stores.py` --
`stores.validate` dispatches per store kind through the internal
`_validate_kind`).
- **Closure-scoped creation:** only secrets referenced (by `source`) from
somewhere in the target service's dependency closure are ever created, so a
top-level secret nothing in the closure references never becomes a
`podman secret create` call (`referenced_names`, `compose2pod/store.py`,
`podman secret create` call (`_referenced_names`, `compose2pod/stores.py`,
driven by the same `startup_order` closure used to decide which services
run at all).
- **Creation:** each referenced secret becomes one pod-namespaced
`podman secret create <pod>-<name> ...` line, emitted right after
`podman pod create` and before any `podman run` by `all_create_lines`
(`compose2pod/store.py`, called from `emit_script`, `compose2pod/emit.py`).
`podman pod create` and before any `podman run` by `stores.create_lines`
(`compose2pod/stores.py`, called from `emit_script`, `compose2pod/emit.py`).
A `file:` source resolves `Path(project_dir, file)` through `to_shell()`,
so a `${VAR}` in the path expands live when the script runs, exactly like
other interpolated fields. An `environment:` source instead pipes
`printf '%s' "${VAR-}"` into `podman secret create ... -`, where the
`${VAR-}` means an *unset* host variable yields an empty secret rather than
failing the script (`create_lines`, `compose2pod/store.py`).
failing the script (`_create_lines_for`, `compose2pod/stores.py`).
- **Mounting:** each service reference becomes a
`--secret source=<pod>-<name>,target=<target>` flag on that service's
`podman run`, assembled per service by `all_flags` (`compose2pod/store.py`,
`podman run`, assembled per service by `stores.flags` (`compose2pod/stores.py`,
called from `emit_script`, `compose2pod/emit.py`), where `target` defaults
to the secret's own name when the reference doesn't give one (short form,
or long form without `target`). `uid`/`gid`/`mode` are only added when the
long form gives them explicitly; `mode` renders as a 4-digit octal string
when given as a Python int (`0o400` becomes `"0400"`) and passes through
verbatim when given as a string (`flags`, `compose2pod/store.py`). When
verbatim when given as a string (`_flags_for`, `compose2pod/stores.py`). When
`uid`/`gid`/`mode` are omitted, podman itself applies its own defaults: the
secret is mounted at `/run/secrets/<target>`, owned `0:0`, mode `0444`.
- **Teardown:** the EXIT trap that force-removes the pod also runs
`podman secret rm <pod>-<name> ...` for every referenced secret, so the
store never outlives the pod even when the script exits abnormally
(`all_teardown_names`, `compose2pod/store.py`, called from `emit_script`,
`compose2pod/emit.py`).
store never outlives the pod even when the script exits abnormally. The
store module returns the complete best-effort trap fragment and
`emit_script` splices it after the pod-removal fragment
(`stores.teardown_line`, `compose2pod/stores.py`; `compose2pod/emit.py`).
- **Variable interpolation:** an `environment:` source's variable name, and
any `${VAR}` inside a `file:` path, both count toward the CLI's
informational stderr note of variables the generated script expands at run
time (`referenced_variables`, `compose2pod/store.py`, assembled across
store kinds by `all_referenced_variables` and folded into
time (`_referenced_variables_for`, `compose2pod/stores.py`, assembled across
store kinds by `stores.referenced_variables` and folded into
`referenced_variables()` in `compose2pod/emit.py`); see Variable
interpolation, below, for the note itself.
- Everything else raises `UnsupportedComposeError` rather than silently doing
Expand All @@ -363,57 +364,58 @@ colon-less entry that is not absolute (e.g. `./cache`) is malformed and raises.
relative), `environment:` (a host environment variable name), or `content:`
(inline literal text), all as a plain string. `external: true` gets its own
rejection message, mirroring secrets; any other unrecognized key raises
generically (`_validate_def`, `compose2pod/store.py`, via the `CONFIG`
`StoreKind` in `compose2pod/configs.py`).
generically (`_validate_def`, `compose2pod/stores.py`, via the `CONFIG`
`StoreKind` in `compose2pod/stores.py`).
- **Per-service `configs:` references:** short form (`- name`) or long form
(a mapping with `source` and optionally `target`, `uid`, `gid`, `mode`).
`source` must name a top-level config; an unknown `source` raises at
`validate()` time (`_ref_source`/`validate_all`, `compose2pod/store.py`).
`validate()` time (`_ref_source`/`stores.validate`, `compose2pod/stores.py`).
- **Closure-scoped creation:** only configs referenced (by `source`) from
somewhere in the target service's dependency closure are ever created, so a
top-level config nothing in the closure references never becomes a
`podman secret create` call -- the same closure-scoped-creation rule
secrets follow (`referenced_names`, `compose2pod/store.py`).
secrets follow (`_referenced_names`, `compose2pod/stores.py`).
- **Creation and delivery:** configs are delivered through podman's secret
store, exactly like secrets, but pod- *and kind*-namespaced with a
`config-` prefix (store name `<pod>-config-<name>`, vs. a secret's bare
`<pod>-<name>`), so a config never collides with a same-named secret. Each
referenced config becomes one `podman secret create <pod>-config-<name>
...` line, emitted right after `podman pod create` and before any
`podman run` by `all_create_lines` (`compose2pod/store.py`, called from
`podman run` by `stores.create_lines` (`compose2pod/stores.py`, called from
`emit_script`, `compose2pod/emit.py`). A `file:` source resolves
`Path(project_dir, file)` through `to_shell()`; an `environment:` source
pipes `printf '%s' "${VAR-}"` into `podman secret create ... -`; and a
`content:` source pipes the literal text through `to_shell()` into that
same `podman secret create ... -` form, so a `${VAR}` written inside
`content:` stays live and expands against the generated script's own
runtime environment when the script runs -- the same deferred-interpolation
model as `file:`/`environment:` (`create_lines`, `compose2pod/store.py`).
model as `file:`/`environment:` (`_create_lines_for`, `compose2pod/stores.py`).
- **Mounting:** each service reference becomes a
`--secret source=<pod>-config-<name>,target=<target>` flag on that
service's `podman run`, assembled per service by `all_flags`
(`compose2pod/store.py`, called from `emit_script`, `compose2pod/emit.py`).
service's `podman run`, assembled per service by `stores.flags`
(`compose2pod/stores.py`, called from `emit_script`, `compose2pod/emit.py`).
Unlike a secret, whose default `target` is its own name (mounted by podman
under `/run/secrets/<target>`), a config's default `target` is the
container-root absolute path `/<name>` (`CONFIG.default_target`,
`compose2pod/configs.py`). A long-form `target` must be an absolute path
`compose2pod/stores.py`). A long-form `target` must be an absolute path
(start with `/`); a relative target raises
(`CONFIG.require_absolute_target`, checked by `_check_target`,
`compose2pod/store.py`). `uid`/`gid`/`mode` behave exactly as for secrets:
`compose2pod/stores.py`). `uid`/`gid`/`mode` behave exactly as for secrets:
only added when the long form gives them explicitly, `mode` renders as a
4-digit octal string when given as a Python int and passes through
verbatim when given as a string (`flags`, `compose2pod/store.py`).
verbatim when given as a string (`_flags_for`, `compose2pod/stores.py`).
- **Teardown:** the EXIT trap that force-removes the pod also runs
`podman secret rm <pod>-config-<name> ...` for every referenced config, so
the store never outlives the pod even when the script exits abnormally --
byte-for-byte the same teardown parity as secrets (`all_teardown_names`,
`compose2pod/store.py`, called from `emit_script`, `compose2pod/emit.py`).
byte-for-byte the same teardown parity as secrets (`stores.teardown_line`,
`compose2pod/stores.py`, spliced into the trap by `emit_script`,
`compose2pod/emit.py`).
- **Variable interpolation:** an `environment:` source's variable name, any
`${VAR}` inside a `file:` path, and any `${VAR}` inside `content:` all
count toward the CLI's informational stderr note of variables the
generated script expands at run time (`referenced_variables`,
`compose2pod/store.py`, assembled across store kinds by
`all_referenced_variables` and folded into `referenced_variables()` in
generated script expands at run time (`_referenced_variables_for`,
`compose2pod/stores.py`, assembled across store kinds by
`stores.referenced_variables` and folded into `referenced_variables()` in
`compose2pod/emit.py`); see Variable interpolation, below, for the note
itself.
- Everything else raises `UnsupportedComposeError` rather than silently
Expand Down
13 changes: 0 additions & 13 deletions compose2pod/configs.py

This file was deleted.

16 changes: 7 additions & 9 deletions compose2pod/emit.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@
from pathlib import Path
from typing import Any

from compose2pod import stores
from compose2pod.exceptions import UnsupportedComposeError
from compose2pod.graph import depends_on, hostnames, startup_order
from compose2pod.healthcheck import health_cmd, interval_seconds
from compose2pod.keys import SERVICE_KEYS, Token, _Expand, _key_value_pairs
from compose2pod.pod import pod_create_flags
from compose2pod.resources import deploy_resource_flags
from compose2pod.shell import to_shell, variable_names
from compose2pod.store import all_create_lines, all_flags, all_referenced_variables, all_teardown_names
from compose2pod.stores import STORE_KINDS


HEALTHY_WAIT_BUDGET_SECONDS = 120
Expand Down Expand Up @@ -105,7 +104,7 @@ def run_flags(name: str, svc: dict[str, Any], pod: str, hosts: list[str], projec
for key, spec in SERVICE_KEYS.items():
if key in svc:
flags += spec.emit(svc[key])
flags += all_flags(svc, pod, STORE_KINDS)
flags += stores.flags(svc, pod)
flags += deploy_resource_flags(svc)
return flags

Expand Down Expand Up @@ -205,17 +204,16 @@ def emit_script(compose: dict[str, Any], options: EmitOptions) -> str:

lines = [_SCRIPT_HEADER]
teardown = f"podman pod rm -f {shlex.quote(options.pod)} >/dev/null 2>&1 || true"
store_names = all_teardown_names(services, order, options.pod, STORE_KINDS)
if store_names:
stores = " ".join(store_names)
teardown += f"; podman secret rm {stores} >/dev/null 2>&1 || true"
store_teardown = stores.teardown_line(compose, order, options.pod)
if store_teardown:
teardown += f"; {store_teardown}"
lines.append(f"trap '{teardown}' EXIT")
pod_flags = pod_create_flags(services, order)
pod_create = f"podman pod create --name {shlex.quote(options.pod)}"
if pod_flags:
pod_create += " " + _render(pod_flags)
lines.append(pod_create)
lines.extend(all_create_lines(compose, order, options.pod, options.project_dir, STORE_KINDS))
lines.extend(stores.create_lines(compose, order, options.pod, options.project_dir))
waited: set[str] = set()
for name in order:
for dep, condition in depends_on(services[name]).items():
Expand Down Expand Up @@ -252,5 +250,5 @@ def referenced_variables(compose: dict[str, Any], options: EmitOptions) -> list[
for token in pod_create_flags(services, order):
if isinstance(token, _Expand):
names.update(variable_names(token.value))
names |= all_referenced_variables(compose, order, options.project_dir, STORE_KINDS)
names |= stores.referenced_variables(compose, order, options.project_dir)
return sorted(names)
5 changes: 2 additions & 3 deletions compose2pod/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@

from typing import Any

from compose2pod import stores
from compose2pod.exceptions import UnsupportedComposeError
from compose2pod.graph import depends_on, hostnames
from compose2pod.healthcheck import has_healthcheck, interval_seconds
from compose2pod.keys import SERVICE_KEYS, STRUCTURAL_KEYS
from compose2pod.pod import uses_pod_options, validate_pod_options
from compose2pod.resources import validate_deploy
from compose2pod.store import validate_all
from compose2pod.stores import STORE_KINDS


SUPPORTED_SERVICE_KEYS = set(SERVICE_KEYS) | STRUCTURAL_KEYS
Expand Down Expand Up @@ -135,7 +134,7 @@ def validate(compose: dict[str, Any]) -> list[str]:
warnings.extend(_validate_service(name, svc))
hostnames(services) # validate hostname/container_name/networks shapes at the gate
_validate_depends_on(services)
validate_all(compose, STORE_KINDS)
stores.validate(compose)
if uses_pod_options(services):
warnings.append(
"dns/sysctls apply pod-wide -- all containers in the pod share one /etc/resolv.conf and sysctl set"
Expand Down
13 changes: 0 additions & 13 deletions compose2pod/secrets.py

This file was deleted.

Loading