223 lines
6.0 KiB
Go
223 lines
6.0 KiB
Go
package api
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"werkdock/internal/registry"
|
|
"werkdock/internal/store"
|
|
)
|
|
|
|
// imageID is the id an API client sees: docker's "sha256:<64 hex>".
|
|
// werkdock's store has no config digest, so the id is a hash of the
|
|
// image metadata — the same one `images` shortens to twelve characters
|
|
// (store.ImageMeta.ID) — widened to docker's shape so that a client
|
|
// can hand it back to us.
|
|
func imageID(meta store.ImageMeta) string {
|
|
meta.Name = ""
|
|
data, err := json.Marshal(meta)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
sum := sha256.Sum256(data)
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// resolveImage finds an image by reference or by the id imageID hands
|
|
// out, and returns its short reference and metadata.
|
|
func (s *Server) resolveImage(name string) (string, store.ImageMeta, error) {
|
|
name = strings.TrimPrefix(name, "docker.io/library/")
|
|
if !strings.HasPrefix(name, "sha256:") {
|
|
meta, err := s.Store.Meta(name)
|
|
if err != nil {
|
|
return "", store.ImageMeta{}, err
|
|
}
|
|
ref, err := store.ParseReference(name)
|
|
if err != nil {
|
|
return "", store.ImageMeta{}, err
|
|
}
|
|
return ref.Short(), meta, nil
|
|
}
|
|
refs, err := s.Store.List()
|
|
if err != nil {
|
|
return "", store.ImageMeta{}, err
|
|
}
|
|
for _, ref := range refs {
|
|
meta, err := s.Store.Meta(ref)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if imageID(meta) == name {
|
|
return ref, meta, nil
|
|
}
|
|
}
|
|
return "", store.ImageMeta{}, fmt.Errorf("no such image: %s", name)
|
|
}
|
|
|
|
func (s *Server) imagesList(w http.ResponseWriter, r *http.Request) {
|
|
refs, err := s.Store.List()
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "%v", err)
|
|
return
|
|
}
|
|
out := []map[string]interface{}{}
|
|
for _, ref := range refs {
|
|
meta, err := s.Store.Meta(ref)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
size, _ := s.Store.Size(ref)
|
|
parsed, err := store.ParseReference(ref)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
out = append(out, map[string]interface{}{
|
|
"Id": imageID(meta),
|
|
"ParentId": "",
|
|
"RepoTags": []string{parsed.String()},
|
|
"RepoDigests": []string{},
|
|
"Created": meta.CreatedAt.Unix(),
|
|
"Size": size,
|
|
"VirtualSize": size,
|
|
"SharedSize": 0,
|
|
"Containers": -1,
|
|
"Labels": meta.Config.Labels,
|
|
})
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
// imageInspect answers GET /images/{name}/json. A 404 here is what
|
|
// makes a client pull, so a missing image must not be an error.
|
|
func (s *Server) imageInspect(w http.ResponseWriter, r *http.Request) {
|
|
name := strings.TrimPrefix(r.URL.Path, "/images/")
|
|
name = strings.TrimSuffix(name, "/json")
|
|
if name == "" || strings.HasSuffix(r.URL.Path, "/history") {
|
|
s.notImplemented(w, r)
|
|
return
|
|
}
|
|
ref, meta, err := s.resolveImage(name)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "no such image: %s", name)
|
|
return
|
|
}
|
|
parsed, _ := store.ParseReference(ref)
|
|
size, _ := s.Store.Size(ref)
|
|
exposed := map[string]struct{}{}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"Id": imageID(meta),
|
|
"RepoTags": []string{parsed.String()},
|
|
"RepoDigests": []string{},
|
|
"Parent": "",
|
|
"Created": meta.CreatedAt,
|
|
"Architecture": orDefault(meta.Architecture, "amd64"),
|
|
"Os": orDefault(meta.OS, "linux"),
|
|
"Size": size,
|
|
"VirtualSize": size,
|
|
"Config": map[string]interface{}{
|
|
"Image": parsed.String(),
|
|
"User": meta.Config.User,
|
|
"Env": meta.Config.Env,
|
|
"Cmd": meta.Config.Cmd,
|
|
"Entrypoint": meta.Config.Entrypoint,
|
|
"WorkingDir": meta.Config.WorkingDir,
|
|
"Labels": meta.Config.Labels,
|
|
"Volumes": meta.Config.Volumes,
|
|
"ExposedPorts": exposed,
|
|
"Tty": false,
|
|
},
|
|
"GraphDriver": map[string]interface{}{"Name": "flat", "Data": nil},
|
|
"RootFS": map[string]interface{}{"Type": "layers", "Layers": []string{}},
|
|
})
|
|
}
|
|
|
|
func orDefault(v, def string) string {
|
|
if v == "" {
|
|
return def
|
|
}
|
|
return v
|
|
}
|
|
|
|
// imagesCreate is docker's pull: the progress stream a client reads is
|
|
// crane's output, wrapped one JSON object per line as docker writes it.
|
|
func (s *Server) imagesCreate(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
from := q.Get("fromImage")
|
|
if from == "" {
|
|
writeError(w, http.StatusBadRequest, "images/create needs fromImage (werkdock has no build and no import over the API)")
|
|
return
|
|
}
|
|
if tag := q.Get("tag"); tag != "" && !strings.Contains(from, "@") {
|
|
from += ":" + tag
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
flusher, _ := w.(http.Flusher)
|
|
enc := json.NewEncoder(w)
|
|
status := func(format string, a ...interface{}) {
|
|
enc.Encode(map[string]interface{}{"status": fmt.Sprintf(format, a...)})
|
|
if flusher != nil {
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
status("Pulling from %s", from)
|
|
progress := &lineWriter{emit: func(line string) { status("%s", line) }}
|
|
loaded, err := registry.Pull(s.Store, from, q.Get("platform"), progress)
|
|
progress.flush()
|
|
if err != nil {
|
|
// The stream has begun, so the error travels in it, as docker's
|
|
// does; the status code was 200 before anyone knew.
|
|
s.logf("images/create %s: %v", from, err)
|
|
enc.Encode(map[string]interface{}{
|
|
"error": err.Error(),
|
|
"errorDetail": map[string]string{"message": err.Error()},
|
|
})
|
|
if flusher != nil {
|
|
flusher.Flush()
|
|
}
|
|
return
|
|
}
|
|
for _, name := range loaded {
|
|
status("Pulled: %s", name)
|
|
}
|
|
status("Status: Downloaded newer image for %s", from)
|
|
}
|
|
|
|
// lineWriter turns crane's byte stream into whole lines.
|
|
type lineWriter struct {
|
|
buf strings.Builder
|
|
emit func(string)
|
|
}
|
|
|
|
func (l *lineWriter) Write(p []byte) (int, error) {
|
|
l.buf.Write(p)
|
|
s := l.buf.String()
|
|
for {
|
|
i := strings.IndexAny(s, "\r\n")
|
|
if i < 0 {
|
|
break
|
|
}
|
|
if line := strings.TrimSpace(s[:i]); line != "" {
|
|
l.emit(line)
|
|
}
|
|
s = s[i+1:]
|
|
}
|
|
l.buf.Reset()
|
|
l.buf.WriteString(s)
|
|
return len(p), nil
|
|
}
|
|
|
|
func (l *lineWriter) flush() {
|
|
if line := strings.TrimSpace(l.buf.String()); line != "" {
|
|
l.emit(line)
|
|
}
|
|
l.buf.Reset()
|
|
}
|
|
|
|
var _ io.Writer = (*lineWriter)(nil)
|