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>
41 lines
1.7 KiB
Text
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.
|