Files
6d6060bb07 RFC 0005 milestone 2: instance lifecycle — run -d, ps, stop, logs, rm, exec (#4)
An instance is a directory under instances/<id>/ (config.json, state.json,
stdout, stderr, run/, volumes/) written by package instance, the service
behind the verbs. run -d and start fork this binary as a supervisor
(werkdock supervise ID) that records the pids and the exit code; stop,
kill, wait, logs, ps, rm, inspect read the files. exec is a second bwrap
over the same instance tree. The command is pid 1 of its sandbox
(bwrap --as-pid-1) so stop's SIGTERM reaches it, as in docker; /run and
the anonymous volumes are instance directories, kept across stop/start
and shared with exec. Version 0.5.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #4
2026-09-10 20:55:59 +02:00

191 lines
5.3 KiB
Go

package cli
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"time"
"werkdock/internal/engine"
"werkdock/internal/instance"
"werkdock/internal/store"
)
// inspectOutput is one element of the JSON array `werkdock inspect`
// prints — docker inspect's shape for the fields werkdock shares
// (RepoTags, Created, Architecture, Os, Config), plus a Werkdock
// section for what is specific to the flat store.
type inspectOutput struct {
ID string `json:"Id"`
RepoTags []string `json:"RepoTags"`
Created time.Time `json:"Created"`
Architecture string `json:"Architecture"`
Os string `json:"Os"`
Config store.ImageConfig `json:"Config"`
Werkdock inspectWerkdock `json:"Werkdock"`
}
type inspectWerkdock struct {
Source string `json:"Source"`
RootFS string `json:"RootFS"`
}
func inspectCmd(args []string) int {
fs := flag.NewFlagSet("inspect", flag.ContinueOnError)
fs.SetOutput(io.Discard)
if err := fs.Parse(args); err != nil {
return fail(err)
}
if len(fs.Args()) == 0 {
return fail(errors.New("inspect needs an IMAGE"))
}
st, err := store.Default()
if err != nil {
return fail(err)
}
// docker inspect takes images and containers alike: an image
// reference first, else an instance id, prefix, or name.
var out []interface{}
m := instance.Manager{Root: st.Root}
for _, key := range fs.Args() {
entry, imgErr := inspectImage(st, key)
if imgErr == nil {
out = append(out, entry)
continue
}
inst, err := m.Get(key)
if err != nil {
if errors.Is(err, instance.ErrNotFound) {
return fail(fmt.Errorf("no such image or instance: %s", key))
}
return fail(err)
}
out = append(out, inspectInstance(inst))
}
return printJSON(os.Stdout, out)
}
// instanceOutput is docker's container inspect shape for what werkdock
// has: identity, State, the Config it ran with, Mounts; plus a
// Werkdock section with the instance directory. Name has no leading
// slash — podman's spelling.
type instanceOutput struct {
ID string `json:"Id"`
Name string `json:"Name"`
Created time.Time `json:"Created"`
Path string `json:"Path"`
Args []string `json:"Args"`
State instanceState `json:"State"`
Image string `json:"Image"`
Config instanceConfig `json:"Config"`
Mounts []instanceMount `json:"Mounts"`
Werkdock instanceWerkdock `json:"Werkdock"`
}
type instanceState struct {
Status string `json:"Status"`
Running bool `json:"Running"`
Pid int `json:"Pid"`
ExitCode int `json:"ExitCode"`
Error string `json:"Error"`
StartedAt time.Time `json:"StartedAt"`
FinishedAt time.Time `json:"FinishedAt"`
}
type instanceConfig struct {
Image string `json:"Image"`
User string `json:"User"`
Env []string `json:"Env"`
Cmd []string `json:"Cmd"`
WorkingDir string `json:"WorkingDir"`
AutoRemove bool `json:"AutoRemove"`
}
type instanceMount struct {
Type string `json:"Type"`
Source string `json:"Source"`
Destination string `json:"Destination"`
RW bool `json:"RW"`
}
type instanceWerkdock struct {
Dir string `json:"Dir"`
Supervisor int `json:"Supervisor"`
Volumes []string `json:"Volumes"`
}
func inspectInstance(inst *instance.Instance) instanceOutput {
cfg, st := inst.Config, inst.State
var env []string
for _, e := range cfg.Spec.Env {
env = append(env, e.Key+"="+e.Value)
}
var mounts []instanceMount
for _, m := range cfg.Spec.Mounts {
mount := instanceMount{Type: "bind", Source: m.Source, Destination: m.Dest, RW: m.Mode != engine.MountRoBind}
if m.Mode == engine.MountTmpfs {
mount.Type = "tmpfs"
}
mounts = append(mounts, mount)
}
var args []string
if len(cfg.Spec.Command) > 1 {
args = cfg.Spec.Command[1:]
}
return instanceOutput{
ID: cfg.ID,
Name: cfg.Name,
Created: cfg.Created,
Path: cfg.Spec.Command[0],
Args: args,
State: instanceState{
Status: string(st.Status), Running: inst.Running(), Pid: st.ChildPID,
ExitCode: st.ExitCode, Error: st.Error, StartedAt: st.StartedAt, FinishedAt: st.FinishedAt,
},
Image: cfg.Image,
Config: instanceConfig{
Image: cfg.Image, User: fmt.Sprintf("%d:%d", cfg.Spec.UID, cfg.Spec.GID), Env: env,
Cmd: cfg.Spec.Command, WorkingDir: cfg.Spec.Workdir, AutoRemove: cfg.AutoRemove,
},
Mounts: mounts,
Werkdock: instanceWerkdock{Dir: inst.Dir, Supervisor: st.PID, Volumes: cfg.Volumes},
}
}
func inspectImage(st store.Store, reference string) (inspectOutput, error) {
ref, err := store.ParseReference(reference)
if err != nil {
return inspectOutput{}, err
}
meta, err := st.Meta(reference)
if err != nil {
return inspectOutput{}, err
}
dir, err := st.Dir(reference)
if err != nil {
return inspectOutput{}, err
}
return inspectOutput{
ID: meta.ID(),
RepoTags: []string{ref.String()},
Created: meta.CreatedAt,
Architecture: meta.Architecture,
Os: meta.OS,
Config: meta.Config,
Werkdock: inspectWerkdock{Source: meta.Source, RootFS: filepath.Join(dir, "rootfs")},
}, nil
}
func printJSON(w io.Writer, v interface{}) int {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
if err := enc.Encode(v); err != nil {
return fail(fmt.Errorf("writing JSON: %w", err))
}
return 0
}