317 lines
7.9 KiB
Go
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
|
|
}
|