Skip to content

Commit 6a56069

Browse files
pofallonclaude
andauthored
feat: set-up — /etc/environment + /etc/profile root patches (PR-6c) (#38)
* feat: set-up subcommand — MVP scaffold + lifecycle hooks (PR-6a) Implements the core value of `set-up` per `docs/subcommand-specs/set-up/SPEC.md`: convert an already-running container into a DevContainer by applying configuration + image metadata and executing lifecycle hooks, emitting a single-line JSON result on stdout. ## CLI surface `deacon set-up --container-id <id> [--config <path>] [--skip-post-create] [--skip-non-blocking-commands] [--remote-env NAME=VALUE]... [--include-configuration] [--include-merged-configuration] [--container-data-folder <path>]` ## What this PR includes - `--container-id` resolution + inspect validation. Missing container fails with the upstream-aligned summary `"Dev container not found."` - Optional `--config` load via `ConfigLoader::load_with_extends` (extends chain honored per CLAUDE.md). Missing path fails with `"Dev container config (<path>) not found."` - Image-metadata extraction from the container's `devcontainer.metadata` label. Tolerates BOTH the JSON-array form (PR-2 / #27) and the single-object form for older images. - Config merge: file config wins over image metadata on scalar fields (spec §4 `mergeConfiguration(config.config, imageMetadata)`). - Variable substitution for both `configuration` and `mergedConfiguration`. - Lifecycle hook execution via `ContainerLifecycle` (onCreate → updateContent → postCreate → postStart → postAttach), gated by `--skip-post-create` (skips ALL phases per spec §2) and `--skip-non-blocking-commands` (stops after the configured `waitFor`). - JSON output per spec §10: `{outcome: "success", configuration?, mergedConfiguration?}`. `containerId` is intentionally excluded (spec §16 design decision). ## Deferred to PR-6b - `/etc/environment` + `/etc/profile` root-side patches with system markers under `/var/devcontainer/` - Dotfiles installer with target-path marker (would reuse `crates/deacon/src/commands/up/dotfiles.rs`) - A second substitution pass against the live container environment (`${containerEnv:VAR}`) — current pass uses the configured `container_env`, not a live `docker exec` env probe These are spec §5 phases 3a and 3c; both marked "best-effort" with graceful fallback on failure. Splitting them out keeps PR-6a reviewable. ## Tests 14 new unit tests in `set_up::tests`: - `--remote-env` parsing (accepts `NAME=VALUE`, rejects malformed input) - `--config` loading (default when absent, error when missing path) - Image-metadata label parsing (missing → None, array form, single-object form, invalid JSON → error) - Config merging (file wins over metadata; file-only when no metadata) - Argument defaults and JSON-result shape (outcome field, optional fields) Verification: - `cargo fmt --all -- --check` - `cargo clippy --all-targets -- -D warnings` - `cargo test -p deacon --lib` → 227 pass - `make test-nextest-fast` → 1927 pass (no regression) Refs: issue #34 (Tier 1 progress tracker), plan `/home/vscode/.claude/plans/let-s-come-up-with-recursive-sutherland.md` (PR-6 sequencing rationale). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: set-up — wire dotfiles installer (PR-6b) Follow-up to PR-6a (#36). Adds the three `--dotfiles-*` flags from spec §2 to the `set-up` CLI surface and threads them as a `DotfilesConfig` into `ContainerLifecycle`. The lifecycle helper already implements clone + auto-detected installer (`install.sh` / `bootstrap` / `setup` / `script/*`) + target-path marker for idempotency — set-up just needed to opt in. ## New flags - `--dotfiles-repository <URL or owner/repo>` - `--dotfiles-install-command <string>` (overrides auto-detection) - `--dotfiles-target-path <path>` (defaults to `~/dotfiles`) No repository → no clone. The lifecycle helper's marker-at-target-path guard provides idempotency across re-runs (spec §6, §16 design decision). ## Tests 3 new unit tests in `set_up::tests::build_dotfiles_config_*`: - `returns_none_when_no_repository` — no repo URL means no opt-in, even if the other flags are set - `forwards_all_three_fields` — all flags round-trip into `DotfilesConfig` - `leaves_defaults_to_lifecycle_helper` — `target_path` / `install_command` stay `None` so the helper computes its standard defaults Verification: - `cargo fmt --all -- --check` - `cargo clippy --all-targets -- -D warnings` - `cargo test -p deacon --lib set_up` → 17 pass (14 from PR-6a + 3 new) ## Deferred to PR-6c - `/etc/environment` + `/etc/profile` root-side patches with system markers under `/var/devcontainer/` (spec §5 phase 3a; net-new code that needs root exec + heredoc plumbing) - Live-container `${containerEnv:VAR}` second-pass substitution Refs: issue #34 (Tier 1 progress tracker); PR-6a (#36). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: set-up — /etc/environment + /etc/profile root patches (PR-6c) Follow-up to PR-6b (#37). Implements spec §5 phase 3a: append-only root patches to `/etc/environment` and `/etc/profile` so login shells inherit the resolved container env / PATH. Both patches are guarded by marker files under `--container-system-data-folder` (default `/var/devcontainer/`) so re-running set-up against the same container is a no-op. ## What this PR adds - **New CLI flag**: `--container-system-data-folder <path>` (spec §6, default `/var/devcontainer`). - **`apply_etc_patches`** wires both patches before the lifecycle hook phase (spec §5 puts system patches in phase 3a). Skipped when `--skip-post-create` is set, mirroring upstream's "post-create is the umbrella for user customization" semantics. - **`build_etc_environment_patch_script`**: emits a shell script that appends a `# >>> deacon set-up >>>` ... `# <<< deacon set-up <<<` block of `KEY="VALUE"` lines to `/etc/environment`. Escapes `\` and `"` so the PAM parser doesn't choke. Touches the marker only after a successful append. Empty env → no-op (does NOT touch the marker, so a later run with a populated config still patches). - **`build_etc_profile_patch_script`**: emits a shell script that re-exports PATH from `/etc/environment` inside a delimited block. This is the spec-§16 design-decision recipe — without it the env appended in `/etc/environment` doesn't reach login shells. - **`collect_env_pairs`**: merges `containerEnv` + `remoteEnv` + CLI `--remote-env` overlays into a sorted, deterministic list. CLI wins on key conflicts. `None`-valued `remote_env` entries are dropped. - **Best-effort**: any non-zero exit from the patch shell (no root, read-only `/etc`, missing `sh`, etc.) emits a WARN and proceeds. Spec §9: system-level patches "do not abort set-up unless critical". ## Tests 10 new unit tests: - `build_etc_environment_patch_returns_noop_when_env_empty` - `build_etc_environment_patch_short_circuits_when_marker_present` - `build_etc_environment_patch_writes_sorted_env_block` - `build_etc_environment_patch_wraps_block_in_delimiters` - `build_etc_environment_patch_escapes_special_chars_in_values` - `build_etc_profile_patch_short_circuits_on_marker` - `build_etc_profile_patch_reexports_path_from_environment` - `build_etc_profile_patch_wraps_in_delimiters` - `collect_env_pairs_merges_config_remote_and_cli` - `collect_env_pairs_returns_sorted_output` Total set-up tests: 27 pass (14 from PR-6a + 3 from PR-6b + 10 new). Verification: - `cargo fmt --all -- --check` - `cargo clippy --all-targets -- -D warnings` - `cargo test -p deacon --lib` → 240 pass (no regression) ## With this PR, the set-up subcommand is functionally complete The only spec-§5 item still on the deferred list is the live-container `${containerEnv:VAR}` second-pass substitution — an enhancement, not a behavior gap; the configured `container_env` is already substituted. Refs: issue #34 (Tier 1 progress tracker); PR-6a (#36), PR-6b (#37). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8c7ea81 commit 6a56069

2 files changed

Lines changed: 419 additions & 12 deletions

File tree

crates/deacon/src/cli.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,10 @@ pub enum Commands {
582582
/// Inside-container user data root (default `~/.devcontainer`).
583583
#[arg(long)]
584584
container_data_folder: Option<PathBuf>,
585+
/// Inside-container system data root for root-owned marker files
586+
/// (default `/var/devcontainer`). Spec §6.
587+
#[arg(long)]
588+
container_system_data_folder: Option<PathBuf>,
585589
},
586590

587591
/// Run user-defined lifecycle commands
@@ -1512,6 +1516,7 @@ impl Cli {
15121516
include_configuration,
15131517
include_merged_configuration,
15141518
container_data_folder,
1519+
container_system_data_folder,
15151520
}) => {
15161521
use crate::commands::set_up::{execute_set_up, SetUpArgs};
15171522

@@ -1530,6 +1535,7 @@ impl Cli {
15301535
include_merged_configuration,
15311536
container_data_folder: container_data_folder
15321537
.or_else(|| self.container_data_folder.clone()),
1538+
container_system_data_folder,
15331539
docker_path: self.docker_path.clone(),
15341540
progress_tracker: progress_tracker.clone(),
15351541
};

0 commit comments

Comments
 (0)