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 }