add possibility to clean untagged manifests
add tests add possibility to clean untagged manifests Signed-off-by: Jesse Haka <haka.jesse@gmail.com> add dry tests Signed-off-by: Jesse Haka <haka.jesse@gmail.com> remove underscores Signed-off-by: Jesse Haka <haka.jesse@gmail.com> fixes Signed-off-by: Jesse Haka <haka.jesse@gmail.com> opts struct+use camelcase Signed-off-by: Jesse Haka <haka.jesse@gmail.com> doublecheck manifest in paths.go Signed-off-by: Jesse Haka <haka.jesse@gmail.com> add gofmt Signed-off-by: Jesse Haka <haka.jesse@gmail.com> fix lint Signed-off-by: Jesse Haka <haka.jesse@gmail.com> add log print Signed-off-by: Jesse Haka <haka.jesse@gmail.com> move log to dryrun as well Signed-off-by: Jesse Haka <haka.jesse@gmail.com> remove counter Signed-off-by: Jesse Haka <haka.jesse@gmail.com> remove manifest tag references Signed-off-by: Jesse Haka <haka.jesse@gmail.com> add tag to tests Signed-off-by: Jesse Haka <haka.jesse@gmail.com> manifestsWithoutTags -> removeUntagged Signed-off-by: Jesse Haka <haka.jesse@gmail.com> remove RemoveManifestTagReferences and use removemanifests Signed-off-by: Jesse Haka <haka.jesse@gmail.com> remove comment Signed-off-by: Jesse Haka <haka.jesse@gmail.com> remove pathfor Signed-off-by: Jesse Haka <haka.jesse@gmail.com> move removemanifest out of manifestenumerator, it does not work correctly if we delete stuff in it Signed-off-by: Jesse Haka <haka.jesse@gmail.com> add comment Signed-off-by: Jesse Haka <haka.jesse@gmail.com> fix context -> dcontext Signed-off-by: Jesse Haka <haka.jesse@gmail.com> fix gofmtmaster
							parent
							
								
									db0bc08b85
								
							
						
					
					
						commit
						ff87ad884c
					
				|  | @ -18,6 +18,7 @@ func init() { | ||||||
| 	RootCmd.AddCommand(ServeCmd) | 	RootCmd.AddCommand(ServeCmd) | ||||||
| 	RootCmd.AddCommand(GCCmd) | 	RootCmd.AddCommand(GCCmd) | ||||||
| 	GCCmd.Flags().BoolVarP(&dryRun, "dry-run", "d", false, "do everything except remove the blobs") | 	GCCmd.Flags().BoolVarP(&dryRun, "dry-run", "d", false, "do everything except remove the blobs") | ||||||
|  | 	GCCmd.Flags().BoolVarP(&removeUntagged, "delete-untagged", "m", false, "delete manifests that are not currently referenced via tag") | ||||||
| 	RootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "show the version and exit") | 	RootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "show the version and exit") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -36,6 +37,7 @@ var RootCmd = &cobra.Command{ | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var dryRun bool | var dryRun bool | ||||||
|  | var removeUntagged bool | ||||||
| 
 | 
 | ||||||
| // GCCmd is the cobra command that corresponds to the garbage-collect subcommand
 | // GCCmd is the cobra command that corresponds to the garbage-collect subcommand
 | ||||||
| var GCCmd = &cobra.Command{ | var GCCmd = &cobra.Command{ | ||||||
|  | @ -75,7 +77,10 @@ var GCCmd = &cobra.Command{ | ||||||
| 			os.Exit(1) | 			os.Exit(1) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		err = storage.MarkAndSweep(ctx, driver, registry, dryRun) | 		err = storage.MarkAndSweep(ctx, driver, registry, storage.GCOpts{ | ||||||
|  | 			DryRun:         dryRun, | ||||||
|  | 			RemoveUntagged: removeUntagged, | ||||||
|  | 		}) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			fmt.Fprintf(os.Stderr, "failed to garbage collect: %v", err) | 			fmt.Fprintf(os.Stderr, "failed to garbage collect: %v", err) | ||||||
| 			os.Exit(1) | 			os.Exit(1) | ||||||
|  |  | ||||||
|  | @ -14,8 +14,21 @@ func emit(format string, a ...interface{}) { | ||||||
| 	fmt.Printf(format+"\n", a...) | 	fmt.Printf(format+"\n", a...) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // GCOpts contains options for garbage collector
 | ||||||
|  | type GCOpts struct { | ||||||
|  | 	DryRun         bool | ||||||
|  | 	RemoveUntagged bool | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ManifestDel contains manifest structure which will be deleted
 | ||||||
|  | type ManifestDel struct { | ||||||
|  | 	Name   string | ||||||
|  | 	Digest digest.Digest | ||||||
|  | 	Tags   []string | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // MarkAndSweep performs a mark and sweep of registry data
 | // MarkAndSweep performs a mark and sweep of registry data
 | ||||||
| func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, registry distribution.Namespace, dryRun bool) error { | func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, registry distribution.Namespace, opts GCOpts) error { | ||||||
| 	repositoryEnumerator, ok := registry.(distribution.RepositoryEnumerator) | 	repositoryEnumerator, ok := registry.(distribution.RepositoryEnumerator) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return fmt.Errorf("unable to convert Namespace to RepositoryEnumerator") | 		return fmt.Errorf("unable to convert Namespace to RepositoryEnumerator") | ||||||
|  | @ -23,6 +36,7 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis | ||||||
| 
 | 
 | ||||||
| 	// mark
 | 	// mark
 | ||||||
| 	markSet := make(map[digest.Digest]struct{}) | 	markSet := make(map[digest.Digest]struct{}) | ||||||
|  | 	manifestArr := make([]ManifestDel, 0) | ||||||
| 	err := repositoryEnumerator.Enumerate(ctx, func(repoName string) error { | 	err := repositoryEnumerator.Enumerate(ctx, func(repoName string) error { | ||||||
| 		emit(repoName) | 		emit(repoName) | ||||||
| 
 | 
 | ||||||
|  | @ -47,6 +61,25 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		err = manifestEnumerator.Enumerate(ctx, func(dgst digest.Digest) error { | 		err = manifestEnumerator.Enumerate(ctx, func(dgst digest.Digest) error { | ||||||
|  | 			if opts.RemoveUntagged { | ||||||
|  | 				// fetch all tags where this manifest is the latest one
 | ||||||
|  | 				tags, err := repository.Tags(ctx).Lookup(ctx, distribution.Descriptor{Digest: dgst}) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return fmt.Errorf("failed to retrieve tags for digest %v: %v", dgst, err) | ||||||
|  | 				} | ||||||
|  | 				if len(tags) == 0 { | ||||||
|  | 					emit("manifest eligible for deletion: %s", dgst) | ||||||
|  | 					// fetch all tags from repository
 | ||||||
|  | 					// all of these tags could contain manifest in history
 | ||||||
|  | 					// which means that we need check (and delete) those references when deleting manifest
 | ||||||
|  | 					allTags, err := repository.Tags(ctx).All(ctx) | ||||||
|  | 					if err != nil { | ||||||
|  | 						return fmt.Errorf("failed to retrieve tags %v", err) | ||||||
|  | 					} | ||||||
|  | 					manifestArr = append(manifestArr, ManifestDel{Name: repoName, Digest: dgst, Tags: allTags}) | ||||||
|  | 					return nil | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
| 			// Mark the manifest's blob
 | 			// Mark the manifest's blob
 | ||||||
| 			emit("%s: marking manifest %s ", repoName, dgst) | 			emit("%s: marking manifest %s ", repoName, dgst) | ||||||
| 			markSet[dgst] = struct{}{} | 			markSet[dgst] = struct{}{} | ||||||
|  | @ -84,6 +117,15 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// sweep
 | 	// sweep
 | ||||||
|  | 	vacuum := NewVacuum(ctx, storageDriver) | ||||||
|  | 	if !opts.DryRun { | ||||||
|  | 		for _, obj := range manifestArr { | ||||||
|  | 			err = vacuum.RemoveManifest(obj.Name, obj.Digest, obj.Tags) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return fmt.Errorf("failed to delete manifest %s: %v", obj.Digest, err) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	blobService := registry.Blobs() | 	blobService := registry.Blobs() | ||||||
| 	deleteSet := make(map[digest.Digest]struct{}) | 	deleteSet := make(map[digest.Digest]struct{}) | ||||||
| 	err = blobService.Enumerate(ctx, func(dgst digest.Digest) error { | 	err = blobService.Enumerate(ctx, func(dgst digest.Digest) error { | ||||||
|  | @ -96,12 +138,10 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("error enumerating blobs: %v", err) | 		return fmt.Errorf("error enumerating blobs: %v", err) | ||||||
| 	} | 	} | ||||||
| 	emit("\n%d blobs marked, %d blobs eligible for deletion", len(markSet), len(deleteSet)) | 	emit("\n%d blobs marked, %d blobs and %d manifests eligible for deletion", len(markSet), len(deleteSet), len(manifestArr)) | ||||||
| 	// Construct vacuum
 |  | ||||||
| 	vacuum := NewVacuum(ctx, storageDriver) |  | ||||||
| 	for dgst := range deleteSet { | 	for dgst := range deleteSet { | ||||||
| 		emit("blob eligible for deletion: %s", dgst) | 		emit("blob eligible for deletion: %s", dgst) | ||||||
| 		if dryRun { | 		if opts.DryRun { | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 		err = vacuum.RemoveBlob(string(dgst)) | 		err = vacuum.RemoveBlob(string(dgst)) | ||||||
|  |  | ||||||
|  | @ -61,6 +61,23 @@ func makeManifestService(t *testing.T, repository distribution.Repository) distr | ||||||
| 	return manifestService | 	return manifestService | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func allManifests(t *testing.T, manifestService distribution.ManifestService) map[digest.Digest]struct{} { | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 	allManMap := make(map[digest.Digest]struct{}) | ||||||
|  | 	manifestEnumerator, ok := manifestService.(distribution.ManifestEnumerator) | ||||||
|  | 	if !ok { | ||||||
|  | 		t.Fatalf("unable to convert ManifestService into ManifestEnumerator") | ||||||
|  | 	} | ||||||
|  | 	err := manifestEnumerator.Enumerate(ctx, func(dgst digest.Digest) error { | ||||||
|  | 		allManMap[dgst] = struct{}{} | ||||||
|  | 		return nil | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error getting all manifests: %v", err) | ||||||
|  | 	} | ||||||
|  | 	return allManMap | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func allBlobs(t *testing.T, registry distribution.Namespace) map[digest.Digest]struct{} { | func allBlobs(t *testing.T, registry distribution.Namespace) map[digest.Digest]struct{} { | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
| 	blobService := registry.Blobs() | 	blobService := registry.Blobs() | ||||||
|  | @ -169,7 +186,10 @@ func TestNoDeletionNoEffect(t *testing.T) { | ||||||
| 	before := allBlobs(t, registry) | 	before := allBlobs(t, registry) | ||||||
| 
 | 
 | ||||||
| 	// Run GC
 | 	// Run GC
 | ||||||
| 	err = MarkAndSweep(context.Background(), inmemoryDriver, registry, false) | 	err = MarkAndSweep(context.Background(), inmemoryDriver, registry, GCOpts{ | ||||||
|  | 		DryRun:         false, | ||||||
|  | 		RemoveUntagged: false, | ||||||
|  | 	}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Failed mark and sweep: %v", err) | 		t.Fatalf("Failed mark and sweep: %v", err) | ||||||
| 	} | 	} | ||||||
|  | @ -180,6 +200,102 @@ func TestNoDeletionNoEffect(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func TestDeleteManifestIfTagNotFound(t *testing.T) { | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 	inmemoryDriver := inmemory.New() | ||||||
|  | 
 | ||||||
|  | 	registry := createRegistry(t, inmemoryDriver) | ||||||
|  | 	repo := makeRepository(t, registry, "deletemanifests") | ||||||
|  | 	manifestService, err := repo.Manifests(ctx) | ||||||
|  | 
 | ||||||
|  | 	// 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) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	manifest2, err := testutil.MakeSchema1Manifest(getKeys(randomLayers2)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("failed to make manifest: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	_, err = manifestService.Put(ctx, manifest1) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("manifest upload failed: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	_, err = manifestService.Put(ctx, manifest2) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("manifest upload failed: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	manifestEnumerator, _ := manifestService.(distribution.ManifestEnumerator) | ||||||
|  | 	err = manifestEnumerator.Enumerate(ctx, func(dgst digest.Digest) error { | ||||||
|  | 		repo.Tags(ctx).Tag(ctx, "test", distribution.Descriptor{Digest: dgst}) | ||||||
|  | 		return nil | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	before1 := allBlobs(t, registry) | ||||||
|  | 	before2 := allManifests(t, manifestService) | ||||||
|  | 
 | ||||||
|  | 	// run GC with dry-run (should not remove anything)
 | ||||||
|  | 	err = MarkAndSweep(context.Background(), inmemoryDriver, registry, GCOpts{ | ||||||
|  | 		DryRun:         true, | ||||||
|  | 		RemoveUntagged: true, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Failed mark and sweep: %v", err) | ||||||
|  | 	} | ||||||
|  | 	afterDry1 := allBlobs(t, registry) | ||||||
|  | 	afterDry2 := allManifests(t, manifestService) | ||||||
|  | 	if len(before1) != len(afterDry1) { | ||||||
|  | 		t.Fatalf("Garbage collection affected blobs storage: %d != %d", len(before1), len(afterDry1)) | ||||||
|  | 	} | ||||||
|  | 	if len(before2) != len(afterDry2) { | ||||||
|  | 		t.Fatalf("Garbage collection affected manifest storage: %d != %d", len(before2), len(afterDry2)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Run GC (removes everything because no manifests with tags exist)
 | ||||||
|  | 	err = MarkAndSweep(context.Background(), inmemoryDriver, registry, GCOpts{ | ||||||
|  | 		DryRun:         false, | ||||||
|  | 		RemoveUntagged: true, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Failed mark and sweep: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	after1 := allBlobs(t, registry) | ||||||
|  | 	after2 := allManifests(t, manifestService) | ||||||
|  | 	if len(before1) == len(after1) { | ||||||
|  | 		t.Fatalf("Garbage collection affected blobs storage: %d == %d", len(before1), len(after1)) | ||||||
|  | 	} | ||||||
|  | 	if len(before2) == len(after2) { | ||||||
|  | 		t.Fatalf("Garbage collection affected manifest storage: %d == %d", len(before2), len(after2)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestGCWithMissingManifests(t *testing.T) { | func TestGCWithMissingManifests(t *testing.T) { | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
| 	d := inmemory.New() | 	d := inmemory.New() | ||||||
|  | @ -200,7 +316,10 @@ func TestGCWithMissingManifests(t *testing.T) { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	err = MarkAndSweep(context.Background(), d, registry, false) | 	err = MarkAndSweep(context.Background(), d, registry, GCOpts{ | ||||||
|  | 		DryRun:         false, | ||||||
|  | 		RemoveUntagged: false, | ||||||
|  | 	}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Failed mark and sweep: %v", err) | 		t.Fatalf("Failed mark and sweep: %v", err) | ||||||
| 	} | 	} | ||||||
|  | @ -227,7 +346,10 @@ func TestDeletionHasEffect(t *testing.T) { | ||||||
| 	manifests.Delete(ctx, image3.manifestDigest) | 	manifests.Delete(ctx, image3.manifestDigest) | ||||||
| 
 | 
 | ||||||
| 	// Run GC
 | 	// Run GC
 | ||||||
| 	err = MarkAndSweep(context.Background(), inmemoryDriver, registry, false) | 	err = MarkAndSweep(context.Background(), inmemoryDriver, registry, GCOpts{ | ||||||
|  | 		DryRun:         false, | ||||||
|  | 		RemoveUntagged: false, | ||||||
|  | 	}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Failed mark and sweep: %v", err) | 		t.Fatalf("Failed mark and sweep: %v", err) | ||||||
| 	} | 	} | ||||||
|  | @ -361,7 +483,10 @@ func TestOrphanBlobDeleted(t *testing.T) { | ||||||
| 	uploadRandomSchema2Image(t, repo) | 	uploadRandomSchema2Image(t, repo) | ||||||
| 
 | 
 | ||||||
| 	// Run GC
 | 	// Run GC
 | ||||||
| 	err = MarkAndSweep(context.Background(), inmemoryDriver, registry, false) | 	err = MarkAndSweep(context.Background(), inmemoryDriver, registry, GCOpts{ | ||||||
|  | 		DryRun:         false, | ||||||
|  | 		RemoveUntagged: false, | ||||||
|  | 	}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Failed mark and sweep: %v", err) | 		t.Fatalf("Failed mark and sweep: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -50,6 +50,40 @@ func (v Vacuum) RemoveBlob(dgst string) error { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // RemoveManifest removes a manifest from the filesystem
 | ||||||
|  | func (v Vacuum) RemoveManifest(name string, dgst digest.Digest, tags []string) error { | ||||||
|  | 	// remove a tag manifest reference, in case of not found continue to next one
 | ||||||
|  | 	for _, tag := range tags { | ||||||
|  | 
 | ||||||
|  | 		tagsPath, err := pathFor(manifestTagIndexEntryPathSpec{name: name, revision: dgst, tag: tag}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		_, err = v.driver.Stat(v.ctx, tagsPath) | ||||||
|  | 		if err != nil { | ||||||
|  | 			switch err := err.(type) { | ||||||
|  | 			case driver.PathNotFoundError: | ||||||
|  | 				continue | ||||||
|  | 			default: | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		dcontext.GetLogger(v.ctx).Infof("deleting manifest tag reference: %s", tagsPath) | ||||||
|  | 		err = v.driver.Delete(v.ctx, tagsPath) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	manifestPath, err := pathFor(manifestRevisionPathSpec{name: name, revision: dgst}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	dcontext.GetLogger(v.ctx).Infof("deleting manifest: %s", manifestPath) | ||||||
|  | 	return v.driver.Delete(v.ctx, manifestPath) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // RemoveRepository removes a repository directory from the
 | // RemoveRepository removes a repository directory from the
 | ||||||
| // filesystem
 | // filesystem
 | ||||||
| func (v Vacuum) RemoveRepository(repoName string) error { | func (v Vacuum) RemoveRepository(repoName string) error { | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue