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 }