feat(core): add file-or-env secret reader (secret_env) (PSA-37) #10

Merged
nrupard merged 2 commits from feat/psa-37-secret-env-core into main 2026-06-03 20:31:13 +02:00
Owner

What

Move the file-or-env secret reader into dunite-core so every consumer shares one implementation. Adds dunite_core::services::secret_env::secret_env(name) (re-exported as dunite_core::services::secret_env).

Semantics (unchanged from bunyip's original)

Resolution order:

  1. {NAME}_FILE: if set and non-empty, the secret is the trimmed contents of that file (a compose secrets: mount under /run/secrets/...). An unreadable file PANICS - a misconfigured secret mount must fail fast at startup, never silently fall back.
  2. {NAME}: the plain environment variable (the dev .env path).

Empty values (empty file or empty env var) are treated as unset and return None, so compose ${VAR:-} defaults and empty secret files both mean "not configured".

Why

This is generic, domain-free infrastructure (the same family as dunite-core's jwt/encryption/password services). Keeping it in bunyip-domain forces every other dunite consumer (mokosh-server, future services) that wants Docker file-based secrets to re-implement the convention with potentially divergent trim/empty/fail-fast semantics. Raised as finding 8 of the bunyip PR #40 review.

Tests

The five unit tests ported from bunyip's config.rs: plain-env fallback (with trim), unset/blank -> None, file precedence over env var, empty file -> None, and fail-fast panic on an unreadable file. No new dependencies (std::env + std::fs only). fmt + clippy -D warnings clean; full cargo test --workspace green.

Follow-up (separate, not in this PR)

Switch bunyip-domain to re-export dunite_core::services::secret_env and delete its local copy. That depends on a dunite-core release and a bunyip dependency bump, so it lands after this merges and a release is cut.

#PSA-37

## What Move the file-or-env secret reader into dunite-core so every consumer shares one implementation. Adds `dunite_core::services::secret_env::secret_env(name)` (re-exported as `dunite_core::services::secret_env`). ## Semantics (unchanged from bunyip's original) Resolution order: 1. `{NAME}_FILE`: if set and non-empty, the secret is the trimmed contents of that file (a compose `secrets:` mount under `/run/secrets/...`). An unreadable file PANICS - a misconfigured secret mount must fail fast at startup, never silently fall back. 2. `{NAME}`: the plain environment variable (the dev `.env` path). Empty values (empty file or empty env var) are treated as unset and return `None`, so compose `${VAR:-}` defaults and empty secret files both mean "not configured". ## Why This is generic, domain-free infrastructure (the same family as dunite-core's jwt/encryption/password services). Keeping it in bunyip-domain forces every other dunite consumer (mokosh-server, future services) that wants Docker file-based secrets to re-implement the convention with potentially divergent trim/empty/fail-fast semantics. Raised as finding 8 of the bunyip PR #40 review. ## Tests The five unit tests ported from bunyip's config.rs: plain-env fallback (with trim), unset/blank -> None, file precedence over env var, empty file -> None, and fail-fast panic on an unreadable file. No new dependencies (std::env + std::fs only). fmt + clippy -D warnings clean; full `cargo test --workspace` green. ## Follow-up (separate, not in this PR) Switch bunyip-domain to re-export `dunite_core::services::secret_env` and delete its local copy. That depends on a dunite-core release and a bunyip dependency bump, so it lands after this merges and a release is cut. #PSA-37
feat(core): add file-or-env secret reader (secret_env) (PSA-37)
All checks were successful
Checks / fmt + clippy + test (pull_request) Successful in 30s
4fafc5aabd
bunyip PR #40 (BUNYIP-38) added `secret_env(name)` to bunyip-domain's config.rs: it resolves a secret from `{NAME}_FILE` (trimmed contents of a Docker Compose secret-file mount, panicking on an unreadable path) with fallback to the plain `{NAME}` env var, treating empty values as unset. That is generic, domain-free infrastructure (the same family as dunite-core's jwt/encryption/password services), so its home is dunite-core. Until it lives here, every consumer (bunyip, mokosh-server, future services) that wants Docker file-based secrets re-implements the convention with potentially divergent trim/empty/fail-fast semantics.

Add it as `dunite_core::services::secret_env::secret_env`, re-exported as `dunite_core::services::secret_env`, with the same semantics and the unit tests ported from bunyip's config.rs (file precedence, plain-env fallback, empty-file/empty-var -> None, fail-fast panic on an unreadable file). No new dependencies (std::env + std::fs only).

The consumer switch (bunyip-domain re-exports this and deletes its local copy) is a follow-up that depends on a dunite-core release and a bunyip dependency bump.

#PSA-37

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs(core): clarify secret_env file precedence, trimming, and startup-only use (PSA-37 review)
All checks were successful
Checks / fmt + clippy + test (pull_request) Successful in 25s
create-release / create-release (pull_request) Has been skipped
07a0d405ae
Code review doc clarifications (no behavior change): document that a set `{NAME}_FILE` is authoritative so an empty file resolves to None and does NOT fall back to the plain `{NAME}` var; that surrounding whitespace is stripped from the resolved value (so whitespace-significant secrets are unsupported and should be encoded); and that the function is intended for startup/config load because an unreadable secret file panics.

#PSA-37

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch feat/psa-37-secret-env-core 2026-06-03 20:31:13 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
psa-systems/dunite!10
No description provided.