werkator build succeeded after 00:03
Three of its four entries were wrong. redis, mariadb and mysql were all listed as moving their port through an environment variable — REDIS_PORT, MARIADB_TCP_PORT, MYSQL_TCP_PORT. None of the three images reads one. Werkdock picked a free port, told nobody, and reported it: a client got a number nothing was listening on, which is the single failure the whole quirk mechanism exists to prevent. It was found the only way it could be — by starting the images and connecting to the port werkdock named. redis-server takes --port; the two database entrypoints read the port out of the server's own configuration, so the flag has to reach the command line there too. A quirk can now move a port by a flag, and does so only while the container still runs the image's own program: appending an argument to a command the client chose would be a guess, and guessing is what this commit is about. Two more things surfaced on the way. Redis could not start at all — its entrypoint chowns its data directory as root, which an unprivileged user namespace cannot do, so it needs the same treatment postgres has. And an image's /run did not show through: /run is the instance's own directory so that sockets survive a stop, which left mariadb without /run/mysqld and dying on "Bind on unix socket: No such file or directory". The instance's /run is now seeded from the image's. postgres was right all along and is untouched; it really does read PGPORT. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
187 lines
8.4 KiB
Go
187 lines
8.4 KiB
Go
// Package cli parses werkdock's docker-shaped command line (RFC 0002)
|
|
// and dispatches to the internal packages. Exit codes follow docker:
|
|
// 125 for werkdock's own errors, otherwise the sandboxed command's code
|
|
// is passed through.
|
|
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// Version of this build. A merge to main is a release, so the branch
|
|
// that changes behavior bumps it here and titles its CHANGELOG section
|
|
// with it; there is no separate release step.
|
|
var Version = "0.8.0"
|
|
|
|
const exitCLIError = 125
|
|
|
|
// Main runs the CLI and returns the process exit code.
|
|
func Main(args []string) int {
|
|
if len(args) == 0 {
|
|
usage(os.Stderr)
|
|
return exitCLIError
|
|
}
|
|
switch args[0] {
|
|
case "run":
|
|
return runCmd(args[1:])
|
|
case "create":
|
|
return createCmd(args[1:])
|
|
case "start":
|
|
return startCmd(args[1:])
|
|
case "stop":
|
|
return stopCmd(args[1:])
|
|
case "kill":
|
|
return killCmd(args[1:])
|
|
case "rm":
|
|
return rmCmd(args[1:])
|
|
case "ps":
|
|
return psCmd(args[1:])
|
|
case "logs":
|
|
return logsCmd(args[1:])
|
|
case "wait":
|
|
return waitCmd(args[1:])
|
|
case "exec":
|
|
return execCmd(args[1:])
|
|
case "system":
|
|
return systemCmd(args[1:])
|
|
case "supervise":
|
|
return superviseCmd(args[1:])
|
|
case "container":
|
|
return subcommand("container", args[1:], map[string]string{"ls": "ps", "list": "ps"}, "create", "start", "stop", "kill", "rm", "ps", "logs", "wait", "exec", "inspect", "run")
|
|
case "image":
|
|
return subcommand("image", args[1:], map[string]string{"ls": "images", "list": "images", "rm": "rmi"}, "images", "rmi", "pull", "load", "import", "inspect")
|
|
case "import":
|
|
return importCmd(args[1:])
|
|
case "load":
|
|
return loadCmd(args[1:])
|
|
case "pull":
|
|
return pullCmd(args[1:])
|
|
case "rmi":
|
|
return rmiCmd(args[1:])
|
|
case "images":
|
|
return imagesCmd(args[1:])
|
|
case "inspect":
|
|
return inspectCmd(args[1:])
|
|
case "doctor":
|
|
return doctorCmd(args[1:])
|
|
case "version", "--version":
|
|
fmt.Printf("werkdock %s\n", Version)
|
|
return 0
|
|
case "help", "--help", "-h":
|
|
usage(os.Stdout)
|
|
return 0
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "werkdock: unknown command %q\n\n", args[0])
|
|
usage(os.Stderr)
|
|
return exitCLIError
|
|
}
|
|
}
|
|
|
|
// subcommand maps docker's management form (`container ls`, `image
|
|
// rm`) onto the top-level verbs.
|
|
func subcommand(group string, args []string, aliases map[string]string, verbs ...string) int {
|
|
if len(args) == 0 {
|
|
fmt.Fprintf(os.Stderr, "werkdock: %s needs a subcommand: %s\n", group, strings.Join(verbs, ", "))
|
|
return exitCLIError
|
|
}
|
|
verb := args[0]
|
|
if alias, ok := aliases[verb]; ok {
|
|
verb = alias
|
|
}
|
|
for _, v := range verbs {
|
|
if v == verb {
|
|
return Main(append([]string{verb}, args[1:]...))
|
|
}
|
|
}
|
|
fmt.Fprintf(os.Stderr, "werkdock: unknown command %q for %s\n", args[0], group)
|
|
return exitCLIError
|
|
}
|
|
|
|
func usage(w io.Writer) {
|
|
fmt.Fprint(w, `werkdock — a docker-like sandbox CLI over bwrap, filesystem isolation only.
|
|
Network, /proc, /dev, and /tmp come from the host by contract; the caller is the
|
|
sandbox's only user, uid 0 unless --user or the image's USER says otherwise.
|
|
|
|
Usage:
|
|
werkdock run [flags] IMAGE [COMMAND] [ARG...] run a command in a sandbox (-d: in the background, as a service)
|
|
werkdock create [flags] IMAGE [COMMAND] [ARG...] create an instance without starting it (docker create)
|
|
werkdock start INSTANCE... start created or stopped instances in the background
|
|
werkdock ps [-a] [-q] [--format FMT] list running (-a: all) instances, docker's columns
|
|
werkdock stop [-t SECONDS] INSTANCE... SIGTERM, then SIGKILL after SECONDS (default 10)
|
|
werkdock kill [-s SIGNAL] INSTANCE... send a signal (default KILL)
|
|
werkdock logs [-f] INSTANCE print the recorded stdout and stderr (-f: follow)
|
|
werkdock wait INSTANCE... wait for exit, print the exit code
|
|
werkdock exec [flags] INSTANCE COMMAND [ARG...] run another command in an instance's sandbox
|
|
werkdock rm [-f] INSTANCE... remove instances (-f: a running one too)
|
|
werkdock inspect IMAGE|INSTANCE... print image or instance metadata as JSON, docker-shaped
|
|
werkdock import [-c CHANGE...] ARCHIVE [IMAGE] create an image from a rootfs archive (docker import)
|
|
werkdock load -i ARCHIVE load a docker save / OCI image archive, layers flattened
|
|
werkdock pull [--platform OS/ARCH] IMAGE fetch an image from a registry through the crane binary
|
|
werkdock images [--format FMT] [-q] [REPO[:TAG]] list images: one reference per line, or docker's table/template
|
|
werkdock rmi IMAGE... remove images (docker rmi)
|
|
werkdock system service [--time D] [unix://PATH] serve the docker API subset (for Testcontainers)
|
|
werkdock doctor [TARGET_DIR] check whether this host can run sandboxes
|
|
werkdock version print the version
|
|
|
|
Run flags:
|
|
-v, --volume SRC:DEST[:ro] bind mount (repeatable; -v, --tmpfs and --mount apply in flag order)
|
|
--tmpfs DEST empty tmpfs at DEST (repeatable; /tmp, /root and /run always are one)
|
|
--mount type=bind,src=SRC,dst=DEST[,readonly] the same, in docker's explicit form (repeatable)
|
|
--mount type=tmpfs,dst=DEST
|
|
-e, --env KEY=VALUE set an environment variable (KEY alone copies it from the host, if set there)
|
|
-w, --workdir DIR working directory inside the sandbox (default /; created if missing and writable)
|
|
--entrypoint PROGRAM run PROGRAM with the arguments after IMAGE (overrides the image's ENTRYPOINT, drops its CMD)
|
|
-u, --user USER[:GROUP] appear as this user inside the sandbox (name from the image's /etc/passwd, or a number);
|
|
default: the image's USER, else uid 0. One uid only: the caller is mapped to it
|
|
--userns keep-id appear as the calling user's own uid and gid (podman's spelling)
|
|
--network host accepted as a no-op: the sandbox shares the host's network by contract
|
|
(other modes and -p/--publish are refused)
|
|
-d, --detach start the instance in the background under a supervisor and print its id
|
|
--name NAME name the instance (default: image basename plus six characters of the id)
|
|
--rm remove the instance when the command exits (a foreground --rm run records no logs)
|
|
-i, --interactive pass stdin to the command (-t is refused: no pseudo-terminal)
|
|
|
|
Exec flags: -e, -w, -u, -i as for run; the rootfs, mounts, uid, environment, /run and the
|
|
anonymous volumes are the instance's, the pid namespace is not (its processes are invisible).
|
|
|
|
Ps flags:
|
|
-a, --all stopped and created instances too
|
|
-q, --quiet print ids only
|
|
--format FMT Go template over .ID .Image .Command .CreatedAt .RunningFor .Status .State .Ports .Names,
|
|
or "table [TEMPLATE]"
|
|
|
|
Import flags:
|
|
-c, --change INSTRUCTION Dockerfile instruction applied to the image (repeatable):
|
|
ENTRYPOINT, CMD (JSON array or shell form), ENV, WORKDIR, USER, VOLUME, LABEL
|
|
|
|
Images flags:
|
|
--format FMT Go template over .Repository .Tag .ID .CreatedAt .CreatedSince .Size,
|
|
or "table [TEMPLATE]" for docker's columns (REPOSITORY TAG IMAGE ID CREATED SIZE)
|
|
-q, --quiet print image IDs only
|
|
|
|
Pull flags:
|
|
--platform OS/ARCH platform to pick from a multi-arch image (default linux/<host arch>)
|
|
pull runs crane (github.com/google/go-containerregistry): found in PATH,
|
|
beside werkdock, or at WERKDOCK_CRANE
|
|
|
|
Without COMMAND, run executes the image's ENTRYPOINT and CMD; with one, it
|
|
replaces CMD. Env and WORKDIR of the image apply unless -e/-w override them.
|
|
IMAGE is a docker/OCI-style reference, [REGISTRY/]REPOSITORY[:TAG], e.g.
|
|
eclipse-temurin:21 or ghcr.io/example/build:2026.09; the tag defaults to
|
|
latest. INSTANCE is an id, a unique id prefix, or a name. The store lives in
|
|
$WERKDOCK_HOME (default ~/.werkdock); instances in its instances/ directory
|
|
with config.json, state.json, stdout, stderr, run/ and volumes/ each.
|
|
docker's management form works too: container ls|start|..., image ls|rm|pull|....
|
|
`+"`"+`werkdock system service`+"`"+` listens on $XDG_RUNTIME_DIR/werkdock.sock by default;
|
|
a client reaches it with DOCKER_HOST=unix://<that path>.
|
|
`)
|
|
}
|
|
|
|
func fail(err error) int {
|
|
fmt.Fprintf(os.Stderr, "werkdock: %v\n", err)
|
|
return exitCLIError
|
|
}
|