feat: basic functionality

This commit is contained in:
2026-06-09 19:09:57 +02:00
parent a3339f9d34
commit 04ed1f25ae
15 changed files with 5374 additions and 63 deletions
+73 -60
View File
@@ -3,14 +3,14 @@
## Synopsis
```
ecr <distro>[:<version>] [-a <arch>] [options] [-- command [args...]]
ecr [OPTIONS] <DISTRO[:VERSION]> -- [COMMAND]...
```
## CLI Interface
### Positional Arguments
- `<distro>` (required): Distribution name
- `<distro>` (required): Distribution name or OCI image reference
- `<version>` (optional): Distribution version/codename
### Options
@@ -18,9 +18,10 @@ ecr <distro>[:<version>] [-a <arch>] [options] [-- command [args...]]
| 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) |
| `--bind <path>` | cwd | Directory to overlay-mount (can be specified multiple times) |
| `--bind-rw <path>` | none | Read-write bind mount at `/mnt/<basename>` (can be specified multiple times, overrides `--bind` for same path) |
| `--no-cache` | false | Download fresh tarball, ignore cache |
| `--no-bind` | false | Skip mounting any directory |
| `-h, --help` | - | Show help |
| `-V, --version` | - | Show version |
@@ -31,8 +32,8 @@ ecr <distro>[:<version>] [-a <arch>] [options] [-- command [args...]]
```
~/.cache/ecr/
├── ubuntu-noble-amd64.tar.gz
├── debian-bookworm-arm64.tar.gz
├── arch-latest-riscv64.tar.gz
├── alpine-latest-x86_64.tar.gz
├── debian-bookworm-amd64.tar.gz
└── ...
```
@@ -45,54 +46,82 @@ No metadata files. Tarballs are downloaded once and never redownloaded. Users ca
```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>` |
### Direct Tarball Downloads
| Distro | Version Format | Source |
|--------|----------------|--------|
| Ubuntu | noble, jammy, mantic or 26.04, 25.10, 22.04, latest, lts | cdimage.ubuntu.com |
| Alpine | 3.20, 3.19, latest, edge | dl-cdn.alpinelinux.org |
### Docker Hub (OCI Registry)
All other distributions use Docker Hub images via OCI registry API:
| Distro | Image Reference |
|--------|-----------------|
| Debian | `library/debian` |
| Arch | `library/archlinux` |
| Fedora | `library/fedora` |
| Gentoo | `gentoo/stage3` |
| Custom | `<image>[:tag]` or `<registry>/<image>[:tag]` |
### Custom Image References
Users can specify any OCI-compatible image:
```
ecr debian:bookworm -- ./build.sh
ecr gentoo/stage3 -- emerge --sync
ecr gcr.io/my-project/my-image:v1.0 -- /app/test
```
### 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 |
| ecr | Ubuntu | Alpine | Docker Hub |
|-----|--------|--------|------------|
| amd64 | amd64 | x86_64 | amd64 |
| arm64 | arm64 | aarch64 | arm64 |
| armhf | armhf | armv7 | arm/v7 |
| riscv64 | riscv64 | riscv64 | riscv64 |
| ppc64el | ppc64el | ppc64le | ppc64le |
| s390x | s390x | s390x | s390x |
### Fedora Container Extraction
### OCI Image Download
For Fedora, download the container image layer and extract:
For Docker Hub images:
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
1. Get anonymous bearer token from `https://auth.docker.io/token`
2. Query manifest list: `GET https://registry.hub.docker.com/v2/<repo>/manifests/<tag>`
3. Select manifest matching target architecture
4. Download layer blobs with authentication
5. Extract layers to rootfs
If architecture is not available in manifest list, error with available architectures:
```
Error: No manifest found for architecture 'riscv64'. Available: amd64, arm64, ppc64le, s390x
```
## Execution Flow
1. Parse CLI arguments and config file
2. Resolve distro/version/arch to tarball URL
1. Parse CLI arguments
2. Resolve distro/version/arch to image source
3. Check cache for existing tarball
4. If not cached, download tarball
4. If not cached, download tarball (direct or OCI)
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
10. Set up overlay mounts for bind paths
11. Set up read-write bind mounts
12. Set environment variables
13. Exec shell or command in chroot
14. On exit, clean up temp directory
## Namespace Setup
@@ -125,6 +154,7 @@ This makes the user appear as root inside the chroot while remaining unprivilege
| /dev | devtmpfs | nosuid |
| /dev/pts | devpts | nosuid,noexec |
| /root/<basename> | overlay | lowerdir=<bind_path>, upperdir=<temp>, workdir=<temp> |
| /mnt/<basename> | bind | rw (for --bind-rw) |
| /etc/resolv.conf | file | written with DNS |
## QEMU Integration
@@ -165,6 +195,8 @@ Overlay configuration:
Changes made inside the chroot are written to upperdir and discarded on exit. The host directory is never modified.
Multiple `--bind` paths can be specified, each creates an overlay at `/root/<basename>`.
Example:
```
$ cd ~/projects/myapp
@@ -175,7 +207,11 @@ $ ecr ubuntu:noble -- make build
### 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.
`--bind-rw <path>` creates a true read-write bind mount at `/mnt/<basename>`. This modifies the host filesystem directly. Use with caution.
Multiple `--bind-rw` paths can be specified. If a path is specified in both `--bind` and `--bind-rw`, the read-write mount takes precedence.
If no path is specified, defaults to current working directory.
### No Mount
@@ -189,7 +225,7 @@ 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:
Override with config file (`~/.config/ecr.yaml`):
```yaml
dns:
@@ -227,26 +263,3 @@ Enable with:
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