github.com/mika/distribution@v2.2.2-0.20160108133430-a75790e3d8e0+incompatible/registry/client/repository.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "net/http" 11 "net/url" 12 "strconv" 13 "time" 14 15 "github.com/docker/distribution" 16 "github.com/docker/distribution/context" 17 "github.com/docker/distribution/digest" 18 "github.com/docker/distribution/reference" 19 "github.com/docker/distribution/registry/api/v2" 20 "github.com/docker/distribution/registry/client/transport" 21 "github.com/docker/distribution/registry/storage/cache" 22 "github.com/docker/distribution/registry/storage/cache/memory" 23 ) 24 25 // Registry provides an interface for calling Repositories, which returns a catalog of repositories. 26 type Registry interface { 27 Repositories(ctx context.Context, repos []string, last string) (n int, err error) 28 } 29 30 // NewRegistry creates a registry namespace which can be used to get a listing of repositories 31 func NewRegistry(ctx context.Context, baseURL string, transport http.RoundTripper) (Registry, error) { 32 ub, err := v2.NewURLBuilderFromString(baseURL) 33 if err != nil { 34 return nil, err 35 } 36 37 client := &http.Client{ 38 Transport: transport, 39 Timeout: 1 * time.Minute, 40 } 41 42 return ®istry{ 43 client: client, 44 ub: ub, 45 context: ctx, 46 }, nil 47 } 48 49 type registry struct { 50 client *http.Client 51 ub *v2.URLBuilder 52 context context.Context 53 } 54 55 // Repositories returns a lexigraphically sorted catalog given a base URL. The 'entries' slice will be filled up to the size 56 // of the slice, starting at the value provided in 'last'. The number of entries will be returned along with io.EOF if there 57 // are no more entries 58 func (r *registry) Repositories(ctx context.Context, entries []string, last string) (int, error) { 59 var numFilled int 60 var returnErr error 61 62 values := buildCatalogValues(len(entries), last) 63 u, err := r.ub.BuildCatalogURL(values) 64 if err != nil { 65 return 0, err 66 } 67 68 resp, err := r.client.Get(u) 69 if err != nil { 70 return 0, err 71 } 72 defer resp.Body.Close() 73 74 if SuccessStatus(resp.StatusCode) { 75 var ctlg struct { 76 Repositories []string `json:"repositories"` 77 } 78 decoder := json.NewDecoder(resp.Body) 79 80 if err := decoder.Decode(&ctlg); err != nil { 81 return 0, err 82 } 83 84 for cnt := range ctlg.Repositories { 85 entries[cnt] = ctlg.Repositories[cnt] 86 } 87 numFilled = len(ctlg.Repositories) 88 89 link := resp.Header.Get("Link") 90 if link == "" { 91 returnErr = io.EOF 92 } 93 } else { 94 return 0, HandleErrorResponse(resp) 95 } 96 97 return numFilled, returnErr 98 } 99 100 // NewRepository creates a new Repository for the given repository name and base URL. 101 func NewRepository(ctx context.Context, name, baseURL string, transport http.RoundTripper) (distribution.Repository, error) { 102 if _, err := reference.ParseNamed(name); err != nil { 103 return nil, err 104 } 105 106 ub, err := v2.NewURLBuilderFromString(baseURL) 107 if err != nil { 108 return nil, err 109 } 110 111 client := &http.Client{ 112 Transport: transport, 113 // TODO(dmcgowan): create cookie jar 114 } 115 116 return &repository{ 117 client: client, 118 ub: ub, 119 name: name, 120 context: ctx, 121 }, nil 122 } 123 124 type repository struct { 125 client *http.Client 126 ub *v2.URLBuilder 127 context context.Context 128 name string 129 } 130 131 func (r *repository) Name() string { 132 return r.name 133 } 134 135 func (r *repository) Blobs(ctx context.Context) distribution.BlobStore { 136 statter := &blobStatter{ 137 name: r.Name(), 138 ub: r.ub, 139 client: r.client, 140 } 141 return &blobs{ 142 name: r.Name(), 143 ub: r.ub, 144 client: r.client, 145 statter: cache.NewCachedBlobStatter(memory.NewInMemoryBlobDescriptorCacheProvider(), statter), 146 } 147 } 148 149 func (r *repository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) { 150 // todo(richardscothern): options should be sent over the wire 151 return &manifests{ 152 name: r.Name(), 153 ub: r.ub, 154 client: r.client, 155 etags: make(map[string]string), 156 }, nil 157 } 158 159 func (r *repository) Tags(ctx context.Context) distribution.TagService { 160 return &tags{ 161 client: r.client, 162 ub: r.ub, 163 context: r.context, 164 name: r.Name(), 165 } 166 } 167 168 // tags implements remote tagging operations. 169 type tags struct { 170 client *http.Client 171 ub *v2.URLBuilder 172 context context.Context 173 name string 174 } 175 176 // All returns all tags 177 func (t *tags) All(ctx context.Context) ([]string, error) { 178 var tags []string 179 180 u, err := t.ub.BuildTagsURL(t.name) 181 if err != nil { 182 return tags, err 183 } 184 185 resp, err := t.client.Get(u) 186 if err != nil { 187 return tags, err 188 } 189 defer resp.Body.Close() 190 191 if SuccessStatus(resp.StatusCode) { 192 b, err := ioutil.ReadAll(resp.Body) 193 if err != nil { 194 return tags, err 195 } 196 197 tagsResponse := struct { 198 Tags []string `json:"tags"` 199 }{} 200 if err := json.Unmarshal(b, &tagsResponse); err != nil { 201 return tags, err 202 } 203 tags = tagsResponse.Tags 204 return tags, nil 205 } 206 return tags, HandleErrorResponse(resp) 207 } 208 209 func descriptorFromResponse(response *http.Response) (distribution.Descriptor, error) { 210 desc := distribution.Descriptor{} 211 headers := response.Header 212 213 ctHeader := headers.Get("Content-Type") 214 if ctHeader == "" { 215 return distribution.Descriptor{}, errors.New("missing or empty Content-Type header") 216 } 217 desc.MediaType = ctHeader 218 219 digestHeader := headers.Get("Docker-Content-Digest") 220 if digestHeader == "" { 221 bytes, err := ioutil.ReadAll(response.Body) 222 if err != nil { 223 return distribution.Descriptor{}, err 224 } 225 _, desc, err := distribution.UnmarshalManifest(ctHeader, bytes) 226 if err != nil { 227 return distribution.Descriptor{}, err 228 } 229 return desc, nil 230 } 231 232 dgst, err := digest.ParseDigest(digestHeader) 233 if err != nil { 234 return distribution.Descriptor{}, err 235 } 236 desc.Digest = dgst 237 238 lengthHeader := headers.Get("Content-Length") 239 if lengthHeader == "" { 240 return distribution.Descriptor{}, errors.New("missing or empty Content-Length header") 241 } 242 length, err := strconv.ParseInt(lengthHeader, 10, 64) 243 if err != nil { 244 return distribution.Descriptor{}, err 245 } 246 desc.Size = length 247 248 return desc, nil 249 250 } 251 252 // Get issues a HEAD request for a Manifest against its named endpoint in order 253 // to construct a descriptor for the tag. If the registry doesn't support HEADing 254 // a manifest, fallback to GET. 255 func (t *tags) Get(ctx context.Context, tag string) (distribution.Descriptor, error) { 256 u, err := t.ub.BuildManifestURL(t.name, tag) 257 if err != nil { 258 return distribution.Descriptor{}, err 259 } 260 var attempts int 261 resp, err := t.client.Head(u) 262 263 check: 264 if err != nil { 265 return distribution.Descriptor{}, err 266 } 267 268 switch { 269 case resp.StatusCode >= 200 && resp.StatusCode < 400: 270 return descriptorFromResponse(resp) 271 case resp.StatusCode == http.StatusMethodNotAllowed: 272 resp, err = t.client.Get(u) 273 attempts++ 274 if attempts > 1 { 275 return distribution.Descriptor{}, err 276 } 277 goto check 278 default: 279 return distribution.Descriptor{}, HandleErrorResponse(resp) 280 } 281 } 282 283 func (t *tags) Lookup(ctx context.Context, digest distribution.Descriptor) ([]string, error) { 284 panic("not implemented") 285 } 286 287 func (t *tags) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error { 288 panic("not implemented") 289 } 290 291 func (t *tags) Untag(ctx context.Context, tag string) error { 292 panic("not implemented") 293 } 294 295 type manifests struct { 296 name string 297 ub *v2.URLBuilder 298 client *http.Client 299 etags map[string]string 300 } 301 302 func (ms *manifests) Exists(ctx context.Context, dgst digest.Digest) (bool, error) { 303 u, err := ms.ub.BuildManifestURL(ms.name, dgst.String()) 304 if err != nil { 305 return false, err 306 } 307 308 resp, err := ms.client.Head(u) 309 if err != nil { 310 return false, err 311 } 312 313 if SuccessStatus(resp.StatusCode) { 314 return true, nil 315 } else if resp.StatusCode == http.StatusNotFound { 316 return false, nil 317 } 318 return false, HandleErrorResponse(resp) 319 } 320 321 // AddEtagToTag allows a client to supply an eTag to Get which will be 322 // used for a conditional HTTP request. If the eTag matches, a nil manifest 323 // and ErrManifestNotModified error will be returned. etag is automatically 324 // quoted when added to this map. 325 func AddEtagToTag(tag, etag string) distribution.ManifestServiceOption { 326 return etagOption{tag, etag} 327 } 328 329 type etagOption struct{ tag, etag string } 330 331 func (o etagOption) Apply(ms distribution.ManifestService) error { 332 if ms, ok := ms.(*manifests); ok { 333 ms.etags[o.tag] = fmt.Sprintf(`"%s"`, o.etag) 334 return nil 335 } 336 return fmt.Errorf("etag options is a client-only option") 337 } 338 339 func (ms *manifests) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { 340 341 var tag string 342 for _, option := range options { 343 if opt, ok := option.(withTagOption); ok { 344 tag = opt.tag 345 } else { 346 err := option.Apply(ms) 347 if err != nil { 348 return nil, err 349 } 350 } 351 } 352 353 var ref string 354 if tag != "" { 355 ref = tag 356 } else { 357 ref = dgst.String() 358 } 359 360 u, err := ms.ub.BuildManifestURL(ms.name, ref) 361 if err != nil { 362 return nil, err 363 } 364 365 req, err := http.NewRequest("GET", u, nil) 366 if err != nil { 367 return nil, err 368 } 369 370 for _, t := range distribution.ManifestMediaTypes() { 371 req.Header.Add("Accept", t) 372 } 373 374 if _, ok := ms.etags[ref]; ok { 375 req.Header.Set("If-None-Match", ms.etags[ref]) 376 } 377 378 resp, err := ms.client.Do(req) 379 if err != nil { 380 return nil, err 381 } 382 defer resp.Body.Close() 383 if resp.StatusCode == http.StatusNotModified { 384 return nil, distribution.ErrManifestNotModified 385 } else if SuccessStatus(resp.StatusCode) { 386 mt := resp.Header.Get("Content-Type") 387 body, err := ioutil.ReadAll(resp.Body) 388 389 if err != nil { 390 return nil, err 391 } 392 m, _, err := distribution.UnmarshalManifest(mt, body) 393 if err != nil { 394 return nil, err 395 } 396 return m, nil 397 } 398 return nil, HandleErrorResponse(resp) 399 } 400 401 // WithTag allows a tag to be passed into Put which enables the client 402 // to build a correct URL. 403 func WithTag(tag string) distribution.ManifestServiceOption { 404 return withTagOption{tag} 405 } 406 407 type withTagOption struct{ tag string } 408 409 func (o withTagOption) Apply(m distribution.ManifestService) error { 410 if _, ok := m.(*manifests); ok { 411 return nil 412 } 413 return fmt.Errorf("withTagOption is a client-only option") 414 } 415 416 // Put puts a manifest. A tag can be specified using an options parameter which uses some shared state to hold the 417 // tag name in order to build the correct upload URL. This state is written and read under a lock. 418 func (ms *manifests) Put(ctx context.Context, m distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { 419 var tag string 420 421 for _, option := range options { 422 if opt, ok := option.(withTagOption); ok { 423 tag = opt.tag 424 } else { 425 err := option.Apply(ms) 426 if err != nil { 427 return "", err 428 } 429 } 430 } 431 432 manifestURL, err := ms.ub.BuildManifestURL(ms.name, tag) 433 if err != nil { 434 return "", err 435 } 436 437 mediaType, p, err := m.Payload() 438 if err != nil { 439 return "", err 440 } 441 442 putRequest, err := http.NewRequest("PUT", manifestURL, bytes.NewReader(p)) 443 if err != nil { 444 return "", err 445 } 446 447 putRequest.Header.Set("Content-Type", mediaType) 448 449 resp, err := ms.client.Do(putRequest) 450 if err != nil { 451 return "", err 452 } 453 defer resp.Body.Close() 454 455 if SuccessStatus(resp.StatusCode) { 456 dgstHeader := resp.Header.Get("Docker-Content-Digest") 457 dgst, err := digest.ParseDigest(dgstHeader) 458 if err != nil { 459 return "", err 460 } 461 462 return dgst, nil 463 } 464 465 return "", HandleErrorResponse(resp) 466 } 467 468 func (ms *manifests) Delete(ctx context.Context, dgst digest.Digest) error { 469 u, err := ms.ub.BuildManifestURL(ms.name, dgst.String()) 470 if err != nil { 471 return err 472 } 473 req, err := http.NewRequest("DELETE", u, nil) 474 if err != nil { 475 return err 476 } 477 478 resp, err := ms.client.Do(req) 479 if err != nil { 480 return err 481 } 482 defer resp.Body.Close() 483 484 if SuccessStatus(resp.StatusCode) { 485 return nil 486 } 487 return HandleErrorResponse(resp) 488 } 489 490 // todo(richardscothern): Restore interface and implementation with merge of #1050 491 /*func (ms *manifests) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) { 492 panic("not supported") 493 }*/ 494 495 type blobs struct { 496 name string 497 ub *v2.URLBuilder 498 client *http.Client 499 500 statter distribution.BlobDescriptorService 501 distribution.BlobDeleter 502 } 503 504 func sanitizeLocation(location, base string) (string, error) { 505 baseURL, err := url.Parse(base) 506 if err != nil { 507 return "", err 508 } 509 510 locationURL, err := url.Parse(location) 511 if err != nil { 512 return "", err 513 } 514 515 return baseURL.ResolveReference(locationURL).String(), nil 516 } 517 518 func (bs *blobs) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { 519 return bs.statter.Stat(ctx, dgst) 520 521 } 522 523 func (bs *blobs) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) { 524 reader, err := bs.Open(ctx, dgst) 525 if err != nil { 526 return nil, err 527 } 528 defer reader.Close() 529 530 return ioutil.ReadAll(reader) 531 } 532 533 func (bs *blobs) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) { 534 blobURL, err := bs.ub.BuildBlobURL(bs.name, dgst) 535 if err != nil { 536 return nil, err 537 } 538 539 return transport.NewHTTPReadSeeker(bs.client, blobURL, 540 func(resp *http.Response) error { 541 if resp.StatusCode == http.StatusNotFound { 542 return distribution.ErrBlobUnknown 543 } 544 return HandleErrorResponse(resp) 545 }), nil 546 } 547 548 func (bs *blobs) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error { 549 panic("not implemented") 550 } 551 552 func (bs *blobs) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) { 553 writer, err := bs.Create(ctx) 554 if err != nil { 555 return distribution.Descriptor{}, err 556 } 557 dgstr := digest.Canonical.New() 558 n, err := io.Copy(writer, io.TeeReader(bytes.NewReader(p), dgstr.Hash())) 559 if err != nil { 560 return distribution.Descriptor{}, err 561 } 562 if n < int64(len(p)) { 563 return distribution.Descriptor{}, fmt.Errorf("short copy: wrote %d of %d", n, len(p)) 564 } 565 566 desc := distribution.Descriptor{ 567 MediaType: mediaType, 568 Size: int64(len(p)), 569 Digest: dgstr.Digest(), 570 } 571 572 return writer.Commit(ctx, desc) 573 } 574 575 func (bs *blobs) Create(ctx context.Context) (distribution.BlobWriter, error) { 576 u, err := bs.ub.BuildBlobUploadURL(bs.name) 577 578 resp, err := bs.client.Post(u, "", nil) 579 if err != nil { 580 return nil, err 581 } 582 defer resp.Body.Close() 583 584 if SuccessStatus(resp.StatusCode) { 585 // TODO(dmcgowan): Check for invalid UUID 586 uuid := resp.Header.Get("Docker-Upload-UUID") 587 location, err := sanitizeLocation(resp.Header.Get("Location"), u) 588 if err != nil { 589 return nil, err 590 } 591 592 return &httpBlobUpload{ 593 statter: bs.statter, 594 client: bs.client, 595 uuid: uuid, 596 startedAt: time.Now(), 597 location: location, 598 }, nil 599 } 600 return nil, HandleErrorResponse(resp) 601 } 602 603 func (bs *blobs) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) { 604 panic("not implemented") 605 } 606 607 func (bs *blobs) Delete(ctx context.Context, dgst digest.Digest) error { 608 return bs.statter.Clear(ctx, dgst) 609 } 610 611 type blobStatter struct { 612 name string 613 ub *v2.URLBuilder 614 client *http.Client 615 } 616 617 func (bs *blobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { 618 u, err := bs.ub.BuildBlobURL(bs.name, dgst) 619 if err != nil { 620 return distribution.Descriptor{}, err 621 } 622 623 resp, err := bs.client.Head(u) 624 if err != nil { 625 return distribution.Descriptor{}, err 626 } 627 defer resp.Body.Close() 628 629 if SuccessStatus(resp.StatusCode) { 630 lengthHeader := resp.Header.Get("Content-Length") 631 if lengthHeader == "" { 632 return distribution.Descriptor{}, fmt.Errorf("missing content-length header for request: %s", u) 633 } 634 635 length, err := strconv.ParseInt(lengthHeader, 10, 64) 636 if err != nil { 637 return distribution.Descriptor{}, fmt.Errorf("error parsing content-length: %v", err) 638 } 639 640 return distribution.Descriptor{ 641 MediaType: resp.Header.Get("Content-Type"), 642 Size: length, 643 Digest: dgst, 644 }, nil 645 } else if resp.StatusCode == http.StatusNotFound { 646 return distribution.Descriptor{}, distribution.ErrBlobUnknown 647 } 648 return distribution.Descriptor{}, HandleErrorResponse(resp) 649 } 650 651 func buildCatalogValues(maxEntries int, last string) url.Values { 652 values := url.Values{} 653 654 if maxEntries > 0 { 655 values.Add("n", strconv.Itoa(maxEntries)) 656 } 657 658 if last != "" { 659 values.Add("last", last) 660 } 661 662 return values 663 } 664 665 func (bs *blobStatter) Clear(ctx context.Context, dgst digest.Digest) error { 666 blobURL, err := bs.ub.BuildBlobURL(bs.name, dgst) 667 if err != nil { 668 return err 669 } 670 671 req, err := http.NewRequest("DELETE", blobURL, nil) 672 if err != nil { 673 return err 674 } 675 676 resp, err := bs.client.Do(req) 677 if err != nil { 678 return err 679 } 680 defer resp.Body.Close() 681 682 if SuccessStatus(resp.StatusCode) { 683 return nil 684 } 685 return HandleErrorResponse(resp) 686 } 687 688 func (bs *blobStatter) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { 689 return nil 690 }