func/vendor/github.com/google/go-containerregistry/pkg/registry/manifest.go

335 lines
8.0 KiB
Go

// Copyright 2018 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"sort"
"strconv"
"strings"
"sync"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/types"
)
type catalog struct {
Repos []string `json:"repositories"`
}
type listTags struct {
Name string `json:"name"`
Tags []string `json:"tags"`
}
type manifest struct {
contentType string
blob []byte
}
type manifests struct {
// maps repo -> manifest tag/digest -> manifest
manifests map[string]map[string]manifest
lock sync.Mutex
log *log.Logger
}
func isManifest(req *http.Request) bool {
elems := strings.Split(req.URL.Path, "/")
elems = elems[1:]
if len(elems) < 4 {
return false
}
return elems[len(elems)-2] == "manifests"
}
func isTags(req *http.Request) bool {
elems := strings.Split(req.URL.Path, "/")
elems = elems[1:]
if len(elems) < 4 {
return false
}
return elems[len(elems)-2] == "tags"
}
func isCatalog(req *http.Request) bool {
elems := strings.Split(req.URL.Path, "/")
elems = elems[1:]
if len(elems) < 2 {
return false
}
return elems[len(elems)-1] == "_catalog"
}
// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-an-image-manifest
// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-an-image
func (m *manifests) handle(resp http.ResponseWriter, req *http.Request) *regError {
elem := strings.Split(req.URL.Path, "/")
elem = elem[1:]
target := elem[len(elem)-1]
repo := strings.Join(elem[1:len(elem)-2], "/")
if req.Method == "GET" {
m.lock.Lock()
defer m.lock.Unlock()
c, ok := m.manifests[repo]
if !ok {
return &regError{
Status: http.StatusNotFound,
Code: "NAME_UNKNOWN",
Message: "Unknown name",
}
}
m, ok := c[target]
if !ok {
return &regError{
Status: http.StatusNotFound,
Code: "MANIFEST_UNKNOWN",
Message: "Unknown manifest",
}
}
rd := sha256.Sum256(m.blob)
d := "sha256:" + hex.EncodeToString(rd[:])
resp.Header().Set("Docker-Content-Digest", d)
resp.Header().Set("Content-Type", m.contentType)
resp.Header().Set("Content-Length", fmt.Sprint(len(m.blob)))
resp.WriteHeader(http.StatusOK)
io.Copy(resp, bytes.NewReader(m.blob))
return nil
}
if req.Method == "HEAD" {
m.lock.Lock()
defer m.lock.Unlock()
if _, ok := m.manifests[repo]; !ok {
return &regError{
Status: http.StatusNotFound,
Code: "NAME_UNKNOWN",
Message: "Unknown name",
}
}
m, ok := m.manifests[repo][target]
if !ok {
return &regError{
Status: http.StatusNotFound,
Code: "MANIFEST_UNKNOWN",
Message: "Unknown manifest",
}
}
rd := sha256.Sum256(m.blob)
d := "sha256:" + hex.EncodeToString(rd[:])
resp.Header().Set("Docker-Content-Digest", d)
resp.Header().Set("Content-Type", m.contentType)
resp.Header().Set("Content-Length", fmt.Sprint(len(m.blob)))
resp.WriteHeader(http.StatusOK)
return nil
}
if req.Method == "PUT" {
m.lock.Lock()
defer m.lock.Unlock()
if _, ok := m.manifests[repo]; !ok {
m.manifests[repo] = map[string]manifest{}
}
b := &bytes.Buffer{}
io.Copy(b, req.Body)
rd := sha256.Sum256(b.Bytes())
digest := "sha256:" + hex.EncodeToString(rd[:])
mf := manifest{
blob: b.Bytes(),
contentType: req.Header.Get("Content-Type"),
}
// If the manifest is a manifest list, check that the manifest
// list's constituent manifests are already uploaded.
// This isn't strictly required by the registry API, but some
// registries require this.
if types.MediaType(mf.contentType).IsIndex() {
im, err := v1.ParseIndexManifest(b)
if err != nil {
return &regError{
Status: http.StatusBadRequest,
Code: "MANIFEST_INVALID",
Message: err.Error(),
}
}
for _, desc := range im.Manifests {
if !desc.MediaType.IsDistributable() {
continue
}
if desc.MediaType.IsIndex() || desc.MediaType.IsImage() {
if _, found := m.manifests[repo][desc.Digest.String()]; !found {
return &regError{
Status: http.StatusNotFound,
Code: "MANIFEST_UNKNOWN",
Message: fmt.Sprintf("Sub-manifest %q not found", desc.Digest),
}
}
} else {
// TODO: Probably want to do an existence check for blobs.
m.log.Printf("TODO: Check blobs for %q", desc.Digest)
}
}
}
// Allow future references by target (tag) and immutable digest.
// See https://docs.docker.com/engine/reference/commandline/pull/#pull-an-image-by-digest-immutable-identifier.
m.manifests[repo][target] = mf
m.manifests[repo][digest] = mf
resp.Header().Set("Docker-Content-Digest", digest)
resp.WriteHeader(http.StatusCreated)
return nil
}
if req.Method == "DELETE" {
m.lock.Lock()
defer m.lock.Unlock()
if _, ok := m.manifests[repo]; !ok {
return &regError{
Status: http.StatusNotFound,
Code: "NAME_UNKNOWN",
Message: "Unknown name",
}
}
_, ok := m.manifests[repo][target]
if !ok {
return &regError{
Status: http.StatusNotFound,
Code: "MANIFEST_UNKNOWN",
Message: "Unknown manifest",
}
}
delete(m.manifests[repo], target)
resp.WriteHeader(http.StatusAccepted)
return nil
}
return &regError{
Status: http.StatusBadRequest,
Code: "METHOD_UNKNOWN",
Message: "We don't understand your method + url",
}
}
func (m *manifests) handleTags(resp http.ResponseWriter, req *http.Request) *regError {
elem := strings.Split(req.URL.Path, "/")
elem = elem[1:]
repo := strings.Join(elem[1:len(elem)-2], "/")
query := req.URL.Query()
nStr := query.Get("n")
n := 1000
if nStr != "" {
n, _ = strconv.Atoi(nStr)
}
if req.Method == "GET" {
m.lock.Lock()
defer m.lock.Unlock()
c, ok := m.manifests[repo]
if !ok {
return &regError{
Status: http.StatusNotFound,
Code: "NAME_UNKNOWN",
Message: "Unknown name",
}
}
var tags []string
countTags := 0
// TODO: implement pagination https://github.com/opencontainers/distribution-spec/blob/b505e9cc53ec499edbd9c1be32298388921bb705/detail.md#tags-paginated
for tag := range c {
if countTags >= n {
break
}
countTags++
if !strings.Contains(tag, "sha256:") {
tags = append(tags, tag)
}
}
sort.Strings(tags)
tagsToList := listTags{
Name: repo,
Tags: tags,
}
msg, _ := json.Marshal(tagsToList)
resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
resp.WriteHeader(http.StatusOK)
io.Copy(resp, bytes.NewReader([]byte(msg)))
return nil
}
return &regError{
Status: http.StatusBadRequest,
Code: "METHOD_UNKNOWN",
Message: "We don't understand your method + url",
}
}
func (m *manifests) handleCatalog(resp http.ResponseWriter, req *http.Request) *regError {
query := req.URL.Query()
nStr := query.Get("n")
n := 10000
if nStr != "" {
n, _ = strconv.Atoi(nStr)
}
if req.Method == "GET" {
m.lock.Lock()
defer m.lock.Unlock()
var repos []string
countRepos := 0
// TODO: implement pagination
for key := range m.manifests {
if countRepos >= n {
break
}
countRepos++
repos = append(repos, key)
}
repositoriesToList := catalog{
Repos: repos,
}
msg, _ := json.Marshal(repositoriesToList)
resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
resp.WriteHeader(http.StatusOK)
io.Copy(resp, bytes.NewReader([]byte(msg)))
return nil
}
return &regError{
Status: http.StatusBadRequest,
Code: "METHOD_UNKNOWN",
Message: "We don't understand your method + url",
}
}