From 8387bf11a681cd434802ee9a8329330af5b8e9b4 Mon Sep 17 00:00:00 2001 From: Maxwell Date: Sat, 25 Jul 2026 16:53:44 +0200 Subject: [PATCH] docs(project): add repository codemap --- .slim/codemap.json | 77 ++++++++++++++++++++ AGENTS.md | 12 ++++ codemap.md | 93 ++++++++++++++++++++++++ src/cli/codemap.md | 70 ++++++++++++++++++ src/codemap.md | 39 ++++++++++ src/compose/codemap.md | 159 +++++++++++++++++++++++++++++++++++++++++ src/config/codemap.md | 95 ++++++++++++++++++++++++ src/docker/codemap.md | 65 +++++++++++++++++ src/env/codemap.md | 65 +++++++++++++++++ src/process/codemap.md | 60 ++++++++++++++++ src/render/codemap.md | 83 +++++++++++++++++++++ src/secrets/codemap.md | 60 ++++++++++++++++ src/testing/codemap.md | 46 ++++++++++++ 13 files changed, 924 insertions(+) create mode 100644 .slim/codemap.json create mode 100644 codemap.md create mode 100644 src/cli/codemap.md create mode 100644 src/codemap.md create mode 100644 src/compose/codemap.md create mode 100644 src/config/codemap.md create mode 100644 src/docker/codemap.md create mode 100644 src/env/codemap.md create mode 100644 src/process/codemap.md create mode 100644 src/render/codemap.md create mode 100644 src/secrets/codemap.md create mode 100644 src/testing/codemap.md diff --git a/.slim/codemap.json b/.slim/codemap.json new file mode 100644 index 0000000..f95beee --- /dev/null +++ b/.slim/codemap.json @@ -0,0 +1,77 @@ +{ + "metadata": { + "version": "1.0.0", + "last_run": "2026-07-25T14:49:48.247Z", + "root": "/Users/maxwellmapako/Git/stackctl", + "include_patterns": [ + "src/**/*.ts", + "deno.json", + "renovate.json" + ], + "exclude_patterns": [ + "**/*_test.ts", + "**/*.test.ts", + "**/*.spec.ts", + "tests/**", + "__tests__/**", + "docs/**", + "*.md", + "LICENSE", + "dist/**", + "build/**", + "node_modules/**", + ".coverage/**" + ], + "exceptions": [] + }, + "file_hashes": { + "deno.json": "5fe7bb0a193c73ba9ab2d7c39a36d2ed", + "renovate.json": "7c9b928b8987124bed6b28883a9687e6", + "src/cli/mod.ts": "00243b0de688f250042b16fee4872c2d", + "src/compose/discover.ts": "940e5f38d0e0f6312bd61b7e968569f4", + "src/compose/generate.ts": "6209433dc425bbe5103fc152ff3cc119", + "src/compose/load.ts": "12b52236167bf99c3e155c384e6549ca", + "src/compose/merge.ts": "b3cd7d2bf92fc1d093596dc8eb7f71db", + "src/compose/mod.ts": "0c83c55a4e4d4e4d591c451d4fff9773", + "src/compose/override.ts": "d60fb396d29a15313d413180f47476c1", + "src/compose/plan.ts": "b2784452e655fe500562b128871ca2b6", + "src/compose/reload.ts": "af2b11dffb4f110e4053cabe6ac5d075", + "src/compose/sync.ts": "ed5c67440bc11a054dc0c4dfe42b5dc2", + "src/compose/transform.ts": "1fdf5d424493b304bcd849e9d0836ed2", + "src/compose/types.ts": "c8b38efa061342ae32904beb0ba7dfa9", + "src/compose/volumes.ts": "967d238507521e085767e5d1fb9ea4fd", + "src/config/defaults.ts": "c8f3cba23f87b199c58617e473431dfd", + "src/config/init.ts": "0cf38639c0f954dd3255cf640b92e394", + "src/config/load.ts": "9171d47d97f1a8c8c0cb547428848e9d", + "src/config/merge.ts": "11e69aa61ad26c71937b46fbc6b06bf0", + "src/config/mod.ts": "aea05aaffc63bb5948525c23d3b29cba", + "src/config/types.ts": "53c49bfaa0a981d3e99ebd57bced12a0", + "src/config/validate.ts": "3912273078fd61ce3e5d8d3942b8d0ed", + "src/docker/mod.ts": "40ffa338a149cc954825c296fbfa24dd", + "src/env/mod.ts": "bc28cb84e7c17e3e7c18c51999e901f2", + "src/env/types.ts": "cf911306a70bee066ecd94b3246d1d96", + "src/main.ts": "49922cd2ec13833d914201fbd00059fd", + "src/process/runner.ts": "69b2c4983181e12ce6fd3c1150e0ff9e", + "src/process/types.ts": "26ead4ac38df3dc0cfd6f9222e3b6c6f", + "src/render/mod.ts": "cbf8cf6dd400c8a499e69bf57febd18b", + "src/secrets/index.ts": "42a570bfd29a052876d235859adea0e9", + "src/secrets/mod.ts": "7fc2c19b3e98e5cd4a4aadea01c4c15b", + "src/secrets/types.ts": "4b9870e0afe18fdafc0d237949d4e4c7", + "src/testing/fakes.ts": "155670849b385ad66a3d7868752e7b51", + "src/testing/mod.ts": "91a805e7ae874928e0d88028a40f617d", + "src/version.ts": "d12cfc326f43822ca2fcd782ff390c2e" + }, + "folder_hashes": { + ".": "5cfbcb64d0082c6c3fe58ded5870c051", + "src": "d8c5df30952b1cbb7286b0f8750e222f", + "src/cli": "d4fe4219490373f57c8def4ad6d32c66", + "src/compose": "079b1338a0d6366f0c1aa99435a5c8ed", + "src/config": "1c9c83c1f66720a8de640711cdc1e084", + "src/docker": "9e79906bb316e466910ea06715c45ba5", + "src/env": "a6747b7e75c24b4c9a95c3889e875c8b", + "src/process": "73b6d490c2df5f37f309c542b2b26cbb", + "src/render": "2ffa53736bca6abd8de0d145e4929036", + "src/secrets": "8055ab3d61611a7eca16d35b20ad34e7", + "src/testing": "ebdfc0eda40532a85abb455d21fa416d" + } +} diff --git a/AGENTS.md b/AGENTS.md index 7a8ca15..6dbe6ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -132,3 +132,15 @@ If no issue number exists, use `/` and link to an issue - This project is in **early development**. All commands are listed as "Planned" in `README.md`; check actual implementation status in `src/cli/mod.ts` before assuming a command exists. - Permissions for the compiled binary are defined in `src/main.ts` shebang and the CI build step. Do not change permissions without updating both. - Git-ignored files: `.stackctl.local`, `.stackctl.local.*`, `*.env` (except `.env.example`), `*.env.enc`, `age-key.txt`, `age.key`, `dist/`, `.rendered/`, `.coverage/`, `cov/`. + +## Repository Map + +A full codemap is available at `codemap.md` in the project root. + +Before working on any task, read `codemap.md` to understand: + +- Project architecture and entry points +- Directory responsibilities and design patterns +- Data flow and integration points between modules + +For deep work on a specific folder, also read that folder's `codemap.md`. diff --git a/codemap.md b/codemap.md new file mode 100644 index 0000000..e9d1846 --- /dev/null +++ b/codemap.md @@ -0,0 +1,93 @@ +# Repository Atlas: stackctl + +## Project Responsibility + +`stackctl` is a Deno 2.x command line application for managing Docker Compose based stacks across +multi-service repositories. It resolves layered `.stackctl` configuration, discovers service compose +files, generates canonical Docker Swarm stack YAML, renders environment substitutions, manages SOPS +and age encrypted dotenv files, and delegates Docker operations through a typed process execution +boundary. + +The package is published as `@anitrend/stackctl` and exports `src/main.ts` as the executable entry +point. + +## System Entry Points + +- `src/main.ts`: Deno executable bootstrap. It passes `Deno.args` to the CLI module and exits with + the returned status code. +- `src/cli/mod.ts`: Cliffy command tree, option parsing, command orchestration, output formatting, + and exit-code mapping. +- `deno.json`: Package metadata, JSR export path, task definitions, formatter settings, lint rules, + compiler options, and JSR dependency map. +- `renovate.json`: Renovate dependency automation configuration, using `dependencies/` branches and + `main` as the base branch. +- `AGENTS.md`: Agent-facing repository instructions, runtime constraints, branch and commit + conventions, and the required codemap entry point. + +## Root Asset Map + +| Path | Responsibility | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `deno.json` | Defines Deno tasks, JSR imports, lint, format, strict compiler options, package name, version, and export target. | +| `deno.lock` | Locks JSR and standard library dependency versions for deterministic Deno resolution. | +| `README.md` | User-facing overview, install path, command status table, feature list, and development commands. | +| `CONTRIBUTING.md` | Contribution workflow, branch naming, commit format, and PR expectations. | +| `AGENTS.md` | Agent operating guide, source layout, runtime rules, and codemap discovery instructions. | +| `renovate.json` | Automated dependency update policy and branch prefix configuration. | +| `.github/` | CI, release, version bump, stale issue handling, release drafter, auto approval, and composite setup action workflows. | +| `.slim/codemap.json` | Codemap state file containing selected source hashes and folder hashes for change detection. | + +## Repository Directory Map + +| Directory | Responsibility Summary | Detailed Map | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------- | +| `src/` | Deno executable entry point and version module. It bootstraps the CLI and centralizes the release version constant. | [View Map](src/codemap.md) | +| `src/cli/` | Command registration, option parsing, runner injection, domain delegation, output formatting, and semantic exit-code adaptation. | [View Map](src/cli/codemap.md) | +| `src/config/` | Configuration shape, defaults, discovery, YAML loading, layered merging, validation, initialization, and public config exports. | [View Map](src/config/codemap.md) | +| `src/compose/` | Compose discovery, loading, generation, merging, overrides, Swarm transforms, named volumes, reload, sync, and operation planning. | [View Map](src/compose/codemap.md) | +| `src/render/` | Docker Compose interpolation, service environment scope construction, recursive substitution, path absolutization, and warning collection. | [View Map](src/render/codemap.md) | +| `src/docker/` | Typed Docker, Docker Compose, and Docker Swarm command wrappers built around the injected `ProcessRunner` boundary. | [View Map](src/docker/codemap.md) | +| `src/secrets/` | SOPS and age dotenv encryption, decryption materialization, secure cleanup, tooling checks, and deploy pipeline integration. | [View Map](src/secrets/codemap.md) | +| `src/env/` | Env example discovery, `.env` scaffolding, profile materialization, key diffing, status listing, and plaintext audit findings. | [View Map](src/env/codemap.md) | +| `src/process/` | External command execution abstraction, real Deno process adapter, dry-run behavior, output capture, streaming, and availability checks. | [View Map](src/process/codemap.md) | +| `src/testing/` | In-memory `ProcessRunner` test harness with configured responses, command recording, dry-run propagation, and assertion helpers. | [View Map](src/testing/codemap.md) | + +## High-Level Data & Control Flow + +1. Runtime starts at `src/main.ts`, which calls `main(Deno.args)` from `src/cli/mod.ts`. +2. The CLI builds a Cliffy command tree, parses user input, and selects a command action. +3. Config-aware commands call `resolveConfig` to merge defaults, base config, profile overlays, + local overlays, and explicit override files into a validated `ResolvedConfig`. +4. Compose workflows discover source compose files with `x-stack` metadata, load stack fragments, + merge compose data, apply override files, transform service definitions for Swarm compatibility, + collect named volumes, and serialize stack YAML. +5. Render workflows parse generated stack YAML, build service-specific interpolation scopes from the + shell environment, `env_file` values, and inline service environment values, then substitute + `${VAR}` and `$VAR` references. +6. Docker workflows pass generated or rendered compose files into Docker wrapper functions. Those + wrappers build explicit argument vectors and execute them through `ProcessRunner`. +7. Secrets workflows use the same process boundary to call SOPS, age, shred, rm, and Docker during + encryption, decryption, cleanup, and deploy operations. +8. Env workflows create or inspect plaintext `.env` artifacts that compose, render, and secrets + workflows consume by convention. + +## Architectural Patterns + +- Command adapter layer: `src/cli/` translates user input into typed calls across the domain + modules. +- Port and adapter boundary: `src/process/` defines `ProcessRunner`, while production uses + `RealProcessRunner` and tests use fakes from `src/testing/`. +- Functional transformation pipeline: compose and render modules prefer pure merge, transform, and + interpolation helpers that return new data structures. +- Layered configuration: config resolution applies deterministic file ordering and validates the + merged result before downstream modules consume it. +- Structured result envelopes: domain operations return warnings, errors, drift state, generated + content, and operation status as data so the CLI can decide presentation and exit codes. + +## Verification Entry Points + +- `deno task fmt:check`: Check formatting using the repository formatter settings. +- `deno task lint`: Run lint rules for `src/`. +- `deno task check`: Type-check `src/main.ts` with strict compiler options. +- `deno task test`: Run the unit test suite with required Deno permissions. +- `deno task coverage`: Generate a detailed coverage report after running coverage-enabled tests. diff --git a/src/cli/codemap.md b/src/cli/codemap.md new file mode 100644 index 0000000..502619c --- /dev/null +++ b/src/cli/codemap.md @@ -0,0 +1,70 @@ +# src/cli/ + +## Responsibility + +The CLI command layer exposes the `stackctl` executable interface. It builds the Cliffy command +tree, maps command-line options and positional arguments into typed operation inputs, invokes the +domain modules, formats human-readable or JSON output, and translates validation, dependency, and +unexpected failures into process exit codes. + +## Design Patterns + +- Command registration is centralized in `buildCli()`, which creates the root `Command`, attaches + global metadata and options, then registers each top-level command and nested command in one + command tree. +- Each command action is a thin adapter. It extracts options from `Record`, casts + Cliffy parsed values to local types, normalizes comma-separated lists, resolves the active working + directory with `Deno.cwd()`, and delegates work to a module function. +- Runner injection is explicit at Docker and secrets boundaries. Command actions instantiate + `RealProcessRunner`, passing `dryRun` where supported, then provide that runner to Docker and + secrets operations instead of invoking external tools directly. +- Exit handling uses the shared `ExitCode` enum for semantic results. Many command actions set the + module-level `exitCode` and return, while some deeper asynchronous paths call `Deno.exit()` for + immediate termination. +- Output handling stays in the CLI layer. Domain results return structured data, warnings, errors, + and generated content. The CLI decides whether to print tables, diffs, YAML, JSON, warnings, or + status lines. + +## Data & Control Flow + +1. `main(args)` calls `buildCli().parse(args)` and returns the accumulated `exitCode`. Parse errors + or uncaught exceptions are converted to exit code `1`. +2. `buildCli()` registers root options such as `--debug` and `--config`, a default help action, and + commands for initialization, generation, rendering, stack lifecycle, health checks, reload, + secrets, environment files, planning, and shell completions. +3. Command actions parse Cliffy options, including boolean flags, required string options, + positional arguments, and comma-separated lists such as `--stacks`, `--override`, and `--paths`. +4. Most commands resolve configuration with `resolveConfig({ profile, cwd })`, derive `repoRoot`, + then pass normalized inputs to config, compose, render, docker, env, or secrets functions. +5. Compose workflows commonly discover stack names, generate stack YAML in memory, parse generated + YAML, render variables, optionally write files, and then deploy, reload, compare, or report. +6. Docker-backed commands create a `RealProcessRunner` and delegate Docker CLI interaction to docker + wrapper functions. Dry-run commands use a dry-run runner or skip mutation and print intended + actions. +7. Result objects drive final control flow. Errors and drift set `ExitCode.DriftOrValidation`, user + configuration errors set `ExitCode.UserConfigError`, missing external tooling sets + `ExitCode.MissingDependency`, and uncaught command exceptions set `ExitCode.UnexpectedError`. + +## Integration Points + +- `config`: `initConfig()` creates `.stackctl` files. `resolveConfig()` loads active configuration + for profile-aware commands. `ExitCode` supplies shared numeric exit semantics. +- `compose`: `generateStacks()` builds canonical stack YAML, `discoverComposeFiles()` discovers + stack names, `syncValidation()` checks generated output against committed stack files, + `reloadStacks()` performs config-first reloads, and `planOperation()` produces deterministic + operation plans. +- `render`: `renderStack()` resolves `${VAR}` placeholders in generated Compose data and reports + unresolved variables and warnings. +- `docker`: Docker wrapper functions perform Swarm and Compose operations through the injected + runner, including stack deploy, removal, service listing, task listing, logs, Compose config + validation, Docker info, and Swarm status checks. +- `env`: Environment commands delegate discovery, creation, diffing, materialization, status + listing, and audit behavior to `discoverEnvExamples()`, `batchCreateEnvs()`, `diffEnvFiles()`, + `materializeEnvFromProfile()`, `getEnvStatusList()`, and `envDoctor()`. +- `secrets`: Secrets commands delegate SOPS and age workflows to `ensureTooling()`, + `checkTooling()`, `findEncryptedEnvFiles()`, `encryptEnvFile()`, `decryptEnvFile()`, + `cleanDecryptedEnvFiles()`, and `deployPipeline()`. +- `process`: `RealProcessRunner` is the runtime adapter for external process execution and dry-run + behavior. +- Deno and standard library APIs provide filesystem writes, temporary files, directory creation, + YAML parsing and serialization, path operations, prompts, and completion command support. diff --git a/src/codemap.md b/src/codemap.md new file mode 100644 index 0000000..5c42ae3 --- /dev/null +++ b/src/codemap.md @@ -0,0 +1,39 @@ +# stackctl Codemap + +## Responsibility + +`stackctl` is a Deno 2.x command line application for repository aware Docker Swarm stack control. +`src/main.ts` is the executable entry point. It delegates all command processing to `src/cli/mod.ts` +and exits with the numeric status returned by that module. `src/version.ts` provides the canonical +CLI version string for runtime use and release automation. + +## Design Patterns + +- Thin bootstrapper, `src/main.ts` contains process startup only and keeps command behavior inside + the CLI module. +- Explicit entry point guard, `import.meta.main` prevents process execution when the module is + imported. +- Centralized version constant, `VERSION` avoids duplicated version literals in source. +- Capability constrained runtime, the shebang declares Deno permissions for file IO, environment + reads, host inspection, and selected external tools. + +## Data & Control Flow + +1. Deno starts `src/main.ts` with the permissions declared in the shebang or compiled binary + metadata. +2. `src/main.ts` imports `main` from `src/cli/mod.ts`. +3. When executed directly, `Deno.args` is passed to `main`. +4. The CLI module resolves commands, performs repository and Docker Swarm operations, and returns an + exit code. +5. `Deno.exit` terminates the process with that code. +6. Consumers that need the release identifier import `VERSION` from `src/version.ts`. + +## Integration Points + +- Runtime, Deno 2.x with JSR imports configured in `deno.json`. +- Package entry, `deno.json` exports `./src/main.ts` as `@anitrend/stackctl`. +- CLI implementation, `src/cli/mod.ts` receives argument vectors and owns command dispatch. +- External tools, Deno run permissions allow `git`, `docker`, `docker-compose`, `sops`, `age`, + `age-keygen`, `shred`, and `rm`. +- Release metadata, `deno.json` version and `src/version.ts` `VERSION` must stay in sync during + version updates. diff --git a/src/compose/codemap.md b/src/compose/codemap.md new file mode 100644 index 0000000..66ddbc3 --- /dev/null +++ b/src/compose/codemap.md @@ -0,0 +1,159 @@ +# src/compose/ + +## Responsibility + +`src/compose` owns Docker Compose source discovery, source loading, stack file generation, +Compose-specific merging, override application, Swarm compatibility transforms, named volume +declaration, reload orchestration, sync validation, and operation planning for compose-related +workflows. It converts per-service `docker-compose.yml` or `docker-compose.yaml` files tagged with +`x-stack` metadata into canonical stack YAML files under the configured stack directory, then feeds +those files into render and Docker deployment flows where applicable. + +Core public entry points are: + +- `discoverComposeFiles(options: DiscoverOptions): Promise` in `discover.ts`. +- `loadCompose(path: string): Promise` and `loadFragment(directory: string)` in + `load.ts`. +- `generateStacks(options: GenerateOptions): Promise` in `generate.ts`. +- `composeDeepMerge(base, override)` in `merge.ts`. +- `composeOverrideMerge(base, override)`, `loadOverrideFile(path, repoRoot)`, and + `applyOverrides(baseCompose, overrides, repoRoot)` in `override.ts`. +- `stripComposeOnlyKeys`, `applyLoggingDefaults`, `rewriteEnvFile`, and `rewriteBindMountPaths` in + `transform.ts`. +- `collectAllNamedVolumes` and `collectNamedVolumes` in `volumes.ts`. +- `reloadStacks(options: ReloadOptions): Promise` in `reload.ts`. +- `sync(opts: SyncOptions): Promise` in `sync.ts`. +- `planOperation(opts: PlanOptions): Promise` in `plan.ts`. + +## Design Patterns + +- Pipeline orchestration: `generateStacks` composes discovery, loading, fragment merging, override + merging, service transforms, named volume collection, and YAML serialization into one generation + pipeline. `reloadStacks`, `sync`, and `planOperation` reuse that pipeline instead of duplicating + low-level compose logic. +- Pure merge and transform helpers: `composeDeepMerge`, `composeOverrideMerge`, + `stripComposeOnlyKeys`, `applyLoggingDefaults`, `rewriteEnvFile`, `rewriteBindMountPaths`, + `collectNamedVolumes`, and `collectAllNamedVolumes` return new objects or arrays and avoid + mutating their inputs. +- Separate merge semantics: `composeDeepMerge` is used for composing source files and + `swarm.fragment.yml` sidecars, with arrays replaced. `composeOverrideMerge` models Docker Compose + override semantics, with arrays appended. +- Metadata normalization boundary: `normalizeStackName` accepts legacy scalar `x-stack: name` and + object form `x-stack: { name: value }`, rejects empty names, unknown object fields, and invalid + shapes, and removes `x-stack` from loaded compose data via `loadCompose`. +- Safety modes: `GenerateOptions.dryRun`, `ReloadOptions.dryRun`, `ReloadOptions.skipUnchanged`, and + planning dry runs are used to prevent writes or deployments when callers need previews or + validation only. +- Typed result envelopes: workflows return structured result objects such as `GenerateResult`, + `ReloadResult`, `SyncResult`, and `PlanResult`, with warnings and errors collected as data rather + than thrown across the top-level operation boundary. +- Barrel exports: `mod.ts` re-exports compose types and most submodule APIs, plus `generateStacks` + and the generation option/result types. + +## Data & Control Flow + +Discovery and loading: + +1. `discoverComposeFiles` walks `DiscoverOptions.repoRoot` with `@std/fs/walk`, includes files only, + skips hidden directories, and filters names to `docker-compose.yml` or `docker-compose.yaml`. +2. `DEFAULT_SKIP_DIRS` excludes `node_modules`, `stacks`, `tools`, `environments`, and + `__pycache__`; `DiscoverOptions.skipDirs` extends that set. +3. Each candidate YAML file is parsed with `@std/yaml.parse`. Files without `x-stack` are ignored. + Invalid YAML or invalid stack metadata are recorded in `DiscoverResult.errors`. +4. `normalizeStackName` converts the `x-stack` value to the grouping key, producing + `DiscoverResult.stacks` as `Record`. +5. `loadCompose` parses a selected compose file, validates `x-stack`, removes it from the returned + `LoadResult.data`, and returns `LoadResult.stackName`. +6. `loadFragment` resolves `swarm.fragment.yml` in the compose directory, returns `{}` when absent, + and parses it when present. + +Generation, merging, overrides, transforms, and volume handling: + +1. `generateStacks` discovers compose files, selects `GenerateOptions.stacks` or all discovered + stack names, creates the output directory unless `dryRun` is enabled, and calls the internal + `generateSingleStack` for each target. +2. `generateSingleStack` loads every compose source and its `swarm.fragment.yml` sidecar. For each + source it merges compose data with the fragment through `composeDeepMerge`, then merges all + sources together through `composeDeepMerge` again. +3. If `GenerateOptions.overrides` is present, `applyOverrides` resolves each string or + `OverrideEntry`, loads it through `loadOverrideFile`, and applies entries left to right through + `composeOverrideMerge`. +4. Service transforms run per service in this order: `stripComposeOnlyKeys` removes Swarm-invalid + `container_name`, `restart`, and `build`; `applyLoggingDefaults` adds the local logging driver + defaults when no logging block exists; `rewriteEnvFile` rewrites relative `env_file` entries to + repo-root-relative paths; `rewriteBindMountPaths` rewrites relative bind mount sources to + repo-root-relative paths. +5. `rewriteBindMountPaths` handles short-form volume strings by recognizing sources that start with + `.`, `/`, or `~`, and handles long-form mounts by rewriting `source` when `type` is `bind` or + absent. Long-form `type: volume` mounts are left unchanged. +6. `collectAllNamedVolumes` scans all transformed services and uses `collectNamedVolumes` to find + named volumes. Short-form named volumes are sources that do not start with `.`, `/`, or `~`. + Long-form named volumes require `type === "volume"` and a string `source`. The generated stack + declares each deduplicated, sorted named volume as `{ external: true }`. +7. The output stack always declares the default network as external `traefik-public`, emits services + when present, emits external named volumes when found, serializes with `@std/yaml.stringify`, and + writes `/.yml` unless `dryRun` is enabled. + +Reload: + +1. `reloadStacks` receives a pre-resolved `ResolvedConfig`, a `ProcessRunner`, and optional stack, + override, dry run, log, checksum, and force-update flags. +2. It computes `repoRoot`, stack directory, render directory, and target stacks from config and + options, then concatenates config overrides with CLI overrides. +3. Unless `skipGenerate` is true, it calls `generateStacks` with `dryRun: false` so the stack files + exist for rendering. +4. For each stack it reads `/.yml`, parses YAML to `ComposeData`, calls + `renderStack` with strict interpolation, serializes rendered YAML, and targets + `/.rendered.yml`. +5. When `skipUnchanged` is true, `computeSha256` and `unchangedCheck` compare the new rendered + content to the previous rendered file. Matching stacks return `unchanged` or `would-skip`. +6. Non-dry runs write the rendered file and call `dockerStackDeploy` with `prune: false` and + `resolveImage: "always"`. Dry runs return `would-deploy`. +7. If enabled, `forceServiceUpdate` lists services through `dockerStackServices`, parses JSON lines, + and calls `dockerServiceUpdate` with `{ force: true }`. `followLogs` uses `dockerServiceLogs` for + deployed stack services on a best-effort basis. + +Sync: + +1. `sync` resolves config through `resolveConfig`, discovers compose files, and selects requested + stacks or discovered stacks. +2. It calls `generateStacks` with `dryRun: true`, then compares each generated YAML string against + the canonical `/.yml` file. +3. Differences set `SyncResult.match` to false and populate `SyncResult.diffs` with a lightweight + textual diff from `generateDiff`, built using `lcsFn`. Sync never renders and never deploys. + +Planning: + +1. `planOperation` resolves config, creates `PlanResult` and stable `PlanJsonOutput`, and always + reports configuration and compose discovery sections. +2. `planComposeDiscovery` uses `discoverComposeFiles` to show discovered stack files and missing + requested stacks. `planOverrides` lists explicit override paths. +3. For `up`, `sync`, `generate`, `reload`, and `all`, `planGeneration` calls `generateStacks` with + `dryRun: true` and lists stack files that would be generated. +4. For `up`, `sync`, `render`, `reload`, and `all`, `planRender` parses generated YAML, calls + `renderStack`, counts interpolation sources from environment values and `env_file`, and reports + rendered output paths. +5. `planDockerCommands` produces non-executed Docker command strings for `up`, `sync`, `down`, + `reload`, and `all`. +6. `planEnv` and `planSecrets` add environment and secrets sections for relevant operations. + `planSecrets` only discovers encrypted inputs and cleanup actions; it does not decrypt files. + +## Integration Points + +- Config: `sync` and `planOperation` call `resolveConfig` from `src/config/load.ts`. `reloadStacks` + accepts a `ResolvedConfig` from the CLI layer. `GenerateOptions.overrides`, + `ReloadOptions.overrides`, and `PlanOptions.overrides` use `OverrideEntry` from + `src/config/types.ts`. +- Render: `reloadStacks` and `planRender` call `renderStack` from `src/render/mod.ts` to interpolate + `${VAR}` placeholders after stack generation. +- Docker: `reloadStacks` calls `dockerStackDeploy`, `dockerStackServices`, `dockerServiceUpdate`, + and `dockerServiceLogs` from `src/docker/mod.ts` through a `ProcessRunner` abstraction. +- Process: Docker commands run through `ProcessRunner` from `src/process/types.ts`, including + `runner.withDryRun(true)` for dry-run reload behavior. +- Env: `planEnv` dynamically imports `discoverEnvExamples` from `src/env/mod.ts` when planning `env` + or `all` operations. +- Secrets: `planSecrets` dynamically imports `findEncryptedEnvFiles` from `src/secrets/mod.ts` and + intentionally limits itself to discovery and cleanup planning. +- Standard library: modules use `@std/yaml` for parsing and serialization, `@std/path` for path + resolution and joining, `@std/fs` for walking, existence checks, and output directory creation, + and Deno file APIs for reading and writing YAML files. diff --git a/src/config/codemap.md b/src/config/codemap.md new file mode 100644 index 0000000..d4c816a --- /dev/null +++ b/src/config/codemap.md @@ -0,0 +1,95 @@ +# src/config/ + +## Responsibility + +The config module owns stackctl configuration shape, default values, file discovery, YAML loading, +layer merging, validation, initial file generation, and the public export surface for configuration +APIs. It converts `.stackctl` files and optional profile or local overlays into a validated +`ResolvedConfig` whose `base` property contains the final `StackctlConfig` consumed by the rest of +the application. + +Core files: + +- `types.ts`: Defines `StackctlConfig`, nested config interfaces, `ProfileConfig`, `OverrideEntry`, + `ResolvedConfig`, and shared `ExitCode` constants. +- `defaults.ts`: Provides `DEFAULT_CONFIG`, the base layer for resolution. +- `merge.ts`: Provides generic deep merge helpers used to combine config layers. +- `validate.ts`: Validates required and optional config fields and returns all validation errors. +- `load.ts`: Discovers config files, loads YAML, applies merge order, validates the result, and + returns `ResolvedConfig`. +- `init.ts`: Generates `.stackctl` and optional `.stackctl.` templates, with preset, + detection, force, and dry run support. +- `mod.ts`: Re-exports the public config module API. + +## Design Patterns + +- Layered configuration: `resolveConfig` starts from `DEFAULT_CONFIG` and applies discovered or + explicit files in a fixed order. +- Partial overlays: `ProfileConfig` is `Partial`, so profile, local, and explicit + file content can override only selected fields. +- Immutable merge return values: `mergeConfig` copies the base object into a new record and returns + a merged object instead of mutating the input object directly. +- Recursive object merge: Plain records are merged field by field when both base and overlay values + are records. +- Replacement semantics for non-record values: Arrays, primitives, and `null` replace the existing + value. `undefined` overlay values are skipped and do not overwrite the base. +- Aggregate validation: `validateConfig` collects all field errors before returning, allowing + callers to report a complete validation failure set. +- Sidecar discovery: Profile and local files are discovered relative to the base `.stackctl` file, + whether the base was found by walking up the directory tree or supplied through `configPath`. +- Template based initialization: `initConfig` writes static YAML templates, optionally modified by + compose file detection. + +## Data & Control Flow + +Configuration resolution in `load.ts` proceeds as follows: + +1. `resolveConfig` chooses the active profile from `ResolveOptions.profile` or the + `STACKCTL_PROFILE` environment variable. +2. If `ResolveOptions.configPath` is supplied, it is converted to an absolute path relative to the + current working directory when needed. Otherwise `discoverConfigFiles` walks upward from `cwd` to + find `.stackctl`. +3. Discovery determines `repoRoot` by searching upward for `.git`, falling back to the directory + containing `.stackctl`. +4. Sidecar files are checked in the base config directory: `.stackctl.`, `.stackctl.local`, + and `.stackctl.local.`. +5. The final config starts as a shallow copy of `DEFAULT_CONFIG`. +6. Existing layers are merged left to right in this order: + - `DEFAULT_CONFIG` + - `.stackctl` + - `.stackctl.` + - `.stackctl.local` + - `.stackctl.local.` +7. Each file is read by `loadConfigFile`, parsed with `@std/yaml`, and merged with `mergeConfig`. +8. `validateConfig` checks the merged result. Validation errors are formatted into one thrown + `Error` from `resolveConfig`. +9. A `ResolvedConfig` is returned with the merged `base`, active profile name, loaded overlay + objects, discovered paths, and an empty `overrides` array. + +Single file loading uses `loadConfig`, which delegates to the YAML loader and returns a partial +config without defaults, sidecar discovery, merging, or validation. + +Initialization in `init.ts` proceeds as follows: + +1. `initConfig` selects the `standard` template by default or a configured preset from `PRESETS`. +2. Unknown presets return an `InitResult` containing an error and no file writes. +3. With `detect`, `applyDetection` scans the first level of `cwd` for compose file names, parses + YAML, extracts service names for `stack.names`, extracts a non-default network name for + `stack.network`, and derives `project` from the directory basename. +4. `writeConfigFile` writes `.stackctl`, unless it already exists and `force` is not set. +5. If `profile` is set, `writeConfigFile` also writes `.stackctl.`. +6. In `dryRun` mode, paths are added to `InitResult.written` without writing files. + +## Integration Points + +- Deno runtime APIs: Uses `Deno.cwd`, `Deno.env.get`, `Deno.readTextFile`, `Deno.writeTextFile`, and + `Deno.readDir` for filesystem and environment access. +- Standard library dependencies: Uses `@std/fs/exists`, `@std/yaml/parse`, and `@std/path` helpers + for path handling and YAML parsing. +- CLI integration: `mod.ts` exports config loading, validation, merge utilities, types, defaults, + and init APIs for command handlers. +- Compose integration: Initialization detection reads compose YAML files to seed project, stack + name, and network values, but it does not use compose module abstractions directly. +- Downstream modules consume `StackctlConfig` fields such as stack directory, stack names, network, + render output directory, environment file options, secrets settings, and command defaults after + `resolveConfig` validates the merged configuration. diff --git a/src/docker/codemap.md b/src/docker/codemap.md new file mode 100644 index 0000000..c4e4b0b --- /dev/null +++ b/src/docker/codemap.md @@ -0,0 +1,65 @@ +# src/docker/ + +## Responsibility + +`src/docker` is the Docker CLI adapter layer for stackctl. It provides small typed wrappers around +Docker, Docker Compose, and Docker Swarm commands so higher level modules do not build or execute +Docker command lines directly. + +The module is responsible for: + +- Deploying a Swarm stack with `docker stack deploy`. +- Removing a Swarm stack with `docker stack rm`. +- Listing stack services and tasks with JSON formatted `docker stack services` and `docker stack ps` + output. +- Streaming service logs through `docker service logs`. +- Updating a Swarm service through `docker service update`. +- Reading Docker daemon information with `docker info`. +- Deriving Swarm activation status from `docker info` JSON output. +- Normalizing Compose configuration with `docker compose -f config`. + +## Design Patterns + +- **ProcessRunner boundary**: Every external command is executed through the injected + `ProcessRunner`. This keeps the module deterministic in tests and prevents direct process spawning + from Docker wrappers. +- **Thin command wrappers**: Each exported function maps one Docker CLI operation to a + `Promise` or a parsed status object. The wrappers do not own business rules beyond + command assembly and minimal result parsing. +- **Typed option objects**: Optional CLI flags are represented by narrow interfaces such as + `DockerDeployOptions`, `DockerLogsOptions`, and `DockerServiceUpdateOptions`. +- **Argument vector construction**: Commands are constructed as string arrays, not shell strings. + This avoids shell interpolation and keeps command arguments explicit. Optional flags are appended + conditionally before positional arguments such as stack names, service names, and compose file + paths. +- **JSON output contract**: Commands that need machine readable output include `--format {{json .}}` + where Docker supports it. `dockerSwarmStatus` parses the JSON produced by `docker info` and + converts it into a small domain specific result. + +## Data & Control Flow + +1. A caller passes a `ProcessRunner`, required identifiers such as `stackName`, `serviceName`, or + `composeFile`, and optional typed settings. +2. The wrapper creates a command vector beginning with `docker` and the relevant subcommands. +3. Optional settings add flags in Docker CLI order. Examples include `--prune`, `--detach`, + `--resolve-image`, `--force`, `--image`, `--tail`, `--since`, and `--timestamps`. +4. Positional arguments are appended last, for example the stack name or service name. +5. Most functions call `runner.run(cmd)` and return the resulting `ProcessResult` unchanged. +6. `dockerServiceLogs` calls `runner.stream(cmd)` because log following is a streaming operation. + The `--follow` flag is enabled by default and omitted only when `follow` is explicitly `false`. +7. `dockerSwarmStatus` calls `docker info --format {{json .}}`, checks command success, parses + `stdout` as JSON, reads `Swarm.LocalNodeState`, and returns `{ active: true, nodeId }` only when + the local node state is `active`. Failed commands, invalid JSON, or inactive states return + `{ active: false }`. + +## Integration Points + +- `src/process`: Supplies `ProcessRunner` and `ProcessResult`. Production code uses the real runner, + while tests can inject fakes. +- `src/compose`: Produces compose files and rendered stack definitions that are later passed into + Docker wrappers such as `dockerStackDeploy` and `dockerComposeConfig`. +- `src/cli`: Orchestrates user facing commands and delegates Docker operations to this module + instead of invoking Docker directly. +- Docker CLI and Docker Swarm: The wrappers assume the `docker` executable is available in the + runtime environment and preserve Docker's stdout, stderr, success state, and exit code through + `ProcessResult` unless a function explicitly parses the output. diff --git a/src/env/codemap.md b/src/env/codemap.md new file mode 100644 index 0000000..73e36b5 --- /dev/null +++ b/src/env/codemap.md @@ -0,0 +1,65 @@ +# src/env/ + +## Responsibility + +`src/env/` owns environment file lifecycle helpers for stack projects. It discovers `.env.example` +and `.env.example.` files, scaffolds `.env` files, materializes profile presets into root +`.env` targets, compares env keys, lists env status, and audits plaintext `.env` files that have or +lack encrypted `.env.enc` counterparts. + +Public entry points in `mod.ts` are `discoverEnvExamples`, `createEnvFromExample`, `diffEnvFiles`, +`batchCreateEnvs`, `getEnvStatusList`, `materializeEnvFromProfile`, and `envDoctor`. Public data +contracts live in `types.ts`. + +## Design Patterns + +- File discovery uses `@std/fs` `walk` and narrows results with deterministic filters: + `DEFAULT_SKIP_DIRS`, `hasSkipAncestor`, `isInHiddenDir`, and `matchesPaths`. +- Env parsing is key focused. `parseEnvKeys` reads `.env` style files, ignores blank lines and + comments, strips an optional `export` prefix, and returns keys before `=` without exposing values. +- Operations are option driven. `DiscoverOptions`, `CreateOptions`, `MaterializeOptions`, and + `DoctorOptions` configure profile selection, scoped paths, overwrite behavior, dry runs, and + remediation suggestions. +- Write operations are conservative. `createEnvFromExample` and `materializeEnvFromProfile` skip + existing targets unless `force` is set, and `backupEnvBeforeOverwrite` creates timestamped + `.bak.` files before overwriting. +- Batch functions accumulate structured results instead of failing the whole operation on item + errors. `BatchCreateResult`, `MaterializeResult`, and `DoctorResult` separate successes, skips, + errors, findings, and warning state. + +## Data & Control Flow + +1. Discovery starts with `discoverEnvExamples(projectDir, options)`. It selects the expected example + suffix from `options.profile`, walks the project tree, excludes skipped and hidden directories, + applies `paths` filters, derives `serviceName` with `deriveServiceName`, and computes `status` by + comparing keys from the example file and corresponding env file. +2. Scaffolding uses `batchCreateEnvs(projectDir, options)` for project wide creation. It calls + `discoverEnvExamples`, optionally filters by `serviceName`, then delegates each item to + `createEnvFromExample`. The single file helper validates the example, refuses existing env files + unless forced, supports `dryRun`, backs up overwritten env files, and copies text content. +3. Profile materialization uses `materializeEnvFromProfile(projectDir, options)`. It requires + `options.profile`, finds `.env.example.` files, applies the same directory and path + filters, and writes each matching file to `.env` in the same directory. Existing targets are + skipped unless `force` is set. +4. Diffing uses `diffEnvFiles(examplePath, envPath, serviceName)`. It parses keys from files that + exist, wraps parse failures with file specific errors, and returns `EnvDiff` arrays for + `onlyInExample`, `onlyInEnv`, and `common`. +5. Status listing uses `getEnvStatusList(projectDir, options)`. It includes base examples from + `discoverEnvExamples`, adds profile variants when no profile filter is set, checks for `.env` and + `.env.enc`, and returns sorted `EnvStatusEntry` records. +6. Audit uses `envDoctor(projectDir, options)`. It walks plaintext `.env` files, applies scope + filters, checks for adjacent `.env.enc`, and emits `DoctorFinding` entries. A plaintext file with + encrypted counterpart is a `warning`; a plaintext file without encrypted counterpart is `info`. + `hasWarnings` is derived from warning findings. + +## Integration Points + +- CLI commands import these helpers through `src/env/mod.ts` to implement env list, create, + materialize, diff, and doctor workflows. +- The secrets subsystem is coupled by convention through adjacent `.env.enc` detection in + `getEnvStatusList` and `envDoctor`, and by remediation messages that reference + `stackctl secrets encrypt` and `stackctl secrets clean`. +- The compose and configuration workflows consume the resulting `.env` files indirectly as project + artifacts. This module does not parse values or render compose content. +- All filesystem side effects use Deno APIs, specifically `Deno.readTextFile` and + `Deno.writeTextFile`, with standard path helpers from `@std/path`. diff --git a/src/process/codemap.md b/src/process/codemap.md new file mode 100644 index 0000000..4c896dd --- /dev/null +++ b/src/process/codemap.md @@ -0,0 +1,60 @@ +# src/process/ + +## Responsibility + +`src/process` defines the process execution boundary for stackctl. It centralizes all external +command invocation behind the `ProcessRunner` interface so Docker, SOPS, age, git, and other system +tools can be executed through one typed abstraction. The module supports real execution, dry-run +behavior, command availability checks, captured output, streamed output callbacks, and test +replacement through fake runners. + +## Design Patterns + +- **Port and adapter:** `ProcessRunner` is the port used by application code. `RealProcessRunner` is + the Deno-backed adapter that calls `Deno.Command`. +- **Dependency inversion:** callers depend on `ProcessRunner` instead of `Deno.Command` directly, + which allows tests to inject fakes and avoids real external process execution in unit tests. +- **Immutable mode switching:** `withDryRun(dryRun)` returns a new `ProcessRunner` instance with the + requested dry-run mode rather than mutating the current runner. +- **Result object:** every command returns a `ProcessResult` containing `stdout`, `stderr`, `code`, + `success`, and the original `command` array for diagnostics. +- **Callback-based streaming:** `stream()` accumulates full output while also emitting complete + stdout and stderr lines to optional handlers. + +## Data & Control Flow + +1. A caller passes a command as `string[]`, where the first element is the executable and remaining + elements are arguments. +2. Empty command arrays short-circuit with `code: 1`, `success: false`, empty output, and the + original command. +3. In dry-run mode, `run()` and `stream()` print the intended command, skip OS execution, and return + `code: 0` with `success: true`. +4. In capture mode, `run()` creates `Deno.Command` with piped stdout and stderr, optional `cwd`, and + optional `env`. It awaits `command.output()`, decodes both byte streams with `TextDecoder`, and + returns the Deno status code and success flag. +5. In streaming mode, `stream()` spawns the command, registers SIGINT and SIGTERM handlers, drains + stdout and stderr streams, awaits child status, then removes signal handlers in `finally`. +6. `drainStream()` reads `ReadableStream` chunks, decodes them incrementally, preserves + partial line buffers, emits only complete lines to callbacks, flushes the final residual line, + releases the reader lock, and returns the full decoded text. +7. `which(name)` invokes the platform `which` command with null stdout and stderr, returning `true` + when the lookup exits successfully and `false` on failure or exception. + +Exit code semantics are normalized through `ProcessResult`: `code` is the process exit code and +`success` mirrors Deno's success flag, effectively successful when the exit code is `0`. Internal +runner errors and invalid empty commands are represented as `code: 1` with `success: false`. Dry-run +commands report success with `code: 0` because no external command is executed. + +`RunOptions.timeout` and `timeoutSignal` are part of the public type contract, but the current +`RealProcessRunner` implementation does not apply timeout cancellation. + +## Integration Points + +- `src/docker` uses the process abstraction to call Docker and Docker Swarm commands. +- `src/secrets` uses the abstraction for SOPS, age, age-keygen, shred, and rm integrations. +- CLI commands can create a real runner and toggle dry-run behavior while keeping downstream code + independent from Deno process APIs. +- `src/testing` provides fake implementations for unit tests, enabling deterministic command + responses and verification of expected invocations. +- `src/main.ts` and release build settings must grant Deno permissions for external commands that + flow through this abstraction. diff --git a/src/render/codemap.md b/src/render/codemap.md new file mode 100644 index 0000000..8521dc1 --- /dev/null +++ b/src/render/codemap.md @@ -0,0 +1,83 @@ +# src/render/ + +## Responsibility + +`src/render` owns Docker Compose interpolation for stack data after compose generation and override +merging, before deployment output is written or applied. Its main entry point is +`renderStack(options: RenderOptions)`, which accepts parsed `ComposeData`, builds per-service +variable scopes, substitutes environment references inside service definitions, rewrites relative +paths to absolute paths, and returns a `RenderResult` with rendered data plus warnings. + +The module handles these rendering responsibilities: + +- Parse simple dotenv files through `parseEnvFile(path)`. +- Resolve `env_file` references through `resolveEnvPath(relPath, projectDir, repoRoot)`. +- Normalize `service.environment` through `coerceEnvironmentToDict(env)`. +- Build service-specific interpolation scopes through + `buildServiceScope(service, baseEnv, + projectDir, repoRoot)`. +- Substitute `${VAR}`, `${VAR-default}`, `${VAR:-default}`, and `$VAR` forms through + `substitute(s, vars)`. +- Recursively interpolate nested compose structures through `deepInterpolate(obj, vars)`. +- Convert relative service `env_file` and bind mount sources through + `absolutizeServicePaths(service, + projectDir, repoRoot)`. + +## Design Patterns + +- Functional pipeline: `renderStack` composes smaller pure or mostly pure helpers in a fixed order, + from scope construction to interpolation to path normalization. +- Non-mutating transforms: `renderStack` shallow-clones `ComposeData`, creates a new `services` map, + and `absolutizeServicePaths` returns a copied `ServiceDef` instead of mutating the input service. +- Layered configuration scope: `buildServiceScope` applies shell environment first, then service + `env_file` values in declaration order, then `service.environment` with highest priority. +- Recursive structural traversal: `deepInterpolate` preserves scalar types and only transforms + string leaves while walking arrays and plain object entries. +- Regex-driven parsing: `INTERP_RE`, `PLAIN_VAR_RE`, `UNRESOLVED_RE`, and `REL_PATH_RE` centralize + interpolation matching, strict-mode detection, and relative path classification. +- Warning accumulation: `renderStack` records missing `env_file` references and unresolved variables + in `RenderResult.warnings` rather than throwing for ordinary render issues. Strict mode annotates + `RenderResult.hasUnresolved` when service values still contain `${VAR}` patterns. + +## Data & Control Flow + +1. Callers pass `RenderOptions` to `renderStack`, including parsed `ComposeData`, `projectDir`, + `repoRoot`, and optional `strict`. +2. `renderStack` reads the shell environment with `Deno.env.toObject()` and shallow-clones the input + compose data. +3. For each service in `data.services`, `renderStack` calls `buildServiceScope`. +4. `buildServiceScope` starts with the shell environment, resolves each service `env_file` with + `resolveEnvPath`, parses readable files with `parseEnvFile`, and overlays values from + `coerceEnvironmentToDict(service.environment)`. +5. `renderStack` independently checks declared `env_file` paths with `Deno.stat` and appends + warnings for missing files. +6. `deepInterpolate` walks the service definition and calls `substitute` for each string value. +7. `substitute` first processes braced expressions with `INTERP_RE`, then plain `$VAR` expressions + with `PLAIN_VAR_RE`. Unresolved variables remain unchanged unless a default expression provides a + replacement. +8. `absolutizeServicePaths` normalizes the interpolated service. It delegates env file paths to the + private `absolutizePath`, short-form volume strings to `absolutizeBindMountString`, long-form + volume objects to `absolutizeVolumeMount`, and relative path construction to `resolvePath`. +9. After service processing, `renderStack` interpolates non-service top-level compose keys with only + the shell environment as scope. +10. `renderStack` scans rendered service JSON for unresolved `${VAR}` patterns. In strict mode it + sets `hasUnresolved` and records strict warnings. In non-strict mode it records warnings while + leaving values unchanged. +11. The function returns `RenderResult` containing the rendered `ComposeData`, warnings, and + optional strict-mode unresolved status. + +## Integration Points + +- `src/compose/types.ts`: Supplies `ComposeData`, `ServiceDef`, and `VolumeMount` type contracts + used by all render operations. +- CLI render and deploy flows: Call `renderStack` after compose generation and override merging, + then consume `RenderResult.data` for YAML output, deployment, or downstream validation. +- Environment files: `parseEnvFile`, `resolveEnvPath`, and `buildServiceScope` read service-level + dotenv files declared by Compose `env_file` fields. +- Host process environment: `renderStack` uses `Deno.env.toObject()` as the base scope for services + and as the only scope for top-level non-service compose keys. +- Filesystem: `resolveEnvPath`, `parseEnvFile`, and `renderStack` use Deno filesystem APIs to + resolve, read, and validate env file paths. +- Docker Compose path semantics: `absolutizeServicePaths` preserves named volumes and absolute paths + while converting relative `env_file` and bind mount sources so rendered YAML remains stable when + executed from a different output directory. diff --git a/src/secrets/codemap.md b/src/secrets/codemap.md new file mode 100644 index 0000000..e281152 --- /dev/null +++ b/src/secrets/codemap.md @@ -0,0 +1,60 @@ +# src/secrets/ + +## Responsibility + +The secrets module provides the public API and implementation for managing dotenv secret files with +SOPS and age. It encrypts plaintext `.env` files to `.env.enc`, decrypts `.env.enc` files back to +`.env` materialized files, verifies required external tooling, securely removes decrypted material, +and runs the deploy pipeline that decrypts, generates stack compose data, renders environment +substitutions, deploys with Docker, and cleans up decrypted files. + +## Design Patterns + +- External command adapter: all subprocess calls use the `ProcessRunner` interface, with + `RealProcessRunner` as the default and injectable runners for tests or callers. +- Result objects over thrown errors for operational steps: encrypt, decrypt, clean, and deploy + return structured success, warning, and error values. Tooling enforcement is the exception, where + `ensureTooling` throws on missing dependencies. +- SOPS-owned key resolution: encryption and decryption invoke SOPS with dotenv input and output + types, and do not pass age recipients or key paths. SOPS resolves age configuration from its own + config, typically `.sops.yaml`. +- Best-effort cleanup: decrypted files are removed with `shred -u` first, then `rm -f` as a fallback + when shredding fails or is unavailable. +- Dry-run propagation: the deploy pipeline records intended mutations as warnings, and cleanup + returns the files that would be removed without deleting them. + +## Data & Control Flow + +1. Tooling checks call `runner.which("sops")` and `runner.which("age")`; status checks also call + ` --version` through `runner.run` and capture the first stdout line. +2. Encryption accepts a plaintext source path, verifies it exists, derives `.enc`, then runs + `sops --encrypt --input-type dotenv --output-type dotenv --output .enc `. +3. Decryption accepts an encrypted source path, strips the `.enc` suffix for the plaintext output + path, verifies the encrypted file exists, then runs + `sops --decrypt --input-type dotenv --output-type dotenv --output `. +4. Discovery walks the working directory and collects files named `.env.enc` or `.env.example`, + skipping `.git`, `.rendered`, and `node_modules`. +5. The deploy pipeline discovers `.env.enc` files, decrypts each one, derives affected stack names + from each encrypted file parent directory, resolves stack configuration, discovers compose files, + generates in-memory stack YAML, renders `${VAR}` placeholders against the repository context, + deploys each rendered stack with Docker, and finally cleans up every materialized `.env` file. +6. Cleanup iterates materialized env files and invokes `shred -u ` through the runner. If that + command does not succeed, it invokes `rm -f ` through the same runner. + +Age key generation is not implemented in this module. The code checks for the `age` binary but does +not invoke `age-keygen`, create key files, or manage recipient material directly. + +## Integration Points + +- `index.ts` re-exports the public types and functions from `types.ts` and `mod.ts`. +- `types.ts` defines deploy, tooling, encryption, decryption, and cleanup result contracts. +- `../process/types.ts` supplies `ProcessRunner`; `../process/runner.ts` supplies + `RealProcessRunner` for command execution. +- `@std/fs` supplies `exists` and `walk` for source validation and repository traversal. +- `../config/mod.ts` provides `resolveConfig` for pipeline configuration and repository root + resolution. +- `../compose/mod.ts` provides compose discovery and in-memory stack generation. +- `../render/mod.ts` provides `renderStack` for environment interpolation before deployment. +- `../docker/mod.ts` provides `dockerStackDeploy` for stack deployment. +- `@std/yaml` parses generated YAML before rendering and stringifies rendered compose data into a + temporary deployment file. diff --git a/src/testing/codemap.md b/src/testing/codemap.md new file mode 100644 index 0000000..3d31e53 --- /dev/null +++ b/src/testing/codemap.md @@ -0,0 +1,46 @@ +# src/testing codemap + +## Responsibility + +`src/testing` provides the in-memory process execution test harness for stackctl modules that depend +on the `ProcessRunner` interface. The harness replaces real external command execution with +configured `ProcessResult` responses, records every command invocation, supports dry-run +propagation, and exposes assertion helpers for command-oriented unit tests. `mod.ts` is the public +barrel export for these testing utilities. + +## Design Patterns + +- Interface substitution: `FakeProcessRunner` implements `ProcessRunner`, allowing production + modules to receive a deterministic test double through the same dependency boundary. +- Builder pattern: `FakeProcessRunnerBuilder` accumulates command response fixtures and produces + configured runner instances. +- Fixture factory functions: `successResult` and `failureResult` construct canonical `ProcessResult` + values for test setup. +- Fail-fast verification: unknown command invocations throw immediately, which forces tests to + declare the complete expected external command surface. +- Immutable observation: the `commands` getter returns a copy of recorded invocations to preserve + runner state integrity. + +## Data & Control Flow + +1. A test creates `CommandResponse` fixtures directly, through `FakeProcessRunnerBuilder`, or with + helper factories for result payloads. +2. The unit under test receives `FakeProcessRunner` as a `ProcessRunner` dependency. +3. Calls to `run`, `stream`, or `which` are recorded in order as command argument arrays. +4. `run` and `stream` resolve responses through prefix or exact command matching, then return a + `ProcessResult` whose `command` field reflects the actual invocation. +5. `which` records a synthetic `which ` command and resolves to the configured success state, + defaulting to `false` when no response matches. +6. Assertion code inspects `recorded`, `commands`, or `containsCommand` to verify command routing + and argument construction. +7. `withDryRun` returns a new fake runner with the same response table and the requested dry-run + flag. + +## Integration Points + +- `../process/types.ts`: supplies `ProcessRunner`, `ProcessResult`, `RunOptions`, and + `StreamOptions`, which define the production contract implemented by the fake harness. +- Command wrapper modules: docker, secrets, compose, env, and other modules can inject the fake + runner to test external command orchestration without invoking Docker, sops, age, or shell tools. +- Public testing import path: `src/testing/mod.ts` re-exports `fakes.ts` so tests can import the + harness through the testing module boundary.