cuelabs.dev/go/oci/ociregistry@v0.0.0-20240906074133-82eb438dd565/ociclient/reader.go (about)

     1  // Copyright 2023 CUE Labs AG
     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 ociclient
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"fmt"
    21  	"io"
    22  	"net/http"
    23  
    24  	"cuelabs.dev/go/oci/ociregistry"
    25  	"cuelabs.dev/go/oci/ociregistry/internal/ocirequest"
    26  	"github.com/opencontainers/go-digest"
    27  )
    28  
    29  func (c *client) GetBlob(ctx context.Context, repo string, digest ociregistry.Digest) (ociregistry.BlobReader, error) {
    30  	return c.read(ctx, &ocirequest.Request{
    31  		Kind:   ocirequest.ReqBlobGet,
    32  		Repo:   repo,
    33  		Digest: string(digest),
    34  	})
    35  }
    36  
    37  func (c *client) GetBlobRange(ctx context.Context, repo string, digest ociregistry.Digest, o0, o1 int64) (_ ociregistry.BlobReader, _err error) {
    38  	if o0 == 0 && o1 < 0 {
    39  		return c.GetBlob(ctx, repo, digest)
    40  	}
    41  	rreq := &ocirequest.Request{
    42  		Kind:   ocirequest.ReqBlobGet,
    43  		Repo:   repo,
    44  		Digest: string(digest),
    45  	}
    46  	req, err := newRequest(ctx, rreq, nil)
    47  	if err != nil {
    48  		return nil, err
    49  	}
    50  	if o1 < 0 {
    51  		req.Header.Set("Range", fmt.Sprintf("bytes=%d-", o0))
    52  	} else {
    53  		req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", o0, o1-1))
    54  	}
    55  	resp, err := c.do(req, http.StatusOK, http.StatusPartialContent)
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  	// TODO this is wrong when the server returns a 200 response.
    60  	// Fix that either by returning ErrUnsupported or by reading the whole
    61  	// blob and returning only the required portion.
    62  	defer closeOnError(&_err, resp.Body)
    63  	desc, err := descriptorFromResponse(resp, ociregistry.Digest(rreq.Digest), requireSize)
    64  	if err != nil {
    65  		return nil, fmt.Errorf("invalid descriptor in response: %v", err)
    66  	}
    67  	return newBlobReaderUnverified(resp.Body, desc), nil
    68  }
    69  
    70  func (c *client) ResolveBlob(ctx context.Context, repo string, digest ociregistry.Digest) (ociregistry.Descriptor, error) {
    71  	return c.resolve(ctx, &ocirequest.Request{
    72  		Kind:   ocirequest.ReqBlobHead,
    73  		Repo:   repo,
    74  		Digest: string(digest),
    75  	})
    76  }
    77  
    78  func (c *client) ResolveManifest(ctx context.Context, repo string, digest ociregistry.Digest) (ociregistry.Descriptor, error) {
    79  	return c.resolve(ctx, &ocirequest.Request{
    80  		Kind:   ocirequest.ReqManifestHead,
    81  		Repo:   repo,
    82  		Digest: string(digest),
    83  	})
    84  }
    85  
    86  func (c *client) ResolveTag(ctx context.Context, repo string, tag string) (ociregistry.Descriptor, error) {
    87  	return c.resolve(ctx, &ocirequest.Request{
    88  		Kind: ocirequest.ReqManifestHead,
    89  		Repo: repo,
    90  		Tag:  tag,
    91  	})
    92  }
    93  
    94  func (c *client) resolve(ctx context.Context, rreq *ocirequest.Request) (ociregistry.Descriptor, error) {
    95  	resp, err := c.doRequest(ctx, rreq)
    96  	if err != nil {
    97  		return ociregistry.Descriptor{}, err
    98  	}
    99  	resp.Body.Close()
   100  	desc, err := descriptorFromResponse(resp, ociregistry.Digest(rreq.Digest), requireSize|requireDigest)
   101  	if err != nil {
   102  		return ociregistry.Descriptor{}, fmt.Errorf("invalid descriptor in response: %v", err)
   103  	}
   104  	return desc, nil
   105  }
   106  
   107  func (c *client) GetManifest(ctx context.Context, repo string, digest ociregistry.Digest) (ociregistry.BlobReader, error) {
   108  	return c.read(ctx, &ocirequest.Request{
   109  		Kind:   ocirequest.ReqManifestGet,
   110  		Repo:   repo,
   111  		Digest: string(digest),
   112  	})
   113  }
   114  
   115  func (c *client) GetTag(ctx context.Context, repo string, tagName string) (ociregistry.BlobReader, error) {
   116  	return c.read(ctx, &ocirequest.Request{
   117  		Kind: ocirequest.ReqManifestGet,
   118  		Repo: repo,
   119  		Tag:  tagName,
   120  	})
   121  }
   122  
   123  // inMemThreshold holds the maximum number of bytes of manifest content
   124  // that we'll hold in memory to obtain a digest before falling back do
   125  // doing a HEAD request.
   126  //
   127  // This is hopefully large enough to be considerably larger than most
   128  // manifests but small enough to fit comfortably into RAM on most
   129  // platforms.
   130  //
   131  // Note: this is only used when talking to registries that fail to return
   132  // a digest when doing a GET on a tag.
   133  const inMemThreshold = 128 * 1024
   134  
   135  func (c *client) read(ctx context.Context, rreq *ocirequest.Request) (_ ociregistry.BlobReader, _err error) {
   136  	resp, err := c.doRequest(ctx, rreq)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	defer closeOnError(&_err, resp.Body)
   141  	desc, err := descriptorFromResponse(resp, ociregistry.Digest(rreq.Digest), requireSize)
   142  	if err != nil {
   143  		return nil, fmt.Errorf("invalid descriptor in response: %v", err)
   144  	}
   145  	if desc.Digest == "" {
   146  		// Returning a digest isn't mandatory according to the spec, and
   147  		// at least one registry (AWS's ECR) fails to return a digest
   148  		// when doing a GET of a tag.
   149  		// We know the request must be a tag-getting
   150  		// request because all other requests take a digest not a tag
   151  		// but sanity check anyway.
   152  		if rreq.Kind != ocirequest.ReqManifestGet {
   153  			return nil, fmt.Errorf("internal error: no digest available for non-tag request")
   154  		}
   155  
   156  		// If the manifest is of a reasonable size, just read it into memory
   157  		// and calculate the digest that way, otherwise issue a HEAD
   158  		// request which should hopefully (and does in the ECR case)
   159  		// give us the digest we need.
   160  		if desc.Size <= inMemThreshold {
   161  			data, err := io.ReadAll(io.LimitReader(resp.Body, desc.Size+1))
   162  			if err != nil {
   163  				return nil, fmt.Errorf("failed to read body to determine digest: %v", err)
   164  			}
   165  			if int64(len(data)) != desc.Size {
   166  				return nil, fmt.Errorf("body size mismatch")
   167  			}
   168  			desc.Digest = digest.FromBytes(data)
   169  			resp.Body.Close()
   170  			resp.Body = io.NopCloser(bytes.NewReader(data))
   171  		} else {
   172  			rreq1 := rreq
   173  			rreq1.Kind = ocirequest.ReqManifestHead
   174  			resp1, err := c.doRequest(ctx, rreq1)
   175  			if err != nil {
   176  				return nil, err
   177  			}
   178  			resp1.Body.Close()
   179  			desc, err = descriptorFromResponse(resp1, ociregistry.Digest(rreq1.Digest), requireSize|requireDigest)
   180  			if err != nil {
   181  				return nil, err
   182  			}
   183  		}
   184  	}
   185  	return newBlobReader(resp.Body, desc), nil
   186  }