Files
miandmhoennig f9c1ae835d Api socket (#5)
Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #5
2026-09-10 22:09:58 +02:00

317 lines
7.9 KiB
Go

package api
import (
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"strconv"
"strings"
"werkdock/internal/engine"
"werkdock/internal/instance"
)
// containerInspect answers GET /containers/{id}/json in docker's shape.
// NetworkSettings.Ports is the field a client reads to learn where the
// service listens; for werkdock the host port is the port the service
// was told to bind (see quirk).
func (s *Server) containerInspect(w http.ResponseWriter, r *http.Request) {
inst, ok := s.instanceOf(w, r)
if !ok {
return
}
writeJSON(w, http.StatusOK, s.inspectBody(inst))
}
func (s *Server) inspectBody(inst *instance.Instance) map[string]interface{} {
cfg, st := inst.Config, inst.State
var args []string
if len(cfg.Spec.Command) > 1 {
args = cfg.Spec.Command[1:]
}
path := ""
if len(cfg.Spec.Command) > 0 {
path = cfg.Spec.Command[0]
}
image := ""
if meta, err := s.Store.Meta(cfg.Image); err == nil {
image = imageID(meta)
}
return map[string]interface{}{
"Id": cfg.ID,
"Created": cfg.Created,
"Path": path,
"Args": args,
"Name": "/" + cfg.Name,
"Image": image,
"Driver": "flat",
"Platform": "linux",
"State": map[string]interface{}{
"Status": string(st.Status),
"Running": inst.Running(),
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": st.ChildPID,
"ExitCode": st.ExitCode,
"Error": st.Error,
"StartedAt": st.StartedAt,
"FinishedAt": st.FinishedAt,
},
"RestartCount": 0,
"Config": map[string]interface{}{
"Hostname": cfg.ID[:12],
"Image": cfg.Image,
"User": fmt.Sprintf("%d:%d", cfg.Spec.UID, cfg.Spec.GID),
"Env": envStrings(cfg.Spec.Env),
"Cmd": cfg.Spec.Command,
"Entrypoint": nil,
"WorkingDir": cfg.Spec.Workdir,
"Labels": labelsOrEmpty(cfg.Labels),
"ExposedPorts": exposedPorts(cfg.Ports),
"Tty": false,
"AttachStdout": false,
"AttachStderr": false,
},
"HostConfig": map[string]interface{}{
"Binds": bindStrings(cfg.Spec.Mounts),
"NetworkMode": "host",
"AutoRemove": cfg.AutoRemove,
"PortBindings": portBindings(cfg.Ports),
"Privileged": false,
"RestartPolicy": map[string]interface{}{"Name": "", "MaximumRetryCount": 0},
},
"NetworkSettings": map[string]interface{}{
"Ports": portBindings(cfg.Ports),
"IPAddress": "127.0.0.1",
"Gateway": "",
"Networks": map[string]interface{}{
"host": map[string]interface{}{"IPAddress": "127.0.0.1", "Gateway": "", "NetworkID": "host", "EndpointID": ""},
},
},
"Mounts": mountList(cfg.Spec.Mounts),
"Werkdock": map[string]interface{}{
"Dir": inst.Dir,
"Supervisor": st.PID,
"Volumes": cfg.Volumes,
},
}
}
func envStrings(env []engine.EnvVar) []string {
out := []string{}
for _, e := range env {
out = append(out, e.Key+"="+e.Value)
}
return out
}
func labelsOrEmpty(l map[string]string) map[string]string {
if l == nil {
return map[string]string{}
}
return l
}
func portKey(p instance.PortMapping) string {
proto := p.Protocol
if proto == "" {
proto = "tcp"
}
return strconv.Itoa(p.ContainerPort) + "/" + proto
}
func exposedPorts(ports []instance.PortMapping) map[string]struct{} {
out := map[string]struct{}{}
for _, p := range ports {
out[portKey(p)] = struct{}{}
}
return out
}
func portBindings(ports []instance.PortMapping) map[string][]portBinding {
out := map[string][]portBinding{}
for _, p := range ports {
out[portKey(p)] = []portBinding{{HostIp: "0.0.0.0", HostPort: strconv.Itoa(p.HostPort)}}
}
return out
}
func bindStrings(mounts []engine.Mount) []string {
out := []string{}
for _, m := range mounts {
if m.Mode == engine.MountTmpfs {
continue
}
s := m.Source + ":" + m.Dest
if m.Mode == engine.MountRoBind {
s += ":ro"
}
out = append(out, s)
}
return out
}
func mountList(mounts []engine.Mount) []map[string]interface{} {
out := []map[string]interface{}{}
for _, m := range mounts {
kind := "bind"
if m.Mode == engine.MountTmpfs {
kind = "tmpfs"
}
out = append(out, map[string]interface{}{
"Type": kind,
"Source": m.Source,
"Destination": m.Dest,
"Mode": "",
"RW": m.Mode != engine.MountRoBind,
"Propagation": "rprivate",
"Name": filepath.Base(m.Dest),
})
}
return out
}
// containersList answers GET /containers/json, including the label
// filter a client uses to find what it left behind.
func (s *Server) containersList(w http.ResponseWriter, r *http.Request) {
all := boolParam(r, "all")
filters, err := parseFilters(r.URL.Query().Get("filters"))
if err != nil {
writeError(w, http.StatusBadRequest, "%v", err)
return
}
instances, err := s.Manager.List()
if err != nil {
writeError(w, http.StatusInternalServerError, "%v", err)
return
}
// docker's limit is "the n most recent"; List is newest first.
limit := intParam(r, "limit", 0)
out := []map[string]interface{}{}
for _, inst := range instances {
if !all && !inst.Running() {
continue
}
if !matches(inst, filters) {
continue
}
image := ""
if meta, err := s.Store.Meta(inst.Config.Image); err == nil {
image = imageID(meta)
}
if limit > 0 && len(out) >= limit {
break
}
out = append(out, map[string]interface{}{
"Id": inst.Config.ID,
"Names": []string{"/" + inst.Config.Name},
"Image": inst.Config.Image,
"ImageID": image,
"Command": strings.Join(inst.Config.Spec.Command, " "),
"Created": inst.Config.Created.Unix(),
"State": string(inst.State.Status),
"Status": statusLine(inst),
"Labels": labelsOrEmpty(inst.Config.Labels),
"Ports": portSummaries(inst.Config.Ports),
"HostConfig": map[string]string{"NetworkMode": "host"},
"NetworkSettings": map[string]interface{}{"Networks": map[string]interface{}{}},
"Mounts": mountList(inst.Config.Spec.Mounts),
})
}
writeJSON(w, http.StatusOK, out)
}
func portSummaries(ports []instance.PortMapping) []map[string]interface{} {
out := []map[string]interface{}{}
for _, p := range ports {
proto := p.Protocol
if proto == "" {
proto = "tcp"
}
out = append(out, map[string]interface{}{
"IP": "0.0.0.0", "PrivatePort": p.ContainerPort, "PublicPort": p.HostPort, "Type": proto,
})
}
return out
}
func statusLine(inst *instance.Instance) string {
switch inst.State.Status {
case instance.StatusRunning:
return "Up"
case instance.StatusExited:
return fmt.Sprintf("Exited (%d)", inst.State.ExitCode)
}
return "Created"
}
// filters is docker's ?filters={"label":["k=v"],"status":["running"]}.
type filters struct {
Label []string `json:"label"`
Status []string `json:"status"`
Name []string `json:"name"`
ID []string `json:"id"`
}
func parseFilters(raw string) (filters, error) {
var f filters
if raw == "" {
return f, nil
}
if err := json.Unmarshal([]byte(raw), &f); err != nil {
return f, fmt.Errorf("invalid filters %q: %v", raw, err)
}
return f, nil
}
func matches(inst *instance.Instance, f filters) bool {
want := map[string]string{}
for _, l := range f.Label {
k, v, _ := strings.Cut(l, "=")
want[k] = v
}
if !inst.HasLabels(want) {
return false
}
if len(f.Status) > 0 && !contains(f.Status, string(inst.State.Status)) {
return false
}
if len(f.Name) > 0 && !containsSubstring(f.Name, inst.Config.Name) {
return false
}
if len(f.ID) > 0 && !hasPrefixAny(f.ID, inst.Config.ID) {
return false
}
return true
}
func contains(list []string, want string) bool {
for _, v := range list {
if v == want {
return true
}
}
return false
}
func containsSubstring(list []string, want string) bool {
for _, v := range list {
if strings.Contains(want, v) {
return true
}
}
return false
}
func hasPrefixAny(list []string, id string) bool {
for _, v := range list {
if strings.HasPrefix(id, v) {
return true
}
}
return false
}