Skip to content

Commit f8be0ec

Browse files
committed
Add distant-docker backend plugin for Docker container interaction
1 parent 5fbddca commit f8be0ec

File tree

21 files changed

+4355
-11
lines changed

21 files changed

+4355
-11
lines changed

.config/nextest.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
[test-groups.ssh-integration]
22
max-threads = 4
33

4+
[test-groups.docker-integration]
5+
max-threads = 2
6+
47
[profile.ci]
58
fail-fast = false
69
retries = 4
@@ -11,3 +14,7 @@ final-status-level = "fail"
1114
[[profile.ci.overrides]]
1215
filter = 'binary_id(distant-ssh::lib)'
1316
test-group = 'ssh-integration'
17+
18+
[[profile.ci.overrides]]
19+
filter = 'binary_id(distant-docker::lib)'
20+
test-group = 'docker-integration'

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ jobs:
128128
- name: Ensure /run/sshd exists on Unix
129129
run: sudo mkdir -p /run/sshd
130130
if: matrix.os == 'ubuntu-latest'
131+
- name: Pre-pull Docker test image (Linux)
132+
run: docker pull ubuntu:22.04
133+
if: matrix.os == 'ubuntu-latest'
134+
- name: Pre-pull Docker test image (Windows)
135+
run: docker pull mcr.microsoft.com/windows/nanoserver:ltsc2025
136+
if: matrix.os == 'windows-latest'
131137
- name: Run all workspace tests (all features)
132138
run: cargo nextest run --profile ci --release --all-features --workspace
133139
- name: Run all doc tests (all features)

AGENTS.md

Lines changed: 154 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,24 @@ manipulation.
88
## Project Overview
99

1010
- **Language:** Rust (Edition 2024, MSRV 1.88.0)
11-
- **Architecture:** Cargo workspace with 4 member crates
11+
- **Architecture:** Cargo workspace with 5 member crates
1212
- **Project Type:** CLI application with client-server architecture
1313
- **Main Crates:**
1414
- `distant` - Main binary implementation providing commands like `distant
1515
api`, `distant connect`, and `distant launch`
1616
- `distant-core` - Core library with API, protocol, plugin trait, and
1717
utilities
18+
- `distant-docker` - Docker backend plugin using the Bollard API to interact
19+
with containers; supports both Unix and Windows (`nanoserver`) containers
20+
via `DockerFamily` dispatch
1821
- `distant-host` - Custom server implementation to run on a host machine that
1922
implements the full distant specification, also providing a client-side
2023
plugin
2124
- `distant-ssh` - pure Rust SSH client-side plugin compatible with distant
2225
specification
2326
- `distant-test-harness` - non-published crate that provides test utilities to
2427
run e2e system tests that involve standing up distant servers, managers,
25-
sshd, and more
28+
sshd, Docker containers, and more
2629

2730
## General AI Workflow
2831

@@ -55,8 +58,9 @@ To move beyond basic code generation, use the following patterns:
5558
on what has changed.
5659
1. **All tests:** `cargo test --all-features --workspace`
5760
2. **Core tests:** `cargo test --all-features -p distant-core`
58-
2. **Host tests:** `cargo test --all-features -p distant-host`
59-
2. **SSH tests:** `cargo test --all-features -p distant-ssh`
61+
3. **Docker tests:** `cargo test --all-features -p distant-docker`
62+
4. **Host tests:** `cargo test --all-features -p distant-host`
63+
5. **SSH tests:** `cargo test --all-features -p distant-ssh`
6064

6165
## Memory Bank Maintenance (`CLAUDE.md` aka `AGENTS.md`)
6266

@@ -84,10 +88,103 @@ Prevent *context drift* by treating project documentation as a living journal:
8488
Decisions we make that are considered shortcuts that we need to come back to
8589
later to resolve will be placed here.
8690

87-
1. `win_service.rs` has `#![allow(dead_code)]` — Windows service integration
91+
Each item is tagged with a category:
92+
93+
- **(Bug)** — Produces incorrect results, wrong behavior, or data corruption.
94+
- **(Limitation)** — Missing or unsupported functionality that is known and
95+
intentionally deferred.
96+
- **(Workaround)** — Correct behavior achieved through a non-ideal mechanism
97+
(e.g. fallback paths, platform shims). Works today but should be replaced
98+
with a cleaner solution.
99+
- **(Acknowledgement)** — Known inconsistency or rough edge that is not
100+
currently causing failures but could in the future.
101+
102+
1. **(Limitation)** `win_service.rs` has `#![allow(dead_code)]` — Windows service integration
88103
may be incomplete/untested.
89-
2. Windows CI SSH tests have intermittent auth failures from resource
90-
contention — mitigated with nextest retries (4x), not root-caused.
104+
2. **(Acknowledgement)** Windows CI SSH tests have intermittent auth failures
105+
from resource contention — mitigated with nextest retries (4x), not
106+
root-caused.
107+
3. **(Limitation)** `distant-docker` search on Windows containers uses
108+
`findstr.exe` which has limited regex support (no `+`, `?`, `|`, `{n,m}`).
109+
`Regex` and `Or` query conditions return `Unsupported` on Windows — only
110+
`Contains`, `Equals`, `StartsWith`, and `EndsWith` are supported.
111+
4. **(Workaround)** `distant-docker` `append_file` on Windows falls back to tar
112+
read-modify-write because there's no good stdin-append equivalent to `cat >>`
113+
on nanoserver.
114+
5. **(Limitation)** Nanoserver `ContainerUser` cannot write to
115+
`C:\Windows\Temp` or other system directories via exec commands (`mkdir`,
116+
`move`, etc.). The Docker tar API bypasses permissions but exec-based
117+
operations require a user-writable directory. Tests use `C:\temp`
118+
(pre-created via tar API in the test harness).
119+
6. **(Workaround)** `distant-docker` tar directory creation uses a `.distant`
120+
zero-byte marker file workaround — Docker's `PUT /archive` API silently
121+
accepts directory-only tar archives on Windows nanoserver but never
122+
materializes the directories. The marker file forces creation; it is
123+
best-effort deleted afterward via exec, but may remain if the delete fails.
124+
A cleaner solution would require Docker engine changes.
125+
7. **(Workaround)** `distant-docker` `rename` on Windows nanoserver falls back
126+
to tar-read + tar-write + exec-delete when the `move` command fails. This
127+
only works for files, not directories. The `move` failure appears to be a
128+
`ContainerUser` permission issue with directory entries created by Docker's
129+
overlay filesystem.
130+
8. **(Workaround)** `distant-ssh` Windows `copy` uses a cmd.exe conditional
131+
(`if exist "src\*"`) to dispatch between `copy /Y` (files) and
132+
`xcopy /E /I /Y` (directories). `xcopy /I` treats the destination as a
133+
directory, which causes "Cannot perform a cyclic copy" when src and dst are
134+
sibling files in the same directory.
135+
9. **(Bug)** Shell injection via unescaped paths — all `run_shell_cmd`
136+
calls in `distant-docker/src/api.rs` embed paths in single quotes
137+
without escaping `'`. A path containing a single-quote character
138+
(e.g. `/home/user/it's_a_file.txt`) causes a shell parse error and
139+
falls through to tar fallback silently. Fix: switch to direct argv via
140+
`run_cmd` where no shell features are needed, or add a
141+
`shell_quote()` helper.
142+
10. **(Bug)** `distant-docker` `read_dir` on Windows always classifies
143+
all entries as `FileType::File``dir /b /a` provides no type
144+
information, and the code hardcodes `FileType::File` for every entry.
145+
Subdirectories are reported as files to the client. Fix: fall through
146+
to tar-based listing on Windows (which already handles types
147+
correctly).
148+
11. **(Bug)** `distant-docker` `exists` on Windows — `if exist "path"`
149+
may not match directories depending on cmd.exe version, and the
150+
tar-based fallback is only reached on exec infrastructure failure (not
151+
on exit code 1). Fix: use both forms:
152+
`if exist "path\" (exit 0) else if exist "path" (exit 0) else (exit 1)`.
153+
12. **(Bug)** `distant-docker` `remove` on Windows — the cmd.exe
154+
compound `rmdir ... 2>nul & if errorlevel 1 del /f ...` has subtle
155+
errorlevel propagation issues with the `&` operator. Fix: use
156+
`rmdir ... 2>nul || del /f ...` with `||` (run second only if first
157+
fails).
158+
13. **(Bug)** Cross-platform path parsing in `distant-docker/src/utils.rs`
159+
`tar_write_file`, `tar_create_dir`, and `tar_create_dir_all` use
160+
`std::path::Path` which parses with the host OS separator. Windows
161+
container paths like `C:\temp\file.txt` are misinterpreted when the
162+
Docker host is a Unix machine (backslash treated as literal, not
163+
separator). Fix: add a manual Windows-aware path splitter for
164+
container paths.
165+
14. **(Bug)** `distant-docker` search shell injection — `path` is
166+
completely unquoted in search commands (`rg`, `grep`, `find`,
167+
`findstr`), and `shell_escape_pattern` doesn't escape single quotes.
168+
Fix: quote paths and escape `'``'\''` in patterns.
169+
15. **(Limitation)** `distant-docker` `copy` tar fallback uses
170+
`tar_read_file` which skips directory entries — directory copies that
171+
fail via exec silently return `NotFound` from the fallback. Needs a
172+
`tar_copy_path` utility that handles both files and directories.
173+
16. **(Limitation)** `auto_remove` on `LaunchOpts` is stored but never
174+
honored — launched containers are never cleaned up automatically.
175+
Needs `auto_remove: bool` on the `Docker` struct and lifecycle cleanup
176+
(e.g. on `ServerRef` drop or shutdown hook).
177+
17. **(Limitation)** `distant-docker` search error handling — `grep`
178+
exit code 1 (no matches) vs exit code 2 (error) are not distinguished.
179+
A non-existent search path produces a silent empty result instead of
180+
an error. Fix: check exit code > 1 as an error, or use `set -o
181+
pipefail` in the shell command.
182+
18. **(Acknowledgement)** OS detection inconsistency —
183+
`is_windows_image` (launch path) uses exact `== "windows"` while
184+
`detect_family_from_inspect` (connect path) uses
185+
`.contains("windows")`. Could misdetect if an image's OS field is
186+
something other than exactly `"windows"` (e.g. `"windows server"`).
187+
Fix: unify with a shared helper using `.contains("windows")`.
91188

92189
## Tooling & Command Reference
93190

@@ -105,6 +202,7 @@ cargo build --all-features --workspace
105202

106203
# Build specific crates
107204
cargo build -p distant-core
205+
cargo build -p distant-docker
108206
cargo build -p distant-host
109207
cargo build -p distant-ssh
110208
```
@@ -191,6 +289,55 @@ Keep a list of patterns to **avoid**:
191289
4. Don't run mass parallel SSH integration tests without throttling — use
192290
nextest `test-groups` with `max-threads` (configured in
193291
`.config/nextest.toml`).
292+
5. Always create a **feature branch** before starting multi-file or multi-phase
293+
work — never commit directly to `master`. Use
294+
`git checkout -b feature/<name>` before writing any code.
295+
6. **Commit per-phase** (or at minimum per logical unit of work) — don't
296+
accumulate an entire feature as uncommitted changes across many phases. Each
297+
phase should be a separate commit with `cargo fmt` and `cargo clippy` passing
298+
before the commit is created.
299+
7. Always **run tests** (`cargo test --all-features -p <crate>`) after creating
300+
or modifying test files — don't assume tests compile or pass without actually
301+
executing them.
302+
8. Don't hardcode `sh -c` for shell commands in `distant-docker` — Windows
303+
containers (nanoserver) don't have `sh`. Use the `run_shell_cmd` /
304+
`run_shell_cmd_stdout` helpers which dispatch to `sh -c` or `cmd /c` based
305+
on `DockerFamily`.
306+
9. Don't hardcode Unix paths like `/tmp` in Docker tests — use `test_temp_dir()`
307+
which returns `/tmp` on Unix and `C:\temp` on Windows. Don't use
308+
`C:\Windows\Temp``ContainerUser` cannot write there.
309+
10. Don't double-wrap Windows commands with `cmd /c` — when using
310+
`run_shell_cmd` the shell prefix is already provided, so command strings
311+
should contain only the inner command (e.g. `mkdir "path"` not
312+
`cmd /c mkdir "path"`).
313+
11. Don't use forward-slash separators inside `PathBuf::join()` for
314+
multi-component paths — `join("a/b/c")` embeds a Unix separator and
315+
can break on Windows. Use chained `.join("a").join("b").join("c")`.
316+
12. When writing Docker tests (commands, paths, filenames), always consider
317+
Windows container compatibility — use `test_temp_dir()` for temp paths,
318+
chain `.join()` calls for path components, and use `cfg!(windows)` for
319+
any platform-specific commands or assertions.
320+
13. Never bypass GPG commit signing — don't use `-c commit.gpgsign=false`
321+
or `--no-gpg-sign`. If `gpg failed to sign the data`, stop and let the
322+
user resolve the signing issue. The user's GPG key also handles SSH
323+
push authentication.
324+
14. Never dismiss test failures as "intermittent" or "pre-existing" without
325+
investigation. Every failure — even if it only reproduces sometimes —
326+
must be analyzed to determine the root cause and a recommendation given
327+
to fix it. If the root cause is a bug in production code, focus the
328+
recommendation on the production code fix, not on test workarounds.
329+
15. Don't rely on directory-only tar archives to create directories on Windows
330+
nanoserver via the Docker archive API — Docker silently accepts the upload
331+
but never materializes the directories. Always include a file entry (even a
332+
zero-byte marker) in the tar to force directory creation.
333+
16. Don't use `xcopy /E /I /Y` for single-file copies on Windows via SSH — the
334+
`/I` flag makes xcopy treat the destination as a directory, causing "Cannot
335+
perform a cyclic copy" when src and dst are in the same directory. Use
336+
`copy /Y` for files and reserve `xcopy` for directory copies.
337+
17. Don't assume exec-based `move` works reliably on Windows nanoserver — it
338+
can fail for files in directories created by Docker's overlay filesystem.
339+
Prefer a tar-read + tar-write + exec-delete fallback pattern for file
340+
renames when exec fails.
194341

195342
### Format standards
196343

0 commit comments

Comments
 (0)