...

Source file src/cuelabs.dev/go/oci/ociregistry/ociclient/reader.go

Documentation: cuelabs.dev/go/oci/ociregistry/ociclient

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

View as plain text