github.com/containerd/nerdctl@v1.7.7/pkg/ipfs/registry.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package ipfs
    18  
    19  import (
    20  	"bufio"
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"net/http"
    27  	"regexp"
    28  	"strconv"
    29  	"strings"
    30  	"time"
    31  
    32  	"github.com/containerd/containerd/content"
    33  	"github.com/containerd/containerd/images"
    34  	"github.com/containerd/log"
    35  	ipfsclient "github.com/containerd/stargz-snapshotter/ipfs/client"
    36  	"github.com/opencontainers/go-digest"
    37  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    38  )
    39  
    40  // RegistryOptions represents options to configure the registry.
    41  type RegistryOptions struct {
    42  
    43  	// Times to retry query on IPFS. Zero or lower value means no retry.
    44  	ReadRetryNum int
    45  
    46  	// ReadTimeout is timeout duration of a read request to IPFS. Zero means no timeout.
    47  	ReadTimeout time.Duration
    48  
    49  	// IpfsPath is the IPFS_PATH value to be used for ipfs command.
    50  	IpfsPath string
    51  }
    52  
    53  func NewRegistry(options RegistryOptions) (http.Handler, error) {
    54  	// HTTP is only supported as of now. We can add https support here if needed (e.g. for connecting to it via proxy, etc)
    55  	iurl, err := ipfsclient.GetIPFSAPIAddress(lookupIPFSPath(options.IpfsPath), "http")
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  	return &server{options, ipfsclient.New(iurl)}, nil
    60  }
    61  
    62  // server is a read-only registry which converts OCI Distribution Spec's pull-related API to IPFS
    63  // https://github.com/opencontainers/distribution-spec/blob/v1.0/spec.md#pull
    64  type server struct {
    65  	config     RegistryOptions
    66  	ipfsclient *ipfsclient.Client
    67  }
    68  
    69  var manifestRegexp = regexp.MustCompile(`/v2/ipfs/([a-z0-9]+)/manifests/(.*)`)
    70  var blobsRegexp = regexp.MustCompile(`/v2/ipfs/([a-z0-9]+)/blobs/(.*)`)
    71  
    72  func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    73  	cid, content, mediaType, size, err := s.serve(r)
    74  	if err != nil {
    75  		log.L.WithError(err).Warnf("failed to serve %q %q", r.Method, r.URL.Path)
    76  		// TODO: support response body following OCI Distribution Spec's error response format spec:
    77  		// https://github.com/opencontainers/distribution-spec/blob/v1.0/spec.md#error-codes
    78  		http.Error(w, "", http.StatusNotFound)
    79  		return
    80  	}
    81  	if content == nil {
    82  		log.L.Debugf("returning without contents")
    83  		w.WriteHeader(200)
    84  		return
    85  	}
    86  	w.Header().Set("Content-Type", mediaType)
    87  	w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
    88  	if r.Method == "GET" {
    89  		http.ServeContent(w, r, "", time.Now(), content)
    90  		log.L.WithField("CID", cid).Debugf("served file")
    91  	}
    92  }
    93  
    94  func (s *server) serve(r *http.Request) (string, io.ReadSeeker, string, int64, error) {
    95  	if r.Method != "GET" && r.Method != "HEAD" {
    96  		return "", nil, "", 0, fmt.Errorf("unsupported method")
    97  	}
    98  
    99  	if r.URL.Path == "/v2/" {
   100  		log.L.Debugf("requested /v2/")
   101  		return "", nil, "", 0, nil
   102  	}
   103  
   104  	if matches := manifestRegexp.FindStringSubmatch(r.URL.Path); len(matches) != 0 {
   105  		cidStr, ref := matches[1], matches[2]
   106  		if _, dgstErr := digest.Parse(ref); dgstErr == nil {
   107  			resolvedCID, content, mediaType, size, err := s.serveContentByDigest(r.Context(), cidStr, ref)
   108  			if !images.IsManifestType(mediaType) && !images.IsIndexType(mediaType) {
   109  				return "", nil, "", 0, fmt.Errorf("cannot serve non-manifest from manifest API: %q", mediaType)
   110  			}
   111  			log.L.WithField("root CID", cidStr).WithField("digest", ref).WithField("resolved CID", resolvedCID).Debugf("resolved manifest by digest")
   112  			return resolvedCID, content, mediaType, size, err
   113  		}
   114  		if ref != "latest" {
   115  			return "", nil, "", 0, fmt.Errorf("tag of %q must be latest but got %q", cidStr, ref)
   116  		}
   117  		resolvedCID, content, mediaType, size, err := s.serveContentByCID(r.Context(), cidStr)
   118  		if err != nil {
   119  			return "", nil, "", 0, err
   120  		}
   121  		log.L.WithField("root CID", cidStr).WithField("resolved CID", resolvedCID).Debugf("resolved manifest by cid")
   122  		return resolvedCID, content, mediaType, size, nil
   123  	}
   124  
   125  	if matches := blobsRegexp.FindStringSubmatch(r.URL.Path); len(matches) != 0 {
   126  		rootCIDStr, dgstStr := matches[1], matches[2]
   127  		resolvedCID, content, mediaType, size, err := s.serveContentByDigest(r.Context(), rootCIDStr, dgstStr)
   128  		if err != nil {
   129  			return "", nil, "", 0, err
   130  		}
   131  		log.L.WithField("root CID", rootCIDStr).WithField("digest", dgstStr).WithField("resolved CID", resolvedCID).Debugf("resolved blob by digest")
   132  		return resolvedCID, content, mediaType, size, nil
   133  	}
   134  
   135  	return "", nil, "", 0, fmt.Errorf("unsupported path")
   136  }
   137  
   138  func (s *server) serveContentByCID(ctx context.Context, targetCID string) (resC string, r io.ReadSeeker, mediaType string, size int64, err error) {
   139  	// TODO: make sure cidStr is a vaild CID?
   140  	c, desc, err := s.resolveCIDOfRootBlob(ctx, targetCID)
   141  	if err != nil {
   142  		return "", nil, "", 0, err
   143  	}
   144  	rc, err := s.getReadSeeker(ctx, c)
   145  	if err != nil {
   146  		return "", nil, "", 0, err
   147  	}
   148  	return c, rc, getMediaType(desc), desc.Size, nil
   149  }
   150  
   151  func (s *server) serveContentByDigest(ctx context.Context, rootCID, digestStr string) (resC string, r io.ReadSeeker, mediaType string, size int64, err error) {
   152  	dgst, err := digest.Parse(digestStr)
   153  	if err != nil {
   154  		return "", nil, "", 0, err
   155  	}
   156  	_, rootDesc, err := s.resolveCIDOfRootBlob(ctx, rootCID)
   157  	if err != nil {
   158  		return "", nil, "", 0, err
   159  	}
   160  	targetCID, targetDesc, err := s.resolveCIDOfDigest(ctx, dgst, rootDesc)
   161  	if err != nil {
   162  		return "", nil, "", 0, err
   163  	}
   164  	rc, err := s.getReadSeeker(ctx, targetCID)
   165  	if err != nil {
   166  		return "", nil, "", 0, err
   167  	}
   168  	return targetCID, rc, getMediaType(targetDesc), targetDesc.Size, nil
   169  }
   170  
   171  func (s *server) getReadSeeker(ctx context.Context, c string) (io.ReadSeeker, error) {
   172  	sr, err := s.getFile(ctx, c)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  	return newBufReadSeeker(sr), nil
   177  }
   178  
   179  func (s *server) getFile(ctx context.Context, c string) (*io.SectionReader, error) {
   180  	st, err := s.ipfsclient.StatCID(c)
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  	ra := &retryReaderAt{
   185  		ctx: ctx,
   186  		readAtFunc: func(ctx context.Context, p []byte, off int64) (int, error) {
   187  			ofst, size := int(off), len(p)
   188  			r, err := s.ipfsclient.Get("/ipfs/"+c, &ofst, &size)
   189  			if err != nil {
   190  				return 0, err
   191  			}
   192  			return io.ReadFull(r, p)
   193  		},
   194  		timeout: s.config.ReadTimeout,
   195  		retry:   s.config.ReadRetryNum,
   196  	}
   197  	return io.NewSectionReader(ra, 0, int64(st.Size)), nil
   198  }
   199  
   200  func (s *server) resolveCIDOfRootBlob(ctx context.Context, c string) (string, ocispec.Descriptor, error) {
   201  	rc, err := s.getReadSeeker(ctx, c)
   202  	if err != nil {
   203  		return "", ocispec.Descriptor{}, err
   204  	}
   205  	var desc ocispec.Descriptor
   206  	if err := json.NewDecoder(rc).Decode(&desc); err != nil {
   207  		return "", ocispec.Descriptor{}, err
   208  	}
   209  	c, err = getIPFSCID(desc)
   210  	if err != nil {
   211  		return "", ocispec.Descriptor{}, err
   212  	}
   213  	return c, desc, nil
   214  }
   215  
   216  func (s *server) resolveCIDOfDigest(ctx context.Context, dgst digest.Digest, desc ocispec.Descriptor) (string, ocispec.Descriptor, error) {
   217  	c, err := getIPFSCID(desc)
   218  	if err != nil {
   219  		return "", ocispec.Descriptor{}, err
   220  	}
   221  	if desc.Digest == dgst {
   222  		return c, desc, nil // hit
   223  	}
   224  	if !images.IsManifestType(desc.MediaType) && !images.IsIndexType(desc.MediaType) {
   225  		// This is not the target blob and have no child. Early return here and avoid querying this blob.
   226  		return "", ocispec.Descriptor{}, fmt.Errorf("blob doesn't match")
   227  	}
   228  	sr, err := s.getFile(ctx, c)
   229  	if err != nil {
   230  		return "", ocispec.Descriptor{}, err
   231  	}
   232  	descs, err := images.Children(ctx, &readerProvider{desc, sr}, desc)
   233  	if err != nil {
   234  		return "", ocispec.Descriptor{}, err
   235  	}
   236  	var errs []error
   237  	for _, desc := range descs {
   238  		gotCID, gotDesc, err := s.resolveCIDOfDigest(ctx, dgst, desc)
   239  		if err != nil {
   240  			errs = append(errs, err)
   241  			continue
   242  		}
   243  		return gotCID, gotDesc, nil
   244  	}
   245  	allErr := errors.Join(errs...)
   246  	if allErr == nil {
   247  		return "", ocispec.Descriptor{}, fmt.Errorf("not found")
   248  	}
   249  	return "", ocispec.Descriptor{}, allErr
   250  }
   251  
   252  func getIPFSCID(desc ocispec.Descriptor) (string, error) {
   253  	for _, u := range desc.URLs {
   254  		if strings.HasPrefix(u, "ipfs://") {
   255  			// support only content addressable URL (ipfs://<CID>)
   256  			return u[7:], nil
   257  		}
   258  	}
   259  	return "", fmt.Errorf("no CID is recorded in %s", desc.Digest)
   260  }
   261  
   262  func getMediaType(desc ocispec.Descriptor) string {
   263  	if images.IsManifestType(desc.MediaType) || images.IsIndexType(desc.MediaType) || images.IsConfigType(desc.MediaType) {
   264  		return desc.MediaType
   265  	}
   266  	return "application/octet-stream"
   267  }
   268  
   269  type retryReaderAt struct {
   270  	ctx        context.Context
   271  	readAtFunc func(ctx context.Context, p []byte, off int64) (int, error)
   272  	timeout    time.Duration
   273  	retry      int
   274  }
   275  
   276  func (r *retryReaderAt) ReadAt(p []byte, off int64) (int, error) {
   277  	if r.retry < 0 {
   278  		r.retry = 0
   279  	}
   280  	for i := 0; i <= r.retry; i++ {
   281  		ctx := r.ctx
   282  		if r.timeout != 0 {
   283  			var cancel context.CancelFunc
   284  			ctx, cancel = context.WithTimeout(ctx, r.timeout)
   285  			defer cancel()
   286  		}
   287  		n, err := r.readAtFunc(ctx, p, off)
   288  		if err == nil {
   289  			return n, nil
   290  		} else if !errors.Is(err, context.DeadlineExceeded) {
   291  			return 0, err
   292  		}
   293  		// deadline exceeded. retry.
   294  	}
   295  	return 0, context.DeadlineExceeded
   296  }
   297  
   298  func newBufReadSeeker(rs io.ReadSeeker) io.ReadSeeker {
   299  	rsc := &bufReadSeeker{
   300  		rs: rs,
   301  	}
   302  	rsc.curR = bufio.NewReaderSize(rsc.rs, 512*1024)
   303  	return rsc
   304  }
   305  
   306  type bufReadSeeker struct {
   307  	rs   io.ReadSeeker
   308  	curR *bufio.Reader
   309  }
   310  
   311  func (r *bufReadSeeker) Read(p []byte) (int, error) {
   312  	return r.curR.Read(p)
   313  }
   314  
   315  func (r *bufReadSeeker) Seek(offset int64, whence int) (int64, error) {
   316  	n, err := r.rs.Seek(offset, whence)
   317  	if err != nil {
   318  		return 0, err
   319  	}
   320  	r.curR.Reset(r.rs)
   321  	return n, nil
   322  }
   323  
   324  type readerProvider struct {
   325  	desc ocispec.Descriptor
   326  	r    *io.SectionReader
   327  }
   328  
   329  func (p *readerProvider) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.ReaderAt, error) {
   330  	if desc.Digest != p.desc.Digest || desc.Size != p.desc.Size {
   331  		return nil, fmt.Errorf("unexpected content")
   332  	}
   333  	return &contentReaderAt{p.r}, nil
   334  }
   335  
   336  type contentReaderAt struct {
   337  	*io.SectionReader
   338  }
   339  
   340  func (r *contentReaderAt) Close() error { return nil }