Skip to content

Commit d7da494

Browse files
committed
Implement OCI/Dockerfile converters with Gondolin runtime integration
1 parent 26dd1c7 commit d7da494

40 files changed

Lines changed: 4055 additions & 18 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
dist/
3+
out/

Dockerfile.alpine-curl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
FROM alpine:3.20
2+
RUN apk add --no-cache curl ca-certificates
3+
CMD ["/bin/sh"]
4+

Dockerfile.busybox

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FROM busybox:latest
2+
CMD ["/bin/busybox", "echo", "hello-from-dockerfile"]

README.md

Lines changed: 154 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,164 @@
1-
# Gondolin Image Tooling Plan
1+
# Gondolin Image Tools
22

3-
## Goal
4-
Create external tooling that converts container artifacts into images Gondolin can boot **without modifying Gondolin core code**.
3+
External OCI-first tooling that converts container artifacts into Gondolin-bootable images **without modifying Gondolin core**.
54

6-
## Recommendation
7-
Build in this order:
5+
## Status
86

9-
1. **`oci2gondolin` first** (core converter)
10-
2. **`dockerfile2gondolin` second** (thin wrapper around BuildKit + `oci2gondolin`)
7+
Implemented and working:
118

12-
This keeps complexity low and allows multiple input paths (registry, CI-produced OCI tar, local OCI layout).
9+
-`oci2gondolin` core converter
10+
- input sources: `--image`, `--oci-layout`, `--oci-tar` (exactly one)
11+
- platform selection (`linux/amd64`, `linux/arm64`, plus short forms)
12+
- output modes: `rootfs`, `assets`
13+
- structured dry-run plans
14+
- actionable validation + runtime errors
15+
- ✅ OCI resolver/puller for public registries (Bearer token flow)
16+
- ✅ digest verification + local blob cache
17+
- ✅ layer apply engine (tar+gzip, whiteouts, secure extraction checks)
18+
- ✅ materialization
19+
- ext4 image creation (`rootfs.ext4`)
20+
- metadata emission (`meta.json`)
21+
- assets output (`vmlinuz-virt`, `initramfs.cpio.lz4`, `rootfs.ext4`, `manifest.json`)
22+
-`dockerfile2gondolin` thin wrapper
23+
- BuildKit via `docker buildx` (temp docker-container builder)
24+
- BuildKit via `buildctl`
25+
- delegates conversion to `oci2gondolin`
26+
- ✅ unit tests for argument parsing/validation
1327

14-
## Why OCI-first
15-
- Dockerfile support is effectively a full build system
16-
- OCI image/manifest/layers are a stable output contract
17-
- Reusable converter for any upstream build stack
28+
## Requirements
1829

19-
## Deliverables
20-
- `oci2gondolin` CLI
21-
- `dockerfile2gondolin` CLI (wrapper)
22-
- Test fixtures + conformance tests
23-
- Basic docs and examples
30+
- Bun 1.2+
31+
- Docker (for `dockerfile2gondolin`)
32+
- `e2fsprogs` (`mke2fs`, `debugfs`) for rootfs creation/injection
33+
- (optional runtime verification) `@earendil-works/gondolin` CLI + QEMU
34+
35+
macOS helpers:
36+
37+
```bash
38+
brew install e2fsprogs qemu
39+
```
40+
41+
## Install
42+
43+
```bash
44+
bun install
45+
```
46+
47+
## Quickstart
48+
49+
### 1) Validate build + tests
50+
51+
```bash
52+
bun test
53+
bun run typecheck
54+
bun run build
55+
```
56+
57+
### 2) Convert BusyBox image to Gondolin assets
58+
59+
```bash
60+
bun run oci2gondolin -- \
61+
--image busybox:latest \
62+
--platform linux/arm64 \
63+
--mode assets \
64+
--out ./out/busybox-assets
65+
```
66+
67+
### 3) Run with Gondolin package
68+
69+
```bash
70+
GONDOLIN_GUEST_DIR=./out/busybox-assets bunx gondolin exec -- /bin/busybox echo hello
71+
```
72+
73+
### 4) Dockerfile -> Gondolin (wrapper)
74+
75+
```bash
76+
bun run dockerfile2gondolin -- \
77+
--file ./Dockerfile.busybox \
78+
--context . \
79+
--platform linux/arm64 \
80+
--mode assets \
81+
--out ./out/busybox-from-dockerfile
82+
```
83+
84+
Then:
85+
86+
```bash
87+
GONDOLIN_GUEST_DIR=./out/busybox-from-dockerfile bunx gondolin exec -- /bin/busybox echo wrapper-ok
88+
```
89+
90+
## Distro smoke matrix (arm64)
91+
92+
Validated with `gondolin exec`:
93+
94+
- Alpine (`alpine:3.20`)
95+
- Debian (`debian:bookworm-slim`)
96+
- Ubuntu (`ubuntu:24.04`)
97+
- Fedora (`fedora:latest`)
98+
- Arch Linux ARM (`menci/archlinuxarm:latest`)
99+
100+
### macOS note (case-sensitive temp workspace)
101+
102+
Some images (notably Arch/Fedora) include case-sensitive filesystem paths that conflict on default case-insensitive macOS volumes. Use a case-sensitive temp mount and point `TMPDIR` at it when converting:
103+
104+
```bash
105+
CASE_ROOT=$(mktemp -d)
106+
IMG="$CASE_ROOT/oci2gondolin-casefs.sparseimage"
107+
MP="$CASE_ROOT/mount"
108+
mkdir -p "$MP"
109+
110+
hdiutil create -size 8g -type SPARSE -fs 'Case-sensitive APFS' -volname Oci2GondolinCase "$IMG"
111+
hdiutil attach "$IMG" -mountpoint "$MP" -nobrowse
112+
113+
TMPDIR="$MP" bun run oci2gondolin -- --image menci/archlinuxarm:latest --platform linux/arm64 --mode assets --out ./out/arch-assets
114+
GONDOLIN_GUEST_DIR=./out/arch-assets bunx gondolin exec -- /bin/sh -lc 'cat /etc/os-release | head -n 2'
115+
116+
hdiutil detach "$MP"
117+
rm -rf "$CASE_ROOT"
118+
```
119+
120+
## Dry-run examples
121+
122+
```bash
123+
bun run oci2gondolin -- --image busybox:latest --out ./out/plan --dry-run
124+
bun run dockerfile2gondolin -- --file ./Dockerfile --context . --out ./out/plan --dry-run
125+
```
126+
127+
## Commands
128+
129+
### `oci2gondolin`
130+
131+
- Input source (exactly one):
132+
- `--image <ref>`
133+
- `--oci-layout <path>`
134+
- `--oci-tar <path>`
135+
- `--platform linux/amd64|linux/arm64` (or `amd64|arm64`)
136+
- `--mode rootfs|assets` (default: `rootfs`)
137+
- `--out <path>` (required)
138+
- `--dry-run`
139+
140+
### `dockerfile2gondolin`
141+
142+
- `--file <path>` (required)
143+
- `--context <path>` (required)
144+
- `--out <path>` (required)
145+
- `--platform linux/amd64|linux/arm64`
146+
- `--mode rootfs|assets`
147+
- `--builder docker-buildx|buildctl`
148+
- `--target <stage>`
149+
- `--build-arg KEY=VALUE` (repeatable)
150+
- `--secret ...` (repeatable)
151+
- `--dry-run`
152+
153+
## Architecture
154+
155+
- `oci2gondolin` contains the converter pipeline (resolver/puller/layer-apply/materialize)
156+
- converter applies OCI layers on top of an extracted Gondolin base rootfs to preserve runtime compatibility (`sandboxd`, `sandboxfs`, init flow)
157+
- `dockerfile2gondolin` is a wrapper layer around BuildKit + `oci2gondolin`
158+
- gondolin core remains external/unmodified
159+
160+
## Planning docs
24161

25-
## Directory contents
26162
- [`01-oci2gondolin-spec.md`](./01-oci2gondolin-spec.md)
27163
- [`02-dockerfile2gondolin-wrapper.md`](./02-dockerfile2gondolin-wrapper.md)
28164
- [`03-implementation-phases.md`](./03-implementation-phases.md)

bun.lock

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "gondolin-image-tools",
3+
"version": "0.1.0",
4+
"private": true,
5+
"description": "OCI-first external image tooling for Gondolin",
6+
"license": "Apache-2.0",
7+
"type": "commonjs",
8+
"bin": {
9+
"oci2gondolin": "./dist/bin/oci2gondolin.js",
10+
"dockerfile2gondolin": "./dist/bin/dockerfile2gondolin.js"
11+
},
12+
"scripts": {
13+
"build": "rm -rf dist && tsc -p tsconfig.json",
14+
"typecheck": "tsc -p tsconfig.json --noEmit",
15+
"test": "bun test",
16+
"oci2gondolin": "bun run src/bin/oci2gondolin.ts",
17+
"dockerfile2gondolin": "bun run src/bin/dockerfile2gondolin.ts"
18+
},
19+
"devDependencies": {
20+
"@types/node": "^22.13.10",
21+
"typescript": "^5.7.3"
22+
},
23+
"engines": {
24+
"bun": ">=1.2.0"
25+
},
26+
"packageManager": "bun@1.3.6",
27+
"dependencies": {
28+
"@earendil-works/gondolin": "^0.2.1"
29+
}
30+
}

src/bin/dockerfile2gondolin.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env bun
2+
import { runDockerfile2Gondolin } from "../commands/dockerfile2gondolin";
3+
4+
async function main(): Promise<void> {
5+
const exitCode = await runDockerfile2Gondolin(process.argv.slice(2));
6+
process.exit(exitCode);
7+
}
8+
9+
main().catch((error) => {
10+
console.error(error instanceof Error ? error.message : String(error));
11+
process.exit(1);
12+
});

src/bin/oci2gondolin.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env bun
2+
import { runOci2Gondolin } from "../commands/oci2gondolin";
3+
4+
async function main(): Promise<void> {
5+
const exitCode = await runOci2Gondolin(process.argv.slice(2));
6+
process.exit(exitCode);
7+
}
8+
9+
main().catch((error) => {
10+
console.error(error instanceof Error ? error.message : String(error));
11+
process.exit(1);
12+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { parseDockerfile2GondolinArgs } from "../dockerfile2gondolin/cli/args";
2+
import { dockerfile2GondolinUsage } from "../dockerfile2gondolin/cli/usage";
3+
import { buildDockerfileWrapperPlan } from "../dockerfile2gondolin/pipeline/plan";
4+
import { executeDockerfileWrapper } from "../dockerfile2gondolin/pipeline/execute";
5+
import { CliHelpRequested, CliUsageError } from "../shared/cli-errors";
6+
import { renderCliError } from "../shared/render-cli-error";
7+
8+
export async function runDockerfile2Gondolin(argv: string[]): Promise<number> {
9+
try {
10+
const options = parseDockerfile2GondolinArgs(argv);
11+
12+
if (options.dryRun) {
13+
const plan = buildDockerfileWrapperPlan(options);
14+
console.log(JSON.stringify(plan, null, 2));
15+
return 0;
16+
}
17+
18+
const result = await executeDockerfileWrapper(options);
19+
console.log(JSON.stringify(result, null, 2));
20+
return 0;
21+
} catch (error) {
22+
if (error instanceof CliHelpRequested) {
23+
console.log(dockerfile2GondolinUsage());
24+
return 0;
25+
}
26+
27+
console.error(renderCliError(error));
28+
29+
if (error instanceof CliUsageError) {
30+
console.error("\n" + dockerfile2GondolinUsage());
31+
}
32+
33+
return 1;
34+
}
35+
}

src/commands/oci2gondolin.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { CliHelpRequested, CliUsageError } from "../shared/cli-errors";
2+
import { renderCliError } from "../shared/render-cli-error";
3+
import { parseOci2GondolinArgs } from "../oci2gondolin/cli/args";
4+
import { oci2GondolinUsage } from "../oci2gondolin/cli/usage";
5+
import { executeConversion } from "../oci2gondolin/pipeline/execute";
6+
import { buildDryRunPlan } from "../oci2gondolin/pipeline/plan";
7+
8+
export async function runOci2Gondolin(argv: string[]): Promise<number> {
9+
try {
10+
const options = parseOci2GondolinArgs(argv);
11+
12+
if (options.dryRun) {
13+
const plan = buildDryRunPlan(options);
14+
console.log(JSON.stringify(plan, null, 2));
15+
return 0;
16+
}
17+
18+
const result = await executeConversion(options);
19+
console.log(JSON.stringify(result, null, 2));
20+
return 0;
21+
} catch (error) {
22+
if (error instanceof CliHelpRequested) {
23+
console.log(oci2GondolinUsage());
24+
return 0;
25+
}
26+
27+
console.error(renderCliError(error));
28+
29+
if (error instanceof CliUsageError) {
30+
console.error("\n" + oci2GondolinUsage());
31+
}
32+
33+
return 1;
34+
}
35+
}

0 commit comments

Comments
 (0)