Skip to content

feat: support compose configs via a shared podman store module#31

Merged
lesnik512 merged 5 commits into
mainfrom
feat/configs
Jul 10, 2026
Merged

feat: support compose configs via a shared podman store module#31
lesnik512 merged 5 commits into
mainfrom
feat/configs

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Summary

Add compose configs support (Bucket B). Configs are the non-sensitive sibling of secrets: same shape, but they mount at the container-root path /<name> (not /run/secrets/<name>) and add an inline content: source.

Rather than duplicate the secret machinery, this branch extracts it into a shared, kind-parameterized compose2pod/store.py (a frozen StoreKind), so secrets.py and configs.py shrink to thin kind-specs and emit.py/parsing.py weave both through one union seam over STORE_KINDS = (SECRET, CONFIG). Because the secret kind keeps store prefix "", the extraction is byte-identical for existing documents.

Design: planning/changes/2026-07-10.03-configs.md.

What's supported

  • Top-level configs: with exactly one string source: file: (host path, resolved against --project-dir), environment: (host env var), or content: (inline literal).
  • content: runs through to_shell(), so ${VAR} inside it expands at script-run time (the tool's deferred-interpolation model) and its var names feed the stderr interpolation note.
  • Service configs: short form [name] and full long form {source, target, uid, gid, mode}.
  • Delivered via podman's secret store, pod-namespaced <pod>-config-<name> so a config never collides with a same-named secret; created after podman pod create, removed in the EXIT trap.
  • Default target is the container-root /<name>; a long-form target: must be absolute (relative targets rejected). external: true rejected.

Architecture

store.py (leaf) owns the machinery parameterized by StoreKind{label, top_key, prefix, sources, default_target, require_absolute_target}. secrets.py/configs.py are the two kind-specs; stores.py aggregates STORE_KINDS. emit.py/parsing.py call the kind-unioning entry points, so adding a kind is a registry change, not new emit logic.

Security

Config/secret names and env-var names flow into generated shell raw, so they are charset-validated with re.fullmatch (^[a-zA-Z0-9][a-zA-Z0-9_.-]*$ / ^[a-zA-Z_][a-zA-Z0-9_]*$) in the shared gate. The new content: surface was adversarially probed: to_shell neutralizes " ` $ \ and never matches $(...), so arbitrary content cannot break out of its double-quoted printf fragment while legitimate ${VAR} stays live. Long-form target is shlex.quoted by the emit renderer. An independent whole-branch review confirmed no injection path.

Testing

just test-ci at 100% coverage (263 tests), just lint-ci clean, just check-planning OK. The ported secret suite (test_store.py) is the byte-identical guard for the refactor; test_configs.py covers file/env/content create lines, /<name> and absolute targets, relative-target rejection, and content var interpolation; test_emit.py proves the unioned trap and that a same-named secret+config produce distinct stores. architecture/supported-subset.md gains a Configs section and its stale post-refactor secret citations were corrected.

Known minor limitations (parity with secrets, non-blocking)

  • A comma in a long-form target is interpreted by podman as extra --secret sub-options (arg-level, not shell injection; the compose author controls both inputs). Same as secrets today.
  • A non-string target bypasses the absolute-path rule (podman rejects it at runtime). Same target-passthrough behavior as secrets.

Fixing these for configs only would diverge the two kinds; left as a possible future hardening pass covering both.

@lesnik512 lesnik512 merged commit 48515c4 into main Jul 10, 2026
6 checks passed
@lesnik512 lesnik512 deleted the feat/configs branch July 10, 2026 19:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant