ZDDC/zddc/runtime.Containerfile
ZDDC da4754b6ef feat(convert): bwrap engine as production default
Replaces the always-spawn-an-OCI-container model with a per-call
bubblewrap sandbox. Pandoc and chromium binaries are baked into the
zddc-server runtime image; each conversion runs them under bwrap's
Linux-namespace isolation. No daemon, no socket, no privileged outer
container, no OCI image pull at conversion time.

Why: the OCI engine paid ≈ 350 MB image pulls + 400 MB persistent
storage + ~300 ms per-conversion startup, plus required either an
on-host daemon socket (zddc-RCE → host-RCE in one hop) or nested
container privileges. bwrap gets the same sandbox properties
(--unshare-all, ro-bind /usr, tmpfs /tmp, clearenv, no-network) at
~5 ms per call and zero external dependencies. This is the same
primitive Flatpak uses for every app launch — battle-tested at scale
for "untrusted-input, short-lived, isolated."

Runner abstraction:
- `Runner.Run` signature: image string → ToolSpec{Image, Binary}.
  Both fields populated by entry points; whichever engine is
  installed reads the one it needs.
- `bwrapRunner` (new): assembles bwrap argv via `buildBwrapArgs`
  helper (testable in isolation), spawns bwrap with the binary.
- `containerRunner` (renamed conceptually to "legacy fallback"):
  unchanged behavior, still reachable for hosts that prefer OCI
  containers per conversion.

Probe order in health.Probe: bwrap → podman → docker. First hit wins.
Engine kinds in Capabilities: "bwrap" | "podman" | "docker". The
no-engine error message now lists all three.

Config (cmd/zddc-server):
- new --convert-pandoc-binary  / ZDDC_CONVERT_PANDOC_BINARY  (default "pandoc")
- new --convert-chromium-binary / ZDDC_CONVERT_CHROMIUM_BINARY (default "chromium-browser")
- existing --convert-pandoc-image / --convert-chromium-image kept
  for the OCI engine, doc updated to clarify they only apply there.
- --convert-engine helptext lists bwrap first.

Images:
- New `zddc/runtime.Containerfile` — alpine + bubblewrap + pandoc-cli +
  chromium + font-noto. Documents build/publish workflow.
- helm/zddc-server-prod/values.yaml.example: runtimeImage default
  switched to a placeholder for the new bundled runtime image; bare
  alpine NO LONGER works for /.convert (clearly called out in the
  comment).
- bitnest dev: /var/lib/zddc-dev-build/Containerfile mirrors the
  production runtime image. Quadlet at /etc/containers/systemd/
  zddc.container drops the podman-socket mount (no longer needed)
  and sets ZDDC_CONVERT_ENGINE=bwrap explicitly to avoid silent
  downgrades if a stray podman ends up on PATH.

Tests:
- convert_test.go: fakeRunner / recordingRunner now record ToolSpec.
- New TestToolSpecPopulation pins that both Image and Binary are
  filled by every entry point.
- New TestBwrapArgs_SandboxFlagsPresent / MountTranslation /
  RejectsBadMountSpec lock in the bwrap argv shape — a refactor that
  drops a hardening flag or misroutes a mount fails this loud.

Docs:
- AGENTS.md § "Server-side document conversion" rewritten around
  the bwrap-first model with podman/docker as legacy fallbacks.
- ARCHITECTURE.md convert reference updated.
- internal/convert package doc reflects the two-engine probe order.

Verified end-to-end on bitnest: probe reports
  engine=bwrap pandoc_binary=pandoc chromium_binary=chromium-browser
on startup. All 15 Go test packages green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 17:42:28 -05:00

41 lines
1.7 KiB
Text

# Runtime image for zddc-server.
#
# Bundles the conversion toolchain (pandoc + chromium + bubblewrap) so
# the MD→DOCX/HTML/PDF endpoint works without an external container
# engine. The convert package's bwrap engine (production default)
# sandboxes each pandoc/chromium invocation in a fresh Linux-namespace;
# no daemon, no socket, no privileged outer container, no OCI image
# pull at conversion time.
#
# Used by helm charts (helm/zddc-server-prod/) as the main-container
# image. The build is independent of zddc-server itself — the binary
# is built by the helm chart's init container from a pinned git ref
# and copied into this runtime image's filesystem at start. Image
# tags should track the upstream package versions (pandoc, chromium)
# more than zddc-server, since the binary is layered in at deploy time.
#
# Build:
# podman build -t zddc-server-runtime:latest \
# -f zddc/runtime.Containerfile zddc/
#
# Publish (example):
# podman tag zddc-server-runtime:latest \
# codeberg.org/varasys/zddc-server-runtime:vYYYYMMDD
# podman push codeberg.org/varasys/zddc-server-runtime:vYYYYMMDD
#
# Size: ≈ 1 GB unpacked (chromium dominates). Container engines
# layer + dedupe the chromium libs across replicas on the same node.
FROM docker.io/library/alpine:3
RUN apk add --no-cache \
bubblewrap \
pandoc-cli \
chromium \
font-noto \
ca-certificates
# The init container in helm/zddc-server-*/templates/deployment.yaml
# writes the compiled zddc-server binary to /zddc/zddc-server in a
# shared emptyDir volume; the main container's command is
# `/zddc/zddc-server`. No CMD/ENTRYPOINT here because the binary
# path is provided by the chart, not baked into the image.