// @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" "strconv" "strings" _ "github.com/anuvu/zot/docs" // nolint (golint) - as required by swaggo "github.com/anuvu/zot/errors" "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" const DistAPIVersion = "Docker-Distribution-API-Version" const DistContentDigestKey = "Docker-Content-Digest" const BlobUploadUUID = "Blob-Upload-UUID" type RouteHandler struct { c *Controller } func NewRouteHandler(c *Controller) *RouteHandler { rh := &RouteHandler{c: c} rh.SetupRoutes() return rh } func (rh *RouteHandler) SetupRoutes() { rh.c.Router.Use(BasicAuthHandler(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.CheckManifest).Methods("HEAD") g.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()), rh.GetManifest).Methods("GET") g.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()), rh.UpdateManifest).Methods("PUT") g.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()), rh.DeleteManifest).Methods("DELETE") g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", NameRegexp.String()), rh.CheckBlob).Methods("HEAD") g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", NameRegexp.String()), rh.GetBlob).Methods("GET") g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", NameRegexp.String()), rh.DeleteBlob).Methods("DELETE") g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/", NameRegexp.String()), rh.CreateBlobUpload).Methods("POST") g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{uuid}", NameRegexp.String()), rh.GetBlobUpload).Methods("GET") g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{uuid}", NameRegexp.String()), rh.PatchBlobUpload).Methods("PATCH") g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{uuid}", NameRegexp.String()), rh.UpdateBlobUpload).Methods("PUT") g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{uuid}", NameRegexp.String()), 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" // @Success 200 {object} api.ImageTags // @Failure 404 {string} string "not found" 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 } tags, err := rh.c.ImageStore.GetImageTags(name) if err != nil { WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) 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, NewError(MANIFEST_INVALID, map[string]string{"reference": reference})) return } _, digest, _, err := rh.c.ImageStore.GetImageManifest(name, reference) if err != nil { switch err { case errors.ErrManifestNotFound: WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})) default: WriteJSON(w, http.StatusInternalServerError, 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, 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, NewError(NAME_UNKNOWN, map[string]string{"name": name})) case errors.ErrRepoBadVersion: WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) case errors.ErrManifestNotFound: WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})) default: w.WriteHeader(http.StatusInternalServerError) } return } WriteData(w, http.StatusOK, mediaType, content) w.Header().Set(DistContentDigestKey, digest) } // 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, 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 { 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, NewError(NAME_UNKNOWN, map[string]string{"name": name})) case errors.ErrManifestNotFound: WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})) case errors.ErrBadManifest: WriteJSON(w, http.StatusBadRequest, NewError(MANIFEST_INVALID, map[string]string{"reference": reference})) case errors.ErrBlobNotFound: WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UNKNOWN, map[string]string{"blob": digest})) default: 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.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) case errors.ErrManifestNotFound: WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})) default: w.WriteHeader(http.StatusInternalServerError) } return } w.WriteHeader(http.StatusOK) } // 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, NewError(DIGEST_INVALID, map[string]string{"digest": digest})) case errors.ErrRepoNotFound: WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) case errors.ErrBlobNotFound: WriteJSON(w, http.StatusNotFound, NewError(BLOB_UNKNOWN, map[string]string{"digest": digest})) default: w.WriteHeader(http.StatusInternalServerError) } return } if !ok { WriteJSON(w, http.StatusNotFound, 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, NewError(DIGEST_INVALID, map[string]string{"digest": digest})) case errors.ErrRepoNotFound: WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) case errors.ErrBlobNotFound: WriteJSON(w, http.StatusNotFound, NewError(BLOB_UNKNOWN, map[string]string{"digest": digest})) default: 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) } // 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, NewError(DIGEST_INVALID, map[string]string{"digest": digest})) case errors.ErrRepoNotFound: WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) case errors.ErrBlobNotFound: WriteJSON(w, http.StatusNotFound, NewError(BLOB_UNKNOWN, map[string]string{"digest": digest})) default: 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/{uuid}" // @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 } u, err := rh.c.ImageStore.NewBlobUpload(name) if err != nil { switch err { case errors.ErrRepoNotFound: WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) default: 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 uuid // @Accept json // @Produce json // @Param name path string true "repository name" // @Param uuid path string true "upload uuid" // @Success 204 {string} string "no content" // @Header 202 {string} Location "/v2/{name}/blobs/uploads/{uuid}" // @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/{uuid} [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 } uuid, ok := vars["uuid"] if !ok || uuid == "" { w.WriteHeader(http.StatusNotFound) return } size, err := rh.c.ImageStore.GetBlobUpload(name, uuid) if err != nil { switch err { case errors.ErrBadUploadRange: WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"uuid": uuid})) case errors.ErrBadBlobDigest: WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"uuid": uuid})) case errors.ErrRepoNotFound: WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) case errors.ErrUploadNotFound: WriteJSON(w, http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid})) default: w.WriteHeader(http.StatusInternalServerError) } return } w.Header().Set("Location", path.Join(r.URL.String(), uuid)) w.Header().Set("Range", fmt.Sprintf("bytes=0-%d", size)) w.WriteHeader(http.StatusNoContent) } // PatchBlobUpload godoc // @Summary Resume image blob/layer upload // @Description Resume an image's blob/layer upload given an uuid // @Accept json // @Produce json // @Param name path string true "repository name" // @Param uuid path string true "upload uuid" // @Success 202 {string} string "accepted" // @Header 202 {string} Location "/v2/{name}/blobs/uploads/{uuid}" // @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/{uuid} [patch] func (rh *RouteHandler) PatchBlobUpload(w http.ResponseWriter, r *http.Request) { rh.c.Log.Info().Interface("headers", r.Header).Msg("request headers") vars := mux.Vars(r) name, ok := vars["name"] if !ok || name == "" { w.WriteHeader(http.StatusNotFound) return } uuid, ok := vars["uuid"] if !ok || uuid == "" { w.WriteHeader(http.StatusNotFound) return } var err error 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) != contentLength { w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) return } if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" { rh.c.Log.Warn().Str("actual", contentType).Str("expected", "application/octet-stream").Msg("invalid media type") w.WriteHeader(http.StatusUnsupportedMediaType) return } clen, err := rh.c.ImageStore.PutBlobChunk(name, uuid, from, to, r.Body) if err != nil { switch err { case errors.ErrBadUploadRange: WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"uuid": uuid})) case errors.ErrRepoNotFound: WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) case errors.ErrUploadNotFound: WriteJSON(w, http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid})) default: w.WriteHeader(http.StatusInternalServerError) } return } w.Header().Set("Location", path.Join(r.URL.String(), uuid)) w.Header().Set("Range", fmt.Sprintf("bytes=0-%d", clen)) w.Header().Set("Content-Length", "0") w.Header().Set(BlobUploadUUID, uuid) 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 uuid path string true "upload uuid" // @Param digest query string true "blob/layer digest" // @Success 201 {string} string "created" // @Header 202 {string} Location "/v2/{name}/blobs/{digest}" // @Header 200 {object} api.DistContentDigestKey // @Failure 404 {string} string "not found" // @Failure 500 {string} string "internal server error" // @Router /v2/{name}/blobs/uploads/{uuid} [put] func (rh *RouteHandler) UpdateBlobUpload(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) name, ok := vars["name"] if !ok || name == "" { w.WriteHeader(http.StatusNotFound) return } uuid, ok := vars["uuid"] if !ok || uuid == "" { w.WriteHeader(http.StatusNotFound) return } digests, ok := r.URL.Query()["digest"] if !ok || len(digests) != 1 { w.WriteHeader(http.StatusBadRequest) return } digest := digests[0] contentPresent := true contentLen, err := strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64) if err != nil || contentLen == 0 { 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 { if r.Header.Get("Content-Type") != "application/octet-stream" { w.WriteHeader(http.StatusUnsupportedMediaType) return } contentRange := r.Header.Get("Content-Range") if contentRange == "" { // monolithic upload from = 0 if contentLen == 0 { w.WriteHeader(http.StatusBadRequest) return } to = contentLen } else if from, to, err = getContentRange(r); err != nil { // finish chunked upload w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) return } _, err = rh.c.ImageStore.PutBlobChunk(name, uuid, from, to, r.Body) if err != nil { switch err { case errors.ErrBadUploadRange: WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"uuid": uuid})) case errors.ErrRepoNotFound: WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) case errors.ErrUploadNotFound: WriteJSON(w, http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid})) default: w.WriteHeader(http.StatusInternalServerError) } return } } // blob chunks already transferred, just finish if err := rh.c.ImageStore.FinishBlobUpload(name, uuid, r.Body, digest); err != nil { switch err { case errors.ErrBadBlobDigest: WriteJSON(w, http.StatusBadRequest, NewError(DIGEST_INVALID, map[string]string{"digest": digest})) case errors.ErrBadUploadRange: WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"uuid": uuid})) case errors.ErrRepoNotFound: WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) case errors.ErrUploadNotFound: WriteJSON(w, http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid})) default: 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 uuid path string true "upload uuid" // @Success 200 {string} string "ok" // @Failure 404 {string} string "not found" // @Failure 500 {string} string "internal server error" // @Router /v2/{name}/blobs/uploads/{uuid} [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 } uuid, ok := vars["uuid"] if !ok || uuid == "" { w.WriteHeader(http.StatusNotFound) return } if err := rh.c.ImageStore.DeleteBlobUpload(name, uuid); err != nil { switch err { case errors.ErrRepoNotFound: WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) case errors.ErrUploadNotFound: WriteJSON(w, http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid})) default: w.WriteHeader(http.StatusInternalServerError) } return } w.WriteHeader(http.StatusOK) } 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 { w.WriteHeader(http.StatusInternalServerError) } WriteData(w, status, "application/json; charset=utf-8", 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) { w.Header().Set("Content-Type", mediaType) w.Header().Set("Content-Length", strconv.FormatInt(length, 10)) const maxSize = 10 * 1024 * 1024 for { size, err := io.CopyN(w, reader, maxSize) if size == 0 { if err != io.EOF { w.WriteHeader(http.StatusInternalServerError) return } break } } w.WriteHeader(status) }