...

Source file src/github.com/google/go-containerregistry/pkg/registry/manifest.go

Documentation: github.com/google/go-containerregistry/pkg/registry

     1  // Copyright 2018 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package registry
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"fmt"
    21  	"io"
    22  	"log"
    23  	"net/http"
    24  	"sort"
    25  	"strconv"
    26  	"strings"
    27  	"sync"
    28  
    29  	v1 "github.com/google/go-containerregistry/pkg/v1"
    30  	"github.com/google/go-containerregistry/pkg/v1/types"
    31  )
    32  
    33  type catalog struct {
    34  	Repos []string `json:"repositories"`
    35  }
    36  
    37  type listTags struct {
    38  	Name string   `json:"name"`
    39  	Tags []string `json:"tags"`
    40  }
    41  
    42  type manifest struct {
    43  	contentType string
    44  	blob        []byte
    45  }
    46  
    47  type manifests struct {
    48  	// maps repo -> manifest tag/digest -> manifest
    49  	manifests map[string]map[string]manifest
    50  	lock      sync.RWMutex
    51  	log       *log.Logger
    52  }
    53  
    54  func isManifest(req *http.Request) bool {
    55  	elems := strings.Split(req.URL.Path, "/")
    56  	elems = elems[1:]
    57  	if len(elems) < 4 {
    58  		return false
    59  	}
    60  	return elems[len(elems)-2] == "manifests"
    61  }
    62  
    63  func isTags(req *http.Request) bool {
    64  	elems := strings.Split(req.URL.Path, "/")
    65  	elems = elems[1:]
    66  	if len(elems) < 4 {
    67  		return false
    68  	}
    69  	return elems[len(elems)-2] == "tags"
    70  }
    71  
    72  func isCatalog(req *http.Request) bool {
    73  	elems := strings.Split(req.URL.Path, "/")
    74  	elems = elems[1:]
    75  	if len(elems) < 2 {
    76  		return false
    77  	}
    78  
    79  	return elems[len(elems)-1] == "_catalog"
    80  }
    81  
    82  // Returns whether this url should be handled by the referrers handler
    83  func isReferrers(req *http.Request) bool {
    84  	elems := strings.Split(req.URL.Path, "/")
    85  	elems = elems[1:]
    86  	if len(elems) < 4 {
    87  		return false
    88  	}
    89  	return elems[len(elems)-2] == "referrers"
    90  }
    91  
    92  // https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-an-image-manifest
    93  // https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-an-image
    94  func (m *manifests) handle(resp http.ResponseWriter, req *http.Request) *regError {
    95  	elem := strings.Split(req.URL.Path, "/")
    96  	elem = elem[1:]
    97  	target := elem[len(elem)-1]
    98  	repo := strings.Join(elem[1:len(elem)-2], "/")
    99  
   100  	switch req.Method {
   101  	case http.MethodGet:
   102  		m.lock.RLock()
   103  		defer m.lock.RUnlock()
   104  
   105  		c, ok := m.manifests[repo]
   106  		if !ok {
   107  			return &regError{
   108  				Status:  http.StatusNotFound,
   109  				Code:    "NAME_UNKNOWN",
   110  				Message: "Unknown name",
   111  			}
   112  		}
   113  		m, ok := c[target]
   114  		if !ok {
   115  			return &regError{
   116  				Status:  http.StatusNotFound,
   117  				Code:    "MANIFEST_UNKNOWN",
   118  				Message: "Unknown manifest",
   119  			}
   120  		}
   121  
   122  		h, _, _ := v1.SHA256(bytes.NewReader(m.blob))
   123  		resp.Header().Set("Docker-Content-Digest", h.String())
   124  		resp.Header().Set("Content-Type", m.contentType)
   125  		resp.Header().Set("Content-Length", fmt.Sprint(len(m.blob)))
   126  		resp.WriteHeader(http.StatusOK)
   127  		io.Copy(resp, bytes.NewReader(m.blob))
   128  		return nil
   129  
   130  	case http.MethodHead:
   131  		m.lock.RLock()
   132  		defer m.lock.RUnlock()
   133  
   134  		if _, ok := m.manifests[repo]; !ok {
   135  			return &regError{
   136  				Status:  http.StatusNotFound,
   137  				Code:    "NAME_UNKNOWN",
   138  				Message: "Unknown name",
   139  			}
   140  		}
   141  		m, ok := m.manifests[repo][target]
   142  		if !ok {
   143  			return &regError{
   144  				Status:  http.StatusNotFound,
   145  				Code:    "MANIFEST_UNKNOWN",
   146  				Message: "Unknown manifest",
   147  			}
   148  		}
   149  
   150  		h, _, _ := v1.SHA256(bytes.NewReader(m.blob))
   151  		resp.Header().Set("Docker-Content-Digest", h.String())
   152  		resp.Header().Set("Content-Type", m.contentType)
   153  		resp.Header().Set("Content-Length", fmt.Sprint(len(m.blob)))
   154  		resp.WriteHeader(http.StatusOK)
   155  		return nil
   156  
   157  	case http.MethodPut:
   158  		b := &bytes.Buffer{}
   159  		io.Copy(b, req.Body)
   160  		h, _, _ := v1.SHA256(bytes.NewReader(b.Bytes()))
   161  		digest := h.String()
   162  		mf := manifest{
   163  			blob:        b.Bytes(),
   164  			contentType: req.Header.Get("Content-Type"),
   165  		}
   166  
   167  		// If the manifest is a manifest list, check that the manifest
   168  		// list's constituent manifests are already uploaded.
   169  		// This isn't strictly required by the registry API, but some
   170  		// registries require this.
   171  		if types.MediaType(mf.contentType).IsIndex() {
   172  			if err := func() *regError {
   173  				m.lock.RLock()
   174  				defer m.lock.RUnlock()
   175  
   176  				im, err := v1.ParseIndexManifest(b)
   177  				if err != nil {
   178  					return &regError{
   179  						Status:  http.StatusBadRequest,
   180  						Code:    "MANIFEST_INVALID",
   181  						Message: err.Error(),
   182  					}
   183  				}
   184  				for _, desc := range im.Manifests {
   185  					if !desc.MediaType.IsDistributable() {
   186  						continue
   187  					}
   188  					if desc.MediaType.IsIndex() || desc.MediaType.IsImage() {
   189  						if _, found := m.manifests[repo][desc.Digest.String()]; !found {
   190  							return &regError{
   191  								Status:  http.StatusNotFound,
   192  								Code:    "MANIFEST_UNKNOWN",
   193  								Message: fmt.Sprintf("Sub-manifest %q not found", desc.Digest),
   194  							}
   195  						}
   196  					} else {
   197  						// TODO: Probably want to do an existence check for blobs.
   198  						m.log.Printf("TODO: Check blobs for %q", desc.Digest)
   199  					}
   200  				}
   201  				return nil
   202  			}(); err != nil {
   203  				return err
   204  			}
   205  		}
   206  
   207  		m.lock.Lock()
   208  		defer m.lock.Unlock()
   209  
   210  		if _, ok := m.manifests[repo]; !ok {
   211  			m.manifests[repo] = make(map[string]manifest, 2)
   212  		}
   213  
   214  		// Allow future references by target (tag) and immutable digest.
   215  		// See https://docs.docker.com/engine/reference/commandline/pull/#pull-an-image-by-digest-immutable-identifier.
   216  		m.manifests[repo][digest] = mf
   217  		m.manifests[repo][target] = mf
   218  		resp.Header().Set("Docker-Content-Digest", digest)
   219  		resp.WriteHeader(http.StatusCreated)
   220  		return nil
   221  
   222  	case http.MethodDelete:
   223  		m.lock.Lock()
   224  		defer m.lock.Unlock()
   225  		if _, ok := m.manifests[repo]; !ok {
   226  			return &regError{
   227  				Status:  http.StatusNotFound,
   228  				Code:    "NAME_UNKNOWN",
   229  				Message: "Unknown name",
   230  			}
   231  		}
   232  
   233  		_, ok := m.manifests[repo][target]
   234  		if !ok {
   235  			return &regError{
   236  				Status:  http.StatusNotFound,
   237  				Code:    "MANIFEST_UNKNOWN",
   238  				Message: "Unknown manifest",
   239  			}
   240  		}
   241  
   242  		delete(m.manifests[repo], target)
   243  		resp.WriteHeader(http.StatusAccepted)
   244  		return nil
   245  
   246  	default:
   247  		return &regError{
   248  			Status:  http.StatusBadRequest,
   249  			Code:    "METHOD_UNKNOWN",
   250  			Message: "We don't understand your method + url",
   251  		}
   252  	}
   253  }
   254  
   255  func (m *manifests) handleTags(resp http.ResponseWriter, req *http.Request) *regError {
   256  	elem := strings.Split(req.URL.Path, "/")
   257  	elem = elem[1:]
   258  	repo := strings.Join(elem[1:len(elem)-2], "/")
   259  
   260  	if req.Method == "GET" {
   261  		m.lock.RLock()
   262  		defer m.lock.RUnlock()
   263  
   264  		c, ok := m.manifests[repo]
   265  		if !ok {
   266  			return &regError{
   267  				Status:  http.StatusNotFound,
   268  				Code:    "NAME_UNKNOWN",
   269  				Message: "Unknown name",
   270  			}
   271  		}
   272  
   273  		var tags []string
   274  		for tag := range c {
   275  			if !strings.Contains(tag, "sha256:") {
   276  				tags = append(tags, tag)
   277  			}
   278  		}
   279  		sort.Strings(tags)
   280  
   281  		// https://github.com/opencontainers/distribution-spec/blob/b505e9cc53ec499edbd9c1be32298388921bb705/detail.md#tags-paginated
   282  		// Offset using last query parameter.
   283  		if last := req.URL.Query().Get("last"); last != "" {
   284  			for i, t := range tags {
   285  				if t > last {
   286  					tags = tags[i:]
   287  					break
   288  				}
   289  			}
   290  		}
   291  
   292  		// Limit using n query parameter.
   293  		if ns := req.URL.Query().Get("n"); ns != "" {
   294  			if n, err := strconv.Atoi(ns); err != nil {
   295  				return &regError{
   296  					Status:  http.StatusBadRequest,
   297  					Code:    "BAD_REQUEST",
   298  					Message: fmt.Sprintf("parsing n: %v", err),
   299  				}
   300  			} else if n < len(tags) {
   301  				tags = tags[:n]
   302  			}
   303  		}
   304  
   305  		tagsToList := listTags{
   306  			Name: repo,
   307  			Tags: tags,
   308  		}
   309  
   310  		msg, _ := json.Marshal(tagsToList)
   311  		resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
   312  		resp.WriteHeader(http.StatusOK)
   313  		io.Copy(resp, bytes.NewReader([]byte(msg)))
   314  		return nil
   315  	}
   316  
   317  	return &regError{
   318  		Status:  http.StatusBadRequest,
   319  		Code:    "METHOD_UNKNOWN",
   320  		Message: "We don't understand your method + url",
   321  	}
   322  }
   323  
   324  func (m *manifests) handleCatalog(resp http.ResponseWriter, req *http.Request) *regError {
   325  	query := req.URL.Query()
   326  	nStr := query.Get("n")
   327  	n := 10000
   328  	if nStr != "" {
   329  		n, _ = strconv.Atoi(nStr)
   330  	}
   331  
   332  	if req.Method == "GET" {
   333  		m.lock.RLock()
   334  		defer m.lock.RUnlock()
   335  
   336  		var repos []string
   337  		countRepos := 0
   338  		// TODO: implement pagination
   339  		for key := range m.manifests {
   340  			if countRepos >= n {
   341  				break
   342  			}
   343  			countRepos++
   344  
   345  			repos = append(repos, key)
   346  		}
   347  
   348  		repositoriesToList := catalog{
   349  			Repos: repos,
   350  		}
   351  
   352  		msg, _ := json.Marshal(repositoriesToList)
   353  		resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
   354  		resp.WriteHeader(http.StatusOK)
   355  		io.Copy(resp, bytes.NewReader([]byte(msg)))
   356  		return nil
   357  	}
   358  
   359  	return &regError{
   360  		Status:  http.StatusBadRequest,
   361  		Code:    "METHOD_UNKNOWN",
   362  		Message: "We don't understand your method + url",
   363  	}
   364  }
   365  
   366  // TODO: implement handling of artifactType querystring
   367  func (m *manifests) handleReferrers(resp http.ResponseWriter, req *http.Request) *regError {
   368  	// Ensure this is a GET request
   369  	if req.Method != "GET" {
   370  		return &regError{
   371  			Status:  http.StatusBadRequest,
   372  			Code:    "METHOD_UNKNOWN",
   373  			Message: "We don't understand your method + url",
   374  		}
   375  	}
   376  
   377  	elem := strings.Split(req.URL.Path, "/")
   378  	elem = elem[1:]
   379  	target := elem[len(elem)-1]
   380  	repo := strings.Join(elem[1:len(elem)-2], "/")
   381  
   382  	// Validate that incoming target is a valid digest
   383  	if _, err := v1.NewHash(target); err != nil {
   384  		return &regError{
   385  			Status:  http.StatusBadRequest,
   386  			Code:    "UNSUPPORTED",
   387  			Message: "Target must be a valid digest",
   388  		}
   389  	}
   390  
   391  	m.lock.RLock()
   392  	defer m.lock.RUnlock()
   393  
   394  	digestToManifestMap, repoExists := m.manifests[repo]
   395  	if !repoExists {
   396  		return &regError{
   397  			Status:  http.StatusNotFound,
   398  			Code:    "NAME_UNKNOWN",
   399  			Message: "Unknown name",
   400  		}
   401  	}
   402  
   403  	im := v1.IndexManifest{
   404  		SchemaVersion: 2,
   405  		MediaType:     types.OCIImageIndex,
   406  		Manifests:     []v1.Descriptor{},
   407  	}
   408  	for digest, manifest := range digestToManifestMap {
   409  		h, err := v1.NewHash(digest)
   410  		if err != nil {
   411  			continue
   412  		}
   413  		var refPointer struct {
   414  			Subject *v1.Descriptor `json:"subject"`
   415  		}
   416  		json.Unmarshal(manifest.blob, &refPointer)
   417  		if refPointer.Subject == nil {
   418  			continue
   419  		}
   420  		referenceDigest := refPointer.Subject.Digest
   421  		if referenceDigest.String() != target {
   422  			continue
   423  		}
   424  		// At this point, we know the current digest references the target
   425  		var imageAsArtifact struct {
   426  			Config struct {
   427  				MediaType string `json:"mediaType"`
   428  			} `json:"config"`
   429  		}
   430  		json.Unmarshal(manifest.blob, &imageAsArtifact)
   431  		im.Manifests = append(im.Manifests, v1.Descriptor{
   432  			MediaType:    types.MediaType(manifest.contentType),
   433  			Size:         int64(len(manifest.blob)),
   434  			Digest:       h,
   435  			ArtifactType: imageAsArtifact.Config.MediaType,
   436  		})
   437  	}
   438  	msg, _ := json.Marshal(&im)
   439  	resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
   440  	resp.Header().Set("Content-Type", string(types.OCIImageIndex))
   441  	resp.WriteHeader(http.StatusOK)
   442  	io.Copy(resp, bytes.NewReader([]byte(msg)))
   443  	return nil
   444  }
   445  

View as plain text