Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ gh pr view 2 --json baseRefName

## UNDERSTAND BRANCH CHAINS

**ALWAYS fetch before investigating branches:**
```bash
git fetch origin
```
Branches may already be merged on remote. Don't waste time on stale local state.

**Run before starting work, committing, or opening PRs:**

```bash
Expand Down Expand Up @@ -499,13 +505,22 @@ See `Containerfile.libfuse-remap` and `Containerfile.pjdfstest` for examples.

**CRITICAL: VM commands BLOCK the terminal.** You MUST use Claude's `run_in_background: true` feature.

**PREFER NON-ROOT TESTING**: Run tests without sudo when possible. Rootless networking mode (`--network rootless`, the default) doesn't require sudo. Only use `sudo` for:
- `--network bridged` tests
- Operations that explicitly need root (iptables, privileged containers)

The ubuntu user has KVM access (`kvm` group), so `fcvm podman run` works without sudo in rootless mode.

```bash
# WRONG - This blocks forever, wastes context, and times out
sudo fcvm podman run --name test nginx:alpine
# PREFERRED - Rootless mode (no sudo needed, use run_in_background: true)
./target/release/fcvm podman run --name test alpine:latest 2>&1 | tee /tmp/vm.log
# Defaults to --network rootless
# Get PID from state and use exec:
ls -t /mnt/fcvm-btrfs/state/*.json | head -1 | xargs cat | jq -r '.pid'
./target/release/fcvm exec --pid <PID> -- hostname

# CORRECT - Run VM in background, then use exec to test
# ONLY WHEN NEEDED - Bridged mode (requires sudo)
sudo ./target/release/fcvm podman run --name test --network bridged nginx:alpine 2>&1 | tee /tmp/vm.log
# Use run_in_background: true in Bash tool call
# Then sleep and check logs:
sleep 30
grep healthy /tmp/vm.log
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ serde_json = "1"
sha2 = "0.10"
hex = "0.4"
toml = "0.8"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "fs", "signal", "io-util", "sync", "time"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "fs", "signal", "io-util", "io-std", "sync", "time"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
which = "6"
uuid = { version = "1", features = ["v4", "serde"] }
Expand Down
222 changes: 136 additions & 86 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
2. **`fcvm exec` Command**
- Execute commands in running VMs
- Supports running in guest OS or inside container (`-c` flag)
- Interactive mode with stdin forwarding (`-i` flag)
- TTY allocation for terminal apps (`-t` flag)

3. **`fcvm snapshot` Commands**
- `fcvm snapshot create`: Create snapshot from running VM
Expand Down Expand Up @@ -907,120 +909,168 @@ The guest is configured to support rootless Podman:

---

## CLI Interface

### Commands
## TTY & Interactive Mode

#### `fcvm setup`
fcvm provides full interactive terminal support for both `podman run -it` and `exec -it`, matching docker/podman semantics.

**Purpose**: Download kernel and create rootfs (first-time setup).
### Architecture

**Usage**:
```bash
fcvm setup
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Host Process (fcvm) │
│ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────────┐│
│ │ User Terminal│────►│ Raw Mode Handler │────►│ exec_proto Encoder ││
│ │ (stdin/out) │◄────│ (tcsetattr) │◄────│ (binary framing) ││
│ └──────────────┘ └──────────────────┘ └──────────┬───────────┘│
│ │ │
└───────────────────────────────────────────────────────────┼────────────┘
│ vsock
┌───────────────────────────────────────────────────────────────────────┐
│ Guest (fc-agent) │ │
│ ┌──────────────────┐ ┌──────────────┐ ┌──────────▼─────────┐ │
│ │ exec_proto │────►│ PTY Master │────►│ Container Process │ │
│ │ Decoder │◄────│ (openpty) │◄────│ (sh, vim, etc.) │ │
│ └──────────────────┘ └──────────────┘ └────────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘
```

**What it does:**
1. Downloads Kata kernel (~15MB, cached by URL hash)
2. Downloads packages via `podman run ubuntu:noble` with `apt-get install --download-only`
3. Creates Layer 2 rootfs (~10GB): boots VM, installs packages, writes config
4. Verifies setup by checking `/etc/fcvm-setup-complete` marker file
5. Creates fc-agent initrd (embeds statically-linked fc-agent binary)

Takes 5-10 minutes on first run. Subsequent runs are instant (cached by content hash).
### Flags

**Note**: Must be run before `fcvm podman run` with bridged networking. For rootless mode, you can use `--setup` flag on `fcvm podman run` instead.
| Flag | Meaning | Implementation |
|------|---------|----------------|
| `-i` | Keep stdin open | Host reads stdin, sends via STDIN messages |
| `-t` | Allocate PTY | Guest allocates PTY master/slave pair |
| `-it` | Both | Interactive shell with full terminal support |
| neither | Plain exec | Pipes for stdin/stdout, no PTY |

#### `fcvm podman run`
### Wire Protocol (exec_proto)

**Purpose**: Launch a container in a new Firecracker VM.
Binary framed protocol over vsock for efficient transport of terminal data:

**Usage**:
```bash
fcvm podman run --name <NAME> [OPTIONS] <IMAGE>
```
┌─────────┬─────────┬──────────────────┐
│ Type(1) │ Len(4) │ Payload(N) │
└─────────┴─────────┴──────────────────┘
```

**Arguments**:
- `IMAGE` - Container image (e.g., `nginx:alpine`, `ghcr.io/org/app:v1.0`)
**Message Types**:
- `DATA (0x00)`: Output from command (stdout/stderr)
- `STDIN (0x01)`: Input from user terminal
- `EXIT (0x02)`: Command exit code (4 bytes, big-endian i32)
- `ERROR (0x03)`: Error message string

**Options**:
**Why binary framing?**
- Handles escape sequences (Ctrl+C = 0x03, Ctrl+D = 0x04)
- Preserves all bytes without escaping
- Efficient for high-throughput terminal output

### Host-Side Implementation (`src/commands/tty.rs`)

```rust
// 1. Set terminal to raw mode
let original = tcgetattr(stdin)?;
let mut raw = original.clone();
cfmakeraw(&mut raw);
tcsetattr(stdin, TCSANOW, &raw)?;

// 2. Spawn reader/writer tasks
tokio::spawn(async move {
// Reader: terminal stdin → vsock
loop {
let n = stdin.read(&mut buf)?;
exec_proto::write_stdin(&mut vsock, &buf[..n])?;
}
});

tokio::spawn(async move {
// Writer: vsock → terminal stdout
loop {
match exec_proto::Message::read_from(&mut vsock)? {
Message::Data(data) => stdout.write_all(&data)?,
Message::Exit(code) => return code,
_ => {}
}
}
});
```
--name <NAME> VM name (required)
--cpu <COUNT> vCPU count (default: 2)
--mem <MB> Memory in MiB (default: 2048)
--network <MODE> Network mode: rootless|bridged (default: rootless)
--map <HOST:GUEST[:ro]> Volume mount via FUSE (can specify multiple)
--env <KEY=VALUE> Environment variable (can specify multiple)
--cmd <COMMAND> Container command override
--publish <MAPPING> Port publish (can specify multiple)
--balloon <MB> Memory balloon target
--health-check <URL> HTTP health check URL
--privileged Run container in privileged mode
--setup Run setup if kernel/rootfs missing (rootless only)

### Guest-Side Implementation (`fc-agent/src/tty.rs`)

```rust
// 1. Allocate PTY
let (master, slave) = openpty()?;

// 2. Fork child process
match fork() {
0 => {
// Child: setup PTY as controlling terminal
setsid();
ioctl(slave, TIOCSCTTY, 0);
dup2(slave, STDIN);
dup2(slave, STDOUT);
dup2(slave, STDERR);
execvp(command, args);
}
pid => {
// Parent: relay between vsock and PTY master
// Reader thread: PTY master → vsock (DATA messages)
// Writer thread: vsock (STDIN messages) → PTY master
}
}
```

**Examples**:
```bash
# Simple nginx (bridged networking, requires sudo)
sudo fcvm podman run --name my-nginx nginx:alpine
### Supported Features

# Rootless mode (no sudo required)
fcvm podman run --name my-nginx --network rootless nginx:alpine
- **Escape sequences**: Colors (ANSI), cursor movement, screen clearing
- **Control characters**: Ctrl+C (SIGINT), Ctrl+D (EOF), Ctrl+Z (SIGTSTP)
- **Line editing**: Arrow keys, backspace, history (shell-dependent)
- **Full-screen apps**: vim, htop, less, nano, tmux

# With port forwarding
sudo fcvm podman run --name web --publish 8080:80 nginx:alpine
### Limitations

# With volumes and environment
sudo fcvm podman run \
--name db \
--map /host/data:/data \
--env DB_HOST=localhost \
postgres:15
- **Window resize (SIGWINCH)**: Not implemented. Terminal size is fixed at session start.
- **Job control**: Background/foreground (`bg`, `fg`) work within the container, but signals are not forwarded to the host.

# With health check
sudo fcvm podman run \
--name web \
--health-check http://localhost/health \
nginx:alpine
---

# High CPU/memory with balloon
sudo fcvm podman run \
--name ml \
--cpu 8 \
--mem 8192 \
--balloon 4096 \
ml-training:latest
```
## CLI Interface

#### `fcvm exec`
> **Full CLI documentation with examples**: See [README.md](README.md#cli-reference)

**Purpose**: Execute a command in a running VM.
### Command Summary

**Usage**:
```bash
fcvm exec --pid <PID> [OPTIONS] -- <COMMAND> [ARGS...]
```
| Command | Purpose |
|---------|---------|
| `fcvm setup` | Download kernel, create rootfs (first-time setup, ~5-10 min) |
| `fcvm podman run` | Launch container in Firecracker VM |
| `fcvm exec` | Execute command in running VM/container |
| `fcvm ls` | List running VMs |
| `fcvm snapshot create` | Create snapshot from running VM |
| `fcvm snapshot serve` | Start UFFD memory server for cloning |
| `fcvm snapshot run` | Spawn clone from memory server |
| `fcvm snapshots` | List available snapshots |

**Options**:
```
--pid <PID> PID of the fcvm process managing the VM (required)
-c, --container Run command inside the container (not just guest OS)
```
### Key CLI Design Decisions

**Examples**:
```bash
# Run command in guest OS
sudo fcvm exec --pid 12345 -- ls -la /
1. **Trailing arguments**: Both `podman run` and `exec` support trailing args after `--`:
```bash
fcvm podman run --name test alpine:latest echo "hello"
fcvm exec --name test -it -- sh -c "ls -la"
```

# Run command inside container
sudo fcvm exec --pid 12345 -c -- curl -s http://localhost/health
2. **`--name` vs `--pid`**: VM identification uses named flags (not positional):
```bash
fcvm exec --name my-vm -- hostname # By name
fcvm exec --pid 12345 -- hostname # By PID
```

# Check egress connectivity from guest
sudo fcvm exec --pid 12345 -- curl -s ifconfig.me
3. **Interactive flags** (`-i`, `-t`): Match docker/podman semantics:
- `-i`: Keep stdin open
- `-t`: Allocate PTY
- `-it`: Both (interactive shell)

# Check egress connectivity from container
sudo fcvm exec --pid 12345 -c -- wget -q -O - http://ifconfig.me
```
4. **Network modes**: `--network rootless` (default, no sudo) or `--network bridged` (sudo required)

#### `fcvm snapshot create`

Expand Down
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ else
NEXTEST_IGNORED :=
endif

# Disable retries when FILTER is set (debugging specific tests)
ifdef FILTER
NEXTEST_RETRIES :=
else
NEXTEST_RETRIES := --retries 2
endif

# Default log level: fcvm debug, suppress FUSE spam
# Override with: RUST_LOG=debug make test-root
TEST_LOG ?= fcvm=debug,health-monitor=info,fuser=warn,fuse_backend_rs=warn,passthrough=warn
Expand Down Expand Up @@ -199,7 +206,7 @@ _test-root:
FCVM_DATA_DIR=$(ROOT_DATA_DIR) \
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUNNER='sudo -E' \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER='sudo -E' \
$(NEXTEST) $(NEXTEST_CAPTURE) $(NEXTEST_IGNORED) --retries 2 --features privileged-tests $(FILTER)
$(NEXTEST) $(NEXTEST_CAPTURE) $(NEXTEST_IGNORED) $(NEXTEST_RETRIES) --features privileged-tests $(FILTER)

# Host targets (with setup, check-disk first to fail fast if disk is full)
test-unit: show-notes check-disk build _test-unit
Expand Down
Loading
Loading