Hello community, here is the log from the commit of package docker-distribution for openSUSE:Factory checked in at 2016-05-29 03:12:40 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/docker-distribution (Old) and /work/SRC/openSUSE:Factory/.docker-distribution.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "docker-distribution" Changes: -------- --- /work/SRC/openSUSE:Factory/docker-distribution/docker-distribution.changes 2016-04-28 16:59:04.000000000 +0200 +++ /work/SRC/openSUSE:Factory/.docker-distribution.new/docker-distribution.changes 2016-05-29 03:13:38.000000000 +0200 @@ -1,0 +2,20 @@ +Wed May 18 20:13:04 UTC 2016 - msab...@suse.com + +- Updated to v2.4.1. It contains the following fixes: + +93d7624 Preserve author information in schema1 manifests +ba672e8 When a blob upload is committed prevent writing out hashstate in the subsequent close. +96230de Add a test with a missing _manifests directory +c0d3813 Move garbage collect code into storage package +011b7e4 Ensure GC continues marking if _manifests is nonexistent +0a1fcf9 Fix wording for dry-run flag in useage message for garbage collector. +ed02e88 Sorting completed parts by part number for a better accordance with the S3 spec +fd5a404 Add blobWrtiter.Close() call into blobWriter.Commit() +3f538ca add cn-north-1 to valid check +3330cc5 wait for DLO segments to show up when Close()ing the writer +775d096 Use correct media type for config blob in schema2 manifest +64a9727 Only check validity of S3 region if not using custom endpoint +dafb59f Ensure we log io.Copy errors and bytes copied/total in uploads +431e46a GCS: FileWriter.Size: return offset + buffer size for Writers that are not closed + +------------------------------------------------------------------- Old: ---- distribution-2.4.0.tar.xz New: ---- distribution-2.4.1.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ docker-distribution.spec ++++++ --- /var/tmp/diff_new_pack.S2iMgg/_old 2016-05-29 03:13:39.000000000 +0200 +++ /var/tmp/diff_new_pack.S2iMgg/_new 2016-05-29 03:13:39.000000000 +0200 @@ -17,7 +17,7 @@ Name: docker-distribution -Version: 2.4.0 +Version: 2.4.1 Release: 0 Summary: The Docker toolset to pack, ship, store, and deliver content License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.S2iMgg/_old 2016-05-29 03:13:39.000000000 +0200 +++ /var/tmp/diff_new_pack.S2iMgg/_new 2016-05-29 03:13:39.000000000 +0200 @@ -3,8 +3,8 @@ <param name="url">https://github.com/docker/distribution.git</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="versionformat">2.4.0</param> - <param name="revision">v2.4.0</param> + <param name="versionformat">2.4.1</param> + <param name="revision">v2.4.1</param> </service> <service name="recompress" mode="disabled"> <param name="file">distribution-*.tar</param> ++++++ distribution-2.4.0.tar.xz -> distribution-2.4.1.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/distribution-2.4.0/manifest/schema1/config_builder.go new/distribution-2.4.1/manifest/schema1/config_builder.go --- old/distribution-2.4.0/manifest/schema1/config_builder.go 2016-04-14 15:11:00.000000000 +0200 +++ new/distribution-2.4.1/manifest/schema1/config_builder.go 2016-05-18 22:10:44.000000000 +0200 @@ -110,7 +110,8 @@ ContainerConfig struct { Cmd []string } `json:"container_config,omitempty"` - ThrowAway bool `json:"throwaway,omitempty"` + Author string `json:"author,omitempty"` + ThrowAway bool `json:"throwaway,omitempty"` } fsLayerList := make([]FSLayer, len(img.History)) @@ -145,6 +146,7 @@ Parent: parent, Comment: h.Comment, Created: h.Created, + Author: h.Author, } v1Compatibility.ContainerConfig.Cmd = []string{img.History[i].CreatedBy} if h.EmptyLayer { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/distribution-2.4.0/manifest/schema1/config_builder_test.go new/distribution-2.4.1/manifest/schema1/config_builder_test.go --- old/distribution-2.4.0/manifest/schema1/config_builder_test.go 2016-04-14 15:11:00.000000000 +0200 +++ new/distribution-2.4.1/manifest/schema1/config_builder_test.go 2016-05-18 22:10:44.000000000 +0200 @@ -163,6 +163,7 @@ "empty_layer": true }, { + "author": "Alyssa P. Hacker \u003calysp...@example.com\u003e", "created": "2015-11-04T23:06:32.083868454Z", "created_by": "/bin/sh -c dd if=/dev/zero of=/file bs=1024 count=1024" }, @@ -252,8 +253,8 @@ } expectedV1Compatibility := []string{ - `{"architecture":"amd64","config":{"AttachStderr":false,"AttachStdin":false,"AttachStdout":false,"Cmd":["/bin/sh","-c","echo hi"],"Domainname":"","Entrypoint":null,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","derived=true","asdf=true"],"Hostname":"23304fc829f9","Image":"sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246","Labels":{},"OnBuild":[],"OpenStdin":false,"StdinOnce":false,"Tty":false,"User":"","Volumes":null,"WorkingDir":""},"container":"e91032eb0403a61bfe085ff5a5a48e3659e5a6deae9f4d678daa2ae399d5a001","container_config":{"AttachStderr":false,"AttachStdin":false,"AttachStdout":false,"Cmd":["/bin/sh","-c","#(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]"],"Domainname":"","Entrypoint":null,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","derived=true","asdf=true"],"Hostname":"23304fc829f9","Image":"sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246","Labels":{},"OnBuild":[],"OpenStdin":false,"StdinOnce":false,"Tty":false,"User":"","Volumes":null,"WorkingDir":""},"created":"2015-11-04T23:06:32.365666163Z","docker_version":"1.9.0-dev","id":"0850bfdeb7b060b1004a09099846c2f023a3f2ecbf33f56b4774384b00ce0323","os":"linux","parent":"74cf9c92699240efdba1903c2748ef57105d5bedc588084c4e88f3bb1c3ef0b0","throwaway":true}`, - `{"id":"74cf9c92699240efdba1903c2748ef57105d5bedc588084c4e88f3bb1c3ef0b0","parent":"178be37afc7c49e951abd75525dbe0871b62ad49402f037164ee6314f754599d","created":"2015-11-04T23:06:32.083868454Z","container_config":{"Cmd":["/bin/sh -c dd if=/dev/zero of=/file bs=1024 count=1024"]}}`, + `{"architecture":"amd64","config":{"AttachStderr":false,"AttachStdin":false,"AttachStdout":false,"Cmd":["/bin/sh","-c","echo hi"],"Domainname":"","Entrypoint":null,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","derived=true","asdf=true"],"Hostname":"23304fc829f9","Image":"sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246","Labels":{},"OnBuild":[],"OpenStdin":false,"StdinOnce":false,"Tty":false,"User":"","Volumes":null,"WorkingDir":""},"container":"e91032eb0403a61bfe085ff5a5a48e3659e5a6deae9f4d678daa2ae399d5a001","container_config":{"AttachStderr":false,"AttachStdin":false,"AttachStdout":false,"Cmd":["/bin/sh","-c","#(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]"],"Domainname":"","Entrypoint":null,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","derived=true","asdf=true"],"Hostname":"23304fc829f9","Image":"sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246","Labels":{},"OnBuild":[],"OpenStdin":false,"StdinOnce":false,"Tty":false,"User":"","Volumes":null,"WorkingDir":""},"created":"2015-11-04T23:06:32.365666163Z","docker_version":"1.9.0-dev","id":"69e5c1bfadad697fdb6db59f6326648fa119e0c031a0eda33b8cfadcab54ba7f","os":"linux","parent":"74cf9c92699240efdba1903c2748ef57105d5bedc588084c4e88f3bb1c3ef0b0","throwaway":true}`, + `{"id":"74cf9c92699240efdba1903c2748ef57105d5bedc588084c4e88f3bb1c3ef0b0","parent":"178be37afc7c49e951abd75525dbe0871b62ad49402f037164ee6314f754599d","created":"2015-11-04T23:06:32.083868454Z","container_config":{"Cmd":["/bin/sh -c dd if=/dev/zero of=/file bs=1024 count=1024"]},"author":"Alyssa P. Hacker \u003calysp...@example.com\u003e"}`, `{"id":"178be37afc7c49e951abd75525dbe0871b62ad49402f037164ee6314f754599d","parent":"b449305a55a283538c4574856a8b701f2a3d5ec08ef8aec47f385f20339a4866","created":"2015-11-04T23:06:31.192097572Z","container_config":{"Cmd":["/bin/sh -c #(nop) ENV asdf=true"]},"throwaway":true}`, `{"id":"b449305a55a283538c4574856a8b701f2a3d5ec08ef8aec47f385f20339a4866","parent":"9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e","created":"2015-11-04T23:06:30.934316144Z","container_config":{"Cmd":["/bin/sh -c #(nop) ENV derived=true"]},"throwaway":true}`, `{"id":"9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e","parent":"3690474eb5b4b26fdfbd89c6e159e8cc376ca76ef48032a30fa6aafd56337880","created":"2015-10-31T22:22:55.613815829Z","container_config":{"Cmd":["/bin/sh -c #(nop) CMD [\"sh\"]"]}}`, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/distribution-2.4.0/manifest/schema2/builder.go new/distribution-2.4.1/manifest/schema2/builder.go --- old/distribution-2.4.0/manifest/schema2/builder.go 2016-04-14 15:11:00.000000000 +0200 +++ new/distribution-2.4.1/manifest/schema2/builder.go 2016-05-18 22:10:44.000000000 +0200 @@ -55,6 +55,9 @@ // Add config to the blob store m.Config, err = mb.bs.Put(ctx, MediaTypeConfig, mb.configJSON) + // Override MediaType, since Put always replaces the specified media + // type with application/octet-stream in the descriptor it returns. + m.Config.MediaType = MediaTypeConfig if err != nil { return nil, err } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/distribution-2.4.0/manifest/schema2/builder_test.go new/distribution-2.4.1/manifest/schema2/builder_test.go --- old/distribution-2.4.0/manifest/schema2/builder_test.go 2016-04-14 15:11:00.000000000 +0200 +++ new/distribution-2.4.1/manifest/schema2/builder_test.go 2016-05-18 22:10:44.000000000 +0200 @@ -32,7 +32,7 @@ d := distribution.Descriptor{ Digest: digest.FromBytes(p), Size: int64(len(p)), - MediaType: mediaType, + MediaType: "application/octet-stream", } bs.descriptors[d.Digest] = d return d, nil diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/distribution-2.4.0/registry/garbagecollect.go new/distribution-2.4.1/registry/garbagecollect.go --- old/distribution-2.4.0/registry/garbagecollect.go 2016-04-14 15:11:00.000000000 +0200 +++ new/distribution-2.4.1/registry/garbagecollect.go 1970-01-01 01:00:00.000000000 +0100 @@ -1,187 +0,0 @@ -package registry - -import ( - "fmt" - "os" - - "github.com/docker/distribution" - "github.com/docker/distribution/context" - "github.com/docker/distribution/digest" - "github.com/docker/distribution/manifest/schema1" - "github.com/docker/distribution/manifest/schema2" - "github.com/docker/distribution/reference" - "github.com/docker/distribution/registry/storage" - "github.com/docker/distribution/registry/storage/driver" - "github.com/docker/distribution/registry/storage/driver/factory" - "github.com/docker/libtrust" - "github.com/spf13/cobra" -) - -func emit(format string, a ...interface{}) { - if dryRun { - fmt.Printf(format+"\n", a...) - } -} - -func markAndSweep(ctx context.Context, storageDriver driver.StorageDriver, registry distribution.Namespace) error { - - repositoryEnumerator, ok := registry.(distribution.RepositoryEnumerator) - if !ok { - return fmt.Errorf("unable to convert Namespace to RepositoryEnumerator") - } - - // mark - markSet := make(map[digest.Digest]struct{}) - err := repositoryEnumerator.Enumerate(ctx, func(repoName string) error { - emit(repoName) - - var err error - named, err := reference.ParseNamed(repoName) - if err != nil { - return fmt.Errorf("failed to parse repo name %s: %v", repoName, err) - } - repository, err := registry.Repository(ctx, named) - if err != nil { - return fmt.Errorf("failed to construct repository: %v", err) - } - - manifestService, err := repository.Manifests(ctx) - if err != nil { - return fmt.Errorf("failed to construct manifest service: %v", err) - } - - manifestEnumerator, ok := manifestService.(distribution.ManifestEnumerator) - if !ok { - return fmt.Errorf("unable to convert ManifestService into ManifestEnumerator") - } - - err = manifestEnumerator.Enumerate(ctx, func(dgst digest.Digest) error { - // Mark the manifest's blob - emit("%s: marking manifest %s ", repoName, dgst) - markSet[dgst] = struct{}{} - - manifest, err := manifestService.Get(ctx, dgst) - if err != nil { - return fmt.Errorf("failed to retrieve manifest for digest %v: %v", dgst, err) - } - - descriptors := manifest.References() - for _, descriptor := range descriptors { - markSet[descriptor.Digest] = struct{}{} - emit("%s: marking blob %s", repoName, descriptor.Digest) - } - - switch manifest.(type) { - case *schema1.SignedManifest: - signaturesGetter, ok := manifestService.(distribution.SignaturesGetter) - if !ok { - return fmt.Errorf("unable to convert ManifestService into SignaturesGetter") - } - signatures, err := signaturesGetter.GetSignatures(ctx, dgst) - if err != nil { - return fmt.Errorf("failed to get signatures for signed manifest: %v", err) - } - for _, signatureDigest := range signatures { - emit("%s: marking signature %s", repoName, signatureDigest) - markSet[signatureDigest] = struct{}{} - } - break - case *schema2.DeserializedManifest: - config := manifest.(*schema2.DeserializedManifest).Config - emit("%s: marking configuration %s", repoName, config.Digest) - markSet[config.Digest] = struct{}{} - break - } - - return nil - }) - - return err - }) - - if err != nil { - return fmt.Errorf("failed to mark: %v\n", err) - } - - // sweep - blobService := registry.Blobs() - deleteSet := make(map[digest.Digest]struct{}) - err = blobService.Enumerate(ctx, func(dgst digest.Digest) error { - // check if digest is in markSet. If not, delete it! - if _, ok := markSet[dgst]; !ok { - deleteSet[dgst] = struct{}{} - } - return nil - }) - if err != nil { - return fmt.Errorf("error enumerating blobs: %v", err) - } - - emit("\n%d blobs marked, %d blobs eligible for deletion", len(markSet), len(deleteSet)) - // Construct vacuum - vacuum := storage.NewVacuum(ctx, storageDriver) - for dgst := range deleteSet { - emit("blob eligible for deletion: %s", dgst) - if dryRun { - continue - } - err = vacuum.RemoveBlob(string(dgst)) - if err != nil { - return fmt.Errorf("failed to delete blob %s: %v\n", dgst, err) - } - } - - return err -} - -func init() { - GCCmd.Flags().BoolVarP(&dryRun, "dry-run", "d", false, "do everything expect remove the blobs") -} - -var dryRun bool - -// GCCmd is the cobra command that corresponds to the garbage-collect subcommand -var GCCmd = &cobra.Command{ - Use: "garbage-collect <config>", - Short: "`garbage-collect` deletes layers not referenced by any manifests", - Long: "`garbage-collect` deletes layers not referenced by any manifests", - Run: func(cmd *cobra.Command, args []string) { - config, err := resolveConfiguration(args) - if err != nil { - fmt.Fprintf(os.Stderr, "configuration error: %v\n", err) - cmd.Usage() - os.Exit(1) - } - - driver, err := factory.Create(config.Storage.Type(), config.Storage.Parameters()) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to construct %s driver: %v", config.Storage.Type(), err) - os.Exit(1) - } - - ctx := context.Background() - ctx, err = configureLogging(ctx, config) - if err != nil { - fmt.Fprintf(os.Stderr, "unable to configure logging with config: %s", err) - os.Exit(1) - } - - k, err := libtrust.GenerateECP256PrivateKey() - if err != nil { - fmt.Fprint(os.Stderr, err) - os.Exit(1) - } - - registry, err := storage.NewRegistry(ctx, driver, storage.DisableSchema1Signatures, storage.Schema1SigningKey(k)) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to construct registry: %v", err) - os.Exit(1) - } - - err = markAndSweep(ctx, driver, registry) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to garbage collect: %v", err) - os.Exit(1) - } - }, -} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/distribution-2.4.0/registry/garbagecollect_test.go new/distribution-2.4.1/registry/garbagecollect_test.go --- old/distribution-2.4.0/registry/garbagecollect_test.go 2016-04-14 15:11:00.000000000 +0200 +++ new/distribution-2.4.1/registry/garbagecollect_test.go 1970-01-01 01:00:00.000000000 +0100 @@ -1,343 +0,0 @@ -package registry - -import ( - "io" - "testing" - - "github.com/docker/distribution" - "github.com/docker/distribution/context" - "github.com/docker/distribution/digest" - "github.com/docker/distribution/reference" - "github.com/docker/distribution/registry/storage" - "github.com/docker/distribution/registry/storage/driver" - "github.com/docker/distribution/registry/storage/driver/inmemory" - "github.com/docker/distribution/testutil" -) - -type image struct { - manifest distribution.Manifest - manifestDigest digest.Digest - layers map[digest.Digest]io.ReadSeeker -} - -func createRegistry(t *testing.T, driver driver.StorageDriver) distribution.Namespace { - ctx := context.Background() - registry, err := storage.NewRegistry(ctx, driver, storage.EnableDelete) - if err != nil { - t.Fatalf("Failed to construct namespace") - } - return registry -} - -func makeRepository(t *testing.T, registry distribution.Namespace, name string) distribution.Repository { - ctx := context.Background() - - // Initialize a dummy repository - named, err := reference.ParseNamed(name) - if err != nil { - t.Fatalf("Failed to parse name %s: %v", name, err) - } - - repo, err := registry.Repository(ctx, named) - if err != nil { - t.Fatalf("Failed to construct repository: %v", err) - } - return repo -} - -func makeManifestService(t *testing.T, repository distribution.Repository) distribution.ManifestService { - ctx := context.Background() - - manifestService, err := repository.Manifests(ctx) - if err != nil { - t.Fatalf("Failed to construct manifest store: %v", err) - } - return manifestService -} - -func allBlobs(t *testing.T, registry distribution.Namespace) map[digest.Digest]struct{} { - ctx := context.Background() - blobService := registry.Blobs() - allBlobsMap := make(map[digest.Digest]struct{}) - err := blobService.Enumerate(ctx, func(dgst digest.Digest) error { - allBlobsMap[dgst] = struct{}{} - return nil - }) - if err != nil { - t.Fatalf("Error getting all blobs: %v", err) - } - return allBlobsMap -} - -func uploadImage(t *testing.T, repository distribution.Repository, im image) digest.Digest { - // upload layers - err := testutil.UploadBlobs(repository, im.layers) - if err != nil { - t.Fatalf("layer upload failed: %v", err) - } - - // upload manifest - ctx := context.Background() - manifestService := makeManifestService(t, repository) - manifestDigest, err := manifestService.Put(ctx, im.manifest) - if err != nil { - t.Fatalf("manifest upload failed: %v", err) - } - - return manifestDigest -} - -func uploadRandomSchema1Image(t *testing.T, repository distribution.Repository) image { - randomLayers, err := testutil.CreateRandomLayers(2) - if err != nil { - t.Fatalf("%v", err) - } - - digests := []digest.Digest{} - for digest := range randomLayers { - digests = append(digests, digest) - } - - manifest, err := testutil.MakeSchema1Manifest(digests) - if err != nil { - t.Fatalf("%v", err) - } - - manifestDigest := uploadImage(t, repository, image{manifest: manifest, layers: randomLayers}) - return image{ - manifest: manifest, - manifestDigest: manifestDigest, - layers: randomLayers, - } -} - -func uploadRandomSchema2Image(t *testing.T, repository distribution.Repository) image { - randomLayers, err := testutil.CreateRandomLayers(2) - if err != nil { - t.Fatalf("%v", err) - } - - digests := []digest.Digest{} - for digest := range randomLayers { - digests = append(digests, digest) - } - - manifest, err := testutil.MakeSchema2Manifest(repository, digests) - if err != nil { - t.Fatalf("%v", err) - } - - manifestDigest := uploadImage(t, repository, image{manifest: manifest, layers: randomLayers}) - return image{ - manifest: manifest, - manifestDigest: manifestDigest, - layers: randomLayers, - } -} - -func TestNoDeletionNoEffect(t *testing.T) { - ctx := context.Background() - inmemoryDriver := inmemory.New() - - registry := createRegistry(t, inmemoryDriver) - repo := makeRepository(t, registry, "palailogos") - manifestService, err := repo.Manifests(ctx) - - image1 := uploadRandomSchema1Image(t, repo) - image2 := uploadRandomSchema1Image(t, repo) - image3 := uploadRandomSchema2Image(t, repo) - - // construct manifestlist for fun. - blobstatter := registry.BlobStatter() - manifestList, err := testutil.MakeManifestList(blobstatter, []digest.Digest{ - image1.manifestDigest, image2.manifestDigest}) - if err != nil { - t.Fatalf("Failed to make manifest list: %v", err) - } - - _, err = manifestService.Put(ctx, manifestList) - if err != nil { - t.Fatalf("Failed to add manifest list: %v", err) - } - - // Run GC - err = markAndSweep(context.Background(), inmemoryDriver, registry) - if err != nil { - t.Fatalf("Failed mark and sweep: %v", err) - } - - blobs := allBlobs(t, registry) - - // the +1 at the end is for the manifestList - // the first +3 at the end for each manifest's blob - // the second +3 at the end for each manifest's signature/config layer - totalBlobCount := len(image1.layers) + len(image2.layers) + len(image3.layers) + 1 + 3 + 3 - if len(blobs) != totalBlobCount { - t.Fatalf("Garbage collection affected storage") - } -} - -func TestDeletionHasEffect(t *testing.T) { - ctx := context.Background() - inmemoryDriver := inmemory.New() - - registry := createRegistry(t, inmemoryDriver) - repo := makeRepository(t, registry, "komnenos") - manifests, err := repo.Manifests(ctx) - - image1 := uploadRandomSchema1Image(t, repo) - image2 := uploadRandomSchema1Image(t, repo) - image3 := uploadRandomSchema2Image(t, repo) - - manifests.Delete(ctx, image2.manifestDigest) - manifests.Delete(ctx, image3.manifestDigest) - - // Run GC - err = markAndSweep(context.Background(), inmemoryDriver, registry) - if err != nil { - t.Fatalf("Failed mark and sweep: %v", err) - } - - blobs := allBlobs(t, registry) - - // check that the image1 manifest and all the layers are still in blobs - if _, ok := blobs[image1.manifestDigest]; !ok { - t.Fatalf("First manifest is missing") - } - - for layer := range image1.layers { - if _, ok := blobs[layer]; !ok { - t.Fatalf("manifest 1 layer is missing: %v", layer) - } - } - - // check that image2 and image3 layers are not still around - for layer := range image2.layers { - if _, ok := blobs[layer]; ok { - t.Fatalf("manifest 2 layer is present: %v", layer) - } - } - - for layer := range image3.layers { - if _, ok := blobs[layer]; ok { - t.Fatalf("manifest 3 layer is present: %v", layer) - } - } -} - -func getAnyKey(digests map[digest.Digest]io.ReadSeeker) (d digest.Digest) { - for d = range digests { - break - } - return -} - -func getKeys(digests map[digest.Digest]io.ReadSeeker) (ds []digest.Digest) { - for d := range digests { - ds = append(ds, d) - } - return -} - -func TestDeletionWithSharedLayer(t *testing.T) { - ctx := context.Background() - inmemoryDriver := inmemory.New() - - registry := createRegistry(t, inmemoryDriver) - repo := makeRepository(t, registry, "tzimiskes") - - // Create random layers - randomLayers1, err := testutil.CreateRandomLayers(3) - if err != nil { - t.Fatalf("failed to make layers: %v", err) - } - - randomLayers2, err := testutil.CreateRandomLayers(3) - if err != nil { - t.Fatalf("failed to make layers: %v", err) - } - - // Upload all layers - err = testutil.UploadBlobs(repo, randomLayers1) - if err != nil { - t.Fatalf("failed to upload layers: %v", err) - } - - err = testutil.UploadBlobs(repo, randomLayers2) - if err != nil { - t.Fatalf("failed to upload layers: %v", err) - } - - // Construct manifests - manifest1, err := testutil.MakeSchema1Manifest(getKeys(randomLayers1)) - if err != nil { - t.Fatalf("failed to make manifest: %v", err) - } - - sharedKey := getAnyKey(randomLayers1) - manifest2, err := testutil.MakeSchema2Manifest(repo, append(getKeys(randomLayers2), sharedKey)) - if err != nil { - t.Fatalf("failed to make manifest: %v", err) - } - - manifestService := makeManifestService(t, repo) - - // Upload manifests - _, err = manifestService.Put(ctx, manifest1) - if err != nil { - t.Fatalf("manifest upload failed: %v", err) - } - - manifestDigest2, err := manifestService.Put(ctx, manifest2) - if err != nil { - t.Fatalf("manifest upload failed: %v", err) - } - - // delete - err = manifestService.Delete(ctx, manifestDigest2) - if err != nil { - t.Fatalf("manifest deletion failed: %v", err) - } - - // check that all of the layers in layer 1 are still there - blobs := allBlobs(t, registry) - for dgst := range randomLayers1 { - if _, ok := blobs[dgst]; !ok { - t.Fatalf("random layer 1 blob missing: %v", dgst) - } - } -} - -func TestOrphanBlobDeleted(t *testing.T) { - inmemoryDriver := inmemory.New() - - registry := createRegistry(t, inmemoryDriver) - repo := makeRepository(t, registry, "michael_z_doukas") - - digests, err := testutil.CreateRandomLayers(1) - if err != nil { - t.Fatalf("Failed to create random digest: %v", err) - } - - if err = testutil.UploadBlobs(repo, digests); err != nil { - t.Fatalf("Failed to upload blob: %v", err) - } - - // formality to create the necessary directories - uploadRandomSchema2Image(t, repo) - - // Run GC - err = markAndSweep(context.Background(), inmemoryDriver, registry) - if err != nil { - t.Fatalf("Failed mark and sweep: %v", err) - } - - blobs := allBlobs(t, registry) - - // check that orphan blob layers are not still around - for dgst := range digests { - if _, ok := blobs[dgst]; ok { - t.Fatalf("Orphan layer is present: %v", dgst) - } - } -} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/distribution-2.4.0/registry/handlers/helpers.go new/distribution-2.4.1/registry/handlers/helpers.go --- old/distribution-2.4.0/registry/handlers/helpers.go 2016-04-14 15:11:00.000000000 +0200 +++ new/distribution-2.4.1/registry/handlers/helpers.go 2016-05-18 22:10:44.000000000 +0200 @@ -46,7 +46,11 @@ // instead of showing 0 for the HTTP status. responseWriter.WriteHeader(499) - ctxu.GetLogger(context).Error("client disconnected during " + action) + ctxu.GetLoggerWithFields(context, map[interface{}]interface{}{ + "error": err, + "copied": copied, + "contentLength": r.ContentLength, + }, "error", "copied", "contentLength").Error("client disconnected during " + action) return errors.New("client disconnected") default: } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/distribution-2.4.0/registry/root.go new/distribution-2.4.1/registry/root.go --- old/distribution-2.4.0/registry/root.go 2016-04-14 15:11:00.000000000 +0200 +++ new/distribution-2.4.1/registry/root.go 2016-05-18 22:10:44.000000000 +0200 @@ -1,7 +1,14 @@ package registry import ( + "fmt" + "os" + + "github.com/docker/distribution/context" + "github.com/docker/distribution/registry/storage" + "github.com/docker/distribution/registry/storage/driver/factory" "github.com/docker/distribution/version" + "github.com/docker/libtrust" "github.com/spf13/cobra" ) @@ -10,6 +17,7 @@ func init() { RootCmd.AddCommand(ServeCmd) RootCmd.AddCommand(GCCmd) + GCCmd.Flags().BoolVarP(&dryRun, "dry-run", "d", false, "do everything except remove the blobs") RootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "show the version and exit") } @@ -26,3 +34,51 @@ cmd.Usage() }, } + +var dryRun bool + +// GCCmd is the cobra command that corresponds to the garbage-collect subcommand +var GCCmd = &cobra.Command{ + Use: "garbage-collect <config>", + Short: "`garbage-collect` deletes layers not referenced by any manifests", + Long: "`garbage-collect` deletes layers not referenced by any manifests", + Run: func(cmd *cobra.Command, args []string) { + config, err := resolveConfiguration(args) + if err != nil { + fmt.Fprintf(os.Stderr, "configuration error: %v\n", err) + cmd.Usage() + os.Exit(1) + } + + driver, err := factory.Create(config.Storage.Type(), config.Storage.Parameters()) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to construct %s driver: %v", config.Storage.Type(), err) + os.Exit(1) + } + + ctx := context.Background() + ctx, err = configureLogging(ctx, config) + if err != nil { + fmt.Fprintf(os.Stderr, "unable to configure logging with config: %s", err) + os.Exit(1) + } + + k, err := libtrust.GenerateECP256PrivateKey() + if err != nil { + fmt.Fprint(os.Stderr, err) + os.Exit(1) + } + + registry, err := storage.NewRegistry(ctx, driver, storage.DisableSchema1Signatures, storage.Schema1SigningKey(k)) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to construct registry: %v", err) + os.Exit(1) + } + + err = storage.MarkAndSweep(ctx, driver, registry, dryRun) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to garbage collect: %v", err) + os.Exit(1) + } + }, +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/distribution-2.4.0/registry/storage/blob_test.go new/distribution-2.4.1/registry/storage/blob_test.go --- old/distribution-2.4.0/registry/storage/blob_test.go 2016-04-14 15:11:00.000000000 +0200 +++ new/distribution-2.4.1/registry/storage/blob_test.go 2016-05-18 22:10:44.000000000 +0200 @@ -16,6 +16,7 @@ "github.com/docker/distribution/registry/storage/cache/memory" "github.com/docker/distribution/registry/storage/driver/inmemory" "github.com/docker/distribution/testutil" + "path" ) // TestWriteSeek tests that the current file size can be @@ -83,6 +84,15 @@ t.Fatalf("unexpected error during upload cancellation: %v", err) } + // get the enclosing directory + uploadPath := path.Dir(blobUpload.(*blobWriter).path) + + // ensure state was cleaned up + _, err = driver.List(ctx, uploadPath) + if err == nil { + t.Fatal("files in upload path after cleanup") + } + // Do a resume, get unknown upload blobUpload, err = bs.Resume(ctx, blobUpload.ID()) if err != distribution.ErrBlobUploadUnknown { @@ -128,6 +138,13 @@ t.Fatalf("unexpected error finishing layer upload: %v", err) } + // ensure state was cleaned up + uploadPath = path.Dir(blobUpload.(*blobWriter).path) + _, err = driver.List(ctx, uploadPath) + if err == nil { + t.Fatal("files in upload path after commit") + } + // After finishing an upload, it should no longer exist. if _, err := bs.Resume(ctx, blobUpload.ID()); err != distribution.ErrBlobUploadUnknown { t.Fatalf("expected layer upload to be unknown, got %v", err) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/distribution-2.4.0/registry/storage/blobwriter.go new/distribution-2.4.1/registry/storage/blobwriter.go --- old/distribution-2.4.0/registry/storage/blobwriter.go 2016-04-14 15:11:00.000000000 +0200 +++ new/distribution-2.4.1/registry/storage/blobwriter.go 2016-05-18 22:10:44.000000000 +0200 @@ -18,8 +18,8 @@ errResumableDigestNotAvailable = errors.New("resumable digest not available") ) -// layerWriter is used to control the various aspects of resumable -// layer upload. It implements the LayerUpload interface. +// blobWriter is used to control the various aspects of resumable +// blob upload. type blobWriter struct { ctx context.Context blobStore *linkedBlobStore @@ -34,6 +34,7 @@ path string resumableDigestEnabled bool + committed bool } var _ distribution.BlobWriter = &blobWriter{} @@ -56,6 +57,8 @@ return distribution.Descriptor{}, err } + bw.Close() + canonical, err := bw.validateBlob(ctx, desc) if err != nil { return distribution.Descriptor{}, err @@ -78,6 +81,7 @@ return distribution.Descriptor{}, err } + bw.committed = true return canonical, nil } @@ -89,11 +93,14 @@ return err } + if err := bw.Close(); err != nil { + context.GetLogger(ctx).Errorf("error closing blobwriter: %s", err) + } + if err := bw.removeResources(ctx); err != nil { return err } - bw.Close() return nil } @@ -130,6 +137,10 @@ } func (bw *blobWriter) Close() error { + if bw.committed { + return errors.New("blobwriter close after commit") + } + if err := bw.storeHashState(bw.blobStore.ctx); err != nil { return err } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/distribution-2.4.0/registry/storage/driver/gcs/gcs.go new/distribution-2.4.1/registry/storage/driver/gcs/gcs.go --- old/distribution-2.4.0/registry/storage/driver/gcs/gcs.go 2016-04-14 15:11:00.000000000 +0200 +++ new/distribution-2.4.1/registry/storage/driver/gcs/gcs.go 2016-05-18 22:10:44.000000000 +0200 @@ -493,6 +493,9 @@ // Size returns the number of bytes written to this FileWriter. func (w *writer) Size() int64 { + if !w.closed { + return w.offset + int64(w.buffSize) + } return w.size } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/distribution-2.4.0/registry/storage/driver/s3-aws/s3.go new/distribution-2.4.1/registry/storage/driver/s3-aws/s3.go --- old/distribution-2.4.0/registry/storage/driver/s3-aws/s3.go 2016-04-14 15:11:00.000000000 +0200 +++ new/distribution-2.4.1/registry/storage/driver/s3-aws/s3.go 2016-05-18 22:10:44.000000000 +0200 @@ -18,6 +18,7 @@ "io/ioutil" "net/http" "reflect" + "sort" "strconv" "strings" "time" @@ -80,6 +81,7 @@ "ap-northeast-1", "ap-northeast-2", "sa-east-1", + "cn-north-1", } { validRegions[region] = struct{}{} } @@ -136,14 +138,21 @@ secretKey = "" } + regionEndpoint := parameters["regionendpoint"] + if regionEndpoint == nil { + regionEndpoint = "" + } + regionName, ok := parameters["region"] if regionName == nil || fmt.Sprint(regionName) == "" { return nil, fmt.Errorf("No region parameter provided") } region := fmt.Sprint(regionName) - _, ok = validRegions[region] - if !ok { - return nil, fmt.Errorf("Invalid region provided: %v", region) + // Don't check the region value if a custom endpoint is provided. + if regionEndpoint == "" { + if _, ok = validRegions[region]; !ok { + return nil, fmt.Errorf("Invalid region provided: %v", region) + } } bucket := parameters["bucket"] @@ -151,11 +160,6 @@ return nil, fmt.Errorf("No bucket parameter provided") } - regionEndpoint := parameters["regionendpoint"] - if regionEndpoint == nil { - regionEndpoint = "" - } - encryptBool := false encrypt := parameters["encrypt"] switch encrypt := encrypt.(type) { @@ -716,6 +720,12 @@ } } +type completedParts []*s3.CompletedPart + +func (a completedParts) Len() int { return len(a) } +func (a completedParts) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a completedParts) Less(i, j int) bool { return *a[i].PartNumber < *a[j].PartNumber } + func (w *writer) Write(p []byte) (int, error) { if w.closed { return 0, fmt.Errorf("already closed") @@ -728,19 +738,22 @@ // If the last written part is smaller than minChunkSize, we need to make a // new multipart upload :sadface: if len(w.parts) > 0 && int(*w.parts[len(w.parts)-1].Size) < minChunkSize { - var completedParts []*s3.CompletedPart + var completedUploadedParts completedParts for _, part := range w.parts { - completedParts = append(completedParts, &s3.CompletedPart{ + completedUploadedParts = append(completedUploadedParts, &s3.CompletedPart{ ETag: part.ETag, PartNumber: part.PartNumber, }) } + + sort.Sort(completedUploadedParts) + _, err := w.driver.S3.CompleteMultipartUpload(&s3.CompleteMultipartUploadInput{ Bucket: aws.String(w.driver.Bucket), Key: aws.String(w.key), UploadId: aws.String(w.uploadID), MultipartUpload: &s3.CompletedMultipartUpload{ - Parts: completedParts, + Parts: completedUploadedParts, }, }) if err != nil { @@ -880,19 +893,23 @@ return err } w.committed = true - var completedParts []*s3.CompletedPart + + var completedUploadedParts completedParts for _, part := range w.parts { - completedParts = append(completedParts, &s3.CompletedPart{ + completedUploadedParts = append(completedUploadedParts, &s3.CompletedPart{ ETag: part.ETag, PartNumber: part.PartNumber, }) } + + sort.Sort(completedUploadedParts) + _, err = w.driver.S3.CompleteMultipartUpload(&s3.CompleteMultipartUploadInput{ Bucket: aws.String(w.driver.Bucket), Key: aws.String(w.key), UploadId: aws.String(w.uploadID), MultipartUpload: &s3.CompletedMultipartUpload{ - Parts: completedParts, + Parts: completedUploadedParts, }, }) if err != nil { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/distribution-2.4.0/registry/storage/driver/swift/swift.go new/distribution-2.4.1/registry/storage/driver/swift/swift.go --- old/distribution-2.4.0/registry/storage/driver/swift/swift.go 2016-04-14 15:11:00.000000000 +0200 +++ new/distribution-2.4.1/registry/storage/driver/swift/swift.go 2016-05-18 22:10:44.000000000 +0200 @@ -698,6 +698,9 @@ if err := w.driver.createManifest(w.path, w.driver.Container+"/"+w.segmentsPath); err != nil { return err } + if err := w.waitForSegmentsToShowUp(); err != nil { + return err + } } w.closed = true @@ -732,10 +735,14 @@ } w.committed = true + return w.waitForSegmentsToShowUp() +} +func (w *writer) waitForSegmentsToShowUp() error { var err error waitingTime := readAfterWriteWait endTime := time.Now().Add(readAfterWriteTimeout) + for { var info swift.Object if info, _, err = w.driver.Conn.Object(w.driver.Container, w.driver.swiftPath(w.path)); err == nil { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/distribution-2.4.0/registry/storage/garbagecollect.go new/distribution-2.4.1/registry/storage/garbagecollect.go --- old/distribution-2.4.0/registry/storage/garbagecollect.go 1970-01-01 01:00:00.000000000 +0100 +++ new/distribution-2.4.1/registry/storage/garbagecollect.go 2016-05-18 22:10:44.000000000 +0200 @@ -0,0 +1,150 @@ +package storage + +import ( + "fmt" + + "github.com/docker/distribution" + "github.com/docker/distribution/context" + "github.com/docker/distribution/digest" + "github.com/docker/distribution/manifest/schema1" + "github.com/docker/distribution/manifest/schema2" + "github.com/docker/distribution/reference" + "github.com/docker/distribution/registry/storage/driver" +) + +func emit(format string, a ...interface{}) { + fmt.Printf(format+"\n", a...) +} + +// MarkAndSweep performs a mark and sweep of registry data +func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, registry distribution.Namespace, dryRun bool) error { + repositoryEnumerator, ok := registry.(distribution.RepositoryEnumerator) + if !ok { + return fmt.Errorf("unable to convert Namespace to RepositoryEnumerator") + } + + // mark + markSet := make(map[digest.Digest]struct{}) + err := repositoryEnumerator.Enumerate(ctx, func(repoName string) error { + if dryRun { + emit(repoName) + } + + var err error + named, err := reference.ParseNamed(repoName) + if err != nil { + return fmt.Errorf("failed to parse repo name %s: %v", repoName, err) + } + repository, err := registry.Repository(ctx, named) + if err != nil { + return fmt.Errorf("failed to construct repository: %v", err) + } + + manifestService, err := repository.Manifests(ctx) + if err != nil { + return fmt.Errorf("failed to construct manifest service: %v", err) + } + + manifestEnumerator, ok := manifestService.(distribution.ManifestEnumerator) + if !ok { + return fmt.Errorf("unable to convert ManifestService into ManifestEnumerator") + } + + err = manifestEnumerator.Enumerate(ctx, func(dgst digest.Digest) error { + // Mark the manifest's blob + if dryRun { + emit("%s: marking manifest %s ", repoName, dgst) + } + markSet[dgst] = struct{}{} + + manifest, err := manifestService.Get(ctx, dgst) + if err != nil { + return fmt.Errorf("failed to retrieve manifest for digest %v: %v", dgst, err) + } + + descriptors := manifest.References() + for _, descriptor := range descriptors { + markSet[descriptor.Digest] = struct{}{} + if dryRun { + emit("%s: marking blob %s", repoName, descriptor.Digest) + } + } + + switch manifest.(type) { + case *schema1.SignedManifest: + signaturesGetter, ok := manifestService.(distribution.SignaturesGetter) + if !ok { + return fmt.Errorf("unable to convert ManifestService into SignaturesGetter") + } + signatures, err := signaturesGetter.GetSignatures(ctx, dgst) + if err != nil { + return fmt.Errorf("failed to get signatures for signed manifest: %v", err) + } + for _, signatureDigest := range signatures { + if dryRun { + emit("%s: marking signature %s", repoName, signatureDigest) + } + markSet[signatureDigest] = struct{}{} + } + break + case *schema2.DeserializedManifest: + config := manifest.(*schema2.DeserializedManifest).Config + if dryRun { + emit("%s: marking configuration %s", repoName, config.Digest) + } + markSet[config.Digest] = struct{}{} + break + } + + return nil + }) + + if err != nil { + // In certain situations such as unfinished uploads, deleting all + // tags in S3 or removing the _manifests folder manually, this + // error may be of type PathNotFound. + // + // In these cases we can continue marking other manifests safely. + if _, ok := err.(driver.PathNotFoundError); ok { + return nil + } + } + + return err + }) + + if err != nil { + return fmt.Errorf("failed to mark: %v\n", err) + } + + // sweep + blobService := registry.Blobs() + deleteSet := make(map[digest.Digest]struct{}) + err = blobService.Enumerate(ctx, func(dgst digest.Digest) error { + // check if digest is in markSet. If not, delete it! + if _, ok := markSet[dgst]; !ok { + deleteSet[dgst] = struct{}{} + } + return nil + }) + if err != nil { + return fmt.Errorf("error enumerating blobs: %v", err) + } + if dryRun { + emit("\n%d blobs marked, %d blobs eligible for deletion", len(markSet), len(deleteSet)) + } + // Construct vacuum + vacuum := NewVacuum(ctx, storageDriver) + for dgst := range deleteSet { + if dryRun { + emit("blob eligible for deletion: %s", dgst) + continue + } + err = vacuum.RemoveBlob(string(dgst)) + if err != nil { + return fmt.Errorf("failed to delete blob %s: %v\n", dgst, err) + } + } + + return err +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/distribution-2.4.0/registry/storage/garbagecollect_test.go new/distribution-2.4.1/registry/storage/garbagecollect_test.go --- old/distribution-2.4.0/registry/storage/garbagecollect_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/distribution-2.4.1/registry/storage/garbagecollect_test.go 2016-05-18 22:10:44.000000000 +0200 @@ -0,0 +1,374 @@ +package storage + +import ( + "io" + "path" + "testing" + + "github.com/docker/distribution" + "github.com/docker/distribution/context" + "github.com/docker/distribution/digest" + "github.com/docker/distribution/reference" + "github.com/docker/distribution/registry/storage/driver" + "github.com/docker/distribution/registry/storage/driver/inmemory" + "github.com/docker/distribution/testutil" +) + +type image struct { + manifest distribution.Manifest + manifestDigest digest.Digest + layers map[digest.Digest]io.ReadSeeker +} + +func createRegistry(t *testing.T, driver driver.StorageDriver) distribution.Namespace { + ctx := context.Background() + registry, err := NewRegistry(ctx, driver, EnableDelete) + if err != nil { + t.Fatalf("Failed to construct namespace") + } + return registry +} + +func makeRepository(t *testing.T, registry distribution.Namespace, name string) distribution.Repository { + ctx := context.Background() + + // Initialize a dummy repository + named, err := reference.ParseNamed(name) + if err != nil { + t.Fatalf("Failed to parse name %s: %v", name, err) + } + + repo, err := registry.Repository(ctx, named) + if err != nil { + t.Fatalf("Failed to construct repository: %v", err) + } + return repo +} + +func makeManifestService(t *testing.T, repository distribution.Repository) distribution.ManifestService { + ctx := context.Background() + + manifestService, err := repository.Manifests(ctx) + if err != nil { + t.Fatalf("Failed to construct manifest store: %v", err) + } + return manifestService +} + +func allBlobs(t *testing.T, registry distribution.Namespace) map[digest.Digest]struct{} { + ctx := context.Background() + blobService := registry.Blobs() + allBlobsMap := make(map[digest.Digest]struct{}) + err := blobService.Enumerate(ctx, func(dgst digest.Digest) error { + allBlobsMap[dgst] = struct{}{} + return nil + }) + if err != nil { + t.Fatalf("Error getting all blobs: %v", err) + } + return allBlobsMap +} + +func uploadImage(t *testing.T, repository distribution.Repository, im image) digest.Digest { + // upload layers + err := testutil.UploadBlobs(repository, im.layers) + if err != nil { + t.Fatalf("layer upload failed: %v", err) + } + + // upload manifest + ctx := context.Background() + manifestService := makeManifestService(t, repository) + manifestDigest, err := manifestService.Put(ctx, im.manifest) + if err != nil { + t.Fatalf("manifest upload failed: %v", err) + } + + return manifestDigest +} + +func uploadRandomSchema1Image(t *testing.T, repository distribution.Repository) image { + randomLayers, err := testutil.CreateRandomLayers(2) + if err != nil { + t.Fatalf("%v", err) + } + + digests := []digest.Digest{} + for digest := range randomLayers { + digests = append(digests, digest) + } + + manifest, err := testutil.MakeSchema1Manifest(digests) + if err != nil { + t.Fatalf("%v", err) + } + + manifestDigest := uploadImage(t, repository, image{manifest: manifest, layers: randomLayers}) + return image{ + manifest: manifest, + manifestDigest: manifestDigest, + layers: randomLayers, + } +} + +func uploadRandomSchema2Image(t *testing.T, repository distribution.Repository) image { + randomLayers, err := testutil.CreateRandomLayers(2) + if err != nil { + t.Fatalf("%v", err) + } + + digests := []digest.Digest{} + for digest := range randomLayers { + digests = append(digests, digest) + } + + manifest, err := testutil.MakeSchema2Manifest(repository, digests) + if err != nil { + t.Fatalf("%v", err) + } + + manifestDigest := uploadImage(t, repository, image{manifest: manifest, layers: randomLayers}) + return image{ + manifest: manifest, + manifestDigest: manifestDigest, + layers: randomLayers, + } +} + +func TestNoDeletionNoEffect(t *testing.T) { + ctx := context.Background() + inmemoryDriver := inmemory.New() + + registry := createRegistry(t, inmemoryDriver) + repo := makeRepository(t, registry, "palailogos") + manifestService, err := repo.Manifests(ctx) + + image1 := uploadRandomSchema1Image(t, repo) + image2 := uploadRandomSchema1Image(t, repo) + image3 := uploadRandomSchema2Image(t, repo) + + // construct manifestlist for fun. + blobstatter := registry.BlobStatter() + manifestList, err := testutil.MakeManifestList(blobstatter, []digest.Digest{ + image1.manifestDigest, image2.manifestDigest}) + if err != nil { + t.Fatalf("Failed to make manifest list: %v", err) + } + + _, err = manifestService.Put(ctx, manifestList) + if err != nil { + t.Fatalf("Failed to add manifest list: %v", err) + } + + // Run GC + err = MarkAndSweep(context.Background(), inmemoryDriver, registry, false) + if err != nil { + t.Fatalf("Failed mark and sweep: %v", err) + } + + blobs := allBlobs(t, registry) + + // the +1 at the end is for the manifestList + // the first +3 at the end for each manifest's blob + // the second +3 at the end for each manifest's signature/config layer + totalBlobCount := len(image1.layers) + len(image2.layers) + len(image3.layers) + 1 + 3 + 3 + if len(blobs) != totalBlobCount { + t.Fatalf("Garbage collection affected storage") + } +} + +func TestGCWithMissingManifests(t *testing.T) { + ctx := context.Background() + d := inmemory.New() + + registry := createRegistry(t, d) + repo := makeRepository(t, registry, "testrepo") + uploadRandomSchema1Image(t, repo) + + // Simulate a missing _manifests directory + revPath, err := pathFor(manifestRevisionsPathSpec{"testrepo"}) + if err != nil { + t.Fatal(err) + } + + _manifestsPath := path.Dir(revPath) + err = d.Delete(ctx, _manifestsPath) + if err != nil { + t.Fatal(err) + } + + err = MarkAndSweep(context.Background(), d, registry, false) + if err != nil { + t.Fatalf("Failed mark and sweep: %v", err) + } + + blobs := allBlobs(t, registry) + if len(blobs) > 0 { + t.Errorf("unexpected blobs after gc") + } +} + +func TestDeletionHasEffect(t *testing.T) { + ctx := context.Background() + inmemoryDriver := inmemory.New() + + registry := createRegistry(t, inmemoryDriver) + repo := makeRepository(t, registry, "komnenos") + manifests, err := repo.Manifests(ctx) + + image1 := uploadRandomSchema1Image(t, repo) + image2 := uploadRandomSchema1Image(t, repo) + image3 := uploadRandomSchema2Image(t, repo) + + manifests.Delete(ctx, image2.manifestDigest) + manifests.Delete(ctx, image3.manifestDigest) + + // Run GC + err = MarkAndSweep(context.Background(), inmemoryDriver, registry, false) + if err != nil { + t.Fatalf("Failed mark and sweep: %v", err) + } + + blobs := allBlobs(t, registry) + + // check that the image1 manifest and all the layers are still in blobs + if _, ok := blobs[image1.manifestDigest]; !ok { + t.Fatalf("First manifest is missing") + } + + for layer := range image1.layers { + if _, ok := blobs[layer]; !ok { + t.Fatalf("manifest 1 layer is missing: %v", layer) + } + } + + // check that image2 and image3 layers are not still around + for layer := range image2.layers { + if _, ok := blobs[layer]; ok { + t.Fatalf("manifest 2 layer is present: %v", layer) + } + } + + for layer := range image3.layers { + if _, ok := blobs[layer]; ok { + t.Fatalf("manifest 3 layer is present: %v", layer) + } + } +} + +func getAnyKey(digests map[digest.Digest]io.ReadSeeker) (d digest.Digest) { + for d = range digests { + break + } + return +} + +func getKeys(digests map[digest.Digest]io.ReadSeeker) (ds []digest.Digest) { + for d := range digests { + ds = append(ds, d) + } + return +} + +func TestDeletionWithSharedLayer(t *testing.T) { + ctx := context.Background() + inmemoryDriver := inmemory.New() + + registry := createRegistry(t, inmemoryDriver) + repo := makeRepository(t, registry, "tzimiskes") + + // Create random layers + randomLayers1, err := testutil.CreateRandomLayers(3) + if err != nil { + t.Fatalf("failed to make layers: %v", err) + } + + randomLayers2, err := testutil.CreateRandomLayers(3) + if err != nil { + t.Fatalf("failed to make layers: %v", err) + } + + // Upload all layers + err = testutil.UploadBlobs(repo, randomLayers1) + if err != nil { + t.Fatalf("failed to upload layers: %v", err) + } + + err = testutil.UploadBlobs(repo, randomLayers2) + if err != nil { + t.Fatalf("failed to upload layers: %v", err) + } + + // Construct manifests + manifest1, err := testutil.MakeSchema1Manifest(getKeys(randomLayers1)) + if err != nil { + t.Fatalf("failed to make manifest: %v", err) + } + + sharedKey := getAnyKey(randomLayers1) + manifest2, err := testutil.MakeSchema2Manifest(repo, append(getKeys(randomLayers2), sharedKey)) + if err != nil { + t.Fatalf("failed to make manifest: %v", err) + } + + manifestService := makeManifestService(t, repo) + + // Upload manifests + _, err = manifestService.Put(ctx, manifest1) + if err != nil { + t.Fatalf("manifest upload failed: %v", err) + } + + manifestDigest2, err := manifestService.Put(ctx, manifest2) + if err != nil { + t.Fatalf("manifest upload failed: %v", err) + } + + // delete + err = manifestService.Delete(ctx, manifestDigest2) + if err != nil { + t.Fatalf("manifest deletion failed: %v", err) + } + + // check that all of the layers in layer 1 are still there + blobs := allBlobs(t, registry) + for dgst := range randomLayers1 { + if _, ok := blobs[dgst]; !ok { + t.Fatalf("random layer 1 blob missing: %v", dgst) + } + } +} + +func TestOrphanBlobDeleted(t *testing.T) { + inmemoryDriver := inmemory.New() + + registry := createRegistry(t, inmemoryDriver) + repo := makeRepository(t, registry, "michael_z_doukas") + + digests, err := testutil.CreateRandomLayers(1) + if err != nil { + t.Fatalf("Failed to create random digest: %v", err) + } + + if err = testutil.UploadBlobs(repo, digests); err != nil { + t.Fatalf("Failed to upload blob: %v", err) + } + + // formality to create the necessary directories + uploadRandomSchema2Image(t, repo) + + // Run GC + err = MarkAndSweep(context.Background(), inmemoryDriver, registry, false) + if err != nil { + t.Fatalf("Failed mark and sweep: %v", err) + } + + blobs := allBlobs(t, registry) + + // check that orphan blob layers are not still around + for dgst := range digests { + if _, ok := blobs[dgst]; ok { + t.Fatalf("Orphan layer is present: %v", dgst) + } + } +}