github.com/npaton/distribution@v2.3.1-rc.0+incompatible/registry/storage/driver/gcs/gcs.go (about) 1 // Package gcs provides a storagedriver.StorageDriver implementation to 2 // store blobs in Google cloud storage. 3 // 4 // This package leverages the google.golang.org/cloud/storage client library 5 //for interfacing with gcs. 6 // 7 // Because gcs is a key, value store the Stat call does not support last modification 8 // time for directories (directories are an abstraction for key, value stores) 9 // 10 // Keep in mind that gcs guarantees only eventual consistency, so do not assume 11 // that a successful write will mean immediate access to the data written (although 12 // in most regions a new object put has guaranteed read after write). The only true 13 // guarantee is that once you call Stat and receive a certain file size, that much of 14 // the file is already accessible. 15 // 16 // +build include_gcs 17 18 package gcs 19 20 import ( 21 "bytes" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "math/rand" 26 "net/http" 27 "net/url" 28 "sort" 29 "strings" 30 "time" 31 32 "golang.org/x/net/context" 33 "golang.org/x/oauth2" 34 "golang.org/x/oauth2/google" 35 "golang.org/x/oauth2/jwt" 36 "google.golang.org/api/googleapi" 37 storageapi "google.golang.org/api/storage/v1" 38 "google.golang.org/cloud" 39 "google.golang.org/cloud/storage" 40 41 ctx "github.com/docker/distribution/context" 42 storagedriver "github.com/docker/distribution/registry/storage/driver" 43 "github.com/docker/distribution/registry/storage/driver/base" 44 "github.com/docker/distribution/registry/storage/driver/factory" 45 ) 46 47 const driverName = "gcs" 48 const dummyProjectID = "<unknown>" 49 50 // driverParameters is a struct that encapsulates all of the driver parameters after all values have been set 51 type driverParameters struct { 52 bucket string 53 config *jwt.Config 54 email string 55 privateKey []byte 56 client *http.Client 57 rootDirectory string 58 } 59 60 func init() { 61 factory.Register(driverName, &gcsDriverFactory{}) 62 } 63 64 // gcsDriverFactory implements the factory.StorageDriverFactory interface 65 type gcsDriverFactory struct{} 66 67 // Create StorageDriver from parameters 68 func (factory *gcsDriverFactory) Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) { 69 return FromParameters(parameters) 70 } 71 72 // driver is a storagedriver.StorageDriver implementation backed by GCS 73 // Objects are stored at absolute keys in the provided bucket. 74 type driver struct { 75 client *http.Client 76 bucket string 77 email string 78 privateKey []byte 79 rootDirectory string 80 } 81 82 // FromParameters constructs a new Driver with a given parameters map 83 // Required parameters: 84 // - bucket 85 func FromParameters(parameters map[string]interface{}) (storagedriver.StorageDriver, error) { 86 bucket, ok := parameters["bucket"] 87 if !ok || fmt.Sprint(bucket) == "" { 88 return nil, fmt.Errorf("No bucket parameter provided") 89 } 90 91 rootDirectory, ok := parameters["rootdirectory"] 92 if !ok { 93 rootDirectory = "" 94 } 95 96 var ts oauth2.TokenSource 97 jwtConf := new(jwt.Config) 98 if keyfile, ok := parameters["keyfile"]; ok { 99 jsonKey, err := ioutil.ReadFile(fmt.Sprint(keyfile)) 100 if err != nil { 101 return nil, err 102 } 103 jwtConf, err = google.JWTConfigFromJSON(jsonKey, storage.ScopeFullControl) 104 if err != nil { 105 return nil, err 106 } 107 ts = jwtConf.TokenSource(context.Background()) 108 } else { 109 var err error 110 ts, err = google.DefaultTokenSource(context.Background(), storage.ScopeFullControl) 111 if err != nil { 112 return nil, err 113 } 114 115 } 116 117 params := driverParameters{ 118 bucket: fmt.Sprint(bucket), 119 rootDirectory: fmt.Sprint(rootDirectory), 120 email: jwtConf.Email, 121 privateKey: jwtConf.PrivateKey, 122 client: oauth2.NewClient(context.Background(), ts), 123 } 124 125 return New(params) 126 } 127 128 // New constructs a new driver 129 func New(params driverParameters) (storagedriver.StorageDriver, error) { 130 rootDirectory := strings.Trim(params.rootDirectory, "/") 131 if rootDirectory != "" { 132 rootDirectory += "/" 133 } 134 d := &driver{ 135 bucket: params.bucket, 136 rootDirectory: rootDirectory, 137 email: params.email, 138 privateKey: params.privateKey, 139 client: params.client, 140 } 141 142 return &base.Base{ 143 StorageDriver: d, 144 }, nil 145 } 146 147 // Implement the storagedriver.StorageDriver interface 148 149 func (d *driver) Name() string { 150 return driverName 151 } 152 153 // GetContent retrieves the content stored at "path" as a []byte. 154 // This should primarily be used for small objects. 155 func (d *driver) GetContent(context ctx.Context, path string) ([]byte, error) { 156 rc, err := d.ReadStream(context, path, 0) 157 if err != nil { 158 return nil, err 159 } 160 defer rc.Close() 161 162 p, err := ioutil.ReadAll(rc) 163 if err != nil { 164 return nil, err 165 } 166 return p, nil 167 } 168 169 // PutContent stores the []byte content at a location designated by "path". 170 // This should primarily be used for small objects. 171 func (d *driver) PutContent(context ctx.Context, path string, contents []byte) error { 172 wc := storage.NewWriter(d.context(context), d.bucket, d.pathToKey(path)) 173 wc.ContentType = "application/octet-stream" 174 defer wc.Close() 175 _, err := wc.Write(contents) 176 return err 177 } 178 179 // ReadStream retrieves an io.ReadCloser for the content stored at "path" 180 // with a given byte offset. 181 // May be used to resume reading a stream by providing a nonzero offset. 182 func (d *driver) ReadStream(context ctx.Context, path string, offset int64) (io.ReadCloser, error) { 183 name := d.pathToKey(path) 184 185 // copied from google.golang.org/cloud/storage#NewReader : 186 // to set the additional "Range" header 187 u := &url.URL{ 188 Scheme: "https", 189 Host: "storage.googleapis.com", 190 Path: fmt.Sprintf("/%s/%s", d.bucket, name), 191 } 192 req, err := http.NewRequest("GET", u.String(), nil) 193 if err != nil { 194 return nil, err 195 } 196 if offset > 0 { 197 req.Header.Set("Range", fmt.Sprintf("bytes=%v-", offset)) 198 } 199 res, err := d.client.Do(req) 200 if err != nil { 201 return nil, err 202 } 203 if res.StatusCode == http.StatusNotFound { 204 res.Body.Close() 205 return nil, storagedriver.PathNotFoundError{Path: path} 206 } 207 if res.StatusCode == http.StatusRequestedRangeNotSatisfiable { 208 res.Body.Close() 209 obj, err := storageStatObject(d.context(context), d.bucket, name) 210 if err != nil { 211 return nil, err 212 } 213 if offset == int64(obj.Size) { 214 return ioutil.NopCloser(bytes.NewReader([]byte{})), nil 215 } 216 return nil, storagedriver.InvalidOffsetError{Path: path, Offset: offset} 217 } 218 if res.StatusCode < 200 || res.StatusCode > 299 { 219 res.Body.Close() 220 return nil, fmt.Errorf("storage: can't read object %v/%v, status code: %v", d.bucket, name, res.Status) 221 } 222 return res.Body, nil 223 } 224 225 // WriteStream stores the contents of the provided io.ReadCloser at a 226 // location designated by the given path. 227 // May be used to resume writing a stream by providing a nonzero offset. 228 // The offset must be no larger than the CurrentSize for this path. 229 func (d *driver) WriteStream(context ctx.Context, path string, offset int64, reader io.Reader) (totalRead int64, err error) { 230 if offset < 0 { 231 return 0, storagedriver.InvalidOffsetError{Path: path, Offset: offset} 232 } 233 234 if offset == 0 { 235 return d.writeCompletely(context, path, 0, reader) 236 } 237 238 service, err := storageapi.New(d.client) 239 if err != nil { 240 return 0, err 241 } 242 objService := storageapi.NewObjectsService(service) 243 var obj *storageapi.Object 244 err = retry(5, func() error { 245 o, err := objService.Get(d.bucket, d.pathToKey(path)).Do() 246 obj = o 247 return err 248 }) 249 // obj, err := retry(5, objService.Get(d.bucket, d.pathToKey(path)).Do) 250 if err != nil { 251 return 0, err 252 } 253 254 // cannot append more chunks, so redo from scratch 255 if obj.ComponentCount >= 1023 { 256 return d.writeCompletely(context, path, offset, reader) 257 } 258 259 // skip from reader 260 objSize := int64(obj.Size) 261 nn, err := skip(reader, objSize-offset) 262 if err != nil { 263 return nn, err 264 } 265 266 // Size <= offset 267 partName := fmt.Sprintf("%v#part-%d#", d.pathToKey(path), obj.ComponentCount) 268 gcsContext := d.context(context) 269 wc := storage.NewWriter(gcsContext, d.bucket, partName) 270 wc.ContentType = "application/octet-stream" 271 272 if objSize < offset { 273 err = writeZeros(wc, offset-objSize) 274 if err != nil { 275 wc.CloseWithError(err) 276 return nn, err 277 } 278 } 279 n, err := io.Copy(wc, reader) 280 if err != nil { 281 wc.CloseWithError(err) 282 return nn, err 283 } 284 err = wc.Close() 285 if err != nil { 286 return nn, err 287 } 288 // wc was closed succesfully, so the temporary part exists, schedule it for deletion at the end 289 // of the function 290 defer storageDeleteObject(gcsContext, d.bucket, partName) 291 292 req := &storageapi.ComposeRequest{ 293 Destination: &storageapi.Object{Bucket: obj.Bucket, Name: obj.Name, ContentType: obj.ContentType}, 294 SourceObjects: []*storageapi.ComposeRequestSourceObjects{ 295 { 296 Name: obj.Name, 297 Generation: obj.Generation, 298 }, { 299 Name: partName, 300 Generation: wc.Object().Generation, 301 }}, 302 } 303 304 err = retry(5, func() error { _, err := objService.Compose(d.bucket, obj.Name, req).Do(); return err }) 305 if err == nil { 306 nn = nn + n 307 } 308 309 return nn, err 310 } 311 312 type request func() error 313 314 func retry(maxTries int, req request) error { 315 backoff := time.Second 316 var err error 317 for i := 0; i < maxTries; i++ { 318 err = req() 319 if err == nil { 320 return nil 321 } 322 323 status, ok := err.(*googleapi.Error) 324 if !ok || (status.Code != 429 && status.Code < http.StatusInternalServerError) { 325 return err 326 } 327 328 time.Sleep(backoff - time.Second + (time.Duration(rand.Int31n(1000)) * time.Millisecond)) 329 if i <= 4 { 330 backoff = backoff * 2 331 } 332 } 333 return err 334 } 335 336 func (d *driver) writeCompletely(context ctx.Context, path string, offset int64, reader io.Reader) (totalRead int64, err error) { 337 wc := storage.NewWriter(d.context(context), d.bucket, d.pathToKey(path)) 338 wc.ContentType = "application/octet-stream" 339 defer wc.Close() 340 341 // Copy the first offset bytes of the existing contents 342 // (padded with zeros if needed) into the writer 343 if offset > 0 { 344 existing, err := d.ReadStream(context, path, 0) 345 if err != nil { 346 return 0, err 347 } 348 defer existing.Close() 349 n, err := io.CopyN(wc, existing, offset) 350 if err == io.EOF { 351 err = writeZeros(wc, offset-n) 352 } 353 if err != nil { 354 return 0, err 355 } 356 } 357 return io.Copy(wc, reader) 358 } 359 360 func skip(reader io.Reader, count int64) (int64, error) { 361 if count <= 0 { 362 return 0, nil 363 } 364 return io.CopyN(ioutil.Discard, reader, count) 365 } 366 367 func writeZeros(wc io.Writer, count int64) error { 368 buf := make([]byte, 32*1024) 369 for count > 0 { 370 size := cap(buf) 371 if int64(size) > count { 372 size = int(count) 373 } 374 n, err := wc.Write(buf[0:size]) 375 if err != nil { 376 return err 377 } 378 count = count - int64(n) 379 } 380 return nil 381 } 382 383 // Stat retrieves the FileInfo for the given path, including the current 384 // size in bytes and the creation time. 385 func (d *driver) Stat(context ctx.Context, path string) (storagedriver.FileInfo, error) { 386 var fi storagedriver.FileInfoFields 387 //try to get as file 388 gcsContext := d.context(context) 389 obj, err := storageStatObject(gcsContext, d.bucket, d.pathToKey(path)) 390 if err == nil { 391 fi = storagedriver.FileInfoFields{ 392 Path: path, 393 Size: obj.Size, 394 ModTime: obj.Updated, 395 IsDir: false, 396 } 397 return storagedriver.FileInfoInternal{FileInfoFields: fi}, nil 398 } 399 //try to get as folder 400 dirpath := d.pathToDirKey(path) 401 402 var query *storage.Query 403 query = &storage.Query{} 404 query.Prefix = dirpath 405 query.MaxResults = 1 406 407 objects, err := storageListObjects(gcsContext, d.bucket, query) 408 if err != nil { 409 return nil, err 410 } 411 if len(objects.Results) < 1 { 412 return nil, storagedriver.PathNotFoundError{Path: path} 413 } 414 fi = storagedriver.FileInfoFields{ 415 Path: path, 416 IsDir: true, 417 } 418 obj = objects.Results[0] 419 if obj.Name == dirpath { 420 fi.Size = obj.Size 421 fi.ModTime = obj.Updated 422 } 423 return storagedriver.FileInfoInternal{FileInfoFields: fi}, nil 424 } 425 426 // List returns a list of the objects that are direct descendants of the 427 //given path. 428 func (d *driver) List(context ctx.Context, path string) ([]string, error) { 429 var query *storage.Query 430 query = &storage.Query{} 431 query.Delimiter = "/" 432 query.Prefix = d.pathToDirKey(path) 433 list := make([]string, 0, 64) 434 for { 435 objects, err := storageListObjects(d.context(context), d.bucket, query) 436 if err != nil { 437 return nil, err 438 } 439 for _, object := range objects.Results { 440 // GCS does not guarantee strong consistency between 441 // DELETE and LIST operationsCheck that the object is not deleted, 442 // so filter out any objects with a non-zero time-deleted 443 if object.Deleted.IsZero() { 444 name := object.Name 445 // Ignore objects with names that end with '#' (these are uploaded parts) 446 if name[len(name)-1] != '#' { 447 name = d.keyToPath(name) 448 list = append(list, name) 449 } 450 } 451 } 452 for _, subpath := range objects.Prefixes { 453 subpath = d.keyToPath(subpath) 454 list = append(list, subpath) 455 } 456 query = objects.Next 457 if query == nil { 458 break 459 } 460 } 461 if path != "/" && len(list) == 0 { 462 // Treat empty response as missing directory, since we don't actually 463 // have directories in Google Cloud Storage. 464 return nil, storagedriver.PathNotFoundError{Path: path} 465 } 466 return list, nil 467 } 468 469 // Move moves an object stored at sourcePath to destPath, removing the 470 // original object. 471 func (d *driver) Move(context ctx.Context, sourcePath string, destPath string) error { 472 prefix := d.pathToDirKey(sourcePath) 473 gcsContext := d.context(context) 474 keys, err := d.listAll(gcsContext, prefix) 475 if err != nil { 476 return err 477 } 478 if len(keys) > 0 { 479 destPrefix := d.pathToDirKey(destPath) 480 copies := make([]string, 0, len(keys)) 481 sort.Strings(keys) 482 var err error 483 for _, key := range keys { 484 dest := destPrefix + key[len(prefix):] 485 _, err = storageCopyObject(gcsContext, d.bucket, key, d.bucket, dest, nil) 486 if err == nil { 487 copies = append(copies, dest) 488 } else { 489 break 490 } 491 } 492 // if an error occurred, attempt to cleanup the copies made 493 if err != nil { 494 for i := len(copies) - 1; i >= 0; i-- { 495 _ = storageDeleteObject(gcsContext, d.bucket, copies[i]) 496 } 497 return err 498 } 499 // delete originals 500 for i := len(keys) - 1; i >= 0; i-- { 501 err2 := storageDeleteObject(gcsContext, d.bucket, keys[i]) 502 if err2 != nil { 503 err = err2 504 } 505 } 506 return err 507 } 508 _, err = storageCopyObject(gcsContext, d.bucket, d.pathToKey(sourcePath), d.bucket, d.pathToKey(destPath), nil) 509 if err != nil { 510 if status := err.(*googleapi.Error); status != nil { 511 if status.Code == http.StatusNotFound { 512 return storagedriver.PathNotFoundError{Path: sourcePath} 513 } 514 } 515 return err 516 } 517 return storageDeleteObject(gcsContext, d.bucket, d.pathToKey(sourcePath)) 518 } 519 520 // listAll recursively lists all names of objects stored at "prefix" and its subpaths. 521 func (d *driver) listAll(context context.Context, prefix string) ([]string, error) { 522 list := make([]string, 0, 64) 523 query := &storage.Query{} 524 query.Prefix = prefix 525 query.Versions = false 526 for { 527 objects, err := storageListObjects(d.context(context), d.bucket, query) 528 if err != nil { 529 return nil, err 530 } 531 for _, obj := range objects.Results { 532 // GCS does not guarantee strong consistency between 533 // DELETE and LIST operationsCheck that the object is not deleted, 534 // so filter out any objects with a non-zero time-deleted 535 if obj.Deleted.IsZero() { 536 list = append(list, obj.Name) 537 } 538 } 539 query = objects.Next 540 if query == nil { 541 break 542 } 543 } 544 return list, nil 545 } 546 547 // Delete recursively deletes all objects stored at "path" and its subpaths. 548 func (d *driver) Delete(context ctx.Context, path string) error { 549 prefix := d.pathToDirKey(path) 550 gcsContext := d.context(context) 551 keys, err := d.listAll(gcsContext, prefix) 552 if err != nil { 553 return err 554 } 555 if len(keys) > 0 { 556 sort.Sort(sort.Reverse(sort.StringSlice(keys))) 557 for _, key := range keys { 558 err := storageDeleteObject(gcsContext, d.bucket, key) 559 // GCS only guarantees eventual consistency, so listAll might return 560 // paths that no longer exist. If this happens, just ignore any not 561 // found error 562 if status, ok := err.(*googleapi.Error); ok { 563 if status.Code == http.StatusNotFound { 564 err = nil 565 } 566 } 567 if err != nil { 568 return err 569 } 570 } 571 return nil 572 } 573 err = storageDeleteObject(gcsContext, d.bucket, d.pathToKey(path)) 574 if err != nil { 575 if status := err.(*googleapi.Error); status != nil { 576 if status.Code == http.StatusNotFound { 577 return storagedriver.PathNotFoundError{Path: path} 578 } 579 } 580 } 581 return err 582 } 583 584 func storageDeleteObject(context context.Context, bucket string, name string) error { 585 return retry(5, func() error { 586 return storage.DeleteObject(context, bucket, name) 587 }) 588 } 589 590 func storageStatObject(context context.Context, bucket string, name string) (*storage.Object, error) { 591 var obj *storage.Object 592 err := retry(5, func() error { 593 var err error 594 obj, err = storage.StatObject(context, bucket, name) 595 return err 596 }) 597 return obj, err 598 } 599 600 func storageListObjects(context context.Context, bucket string, q *storage.Query) (*storage.Objects, error) { 601 var objs *storage.Objects 602 err := retry(5, func() error { 603 var err error 604 objs, err = storage.ListObjects(context, bucket, q) 605 return err 606 }) 607 return objs, err 608 } 609 610 func storageCopyObject(context context.Context, srcBucket, srcName string, destBucket, destName string, attrs *storage.ObjectAttrs) (*storage.Object, error) { 611 var obj *storage.Object 612 err := retry(5, func() error { 613 var err error 614 obj, err = storage.CopyObject(context, srcBucket, srcName, destBucket, destName, attrs) 615 return err 616 }) 617 return obj, err 618 } 619 620 // URLFor returns a URL which may be used to retrieve the content stored at 621 // the given path, possibly using the given options. 622 // Returns ErrUnsupportedMethod if this driver has no privateKey 623 func (d *driver) URLFor(context ctx.Context, path string, options map[string]interface{}) (string, error) { 624 if d.privateKey == nil { 625 return "", storagedriver.ErrUnsupportedMethod{} 626 } 627 628 name := d.pathToKey(path) 629 methodString := "GET" 630 method, ok := options["method"] 631 if ok { 632 methodString, ok = method.(string) 633 if !ok || (methodString != "GET" && methodString != "HEAD") { 634 return "", storagedriver.ErrUnsupportedMethod{} 635 } 636 } 637 638 expiresTime := time.Now().Add(20 * time.Minute) 639 expires, ok := options["expiry"] 640 if ok { 641 et, ok := expires.(time.Time) 642 if ok { 643 expiresTime = et 644 } 645 } 646 647 opts := &storage.SignedURLOptions{ 648 GoogleAccessID: d.email, 649 PrivateKey: d.privateKey, 650 Method: methodString, 651 Expires: expiresTime, 652 } 653 return storage.SignedURL(d.bucket, name, opts) 654 } 655 656 func (d *driver) context(context ctx.Context) context.Context { 657 return cloud.WithContext(context, dummyProjectID, d.client) 658 } 659 660 func (d *driver) pathToKey(path string) string { 661 return strings.TrimRight(d.rootDirectory+strings.TrimLeft(path, "/"), "/") 662 } 663 664 func (d *driver) pathToDirKey(path string) string { 665 return d.pathToKey(path) + "/" 666 } 667 668 func (d *driver) keyToPath(key string) string { 669 return "/" + strings.Trim(strings.TrimPrefix(key, d.rootDirectory), "/") 670 }