mirror of https://github.com/knative/func.git
				
				
				
			
		
			
				
	
	
		
			335 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			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 ®Error{
 | 
						|
				Status:  http.StatusNotFound,
 | 
						|
				Code:    "NAME_UNKNOWN",
 | 
						|
				Message: "Unknown name",
 | 
						|
			}
 | 
						|
		}
 | 
						|
		m, ok := c[target]
 | 
						|
		if !ok {
 | 
						|
			return ®Error{
 | 
						|
				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 ®Error{
 | 
						|
				Status:  http.StatusNotFound,
 | 
						|
				Code:    "NAME_UNKNOWN",
 | 
						|
				Message: "Unknown name",
 | 
						|
			}
 | 
						|
		}
 | 
						|
		m, ok := m.manifests[repo][target]
 | 
						|
		if !ok {
 | 
						|
			return ®Error{
 | 
						|
				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 ®Error{
 | 
						|
					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 ®Error{
 | 
						|
							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 ®Error{
 | 
						|
				Status:  http.StatusNotFound,
 | 
						|
				Code:    "NAME_UNKNOWN",
 | 
						|
				Message: "Unknown name",
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		_, ok := m.manifests[repo][target]
 | 
						|
		if !ok {
 | 
						|
			return ®Error{
 | 
						|
				Status:  http.StatusNotFound,
 | 
						|
				Code:    "MANIFEST_UNKNOWN",
 | 
						|
				Message: "Unknown manifest",
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		delete(m.manifests[repo], target)
 | 
						|
		resp.WriteHeader(http.StatusAccepted)
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	return ®Error{
 | 
						|
		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 ®Error{
 | 
						|
				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 ®Error{
 | 
						|
		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 ®Error{
 | 
						|
		Status:  http.StatusBadRequest,
 | 
						|
		Code:    "METHOD_UNKNOWN",
 | 
						|
		Message: "We don't understand your method + url",
 | 
						|
	}
 | 
						|
}
 |