werkator build succeeded after 00:09
0.9.0 listed mosquitto as the image werkdock cannot move, and that was true of everything mosquitto offers: -p is ignored once a config file declares a listener (the manual says so, and it was measured), no environment variable names the port, and the config file expands none. What mosquitto does read is the config file — and a config file is the one thing werkdock can replace that a flag or a variable cannot reach. A quirk can now name a PortFile: which file the image ships carries the port and how the line reads. Werkdock copies that file into the instance directory with the line rewritten to the chosen port and binds the copy read-only over the image's original, which is never touched. Both of mosquitto's listeners live in the same file, so the dashboard's HTTP API on 9883 moves along with MQTT on 1883. Measured with 2.1.2 through the API socket: two instances at once, each on its own pair of ports, each accepting an anonymous client and answering on its dashboard, 1883 left free. The rewritten copy applies only while the command still names the file and the client has not bound a file of their own over that path; either way the copy would not be what the server reads, and the port is then reported unmoved. An image whose file no longer has the expected line is refused at create with the line named, not started on a port werkdock only claimed. Co-Authored-By: Claude Fable 5.1 <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.10.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
|
|
}
|