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 }