Files
ecr/SPEC.md
T

252 lines
6.9 KiB
Markdown

# ecr - implementation specification
## Synopsis
```
ecr <distro>[:<version>] [-a <arch>] [options] [-- command [args...]]
```
## CLI Interface
### Positional Arguments
- `<distro>` (required): Distribution name
- `<version>` (optional): Distribution version/codename
### Options
| Flag | Default | Description |
|------|---------|-------------|
| `-a, --arch <arch>` | host arch | Target architecture |
| `--bind [path]` | cwd | Directory to overlay-mount |
| `--bind-rw [path]` | none | Read-write bind mount at `/root/<basename>` (bypasses overlay) |
| `--no-cache` | false | Download fresh tarball, ignore cache |
| `-h, --help` | - | Show help |
| `-V, --version` | - | Show version |
## File Layout
### Cache Directory
```
~/.cache/ecr/
├── ubuntu-noble-amd64.tar.gz
├── debian-bookworm-arm64.tar.gz
├── arch-latest-riscv64.tar.gz
└── ...
```
No metadata files. Tarballs are downloaded once and never redownloaded. Users can delete files manually or use `--no-cache` to fetch fresh.
### Config File
`~/.config/ecr.yaml`:
```yaml
dns:
- 1.1.1.1
cache_dir: ~/.cache/ecr
```
## Distro Sources
| Distro | Version Format | URL Pattern |
|--------|----------------|-------------|
| Ubuntu | noble, jammy, mantic or 26.04, 25.10, 22.04, latest, lts | https://cdimage.ubuntu.com/ubuntu-base/bionic/daily/current/bionic-base-amd64.tar.gz |
| Debian | bookworm, bullseye, sid, latest | https://cloud.debian.org/images/cloud/trixie/latest/debian-13-nocloud-amd64.tar.xz |
| Arch | latest | https://geo.mirror.pkgbuild.com/iso/latest/archlinux-bootstrap-x86_64.tar.zst |
| Alpine | 3.20, 3.19, latest, edge | https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/x86_64/alpine-minirootfs-3.23.0-x86_64.tar.gz |
| Fedora | 40, 41, rawhide, latest | Extract from `registry.fedoraproject.org/fedora:<version>` |
### Architecture Mapping
| ecr | Ubuntu | Debian | Arch | Alpine | Fedora |
|-----|--------|--------|------|--------|--------|
| amd64 | amd64 | amd64 | x86_64 | x86_64 | x86_64 |
| arm64 | arm64 | arm64 | aarch64 | aarch64 | aarch64 |
| armhf | armhf | armhf | - | armv7 | - |
| riscv64 | riscv64 | riscv64 | riscv64 | riscv64 | riscv64 |
| ppc64el | ppc64el | ppc64el | - | ppc64le | ppc64le |
| s390x | s390x | s390x | - | s390x | s390x |
### Fedora Container Extraction
For Fedora, download the container image layer and extract:
1. Query manifest: `GET https://registry.fedoraproject.org/v2/fedora/manifests/<version>`
2. Parse manifest, get layer digest
3. Download layer blob
4. Extract tarball
## Execution Flow
1. Parse CLI arguments and config file
2. Resolve distro/version/arch to tarball URL
3. Check cache for existing tarball
4. If not cached, download tarball
5. Create temp directory for extraction
6. Extract tarball to temp directory
7. Create namespaces: user, pid, mount, uts
8. Set up mounts: /proc, /sys (ro), /dev, /dev/pts
9. Write /etc/resolv.conf with DNS servers
10. Set up overlay mount for workspace directory
11. Set environment variables
12. Exec shell or command in chroot
13. On exit, clean up temp directory
## Namespace Setup
### Namespaces (Always Created)
- **user**: Map current user to root (UID 0) inside
- **pid**: Isolated process tree
- **mount**: Private mounts for chroot setup
- **uts**: Hostname set to `ecr-<distro>-<random>`
### Network
Host network namespace (no isolation).
### User Namespace Mapping
```
uid_map: 0 <current_uid> 1
gid_map: 0 <current_gid> 1
```
This makes the user appear as root inside the chroot while remaining unprivileged on the host.
### Mounts Inside Chroot
| Path | Type | Options |
|------|------|---------|
| /proc | proc | defaults |
| /sys | sysfs | ro,nosuid,nodev,noexec |
| /dev | devtmpfs | nosuid |
| /dev/pts | devpts | nosuid,noexec |
| /root/<basename> | overlay | lowerdir=<bind_path>, upperdir=<temp>, workdir=<temp> |
| /etc/resolv.conf | file | written with DNS |
## QEMU Integration
### Foreign Architecture Detection
If `--arch` differs from host architecture, QEMU is required.
### binfmt_misc Check
Before entering chroot, verify binfmt_misc is registered for target architecture by checking `/proc/sys/fs/binfmt_misc/qemu-<arch>`.
If not registered, error with message:
```
Error: binfmt_misc not registered for riscv64
Install QEMU user emulation:
Ubuntu/Debian: sudo apt install qemu-user-static
Arch: sudo pacman -S qemu-user-static-binfmt
Alpine: sudo apk add qemu-user-static
```
### QEMU Binary
No action required. Modern qemu-user-static packages register binfmt_misc with the `F` (fix binary) flag, loading the interpreter into kernel memory. The kernel handles foreign binary execution transparently.
## File Handling
### Overlay Mount (Default)
By default, the current working directory is mounted as an overlay filesystem at `/root/<basename>` inside the chroot, where `<basename>` is the name of the current directory.
Overlay configuration:
- `lowerdir`: the source directory (read-only)
- `upperdir`: temp directory for modifications
- `workdir`: temp directory required by overlayfs
Changes made inside the chroot are written to upperdir and discarded on exit. The host directory is never modified.
Example:
```
$ cd ~/projects/myapp
$ ecr ubuntu:noble -- make build
# ~/projects/myapp mounted at /root/myapp
# Build artifacts written to overlay, discarded on exit
```
### Read-Write Bind Mount
`--bind-rw <path>` bypasses overlay and creates a true read-write bind mount at `/mnt/<basename>`. This modifies the host filesystem directly. Use with caution.
### No Mount
`--no-bind` skips mounting any directory.
## DNS
Default DNS server is 1.1.1.1. Configured via `/etc/resolv.conf` in chroot:
```
nameserver 1.1.1.1
```
Override with `--dns` flag or config file:
```yaml
dns:
- 8.8.8.8
- 8.8.4.4
```
## Environment Variables
Default environment inside chroot:
- HOME=/root
- USER=root
- SHELL=/bin/bash (or /bin/sh if bash unavailable)
- TERM=<from host>
- PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Host environment is not inherited.
## Signal Handling
Forward SIGINT, SIGTERM, SIGHUP, SIGQUIT to child process. Wait for child to exit before cleanup.
## Security Requirements
### User Namespace Required
`ecr` requires unprivileged user namespaces. If unavailable (sysctl `kernel.unprivileged_userns_clone=0` or AppArmor restrictions), error with:
```
Error: User namespaces not available
Enable with:
sysctl -w kernel.unprivileged_userns_clone=1
Or check AppArmor profile restrictions.
```
### No Root Fallback
There is no fallback to running as real root. The tool is designed for unprivileged use.
## Implementation
### Language
Rust.
### Dependencies
- `nix`: Unix syscall bindings (clone, unshare, mount, chroot, namespaces)
- `serde` + `serde_yaml`: config parsing
- `reqwest`: HTTP downloads
- `tar`: tarball extraction
- `xz2`: xz decompression
- `zstd`: zstd decompression
- `flate2`: gzip decompression
- `clap`: CLI parsing
- `signal-hook`: signal handling
- `tempfile`: temp directories