github.com/lusis/distribution@v2.0.1+incompatible/registry/client/client.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "regexp" 10 "strconv" 11 12 "github.com/docker/distribution/digest" 13 "github.com/docker/distribution/manifest" 14 "github.com/docker/distribution/registry/api/v2" 15 ) 16 17 // Client implements the client interface to the registry http api 18 type Client interface { 19 // GetImageManifest returns an image manifest for the image at the given 20 // name, tag pair. 21 GetImageManifest(name, tag string) (*manifest.SignedManifest, error) 22 23 // PutImageManifest uploads an image manifest for the image at the given 24 // name, tag pair. 25 PutImageManifest(name, tag string, imageManifest *manifest.SignedManifest) error 26 27 // DeleteImage removes the image at the given name, tag pair. 28 DeleteImage(name, tag string) error 29 30 // ListImageTags returns a list of all image tags with the given repository 31 // name. 32 ListImageTags(name string) ([]string, error) 33 34 // BlobLength returns the length of the blob stored at the given name, 35 // digest pair. 36 // Returns a length value of -1 on error or if the blob does not exist. 37 BlobLength(name string, dgst digest.Digest) (int, error) 38 39 // GetBlob returns the blob stored at the given name, digest pair in the 40 // form of an io.ReadCloser with the length of this blob. 41 // A nonzero byteOffset can be provided to receive a partial blob beginning 42 // at the given offset. 43 GetBlob(name string, dgst digest.Digest, byteOffset int) (io.ReadCloser, int, error) 44 45 // InitiateBlobUpload starts a blob upload in the given repository namespace 46 // and returns a unique location url to use for other blob upload methods. 47 InitiateBlobUpload(name string) (string, error) 48 49 // GetBlobUploadStatus returns the byte offset and length of the blob at the 50 // given upload location. 51 GetBlobUploadStatus(location string) (int, int, error) 52 53 // UploadBlob uploads a full blob to the registry. 54 UploadBlob(location string, blob io.ReadCloser, length int, dgst digest.Digest) error 55 56 // UploadBlobChunk uploads a blob chunk with a given length and startByte to 57 // the registry. 58 // FinishChunkedBlobUpload must be called to finalize this upload. 59 UploadBlobChunk(location string, blobChunk io.ReadCloser, length, startByte int) error 60 61 // FinishChunkedBlobUpload completes a chunked blob upload at a given 62 // location. 63 FinishChunkedBlobUpload(location string, length int, dgst digest.Digest) error 64 65 // CancelBlobUpload deletes all content at the unfinished blob upload 66 // location and invalidates any future calls to this blob upload. 67 CancelBlobUpload(location string) error 68 } 69 70 var ( 71 patternRangeHeader = regexp.MustCompile("bytes=0-(\\d+)/(\\d+)") 72 ) 73 74 // New returns a new Client which operates against a registry with the 75 // given base endpoint 76 // This endpoint should not include /v2/ or any part of the url after this. 77 func New(endpoint string) (Client, error) { 78 ub, err := v2.NewURLBuilderFromString(endpoint) 79 if err != nil { 80 return nil, err 81 } 82 83 return &clientImpl{ 84 endpoint: endpoint, 85 ub: ub, 86 }, nil 87 } 88 89 // clientImpl is the default implementation of the Client interface 90 type clientImpl struct { 91 endpoint string 92 ub *v2.URLBuilder 93 } 94 95 // TODO(bbland): use consistent route generation between server and client 96 97 func (r *clientImpl) GetImageManifest(name, tag string) (*manifest.SignedManifest, error) { 98 manifestURL, err := r.ub.BuildManifestURL(name, tag) 99 if err != nil { 100 return nil, err 101 } 102 103 response, err := http.Get(manifestURL) 104 if err != nil { 105 return nil, err 106 } 107 defer response.Body.Close() 108 109 // TODO(bbland): handle other status codes, like 5xx errors 110 switch { 111 case response.StatusCode == http.StatusOK: 112 break 113 case response.StatusCode == http.StatusNotFound: 114 return nil, &ImageManifestNotFoundError{Name: name, Tag: tag} 115 case response.StatusCode >= 400 && response.StatusCode < 500: 116 var errs v2.Errors 117 118 decoder := json.NewDecoder(response.Body) 119 err = decoder.Decode(&errs) 120 if err != nil { 121 return nil, err 122 } 123 return nil, &errs 124 default: 125 return nil, &UnexpectedHTTPStatusError{Status: response.Status} 126 } 127 128 decoder := json.NewDecoder(response.Body) 129 130 manifest := new(manifest.SignedManifest) 131 err = decoder.Decode(manifest) 132 if err != nil { 133 return nil, err 134 } 135 return manifest, nil 136 } 137 138 func (r *clientImpl) PutImageManifest(name, tag string, manifest *manifest.SignedManifest) error { 139 manifestURL, err := r.ub.BuildManifestURL(name, tag) 140 if err != nil { 141 return err 142 } 143 144 putRequest, err := http.NewRequest("PUT", manifestURL, bytes.NewReader(manifest.Raw)) 145 if err != nil { 146 return err 147 } 148 149 response, err := http.DefaultClient.Do(putRequest) 150 if err != nil { 151 return err 152 } 153 defer response.Body.Close() 154 155 // TODO(bbland): handle other status codes, like 5xx errors 156 switch { 157 case response.StatusCode == http.StatusOK || response.StatusCode == http.StatusAccepted: 158 return nil 159 case response.StatusCode >= 400 && response.StatusCode < 500: 160 var errors v2.Errors 161 decoder := json.NewDecoder(response.Body) 162 err = decoder.Decode(&errors) 163 if err != nil { 164 return err 165 } 166 167 return &errors 168 default: 169 return &UnexpectedHTTPStatusError{Status: response.Status} 170 } 171 } 172 173 func (r *clientImpl) DeleteImage(name, tag string) error { 174 manifestURL, err := r.ub.BuildManifestURL(name, tag) 175 if err != nil { 176 return err 177 } 178 179 deleteRequest, err := http.NewRequest("DELETE", manifestURL, nil) 180 if err != nil { 181 return err 182 } 183 184 response, err := http.DefaultClient.Do(deleteRequest) 185 if err != nil { 186 return err 187 } 188 defer response.Body.Close() 189 190 // TODO(bbland): handle other status codes, like 5xx errors 191 switch { 192 case response.StatusCode == http.StatusNoContent: 193 break 194 case response.StatusCode == http.StatusNotFound: 195 return &ImageManifestNotFoundError{Name: name, Tag: tag} 196 case response.StatusCode >= 400 && response.StatusCode < 500: 197 var errs v2.Errors 198 decoder := json.NewDecoder(response.Body) 199 err = decoder.Decode(&errs) 200 if err != nil { 201 return err 202 } 203 return &errs 204 default: 205 return &UnexpectedHTTPStatusError{Status: response.Status} 206 } 207 208 return nil 209 } 210 211 func (r *clientImpl) ListImageTags(name string) ([]string, error) { 212 tagsURL, err := r.ub.BuildTagsURL(name) 213 if err != nil { 214 return nil, err 215 } 216 217 response, err := http.Get(tagsURL) 218 if err != nil { 219 return nil, err 220 } 221 defer response.Body.Close() 222 223 // TODO(bbland): handle other status codes, like 5xx errors 224 switch { 225 case response.StatusCode == http.StatusOK: 226 break 227 case response.StatusCode == http.StatusNotFound: 228 return nil, &RepositoryNotFoundError{Name: name} 229 case response.StatusCode >= 400 && response.StatusCode < 500: 230 var errs v2.Errors 231 decoder := json.NewDecoder(response.Body) 232 err = decoder.Decode(&errs) 233 if err != nil { 234 return nil, err 235 } 236 return nil, &errs 237 default: 238 return nil, &UnexpectedHTTPStatusError{Status: response.Status} 239 } 240 241 tags := struct { 242 Tags []string `json:"tags"` 243 }{} 244 245 decoder := json.NewDecoder(response.Body) 246 err = decoder.Decode(&tags) 247 if err != nil { 248 return nil, err 249 } 250 251 return tags.Tags, nil 252 } 253 254 func (r *clientImpl) BlobLength(name string, dgst digest.Digest) (int, error) { 255 blobURL, err := r.ub.BuildBlobURL(name, dgst) 256 if err != nil { 257 return -1, err 258 } 259 260 response, err := http.Head(blobURL) 261 if err != nil { 262 return -1, err 263 } 264 defer response.Body.Close() 265 266 // TODO(bbland): handle other status codes, like 5xx errors 267 switch { 268 case response.StatusCode == http.StatusOK: 269 lengthHeader := response.Header.Get("Content-Length") 270 length, err := strconv.ParseInt(lengthHeader, 10, 64) 271 if err != nil { 272 return -1, err 273 } 274 return int(length), nil 275 case response.StatusCode == http.StatusNotFound: 276 return -1, nil 277 case response.StatusCode >= 400 && response.StatusCode < 500: 278 var errs v2.Errors 279 decoder := json.NewDecoder(response.Body) 280 err = decoder.Decode(&errs) 281 if err != nil { 282 return -1, err 283 } 284 return -1, &errs 285 default: 286 return -1, &UnexpectedHTTPStatusError{Status: response.Status} 287 } 288 } 289 290 func (r *clientImpl) GetBlob(name string, dgst digest.Digest, byteOffset int) (io.ReadCloser, int, error) { 291 blobURL, err := r.ub.BuildBlobURL(name, dgst) 292 if err != nil { 293 return nil, 0, err 294 } 295 296 getRequest, err := http.NewRequest("GET", blobURL, nil) 297 if err != nil { 298 return nil, 0, err 299 } 300 301 getRequest.Header.Add("Range", fmt.Sprintf("%d-", byteOffset)) 302 response, err := http.DefaultClient.Do(getRequest) 303 if err != nil { 304 return nil, 0, err 305 } 306 307 // TODO(bbland): handle other status codes, like 5xx errors 308 switch { 309 case response.StatusCode == http.StatusOK: 310 lengthHeader := response.Header.Get("Content-Length") 311 length, err := strconv.ParseInt(lengthHeader, 10, 0) 312 if err != nil { 313 return nil, 0, err 314 } 315 return response.Body, int(length), nil 316 case response.StatusCode == http.StatusNotFound: 317 response.Body.Close() 318 return nil, 0, &BlobNotFoundError{Name: name, Digest: dgst} 319 case response.StatusCode >= 400 && response.StatusCode < 500: 320 var errs v2.Errors 321 decoder := json.NewDecoder(response.Body) 322 err = decoder.Decode(&errs) 323 if err != nil { 324 return nil, 0, err 325 } 326 return nil, 0, &errs 327 default: 328 response.Body.Close() 329 return nil, 0, &UnexpectedHTTPStatusError{Status: response.Status} 330 } 331 } 332 333 func (r *clientImpl) InitiateBlobUpload(name string) (string, error) { 334 uploadURL, err := r.ub.BuildBlobUploadURL(name) 335 if err != nil { 336 return "", err 337 } 338 339 postRequest, err := http.NewRequest("POST", uploadURL, nil) 340 if err != nil { 341 return "", err 342 } 343 344 response, err := http.DefaultClient.Do(postRequest) 345 if err != nil { 346 return "", err 347 } 348 defer response.Body.Close() 349 350 // TODO(bbland): handle other status codes, like 5xx errors 351 switch { 352 case response.StatusCode == http.StatusAccepted: 353 return response.Header.Get("Location"), nil 354 // case response.StatusCode == http.StatusNotFound: 355 // return 356 case response.StatusCode >= 400 && response.StatusCode < 500: 357 var errs v2.Errors 358 decoder := json.NewDecoder(response.Body) 359 err = decoder.Decode(&errs) 360 if err != nil { 361 return "", err 362 } 363 return "", &errs 364 default: 365 return "", &UnexpectedHTTPStatusError{Status: response.Status} 366 } 367 } 368 369 func (r *clientImpl) GetBlobUploadStatus(location string) (int, int, error) { 370 response, err := http.Get(location) 371 if err != nil { 372 return 0, 0, err 373 } 374 defer response.Body.Close() 375 376 // TODO(bbland): handle other status codes, like 5xx errors 377 switch { 378 case response.StatusCode == http.StatusNoContent: 379 return parseRangeHeader(response.Header.Get("Range")) 380 case response.StatusCode == http.StatusNotFound: 381 return 0, 0, &BlobUploadNotFoundError{Location: location} 382 case response.StatusCode >= 400 && response.StatusCode < 500: 383 var errs v2.Errors 384 decoder := json.NewDecoder(response.Body) 385 err = decoder.Decode(&errs) 386 if err != nil { 387 return 0, 0, err 388 } 389 return 0, 0, &errs 390 default: 391 return 0, 0, &UnexpectedHTTPStatusError{Status: response.Status} 392 } 393 } 394 395 func (r *clientImpl) UploadBlob(location string, blob io.ReadCloser, length int, dgst digest.Digest) error { 396 defer blob.Close() 397 398 putRequest, err := http.NewRequest("PUT", location, blob) 399 if err != nil { 400 return err 401 } 402 403 values := putRequest.URL.Query() 404 values.Set("digest", dgst.String()) 405 putRequest.URL.RawQuery = values.Encode() 406 407 putRequest.Header.Set("Content-Type", "application/octet-stream") 408 putRequest.Header.Set("Content-Length", fmt.Sprint(length)) 409 410 response, err := http.DefaultClient.Do(putRequest) 411 if err != nil { 412 return err 413 } 414 defer response.Body.Close() 415 416 // TODO(bbland): handle other status codes, like 5xx errors 417 switch { 418 case response.StatusCode == http.StatusCreated: 419 return nil 420 case response.StatusCode == http.StatusNotFound: 421 return &BlobUploadNotFoundError{Location: location} 422 case response.StatusCode >= 400 && response.StatusCode < 500: 423 var errs v2.Errors 424 decoder := json.NewDecoder(response.Body) 425 err = decoder.Decode(&errs) 426 if err != nil { 427 return err 428 } 429 return &errs 430 default: 431 return &UnexpectedHTTPStatusError{Status: response.Status} 432 } 433 } 434 435 func (r *clientImpl) UploadBlobChunk(location string, blobChunk io.ReadCloser, length, startByte int) error { 436 defer blobChunk.Close() 437 438 putRequest, err := http.NewRequest("PUT", location, blobChunk) 439 if err != nil { 440 return err 441 } 442 443 endByte := startByte + length 444 445 putRequest.Header.Set("Content-Type", "application/octet-stream") 446 putRequest.Header.Set("Content-Length", fmt.Sprint(length)) 447 putRequest.Header.Set("Content-Range", 448 fmt.Sprintf("%d-%d/%d", startByte, endByte, endByte)) 449 450 response, err := http.DefaultClient.Do(putRequest) 451 if err != nil { 452 return err 453 } 454 defer response.Body.Close() 455 456 // TODO(bbland): handle other status codes, like 5xx errors 457 switch { 458 case response.StatusCode == http.StatusAccepted: 459 return nil 460 case response.StatusCode == http.StatusRequestedRangeNotSatisfiable: 461 lastValidRange, blobSize, err := parseRangeHeader(response.Header.Get("Range")) 462 if err != nil { 463 return err 464 } 465 return &BlobUploadInvalidRangeError{ 466 Location: location, 467 LastValidRange: lastValidRange, 468 BlobSize: blobSize, 469 } 470 case response.StatusCode == http.StatusNotFound: 471 return &BlobUploadNotFoundError{Location: location} 472 case response.StatusCode >= 400 && response.StatusCode < 500: 473 var errs v2.Errors 474 decoder := json.NewDecoder(response.Body) 475 err = decoder.Decode(&errs) 476 if err != nil { 477 return err 478 } 479 return &errs 480 default: 481 return &UnexpectedHTTPStatusError{Status: response.Status} 482 } 483 } 484 485 func (r *clientImpl) FinishChunkedBlobUpload(location string, length int, dgst digest.Digest) error { 486 putRequest, err := http.NewRequest("PUT", location, nil) 487 if err != nil { 488 return err 489 } 490 491 values := putRequest.URL.Query() 492 values.Set("digest", dgst.String()) 493 putRequest.URL.RawQuery = values.Encode() 494 495 putRequest.Header.Set("Content-Type", "application/octet-stream") 496 putRequest.Header.Set("Content-Length", "0") 497 putRequest.Header.Set("Content-Range", 498 fmt.Sprintf("%d-%d/%d", length, length, length)) 499 500 response, err := http.DefaultClient.Do(putRequest) 501 if err != nil { 502 return err 503 } 504 defer response.Body.Close() 505 506 // TODO(bbland): handle other status codes, like 5xx errors 507 switch { 508 case response.StatusCode == http.StatusCreated: 509 return nil 510 case response.StatusCode == http.StatusNotFound: 511 return &BlobUploadNotFoundError{Location: location} 512 case response.StatusCode >= 400 && response.StatusCode < 500: 513 var errs v2.Errors 514 decoder := json.NewDecoder(response.Body) 515 err = decoder.Decode(&errs) 516 if err != nil { 517 return err 518 } 519 return &errs 520 default: 521 return &UnexpectedHTTPStatusError{Status: response.Status} 522 } 523 } 524 525 func (r *clientImpl) CancelBlobUpload(location string) error { 526 deleteRequest, err := http.NewRequest("DELETE", location, nil) 527 if err != nil { 528 return err 529 } 530 531 response, err := http.DefaultClient.Do(deleteRequest) 532 if err != nil { 533 return err 534 } 535 defer response.Body.Close() 536 537 // TODO(bbland): handle other status codes, like 5xx errors 538 switch { 539 case response.StatusCode == http.StatusNoContent: 540 return nil 541 case response.StatusCode == http.StatusNotFound: 542 return &BlobUploadNotFoundError{Location: location} 543 case response.StatusCode >= 400 && response.StatusCode < 500: 544 var errs v2.Errors 545 decoder := json.NewDecoder(response.Body) 546 err = decoder.Decode(&errs) 547 if err != nil { 548 return err 549 } 550 return &errs 551 default: 552 return &UnexpectedHTTPStatusError{Status: response.Status} 553 } 554 } 555 556 // parseRangeHeader parses out the offset and length from a returned Range 557 // header 558 func parseRangeHeader(byteRangeHeader string) (int, int, error) { 559 submatches := patternRangeHeader.FindStringSubmatch(byteRangeHeader) 560 if submatches == nil || len(submatches) < 3 { 561 return 0, 0, fmt.Errorf("Malformed Range header") 562 } 563 564 offset, err := strconv.Atoi(submatches[1]) 565 if err != nil { 566 return 0, 0, err 567 } 568 length, err := strconv.Atoi(submatches[2]) 569 if err != nil { 570 return 0, 0, err 571 } 572 return offset, length, nil 573 }