0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00
zot/pkg/api/routes.go
Ramkumar Chinchani 25f5a45296 dedupe: use hard links to dedupe blobs
As the number of repos and layers increases, the greater the probability
that layers are duplicated. We dedupe using hard links when content is
the same. This is intended to be purely a storage layer optimization.
Access control when available is orthogonal this optimization.

Add a durable cache to help speed up layer lookups.

Update README.

Add more unit tests.
2020-04-03 09:29:12 -07:00

1130 lines
34 KiB
Go

// @title Open Container Initiative Distribution Specification
// @version v0.1.0-dev
// @description APIs for Open Container Initiative Distribution Specification
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
package api
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"path"
"sort"
"strconv"
"strings"
_ "github.com/anuvu/zot/docs" // nolint (golint) - as required by swaggo
"github.com/anuvu/zot/errors"
"github.com/anuvu/zot/pkg/log"
"github.com/gorilla/mux"
jsoniter "github.com/json-iterator/go"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
httpSwagger "github.com/swaggo/http-swagger"
)
const (
RoutePrefix = "/v2"
DistAPIVersion = "Docker-Distribution-API-Version"
DistContentDigestKey = "Docker-Content-Digest"
BlobUploadUUID = "Blob-Upload-UUID"
DefaultMediaType = "application/json"
BinaryMediaType = "application/octet-stream"
)
type RouteHandler struct {
c *Controller
}
func NewRouteHandler(c *Controller) *RouteHandler {
rh := &RouteHandler{c: c}
rh.SetupRoutes()
return rh
}
// blobRLockWrapper calls the real handler with read-lock held
func (rh *RouteHandler) blobRLockWrapper(f func(w http.ResponseWriter,
r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
rh.c.ImageStore.RLock()
f(w, r)
rh.c.ImageStore.RUnlock()
}
}
// blobLockWrapper calls the real handler with write-lock held
func (rh *RouteHandler) blobLockWrapper(f func(w http.ResponseWriter,
r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
rh.c.ImageStore.Lock()
f(w, r)
rh.c.ImageStore.Unlock()
}
}
func (rh *RouteHandler) SetupRoutes() {
rh.c.Router.Use(AuthHandler(rh.c))
g := rh.c.Router.PathPrefix(RoutePrefix).Subrouter()
{
g.HandleFunc(fmt.Sprintf("/{name:%s}/tags/list", NameRegexp.String()),
rh.ListTags).Methods("GET")
g.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()),
rh.blobRLockWrapper(rh.CheckManifest)).Methods("HEAD")
g.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()),
rh.blobRLockWrapper(rh.GetManifest)).Methods("GET")
g.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()),
rh.blobLockWrapper(rh.UpdateManifest)).Methods("PUT")
g.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()),
rh.blobLockWrapper(rh.DeleteManifest)).Methods("DELETE")
g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", NameRegexp.String()),
rh.blobRLockWrapper(rh.CheckBlob)).Methods("HEAD")
g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", NameRegexp.String()),
rh.blobRLockWrapper(rh.GetBlob)).Methods("GET")
g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", NameRegexp.String()),
rh.blobLockWrapper(rh.DeleteBlob)).Methods("DELETE")
g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/", NameRegexp.String()),
rh.blobLockWrapper(rh.CreateBlobUpload)).Methods("POST")
g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", NameRegexp.String()),
rh.blobRLockWrapper(rh.GetBlobUpload)).Methods("GET")
g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", NameRegexp.String()),
rh.blobLockWrapper(rh.PatchBlobUpload)).Methods("PATCH")
g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", NameRegexp.String()),
rh.blobLockWrapper(rh.UpdateBlobUpload)).Methods("PUT")
g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", NameRegexp.String()),
rh.blobLockWrapper(rh.DeleteBlobUpload)).Methods("DELETE")
g.HandleFunc("/_catalog",
rh.ListRepositories).Methods("GET")
g.HandleFunc("/",
rh.CheckVersionSupport).Methods("GET")
}
// swagger docs "/swagger/v2/index.html"
rh.c.Router.PathPrefix("/swagger/v2/").Methods("GET").Handler(httpSwagger.WrapHandler)
}
// Method handlers
// CheckVersionSupport godoc
// @Summary Check API support
// @Description Check if this API version is supported
// @Router /v2/ [get]
// @Accept json
// @Produce json
// @Success 200 {string} string "ok"
func (rh *RouteHandler) CheckVersionSupport(w http.ResponseWriter, r *http.Request) {
w.Header().Set(DistAPIVersion, "registry/2.0")
WriteData(w, http.StatusOK, "application/json", []byte{})
}
type ImageTags struct {
Name string `json:"name"`
Tags []string `json:"tags"`
}
// ListTags godoc
// @Summary List image tags
// @Description List all image tags in a repository
// @Router /v2/{name}/tags/list [get]
// @Accept json
// @Produce json
// @Param name path string true "test"
// @Param n query integer true "limit entries for pagination"
// @Param last query string true "last tag value for pagination"
// @Success 200 {object} api.ImageTags
// @Failure 404 {string} string "not found"
// @Failure 400 {string} string "bad request"
func (rh *RouteHandler) ListTags(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name, ok := vars["name"]
if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return
}
paginate := false
n := -1
var err error
nQuery, ok := r.URL.Query()["n"]
if ok {
if len(nQuery) != 1 {
w.WriteHeader(http.StatusBadRequest)
return
}
var n1 int64
if n1, err = strconv.ParseInt(nQuery[0], 10, 0); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
n = int(n1)
paginate = true
}
last := ""
lastQuery, ok := r.URL.Query()["last"]
if ok {
if len(lastQuery) != 1 {
w.WriteHeader(http.StatusBadRequest)
return
}
last = lastQuery[0]
}
tags, err := rh.c.ImageStore.GetImageTags(name)
if err != nil {
WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name})))
return
}
if paginate && (n < len(tags)) {
sort.Strings(tags)
pTags := ImageTags{Name: name}
if last == "" {
// first
pTags.Tags = tags[:n]
} else {
// next
i := -1
tag := ""
found := false
for i, tag = range tags {
if tag == last {
found = true
break
}
}
if !found {
w.WriteHeader(http.StatusNotFound)
return
}
if n >= len(tags)-i {
pTags.Tags = tags[i+1:]
WriteJSON(w, http.StatusOK, pTags)
return
}
pTags.Tags = tags[i+1 : i+1+n]
}
last = pTags.Tags[len(pTags.Tags)-1]
w.Header().Set("Link", fmt.Sprintf("/v2/%s/tags/list?n=%d&last=%s; rel=\"next\"", name, n, last))
WriteJSON(w, http.StatusOK, pTags)
return
}
WriteJSON(w, http.StatusOK, ImageTags{Name: name, Tags: tags})
}
// CheckManifest godoc
// @Summary Check image manifest
// @Description Check an image's manifest given a reference or a digest
// @Router /v2/{name}/manifests/{reference} [head]
// @Accept json
// @Produce json
// @Param name path string true "repository name"
// @Param reference path string true "image reference or digest"
// @Success 200 {string} string "ok"
// @Header 200 {object} api.DistContentDigestKey
// @Failure 404 {string} string "not found"
// @Failure 500 {string} string "internal server error"
func (rh *RouteHandler) CheckManifest(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name, ok := vars["name"]
if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return
}
reference, ok := vars["reference"]
if !ok || reference == "" {
WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference})))
return
}
_, digest, _, err := rh.c.ImageStore.GetImageManifest(name, reference)
if err != nil {
switch err {
case errors.ErrRepoNotFound:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"reference": reference})))
case errors.ErrManifestNotFound:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})))
default:
rh.c.Log.Error().Err(err).Msg("unexpected error")
WriteJSON(w, http.StatusInternalServerError,
NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference})))
}
return
}
w.Header().Set(DistContentDigestKey, digest)
w.Header().Set("Content-Length", "0")
w.WriteHeader(http.StatusOK)
}
// NOTE: https://github.com/swaggo/swag/issues/387
type ImageManifest struct {
ispec.Manifest
}
// GetManifest godoc
// @Summary Get image manifest
// @Description Get an image's manifest given a reference or a digest
// @Accept json
// @Produce application/vnd.oci.image.manifest.v1+json
// @Param name path string true "repository name"
// @Param reference path string true "image reference or digest"
// @Success 200 {object} api.ImageManifest
// @Header 200 {object} api.DistContentDigestKey
// @Failure 404 {string} string "not found"
// @Failure 500 {string} string "internal server error"
// @Router /v2/{name}/manifests/{reference} [get]
func (rh *RouteHandler) GetManifest(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name, ok := vars["name"]
if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return
}
reference, ok := vars["reference"]
if !ok || reference == "" {
WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})))
return
}
content, digest, mediaType, err := rh.c.ImageStore.GetImageManifest(name, reference)
if err != nil {
switch err {
case errors.ErrRepoNotFound:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name})))
case errors.ErrRepoBadVersion:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name})))
case errors.ErrManifestNotFound:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})))
default:
rh.c.Log.Error().Err(err).Msg("unexpected error")
w.WriteHeader(http.StatusInternalServerError)
}
return
}
w.Header().Set(DistContentDigestKey, digest)
WriteData(w, http.StatusOK, mediaType, content)
}
// UpdateManifest godoc
// @Summary Update image manifest
// @Description Update an image's manifest given a reference or a digest
// @Accept json
// @Produce json
// @Param name path string true "repository name"
// @Param reference path string true "image reference or digest"
// @Header 201 {object} api.DistContentDigestKey
// @Success 201 {string} string "created"
// @Failure 400 {string} string "bad request"
// @Failure 404 {string} string "not found"
// @Failure 500 {string} string "internal server error"
// @Router /v2/{name}/manifests/{reference} [put]
func (rh *RouteHandler) UpdateManifest(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name, ok := vars["name"]
if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return
}
reference, ok := vars["reference"]
if !ok || reference == "" {
WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference})))
return
}
mediaType := r.Header.Get("Content-Type")
if mediaType != ispec.MediaTypeImageManifest {
w.WriteHeader(http.StatusUnsupportedMediaType)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
rh.c.Log.Error().Err(err).Msg("unexpected error")
w.WriteHeader(http.StatusInternalServerError)
return
}
digest, err := rh.c.ImageStore.PutImageManifest(name, reference, mediaType, body)
if err != nil {
switch err {
case errors.ErrRepoNotFound:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name})))
case errors.ErrManifestNotFound:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})))
case errors.ErrBadManifest:
WriteJSON(w, http.StatusBadRequest,
NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference})))
case errors.ErrBlobNotFound:
WriteJSON(w, http.StatusBadRequest,
NewErrorList(NewError(BLOB_UNKNOWN, map[string]string{"blob": digest})))
default:
rh.c.Log.Error().Err(err).Msg("unexpected error")
w.WriteHeader(http.StatusInternalServerError)
}
return
}
w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, digest))
w.Header().Set(DistContentDigestKey, digest)
w.WriteHeader(http.StatusCreated)
}
// DeleteManifest godoc
// @Summary Delete image manifest
// @Description Delete an image's manifest given a reference or a digest
// @Accept json
// @Produce json
// @Param name path string true "repository name"
// @Param reference path string true "image reference or digest"
// @Success 200 {string} string "ok"
// @Router /v2/{name}/manifests/{reference} [delete]
func (rh *RouteHandler) DeleteManifest(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name, ok := vars["name"]
if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return
}
reference, ok := vars["reference"]
if !ok || reference == "" {
w.WriteHeader(http.StatusNotFound)
return
}
err := rh.c.ImageStore.DeleteImageManifest(name, reference)
if err != nil {
switch err {
case errors.ErrRepoNotFound:
WriteJSON(w, http.StatusBadRequest,
NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name})))
case errors.ErrManifestNotFound:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})))
case errors.ErrBadManifest:
WriteJSON(w, http.StatusBadRequest,
NewErrorList(NewError(UNSUPPORTED, map[string]string{"reference": reference})))
default:
rh.c.Log.Error().Err(err).Msg("unexpected error")
w.WriteHeader(http.StatusInternalServerError)
}
return
}
w.WriteHeader(http.StatusAccepted)
}
// CheckBlob godoc
// @Summary Check image blob/layer
// @Description Check an image's blob/layer given a digest
// @Accept json
// @Produce json
// @Param name path string true "repository name"
// @Param digest path string true "blob/layer digest"
// @Success 200 {object} api.ImageManifest
// @Header 200 {object} api.DistContentDigestKey
// @Router /v2/{name}/blobs/{digest} [head]
func (rh *RouteHandler) CheckBlob(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name, ok := vars["name"]
if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return
}
digest, ok := vars["digest"]
if !ok || digest == "" {
w.WriteHeader(http.StatusNotFound)
return
}
mediaType := r.Header.Get("Accept")
ok, blen, err := rh.c.ImageStore.CheckBlob(name, digest, mediaType)
if err != nil {
switch err {
case errors.ErrBadBlobDigest:
WriteJSON(w, http.StatusBadRequest, NewErrorList(NewError(DIGEST_INVALID, map[string]string{"digest": digest})))
case errors.ErrRepoNotFound:
WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name})))
case errors.ErrBlobNotFound:
WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(BLOB_UNKNOWN, map[string]string{"digest": digest})))
default:
rh.c.Log.Error().Err(err).Msg("unexpected error")
w.WriteHeader(http.StatusInternalServerError)
}
return
}
if !ok {
WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(BLOB_UNKNOWN, map[string]string{"digest": digest})))
return
}
w.Header().Set("Content-Length", fmt.Sprintf("%d", blen))
w.Header().Set(DistContentDigestKey, digest)
w.WriteHeader(http.StatusOK)
}
// GetBlob godoc
// @Summary Get image blob/layer
// @Description Get an image's blob/layer given a digest
// @Accept json
// @Produce application/vnd.oci.image.layer.v1.tar+gzip
// @Param name path string true "repository name"
// @Param digest path string true "blob/layer digest"
// @Header 200 {object} api.DistContentDigestKey
// @Success 200 {object} api.ImageManifest
// @Router /v2/{name}/blobs/{digest} [get]
func (rh *RouteHandler) GetBlob(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name, ok := vars["name"]
if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return
}
digest, ok := vars["digest"]
if !ok || digest == "" {
w.WriteHeader(http.StatusNotFound)
return
}
mediaType := r.Header.Get("Accept")
br, blen, err := rh.c.ImageStore.GetBlob(name, digest, mediaType)
if err != nil {
switch err {
case errors.ErrBadBlobDigest:
WriteJSON(w, http.StatusBadRequest, NewErrorList(NewError(DIGEST_INVALID, map[string]string{"digest": digest})))
case errors.ErrRepoNotFound:
WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name})))
case errors.ErrBlobNotFound:
WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(BLOB_UNKNOWN, map[string]string{"digest": digest})))
default:
rh.c.Log.Error().Err(err).Msg("unexpected error")
w.WriteHeader(http.StatusInternalServerError)
}
return
}
w.Header().Set("Content-Length", fmt.Sprintf("%d", blen))
w.Header().Set(DistContentDigestKey, digest)
// return the blob data
WriteDataFromReader(w, http.StatusOK, blen, mediaType, br, rh.c.Log)
}
// DeleteBlob godoc
// @Summary Delete image blob/layer
// @Description Delete an image's blob/layer given a digest
// @Accept json
// @Produce json
// @Param name path string true "repository name"
// @Param digest path string true "blob/layer digest"
// @Success 202 {string} string "accepted"
// @Router /v2/{name}/blobs/{digest} [delete]
func (rh *RouteHandler) DeleteBlob(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name, ok := vars["name"]
if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return
}
digest, ok := vars["digest"]
if !ok || digest == "" {
w.WriteHeader(http.StatusNotFound)
return
}
err := rh.c.ImageStore.DeleteBlob(name, digest)
if err != nil {
switch err {
case errors.ErrBadBlobDigest:
WriteJSON(w, http.StatusBadRequest, NewErrorList(NewError(DIGEST_INVALID, map[string]string{"digest": digest})))
case errors.ErrRepoNotFound:
WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name})))
case errors.ErrBlobNotFound:
WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(BLOB_UNKNOWN, map[string]string{"digest": digest})))
default:
rh.c.Log.Error().Err(err).Msg("unexpected error")
w.WriteHeader(http.StatusInternalServerError)
}
return
}
w.WriteHeader(http.StatusAccepted)
}
// CreateBlobUpload godoc
// @Summary Create image blob/layer upload
// @Description Create a new image blob/layer upload
// @Accept json
// @Produce json
// @Param name path string true "repository name"
// @Success 202 {string} string "accepted"
// @Header 202 {string} Location "/v2/{name}/blobs/uploads/{session_id}"
// @Header 202 {string} Range "bytes=0-0"
// @Failure 404 {string} string "not found"
// @Failure 500 {string} string "internal server error"
// @Router /v2/{name}/blobs/uploads [post]
func (rh *RouteHandler) CreateBlobUpload(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name, ok := vars["name"]
if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return
}
// blob mounts not allowed since we don't have access control yet, and this
// may be a uncommon use case, but remain compliant
if _, ok := r.URL.Query()["mount"]; ok {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if _, ok := r.URL.Query()["from"]; ok {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// a full blob upload if "digest" is present
digests, ok := r.URL.Query()["digest"]
if ok {
if len(digests) != 1 {
w.WriteHeader(http.StatusBadRequest)
return
}
digest := digests[0]
if contentType := r.Header.Get("Content-Type"); contentType != BinaryMediaType {
rh.c.Log.Warn().Str("actual", contentType).Str("expected", BinaryMediaType).Msg("invalid media type")
w.WriteHeader(http.StatusUnsupportedMediaType)
return
}
rh.c.Log.Info().Int64("r.ContentLength", r.ContentLength).Msg("DEBUG")
var contentLength int64
var err error
if contentLength, err = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64); err != nil || contentLength <= 0 {
rh.c.Log.Warn().Str("actual", r.Header.Get("Content-Length")).Msg("invalid content length")
WriteJSON(w, http.StatusBadRequest,
NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"digest": digest})))
return
}
sessionID, size, err := rh.c.ImageStore.FullBlobUpload(name, r.Body, digest)
if err != nil {
rh.c.Log.Error().Err(err).Int64("actual", size).Int64("expected", contentLength).Msg("failed full upload")
w.WriteHeader(http.StatusInternalServerError)
return
}
if size != contentLength {
rh.c.Log.Warn().Int64("actual", size).Int64("expected", contentLength).Msg("invalid content length")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Location", fmt.Sprintf("/v2/%s/blobs/%s", name, digest))
w.Header().Set(BlobUploadUUID, sessionID)
w.WriteHeader(http.StatusCreated)
return
}
u, err := rh.c.ImageStore.NewBlobUpload(name)
if err != nil {
switch err {
case errors.ErrRepoNotFound:
WriteJSON(w, http.StatusNotFound, NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name})))
default:
rh.c.Log.Error().Err(err).Msg("unexpected error")
w.WriteHeader(http.StatusInternalServerError)
}
return
}
w.Header().Set("Location", path.Join(r.URL.String(), u))
w.Header().Set("Range", "bytes=0-0")
w.WriteHeader(http.StatusAccepted)
}
// GetBlobUpload godoc
// @Summary Get image blob/layer upload
// @Description Get an image's blob/layer upload given a session_id
// @Accept json
// @Produce json
// @Param name path string true "repository name"
// @Param session_id path string true "upload session_id"
// @Success 204 {string} string "no content"
// @Header 202 {string} Location "/v2/{name}/blobs/uploads/{session_id}"
// @Header 202 {string} Range "bytes=0-128"
// @Failure 404 {string} string "not found"
// @Failure 500 {string} string "internal server error"
// @Router /v2/{name}/blobs/uploads/{session_id} [get]
func (rh *RouteHandler) GetBlobUpload(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name, ok := vars["name"]
if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return
}
sessionID, ok := vars["session_id"]
if !ok || sessionID == "" {
w.WriteHeader(http.StatusNotFound)
return
}
size, err := rh.c.ImageStore.GetBlobUpload(name, sessionID)
if err != nil {
switch err {
case errors.ErrBadUploadRange:
WriteJSON(w, http.StatusBadRequest,
NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID})))
case errors.ErrBadBlobDigest:
WriteJSON(w, http.StatusBadRequest,
NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID})))
case errors.ErrRepoNotFound:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name})))
case errors.ErrUploadNotFound:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID})))
default:
rh.c.Log.Error().Err(err).Msg("unexpected error")
w.WriteHeader(http.StatusInternalServerError)
}
return
}
w.Header().Set("Location", path.Join(r.URL.String(), sessionID))
w.Header().Set("Range", fmt.Sprintf("bytes=0-%d", size-1))
w.WriteHeader(http.StatusNoContent)
}
// PatchBlobUpload godoc
// @Summary Resume image blob/layer upload
// @Description Resume an image's blob/layer upload given an session_id
// @Accept json
// @Produce json
// @Param name path string true "repository name"
// @Param session_id path string true "upload session_id"
// @Success 202 {string} string "accepted"
// @Header 202 {string} Location "/v2/{name}/blobs/uploads/{session_id}"
// @Header 202 {string} Range "bytes=0-128"
// @Header 200 {object} api.BlobUploadUUID
// @Failure 400 {string} string "bad request"
// @Failure 404 {string} string "not found"
// @Failure 416 {string} string "range not satisfiable"
// @Failure 500 {string} string "internal server error"
// @Router /v2/{name}/blobs/uploads/{session_id} [patch]
func (rh *RouteHandler) PatchBlobUpload(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name, ok := vars["name"]
if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return
}
sessionID, ok := vars["session_id"]
if !ok || sessionID == "" {
w.WriteHeader(http.StatusNotFound)
return
}
if contentType := r.Header.Get("Content-Type"); contentType != BinaryMediaType {
rh.c.Log.Warn().Str("actual", contentType).Str("expected", BinaryMediaType).Msg("invalid media type")
w.WriteHeader(http.StatusUnsupportedMediaType)
return
}
var err error
var clen int64
if r.Header.Get("Content-Length") == "" || r.Header.Get("Content-Range") == "" {
// streamed blob upload
clen, err = rh.c.ImageStore.PutBlobChunkStreamed(name, sessionID, r.Body)
} else {
// chunked blob upload
var contentLength int64
if contentLength, err = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64); err != nil {
rh.c.Log.Warn().Str("actual", r.Header.Get("Content-Length")).Msg("invalid content length")
w.WriteHeader(http.StatusBadRequest)
return
}
contentRange := r.Header.Get("Content-Range")
if contentRange == "" {
rh.c.Log.Warn().Str("actual", r.Header.Get("Content-Range")).Msg("invalid content range")
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
return
}
var from, to int64
if from, to, err = getContentRange(r); err != nil || (to-from)+1 != contentLength {
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
return
}
clen, err = rh.c.ImageStore.PutBlobChunk(name, sessionID, from, to, r.Body)
}
if err != nil {
switch err {
case errors.ErrBadUploadRange:
WriteJSON(w, http.StatusRequestedRangeNotSatisfiable,
NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID})))
case errors.ErrRepoNotFound:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name})))
case errors.ErrUploadNotFound:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID})))
default:
rh.c.Log.Error().Err(err).Msg("unexpected error")
w.WriteHeader(http.StatusInternalServerError)
}
return
}
w.Header().Set("Location", r.URL.String())
w.Header().Set("Range", fmt.Sprintf("bytes=0-%d", clen-1))
w.Header().Set("Content-Length", "0")
w.Header().Set(BlobUploadUUID, sessionID)
w.WriteHeader(http.StatusAccepted)
}
// UpdateBlobUpload godoc
// @Summary Update image blob/layer upload
// @Description Update and finish an image's blob/layer upload given a digest
// @Accept json
// @Produce json
// @Param name path string true "repository name"
// @Param session_id path string true "upload session_id"
// @Param digest query string true "blob/layer digest"
// @Success 201 {string} string "created"
// @Header 202 {string} Location "/v2/{name}/blobs/uploads/{digest}"
// @Header 200 {object} api.DistContentDigestKey
// @Failure 404 {string} string "not found"
// @Failure 500 {string} string "internal server error"
// @Router /v2/{name}/blobs/uploads/{session_id} [put]
func (rh *RouteHandler) UpdateBlobUpload(w http.ResponseWriter, r *http.Request) {
rh.c.Log.Info().Interface("headers", r.Header).Msg("HEADERS")
vars := mux.Vars(r)
name, ok := vars["name"]
if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return
}
sessionID, ok := vars["session_id"]
if !ok || sessionID == "" {
w.WriteHeader(http.StatusNotFound)
return
}
digests, ok := r.URL.Query()["digest"]
if !ok || len(digests) != 1 {
w.WriteHeader(http.StatusBadRequest)
return
}
digest := digests[0]
rh.c.Log.Info().Int64("r.ContentLength", r.ContentLength).Msg("DEBUG")
contentPresent := true
contentLen, err := strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64)
if err != nil {
contentPresent = false
}
contentRangePresent := true
if r.Header.Get("Content-Range") == "" {
contentRangePresent = false
}
// we expect at least one of "Content-Length" or "Content-Range" to be
// present
if !contentPresent && !contentRangePresent {
w.WriteHeader(http.StatusBadRequest)
return
}
var from, to int64
if contentPresent {
contentRange := r.Header.Get("Content-Range")
if contentRange == "" { // monolithic upload
from = 0
if contentLen == 0 {
goto finish // FIXME:
}
to = contentLen
} else if from, to, err = getContentRange(r); err != nil { // finish chunked upload
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
return
}
if r.Header.Get("Content-Type") != BinaryMediaType {
w.WriteHeader(http.StatusUnsupportedMediaType)
return
}
_, err = rh.c.ImageStore.PutBlobChunk(name, sessionID, from, to, r.Body)
if err != nil {
switch err {
case errors.ErrBadUploadRange:
WriteJSON(w, http.StatusBadRequest,
NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID})))
case errors.ErrRepoNotFound:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name})))
case errors.ErrUploadNotFound:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID})))
default:
rh.c.Log.Error().Err(err).Msg("unexpected error")
w.WriteHeader(http.StatusInternalServerError)
}
return
}
}
finish:
if r.Header.Get("Content-Type") != BinaryMediaType {
w.WriteHeader(http.StatusUnsupportedMediaType)
return
}
// blob chunks already transferred, just finish
if err := rh.c.ImageStore.FinishBlobUpload(name, sessionID, r.Body, digest); err != nil {
switch err {
case errors.ErrBadBlobDigest:
WriteJSON(w, http.StatusBadRequest,
NewErrorList(NewError(DIGEST_INVALID, map[string]string{"digest": digest})))
case errors.ErrBadUploadRange:
WriteJSON(w, http.StatusBadRequest,
NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID})))
case errors.ErrRepoNotFound:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name})))
case errors.ErrUploadNotFound:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID})))
default:
rh.c.Log.Error().Err(err).Msg("unexpected error")
w.WriteHeader(http.StatusInternalServerError)
}
return
}
w.Header().Set("Location", fmt.Sprintf("/v2/%s/blobs/%s", name, digest))
w.Header().Set("Content-Length", "0")
w.Header().Set(DistContentDigestKey, digest)
w.WriteHeader(http.StatusCreated)
}
// DeleteBlobUpload godoc
// @Summary Delete image blob/layer
// @Description Delete an image's blob/layer given a digest
// @Accept json
// @Produce json
// @Param name path string true "repository name"
// @Param session_id path string true "upload session_id"
// @Success 200 {string} string "ok"
// @Failure 404 {string} string "not found"
// @Failure 500 {string} string "internal server error"
// @Router /v2/{name}/blobs/uploads/{session_id} [delete]
func (rh *RouteHandler) DeleteBlobUpload(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name, ok := vars["name"]
if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return
}
sessionID, ok := vars["session_id"]
if !ok || sessionID == "" {
w.WriteHeader(http.StatusNotFound)
return
}
if err := rh.c.ImageStore.DeleteBlobUpload(name, sessionID); err != nil {
switch err {
case errors.ErrRepoNotFound:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name})))
case errors.ErrUploadNotFound:
WriteJSON(w, http.StatusNotFound,
NewErrorList(NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID})))
default:
rh.c.Log.Error().Err(err).Msg("unexpected error")
w.WriteHeader(http.StatusInternalServerError)
}
return
}
w.WriteHeader(http.StatusNoContent)
}
type RepositoryList struct {
Repositories []string `json:"repositories"`
}
// ListRepositories godoc
// @Summary List image repositories
// @Description List all image repositories
// @Accept json
// @Produce json
// @Success 200 {object} api.RepositoryList
// @Failure 500 {string} string "internal server error"
// @Router /v2/_catalog [get]
func (rh *RouteHandler) ListRepositories(w http.ResponseWriter, r *http.Request) {
repos, err := rh.c.ImageStore.GetRepositories()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
is := RepositoryList{Repositories: repos}
WriteJSON(w, http.StatusOK, is)
}
// helper routines
func getContentRange(r *http.Request) (int64 /* from */, int64 /* to */, error) {
contentRange := r.Header.Get("Content-Range")
tokens := strings.Split(contentRange, "-")
from, err := strconv.ParseInt(tokens[0], 10, 64)
if err != nil {
return -1, -1, errors.ErrBadUploadRange
}
to, err := strconv.ParseInt(tokens[1], 10, 64)
if err != nil {
return -1, -1, errors.ErrBadUploadRange
}
if from > to {
return -1, -1, errors.ErrBadUploadRange
}
return from, to, nil
}
func WriteJSON(w http.ResponseWriter, status int, data interface{}) {
var json = jsoniter.ConfigCompatibleWithStandardLibrary
body, err := json.Marshal(data)
if err != nil {
panic(err)
}
WriteData(w, status, DefaultMediaType, body)
}
func WriteData(w http.ResponseWriter, status int, mediaType string, data []byte) {
w.Header().Set("Content-Type", mediaType)
w.WriteHeader(status)
_, _ = w.Write(data)
}
func WriteDataFromReader(w http.ResponseWriter, status int, length int64, mediaType string,
reader io.Reader, logger log.Logger) {
w.Header().Set("Content-Type", mediaType)
w.Header().Set("Content-Length", strconv.FormatInt(length, 10))
w.WriteHeader(status)
const maxSize = 10 * 1024 * 1024
for {
_, err := io.CopyN(w, reader, maxSize)
if err == io.EOF {
break
} else if err != nil {
// other kinds of intermittent errors can occur, e.g, io.ErrShortWrite
logger.Error().Err(err).Msg("copying data into http response")
return
}
}
}