From ddd71447a12a48002d104a8085b10a8ee1981636 Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Mon, 8 Jul 2019 14:38:46 -0400 Subject: [PATCH 1/3] add label-based selector support refactor to make this make a bit more sense maybe removing small jawn refactoring and trying to fix test cases fixing tests improving tests tweaks moving more tests into yaml configs better tests dont cache test results updating tests to make matching predicate on labels work adding docs --- Dockerfile | 1 + Makefile | 7 +- cmd/main.go | 34 +- docs/config.md | 17 + go.mod | 1 - internal/pkg/rules/rules2_test.go | 274 ++++++++++++ internal/pkg/rules/rules_test.go | 197 +++++---- pkg/{registry => client}/client.go | 50 ++- pkg/config/config.go | 7 +- pkg/config/config_test.go | 28 +- pkg/graph/layer.go | 9 - pkg/graph/tag.go | 10 - pkg/registry/docker_distribution_compat.go | 59 ++- pkg/registry/manifest.go | 132 +----- pkg/registry/rules_test.go | 240 ----------- pkg/rules/rule.go | 102 ++++- pkg/rules/selector.go | 63 ++- ...nvalid-rule-missing-repos-and-labels.yaml} | 0 test/fixtures/manifest_tests/apply-rules.yaml | 398 ++++++++++++++++++ .../manifest_tests/filter_repo_tags.yaml | 133 ++++++ .../manifest_tests/manifest_matching_1.yaml | 90 ++++ .../rules/labels-devel-3-versions.yaml | 8 + test/fixtures/rules/labels-prod-3-latest.yaml | 8 + test/fixtures/rules/redpop-pr.yaml | 8 - .../repo-and-labels-devel-3-versions.yaml | 10 + 25 files changed, 1320 insertions(+), 566 deletions(-) create mode 100644 internal/pkg/rules/rules2_test.go rename pkg/{registry => client}/client.go (81%) delete mode 100644 pkg/graph/layer.go delete mode 100644 pkg/graph/tag.go delete mode 100644 pkg/registry/rules_test.go rename test/fixtures/config/{invalid-rule-missing-repos.yaml => invalid-rule-missing-repos-and-labels.yaml} (100%) create mode 100644 test/fixtures/manifest_tests/apply-rules.yaml create mode 100644 test/fixtures/manifest_tests/filter_repo_tags.yaml create mode 100644 test/fixtures/manifest_tests/manifest_matching_1.yaml create mode 100644 test/fixtures/rules/labels-devel-3-versions.yaml create mode 100644 test/fixtures/rules/labels-prod-3-latest.yaml delete mode 100644 test/fixtures/rules/redpop-pr.yaml create mode 100644 test/fixtures/rules/repo-and-labels-devel-3-versions.yaml diff --git a/Dockerfile b/Dockerfile index 774172a..41abb10 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ COPY . . RUN make && rm -rf vendor/ FROM alpine:latest +LABEL maintainer="Tumblr" RUN apk --no-cache add ca-certificates COPY --from=0 /app/bin/docker-registry-pruner /bin/docker-registry-pruner COPY ./entrypoint.sh /bin/entrypoint.sh diff --git a/Makefile b/Makefile index 5e4744a..18d9ea9 100644 --- a/Makefile +++ b/Makefile @@ -51,6 +51,7 @@ GO2XUNIT = $(BIN)/go2xunit $(BIN)/go2xunit: | ; $(info $(M) building go2xunit…) $Q go get github.com/tebeka/go2xunit + # Tests # @@ -63,10 +64,10 @@ test-race: ARGS=-race ## Run tests with race detector $(TEST_TARGETS): NAME=$(MAKECMDGOALS:test-%=%) $(TEST_TARGETS): test check test tests: fmt lint vendor | ; $(info $(M) running $(NAME:%=% )tests…) @ ## Run tests - $Q $(GO) test -timeout $(TIMEOUT)s $(ARGS) $(TESTPKGS) + $Q $(GO) test -count=1 -timeout $(TIMEOUT)s $(ARGS) $(TESTPKGS) test-xml: fmt lint vendor | $(GO2XUNIT) ; $(info $(M) running $(NAME:%=% )tests…) @ ## Run tests with xUnit output - $Q 2>&1 $(GO) test -timeout 20s -v $(TESTPKGS) | tee test/tests.output + $Q 2>&1 $(GO) test -count=1 -timeout 20s -v $(TESTPKGS) | tee test/tests.output $(GO2XUNIT) -fail -input test/tests.output -output test/tests.xml COVERAGE_MODE = atomic @@ -79,7 +80,7 @@ test-coverage: COVERAGE_DIR := $(CURDIR)/test/coverage.$(shell date -u +"%Y-%m-% test-coverage: fmt lint vendor test-coverage-tools | ; $(info $(M) running coverage tests…) @ ## Run coverage tests $Q mkdir -p $(COVERAGE_DIR)/coverage $Q for pkg in $(TESTPKGS); do \ - $(GO) test \ + $(GO) test -count=1 \ -coverpkg=$$($(GO) list -f '{{ join .Deps "\n" }}' $$pkg | \ grep '^$(PACKAGE)/' | grep -v '^$(PACKAGE)/vendor/' | \ tr '\n' ',')$$pkg \ diff --git a/cmd/main.go b/cmd/main.go index d9921df..6c81b8c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -8,6 +8,7 @@ import ( "text/tabwriter" "time" + "github.com/tumblr/docker-registry-pruner/pkg/client" "github.com/tumblr/docker-registry-pruner/pkg/config" "github.com/tumblr/docker-registry-pruner/pkg/registry" "github.com/tumblr/docker-registry-pruner/pkg/rules" @@ -43,7 +44,7 @@ func main() { log.Fatal(err) } - hub, err := registry.New(cfg) + hub, err := client.New(cfg) if err != nil { log.Fatal(err) } @@ -62,7 +63,7 @@ func main() { repos = append(repos, repo) } - for _, rule := range hub.Rules { + for _, rule := range hub.Config.Rules { log.Infof("Loaded rule: %s", rule.String()) } @@ -94,24 +95,31 @@ func PrintTableManifests(matches map[string][]*registry.Manifest) { w.Flush() } -func FetchImagesAndApplyRules(hub *registry.Client, repos []string) map[string][]*registry.Manifest { +func FetchImagesAndApplyRules(hub *client.Client, repos []string) map[string][]*registry.Manifest { repoTags, err := hub.RepoTags(repos) if err != nil { log.Fatal(err) } - selectors := rules.RulesToSelectors(hub.Rules) - filteredRepoTags := rules.FilterRepoTags(repoTags, selectors) - for repo, tags := range filteredRepoTags { - log.Debugf("Repo %s has %d tag matching ruless\n", repo, len(tags)) - } - - allManifests, err := hub.Manifests(filteredRepoTags) + selectors := rules.RulesToSelectors(hub.Config.Rules) + allManifests, err := hub.Manifests(repoTags) if err != nil { log.Fatal(err) } - keep, delete := registry.ApplyRules(hub.Rules, allManifests) + filteredManifestsByRepo := rules.FilterManifests(allManifests, selectors) + filteredManifests := []*registry.Manifest{} + filteredCount := 0 + for n, manifests := range filteredManifestsByRepo { + filteredCount += len(manifests) + log.Debugf("%s: filtered to %d manifests", n, len(manifests)) + for _, m := range manifests { + filteredManifests = append(filteredManifests, m) + } + } + log.Debugf("Selector filtering %d manifests to %d manifests", len(allManifests), len(filteredManifests)) + + keep, delete := rules.ApplyRules(hub.Config.Rules, filteredManifests) matches := map[string][]*registry.Manifest{ "keep": keep, "delete": delete, @@ -119,7 +127,7 @@ func FetchImagesAndApplyRules(hub *registry.Client, repos []string) map[string][ return matches } -func ShowMatchingRepos(hub *registry.Client, repos []string) { +func ShowMatchingRepos(hub *client.Client, repos []string) { log.Infof("Querying for manifests. This may take a while...") matches := FetchImagesAndApplyRules(hub, repos) deletes, keeps := len(matches["delete"]), len(matches["keep"]) @@ -127,7 +135,7 @@ func ShowMatchingRepos(hub *registry.Client, repos []string) { fmt.Fprintf(os.Stderr, "deleting %d images, keeping %d images\n", deletes, keeps) } -func DeleteMatchingImages(hub *registry.Client, repos []string) bool { +func DeleteMatchingImages(hub *client.Client, repos []string) bool { log.Infof("Querying for manifests. This may take a while...") matches := FetchImagesAndApplyRules(hub, repos) log.Infof("Beginning deletion of %d images", len(matches["delete"])) diff --git a/docs/config.md b/docs/config.md index 315b8f6..a0affe3 100644 --- a/docs/config.md +++ b/docs/config.md @@ -9,11 +9,14 @@ A Rule is made up of a Selector, and an Action. See below for more details. A selector is a predicate that images must satisfy to be considered by the `Action` for deletion. * `repos` is a list of repositories to apply this rule to. This is literal string matching, _not_ regex. (i.e. `tumblr/plumbus`) +* `labels` is a map of Docker labels that must be present on the Manifest. You can set these in your Dockerfiles with `LABEL foo=bar`. This is useful to create blanket rules for image retention that allow image owners to opt in to cleanups on their own. * `match_tags` is a list of regexp. Any matching image will have the rule action evaluated against it (i.e. `^v\d+`) * `ignore_tags` is a list of regexp. Any matching image will explicitly not be evaluated, even if it would have matched `match_tags` NOTE: the `^latest$` tag is always implicitly inherited into `ignore_tags`. +At least one of the predicates `repos`, `labels` must be present. You may combine `repos` and `labels`, as described in the examples below. + ## Actions You must provide one action, either `keep_versions`, `keep_recent`, or `keep_days`. Images that match the selector and fail the action predicate will be marked for deletion. @@ -87,5 +90,19 @@ rules: - repos: - web/devtools keep_recent: 5 + + # for any image that has the labels {prune=true,environment=development}, expire images after 15 days + - labels: + prune: "true" + environment: "development" + keep_days: 15 + + # for any repo matching some/image||another/image, if they have the environment=production label, keep the last 5 versions + - repos: + - some/image + - another/image + labels: + environment: production + keep_versions: 5 ``` diff --git a/go.mod b/go.mod index cf29c9e..6fe547f 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,5 @@ require ( go.uber.org/atomic v1.3.2 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.10.0 - gonum.org/v1/gonum v0.0.0-20190430173231-ac0c935b54d8 gopkg.in/yaml.v2 v2.2.2 ) diff --git a/internal/pkg/rules/rules2_test.go b/internal/pkg/rules/rules2_test.go new file mode 100644 index 0000000..c3ab7ab --- /dev/null +++ b/internal/pkg/rules/rules2_test.go @@ -0,0 +1,274 @@ +package rules + +import ( + "reflect" + "sort" + "testing" + "time" + + _ "github.com/tumblr/docker-registry-pruner/internal/pkg/testing" + "github.com/tumblr/docker-registry-pruner/pkg/config" + "github.com/tumblr/docker-registry-pruner/pkg/registry" + "github.com/tumblr/docker-registry-pruner/pkg/rules" +) + +// helper function to make a test fixture manifest +func mkmanifest(r, t string, daysOld int64, labels map[string]string) *registry.Manifest { + if labels == nil { + labels = map[string]string{} + } + return must(registry.NewManifest(r, t, tNow.Add(time.Duration(-daysOld*24)*time.Hour), labels)) +} + +func must(m *registry.Manifest, err error) *registry.Manifest { + if err != nil { + panic(err) + } + return m +} + +var ( + rulesDir = "test/fixtures/rules" + + tNow = time.Now() + /* + manifests = []*registry.Manifest{ + mkmanifest("tumblr/plumbus", "v1.2.3", 4, nil), + mkmanifest("tumblr/plumbus", "v1.0.3+metadata", 69, nil), + mkmanifest("tumblr/plumbus", "pr-69420+13d", 13, nil), + mkmanifest("tumblr/plumbus", "pr-69420+14d", 14, nil), + mkmanifest("tumblr/plumbus", "master-v1.2.3-69", 14, nil), + mkmanifest("tumblr/plumbus", "master-2019", 14, nil), + mkmanifest("tumblr/plumbus", "pr-420", 1, nil), + mkmanifest("tumblr/plumbus", "pr-69", 5, nil), + mkmanifest("tumblr/plumbus", "pr-69421+15d", 15, nil), + mkmanifest("tumblr/plumbus", "pr-69419+16d", 16, nil), + mkmanifest("image/latest", "latest", 69420, nil), + mkmanifest("tumblr/fleeble", "latest", 0, nil), + mkmanifest("tumblr/fleeble", "garbage", 5, nil), + mkmanifest("tumblr/fleeble", "v0.4.2-259-something", 5, nil), + mkmanifest("tumblr/fleeble", "v0.5.0-260", 4, nil), + mkmanifest("tumblr/fleeble", "v0.5.1-260", 4, nil), + mkmanifest("tumblr/fleeble", "v0.5.23+test", 3, nil), + mkmanifest("tumblr/fleeble", "v0.5.2", 3, nil), + mkmanifest("tumblr/fleeble", "some-ignored-tag", 69, nil), + mkmanifest("tumblr/fleeble", "oldtag-1", 69, nil), + mkmanifest("tumblr/fleeble", "oldtag-2", 70, nil), + mkmanifest("tumblr/fleeble", "v0.6.1-261-gbb41394", 0, nil), + mkmanifest("tumblr/fleeble", "v0.5.3-nice", 1, nil), + mkmanifest("tumblr/fleeble", "v0.69-6969", 1, nil), + mkmanifest("tumblr/fleeble", "v0.5.5-420", 1, nil), + mkmanifest("tumblr/fleeble", "v0.6.1-262", 0, nil), + mkmanifest("tumblr/fleeble", "branch-v1.2.3-69", 14, nil), + mkmanifest("tumblr/fleeble", "v0.69.1-262", 0, nil), + mkmanifest("tumblr/fleeble", "abc123f", 0, nil), + + mkmanifest("image/x", "v0.1.1+x", 0, nil), + mkmanifest("image/x", "v0.6.9+x", 0, nil), + mkmanifest("image/x", "v4.2.1+x", 0, nil), + mkmanifest("image/x", "0.0.1+x", 0, nil), + mkmanifest("image/x", "0.0.2+x", 0, nil), + mkmanifest("image/y", "v0.1.0+y", 0, nil), + mkmanifest("image/y", "v0.69.420+y", 0, nil), + mkmanifest("image/y", "v4.2.0+y", 0, nil), + mkmanifest("image/y", "0.0.1+y", 0, nil), + mkmanifest("image/y", "0.0.2+y", 0, nil), + } + */ + /* + tests = []testpayload{ + { + rulesFile: "multiple-repo-keep-latest.yaml", + input: manifests, + // should keep the latest 4 images from both fleeble and plumbus + keepImages: []string{ + "tumblr/fleeble:abc123f", // modified: 0 days ago + "tumblr/fleeble:v0.6.1-261-gbb41394", // modified: 0 days ago + "tumblr/fleeble:v0.6.1-262", // modified: 0 days ago + "tumblr/fleeble:v0.69.1-262", // modified: 0 days ago + "tumblr/plumbus:pr-420", // modified: 1 days ago + "tumblr/plumbus:v1.2.3", // modified: 4 days ago + "tumblr/plumbus:pr-69", // modified: 5 days ago + "tumblr/plumbus:pr-69420+13d", // modified: 13 days ago + }, + deleteImages: []string{ + "tumblr/fleeble:branch-v1.2.3-69", + "tumblr/fleeble:garbage", + "tumblr/fleeble:oldtag-1", + "tumblr/fleeble:oldtag-2", + "tumblr/fleeble:some-ignored-tag", + "tumblr/fleeble:v0.4.2-259-something", + "tumblr/fleeble:v0.5.0-260", + "tumblr/fleeble:v0.5.1-260", + "tumblr/fleeble:v0.5.2", + "tumblr/fleeble:v0.5.23+test", + "tumblr/fleeble:v0.5.3-nice", + "tumblr/fleeble:v0.5.5-420", + "tumblr/fleeble:v0.69-6969", + "tumblr/plumbus:master-2019", + "tumblr/plumbus:master-v1.2.3-69", + "tumblr/plumbus:pr-69419+16d", + "tumblr/plumbus:pr-69420+14d", + "tumblr/plumbus:pr-69421+15d", + "tumblr/plumbus:v1.0.3+metadata", + }, + }, + { + rulesFile: "onlylatest.yaml", + input: manifests, + keepImages: []string{}, + deleteImages: []string{}, // we dont want to see it in delete, cause its ignored + }, + { + rulesFile: "plumbus-pr.yaml", + input: manifests, + keepImages: []string{ + "tumblr/plumbus:pr-420", + "tumblr/plumbus:pr-69", + "tumblr/plumbus:pr-69420+13d", + "tumblr/plumbus:pr-69420+14d"}, + deleteImages: []string{ + "tumblr/plumbus:pr-69419+16d", + "tumblr/plumbus:pr-69421+15d"}, + }, + { + // ignore some tags that would otherwise get cleaned up by date predicates + rulesFile: "fleeble-ignore-some.yaml", + input: manifests, + keepImages: []string{"tumblr/fleeble:abc123f"}, + deleteImages: []string{"tumblr/fleeble:branch-v1.2.3-69", "tumblr/fleeble:garbage", "tumblr/fleeble:oldtag-1", "tumblr/fleeble:oldtag-2"}, + }, + { + rulesFile: "fleeble-match-version.yaml", + // this rule should retain only 5 latest version tags + // and will implicitly skip all versions taht dont parse correctly as a Version + // meaning there should be no deletedTags that arent correct semantic versions + input: manifests, + keepImages: []string{ + "tumblr/fleeble:v0.5.23+test", + "tumblr/fleeble:v0.6.1-261-gbb41394", + "tumblr/fleeble:v0.6.1-262", + "tumblr/fleeble:v0.69-6969", + "tumblr/fleeble:v0.69.1-262", + }, + deleteImages: []string{ + "tumblr/fleeble:v0.4.2-259-something", + "tumblr/fleeble:v0.5.0-260", + "tumblr/fleeble:v0.5.1-260", + "tumblr/fleeble:v0.5.2", + "tumblr/fleeble:v0.5.3-nice", + "tumblr/fleeble:v0.5.5-420", + }, + }, + { + // this should keep 2 latest version tags, and the last 2 days of all tags. + // this means there are some versions that would have been deleted, that are still retained + rulesFile: "fleeble-multiple.yaml", + input: manifests, + keepImages: []string{ + "tumblr/fleeble:abc123f", + "tumblr/fleeble:v0.69-6969", + "tumblr/fleeble:v0.69.1-262", + }, + deleteImages: []string{ + "tumblr/fleeble:branch-v1.2.3-69", + "tumblr/fleeble:garbage", + "tumblr/fleeble:oldtag-1", + "tumblr/fleeble:oldtag-2", + "tumblr/fleeble:some-ignored-tag", // ignored by 1 rule, but deleted by the nDays rule! + "tumblr/fleeble:v0.4.2-259-something", + "tumblr/fleeble:v0.5.0-260", + "tumblr/fleeble:v0.5.1-260", + "tumblr/fleeble:v0.5.2", + "tumblr/fleeble:v0.5.23+test", + "tumblr/fleeble:v0.5.3-nice", + "tumblr/fleeble:v0.5.5-420", + "tumblr/fleeble:v0.6.1-261-gbb41394", + "tumblr/fleeble:v0.6.1-262", + }, + }, + { + rulesFile: "multiple-repo-versions.yaml", + input: manifests, + keepImages: []string{ + "image/x:v0.1.1+x", + "image/x:v0.6.9+x", + "image/x:v4.2.1+x", + "image/y:v0.1.0+y", + "image/y:v0.69.420+y", + "image/y:v4.2.0+y", + }, + deleteImages: []string{ + "image/x:0.0.1+x", + "image/x:0.0.2+x", + "image/y:0.0.1+y", + "image/y:0.0.2+y", + }, + }, + } + */ +) + +type testpayload struct { + rulesFile string + input []*registry.Manifest + keepImages []string + deleteImages []string +} + +func TestApplyRules(t *testing.T) { + + tc, err := loadTestConfig("test/fixtures/manifest_tests/apply-rules.yaml") + if err != nil { + t.Error(err) + t.FailNow() + } + + for _, test := range tc.Tests { + // sort any expected tag sets + for _, tags := range test.Expected.Keep { + sort.Strings(tags) + } + for _, tags := range test.Expected.Keep { + sort.Strings(tags) + } + t.Logf("%s: loading rules from %s", tc.SourceFile, test.Config) + + cfg, err := config.LoadFromFile(test.Config) + if err != nil { + t.Error(err) + t.FailNow() + } + + keep, delete := rules.ApplyRules(cfg.Rules, tc.Manifests) + keep_tags := manifestsAsImageMap(keep) + delete_tags := manifestsAsImageMap(delete) + if test.Config == "test/fixtures/rules/labels-devel-3-versions.yaml" { + t.Logf("%s: expected: %+v", test.Config, test) + t.Logf("%s: kept: %+v", test.Config, keep_tags) + t.Logf("%s: deleted: %+v", test.Config, delete_tags) + } + + if !reflect.DeepEqual(test.Expected.Keep, keep_tags) { + t.Errorf("%s: expected keep images to be %v but was actually %v", test.Config, test.Expected.Keep, keep_tags) + t.FailNow() + } + if !reflect.DeepEqual(test.Expected.Delete, delete_tags) { + t.Errorf("%s: expected delete images tags to be %v but was actually %v", test.Config, test.Expected.Delete, delete_tags) + t.FailNow() + } + } +} + +// turn a list of Manifest into a map of repo->list of tags +func manifestsAsImageMap(ms []*registry.Manifest) map[string][]string { + res := map[string][]string{} + for _, m := range ms { + if _, ok := res[m.Name]; !ok { + res[m.Name] = []string{} + } + res[m.Name] = append(res[m.Name], m.Tag) + sort.Strings(res[m.Name]) + } + return res +} diff --git a/internal/pkg/rules/rules_test.go b/internal/pkg/rules/rules_test.go index 2e7e76f..7e2ace8 100644 --- a/internal/pkg/rules/rules_test.go +++ b/internal/pkg/rules/rules_test.go @@ -6,130 +6,155 @@ between config and rules */ import ( + "gopkg.in/yaml.v2" + "io/ioutil" "reflect" "sort" "testing" _ "github.com/tumblr/docker-registry-pruner/internal/pkg/testing" "github.com/tumblr/docker-registry-pruner/pkg/config" + "github.com/tumblr/docker-registry-pruner/pkg/registry" "github.com/tumblr/docker-registry-pruner/pkg/rules" ) -var ( - rulesDir = "test/fixtures/rules" - testImages = map[string][]string{ - "tumblr/fleeble": []string{"v0.6.0-480-g5d09186", "v0.6.0-486-g77397a0", "v0.6.0-413-g463a787", "latest", "v4.2.0", "v4.2.1", "some-ignored-tag", "anothertag", "0.1.2+notignored"}, - "gar/nix": []string{}, - "foo/bar": []string{"1.2.3", "abf273", "henlo"}, - "image/x": []string{"v0.1.1+x", "v0.6.9+x", "v4.2.1+x", "0.0.1+x", "0.0.2+x"}, - "image/y": []string{"v0.1.0+y", "v0.69.420+y", "v4.2.0+y", "0.0.1+y", "0.0.2+y"}, - "tumblr/plumbus": []string{ - "abcdef123", "v1.2.3", "v1.2.4", "v1.2.5", "v2.0+hello", "pr-123", "pr-124", "pr-2345", - }, +func manifestObjectsToManifests(objs []*manifestObject) []*registry.Manifest { + ms := []*registry.Manifest{} + for _, o := range objs { + m := mkmanifest(o.Name, o.Tag, o.DaysOld, o.Labels) + ms = append(ms, m) } -) + return ms +} + +// testConfig is a configuration that defines a set of test. It is comprised of: +// * SourceManifests: all Manifests that will be parsed into a registry.Manifest via mkmanifest. These are source material for the test suite +// * Tests: List of `testCase` +type testConfig struct { + SourceFile string + Manifests []*registry.Manifest + SourceManifests []*manifestObject `yaml:"source_manifests"` + Tests []testCase `yaml:"tests"` +} + +// manifestObject will be parsed from test configs, and then pumped into mkmanifest() +// to turn it into a registry.Manifest. +type manifestObject struct { + Name string + Tag string + DaysOld int64 `yaml:"days_old"` + Labels map[string]string `yaml:"labels"` +} -func TestLoadRules(t *testing.T) { - tests := map[string]int{ - "fleeble-ignore-some.yaml": 2, - "fleeble-match-version.yaml": 2, - "fleeble-match-all.yaml": 2, - "plumbus-pr.yaml": 1, - "fleeble-multiple.yaml": 3, - "multiple-repos.yaml": 3, +// testCase is a struct to define a specific test case. It is comprised of: +// * Config: the yaml config that contains the Rule sets +// * Expected: The map[repo][]tags that the rest should produce from the testConfig.Manifests as input +type testCase struct { + Config string `yaml:"config"` + Expected struct { + Keep map[string][]string `yaml:"keep"` + Delete map[string][]string `yaml:"delete"` + } `yaml:"expected"` +} + +func loadTestConfig(cfg string) (*testConfig, error) { + d, err := ioutil.ReadFile(cfg) + if err != nil { + return nil, err } - for f, nExpected := range tests { - cfg, err := config.LoadFromFile(rulesDir + "/" + f) - if err != nil { - t.Error(err) - t.Fail() - } - if len(cfg.Rules) != nExpected { - t.Errorf("%s: expected %d rules loaded but found %d", f, nExpected, len(cfg.Rules)) - t.Fail() - } - t.Logf("Loaded %d rules\n", len(cfg.Rules)) + + tc := testConfig{} + err = yaml.Unmarshal(d, &tc) + if err != nil { + return nil, err } + tc.SourceFile = cfg + tc.Manifests = manifestObjectsToManifests(tc.SourceManifests) + + return &tc, nil } func TestMatching(t *testing.T) { - fixturesExpected := map[string][]string{ - "fleeble-match-all.yaml": []string{"v0.6.0-480-g5d09186", "v0.6.0-486-g77397a0", "v0.6.0-413-g463a787", "v4.2.0", "v4.2.1", "some-ignored-tag", "0.1.2+notignored", "anothertag"}, - "fleeble-match-version.yaml": []string{"v0.6.0-480-g5d09186", "v0.6.0-486-g77397a0", "v0.6.0-413-g463a787", "v4.2.0", "v4.2.1"}, - "fleeble-ignore-some.yaml": []string{"0.1.2+notignored", "anothertag"}, + tc, err := loadTestConfig("test/fixtures/manifest_tests/manifest_matching_1.yaml") + if err != nil { + t.Error(err) + t.FailNow() } - repo := "tumblr/fleeble" - for f, expectedTags := range fixturesExpected { - fixture := rulesDir + "/" + f - t.Logf("loading rules %s for %s", fixture, repo) - tags := testImages[repo] - cfg, err := config.LoadFromFile(fixture) + for _, test := range tc.Tests { + // sort any expected tag sets + for _, tags := range test.Expected.Keep { + sort.Strings(tags) + } + for _, tags := range test.Expected.Delete { + sort.Strings(tags) + } + t.Logf("%s: loading rules from %s", tc.SourceFile, test.Config) + + cfg, err := config.LoadFromFile(test.Config) if err != nil { t.Error(err) - t.Fail() + t.FailNow() } - t.Logf("Loaded %d rules\n", len(cfg.Rules)) + t.Logf("%s: Loaded %d rules from %s (%d manifests)", tc.SourceFile, len(cfg.Rules), test.Config, len(tc.Manifests)) selectors := rules.RulesToSelectors(cfg.Rules) - foundTags := []string{} - for _, tag := range tags { - if rules.MatchAny(selectors, repo, tag) { - foundTags = append(foundTags, tag) + matchedManifests := map[string][]string{} + for _, manifest := range tc.Manifests { + if rules.MatchAny(selectors, manifest) { + if matchedManifests[manifest.Name] == nil { + matchedManifests[manifest.Name] = []string{} + } + matchedManifests[manifest.Name] = append(matchedManifests[manifest.Name], manifest.Tag) + sort.Strings(matchedManifests[manifest.Name]) } } - sort.Strings(foundTags) - sort.Strings(expectedTags) - if !reflect.DeepEqual(expectedTags, foundTags) { - t.Errorf("%s: expected matching tags to be %v but got %v", fixture, expectedTags, foundTags) - t.Fail() + if !reflect.DeepEqual(test.Expected.Keep, matchedManifests) { + t.Errorf("%s: (rules %s) expected matching tags to be %v but got %v", tc.SourceFile, test.Config, test.Expected.Keep, matchedManifests) + t.FailNow() } } } func TestFilterRepoTags(t *testing.T) { - repoTags := testImages - fixturesRulesToExpected := map[string]map[string][]string{ - "fleeble-ignore-some.yaml": map[string][]string{ - "tumblr/fleeble": []string{"0.1.2+notignored", "anothertag"}, - }, - "fleeble-match-all.yaml": map[string][]string{ - "tumblr/fleeble": []string{"0.1.2+notignored", "anothertag", "some-ignored-tag", "v0.6.0-413-g463a787", "v0.6.0-480-g5d09186", "v0.6.0-486-g77397a0", "v4.2.0", "v4.2.1"}, - }, - "fleeble-match-version.yaml": map[string][]string{ - "tumblr/fleeble": []string{"v0.6.0-413-g463a787", "v0.6.0-480-g5d09186", "v0.6.0-486-g77397a0", "v4.2.0", "v4.2.1"}, - }, - "plumbus-pr.yaml": map[string][]string{ - "tumblr/plumbus": []string{"pr-123", "pr-124", "pr-2345"}, - }, - "fleeble-tagselectors.yaml": map[string][]string{ - "tumblr/fleeble": []string{"v0.6.0-413-g463a787", "v0.6.0-480-g5d09186", "v0.6.0-486-g77397a0", "v4.2.0", "v4.2.1"}, - }, - "multiple-repos.yaml": map[string][]string{ - "tumblr/fleeble": []string{"v0.6.0-413-g463a787", "v0.6.0-480-g5d09186", "v0.6.0-486-g77397a0", "v4.2.0", "v4.2.1"}, - "tumblr/plumbus": []string{"pr-123", "pr-124", "pr-2345"}, - }, - "multiple-repo-versions.yaml": map[string][]string{ - "image/x": []string{"0.0.1+x", "0.0.2+x", "v0.1.1+x", "v0.6.9+x", "v4.2.1+x"}, - "image/y": []string{"0.0.1+y", "0.0.2+y", "v0.1.0+y", "v0.69.420+y", "v4.2.0+y"}, - }, + tc, err := loadTestConfig("test/fixtures/manifest_tests/filter_repo_tags.yaml") + if err != nil { + t.Error(err) + t.FailNow() } - for fixtureFile, expected := range fixturesRulesToExpected { - fixture := rulesDir + "/" + fixtureFile - t.Logf("Applying filters from fixture %s...", fixture) - cfg, err := config.LoadFromFile(fixture) + for _, test := range tc.Tests { + // sort any expected tag sets + for _, tags := range test.Expected.Keep { + sort.Strings(tags) + } + for _, tags := range test.Expected.Keep { + sort.Strings(tags) + } + t.Logf("%s: loading rules from %s", tc.SourceFile, test.Config) + + cfg, err := config.LoadFromFile(test.Config) if err != nil { t.Error(err) - t.Fail() + t.FailNow() } selectors := rules.RulesToSelectors(cfg.Rules) - actualRepoTags := rules.FilterRepoTags(repoTags, selectors) - if !reflect.DeepEqual(expected, actualRepoTags) { - t.Errorf("%s: expected matching tags to be %v but got %v", fixture, expected, actualRepoTags) - t.Fail() + actualManifests := rules.FilterManifests(tc.Manifests, selectors) + // construct a map[string]map[string][]string from actualManifests to aid in comparison + actualManifestsTags := map[string][]string{} + for repo, ms := range actualManifests { + actualManifestsTags[repo] = []string{} + for _, m := range ms { + actualManifestsTags[repo] = append(actualManifestsTags[repo], m.Tag) + } + sort.Strings(actualManifestsTags[repo]) + } + + if !reflect.DeepEqual(test.Expected.Keep, actualManifestsTags) { + t.Errorf("%s: (rules %s) expected matching tags to be:\n%v\nbut got:\n%v", tc.SourceFile, test.Config, test.Expected.Keep, actualManifestsTags) + t.FailNow() } } } diff --git a/pkg/registry/client.go b/pkg/client/client.go similarity index 81% rename from pkg/registry/client.go rename to pkg/client/client.go index b8a8bf1..5ebb12f 100644 --- a/pkg/registry/client.go +++ b/pkg/client/client.go @@ -1,4 +1,4 @@ -package registry +package client import ( "fmt" @@ -6,7 +6,7 @@ import ( r "github.com/nokia/docker-registry-client/registry" "github.com/tumblr/docker-registry-pruner/pkg/config" - "github.com/tumblr/docker-registry-pruner/pkg/rules" + "github.com/tumblr/docker-registry-pruner/pkg/registry" "go.uber.org/zap" ) @@ -17,8 +17,7 @@ var ( type Client struct { r.Registry - Rules []*rules.Rule - nWorkers int + Config *config.Config } type repoTagList struct { @@ -55,8 +54,7 @@ func New(c *config.Config) (*Client, error) { client := Client{ Registry: *hub, - Rules: c.Rules, - nWorkers: c.Parallelism, + Config: c, } return &client, nil } @@ -79,7 +77,7 @@ func (hub *Client) tagFetchWorker(id int, workCh <-chan string, resultCh chan<- log.Debugf("%d: tag fetcher exiting", id) } -func (hub *Client) manifestFetchWorker(id int, workCh <-chan repoTag, resultCh chan<- *Manifest) { +func (hub *Client) manifestFetchWorker(id int, workCh <-chan repoTag, resultCh chan<- *registry.Manifest) { for rt := range workCh { log.Debugf("looking up manifest for %s:%s", rt.Repo, rt.Tag) m, err := hub.Manifest(rt.Repo, rt.Tag) @@ -93,7 +91,7 @@ func (hub *Client) manifestFetchWorker(id int, workCh <-chan repoTag, resultCh c log.Debugf("%d: manifest fetcher exiting", id) } -func (hub *Client) deleteManifestWorker(id int, workCh <-chan *Manifest, resultCh chan<- error) { +func (hub *Client) deleteManifestWorker(id int, workCh <-chan *registry.Manifest, resultCh chan<- error) { for m := range workCh { log.Infof("%d: deleting manifest for %s:%s", id, m.Name, m.Tag) err := hub.DeleteManifest(m) @@ -123,12 +121,12 @@ func (hub *Client) RepoTags(repos []string) (map[string][]string, error) { wg := sync.WaitGroup{} workCh := make(chan string) resultCh := make(chan repoTagList) - nWorkers := hub.nWorkers + nWorkers := hub.Config.Parallelism if nWorkers <= 0 { nWorkers = 1 } go func(wg *sync.WaitGroup, resultCh chan repoTagList, workCh chan string) { - for i := 0; i < hub.nWorkers; i++ { + for i := 0; i < nWorkers; i++ { wg.Add(1) go func(i int, wg *sync.WaitGroup) { hub.tagFetchWorker(i, workCh, resultCh) @@ -156,25 +154,25 @@ func (hub *Client) RepoTags(repos []string) (map[string][]string, error) { return repoTags, nil } -func (hub *Client) Manifest(repo, tag string) (*Manifest, error) { +func (hub *Client) Manifest(repo, tag string) (*registry.Manifest, error) { m, err := hub.ManifestV1(repo, tag) if err != nil { return nil, err } - return FromSignedManifest(m) + return registry.FromSignedManifest(m) } -func (hub *Client) Manifests(repoTags map[string][]string) ([]*Manifest, error) { +func (hub *Client) Manifests(repoTags map[string][]string) ([]*registry.Manifest, error) { wg := sync.WaitGroup{} workCh := make(chan repoTag) - resultCh := make(chan *Manifest) - nWorkers := hub.nWorkers + resultCh := make(chan *registry.Manifest) + nWorkers := hub.Config.Parallelism if nWorkers <= 0 { nWorkers = 1 } - go func(wg *sync.WaitGroup, resultCh chan *Manifest, workCh chan repoTag) { - for i := 0; i < hub.nWorkers; i++ { + go func(wg *sync.WaitGroup, resultCh chan *registry.Manifest, workCh chan repoTag) { + for i := 0; i < nWorkers; i++ { wg.Add(1) go func(i int, wg *sync.WaitGroup) { hub.manifestFetchWorker(i, workCh, resultCh) @@ -200,7 +198,7 @@ func (hub *Client) Manifests(repoTags map[string][]string) ([]*Manifest, error) }(workCh, repoTags) // read from the results channel and stuff results into our tracking map - manifests := []*Manifest{} + manifests := []*registry.Manifest{} for res := range resultCh { manifests = append(manifests, res) } @@ -208,18 +206,18 @@ func (hub *Client) Manifests(repoTags map[string][]string) ([]*Manifest, error) return manifests, nil } -func (hub *Client) DeleteManifestsParallel(manifests []*Manifest) (int, []error) { +func (hub *Client) DeleteManifestsParallel(manifests []*registry.Manifest) (int, []error) { // TODO(gabe) we should figure out how to abstract this parallel worker pattern into a generic system wg := sync.WaitGroup{} - workCh := make(chan *Manifest) + workCh := make(chan *registry.Manifest) resultCh := make(chan error) - nWorkers := hub.nWorkers + nWorkers := hub.Config.Parallelism if nWorkers <= 0 { nWorkers = 1 } - go func(wg *sync.WaitGroup, resultCh chan error, workCh chan *Manifest) { - for i := 0; i < hub.nWorkers; i++ { + go func(wg *sync.WaitGroup, resultCh chan error, workCh chan *registry.Manifest) { + for i := 0; i < nWorkers; i++ { wg.Add(1) go func(i int, wg *sync.WaitGroup) { hub.deleteManifestWorker(i, workCh, resultCh) @@ -231,7 +229,7 @@ func (hub *Client) DeleteManifestsParallel(manifests []*Manifest) (int, []error) }(&wg, resultCh, workCh) // enqueue the work to be done - go func(workCh chan<- *Manifest, manifests []*Manifest) { + go func(workCh chan<- *registry.Manifest, manifests []*registry.Manifest) { for _, m := range manifests { log.Debugf("enqueuing deletion of manifest for %s:%s\n", m.Name, m.Tag) workCh <- m @@ -252,7 +250,7 @@ func (hub *Client) DeleteManifestsParallel(manifests []*Manifest) (int, []error) return deleted, errs } -func (hub *Client) DeleteManifests(manifests []*Manifest) []error { +func (hub *Client) DeleteManifests(manifests []*registry.Manifest) []error { errs := []error{} for _, m := range manifests { err := hub.DeleteManifest(m) @@ -264,7 +262,7 @@ func (hub *Client) DeleteManifests(manifests []*Manifest) []error { return errs } -func (hub *Client) DeleteManifest(m *Manifest) error { +func (hub *Client) DeleteManifest(m *registry.Manifest) error { desc, err := hub.Registry.ManifestDescriptor(m.Name, m.Tag) if err != nil { return err diff --git a/pkg/config/config.go b/pkg/config/config.go index dff9ef3..b8a385b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -32,7 +32,8 @@ type Config struct { } type ConfigRule struct { - Repos []string + Repos []string + Labels map[string]string // IgnoreTags will ignore all manifests with the matching tags (regex) IgnoreTags []string `yaml:"ignore_tags"` // MatchTags will restrict the rule to only apply to manifests matching the regex tag @@ -130,6 +131,7 @@ func ruleFromConfigRule(cr *ConfigRule) (*rules.Rule, error) { r := rules.Rule{ Selector: rules.Selector{ Repos: cr.Repos, + Labels: cr.Labels, MatchTags: []*regexp.Regexp{}, IgnoreTags: []*regexp.Regexp{}, }, @@ -137,6 +139,9 @@ func ruleFromConfigRule(cr *ConfigRule) (*rules.Rule, error) { KeepVersions: cr.KeepVersions, KeepMostRecent: cr.KeepMostRecent, } + if r.Selector.Labels == nil { + r.Selector.Labels = map[string]string{} + } for _, re := range cr.MatchTags { x, err := regexp.Compile(re) if err != nil { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 19018ae..cd8b476 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -13,6 +13,7 @@ type errorFixture struct { } var ( + rulesDir = "test/fixtures/rules" fixtureDirectory = "test/fixtures/config" errorTests = []errorFixture{ { @@ -20,8 +21,8 @@ var ( expected: ErrMissingRegistry, }, { - file: "invalid-rule-missing-repos.yaml", - expected: rules.ErrMissingRepos, + file: "invalid-rule-missing-repos-and-labels.yaml", + expected: rules.ErrMissingReposOrLabels, }, { file: "invalid-rule-missing-action.yaml", @@ -57,3 +58,26 @@ func TestLoadInvalidConfigs(t *testing.T) { } } } + +func TestLoadRules(t *testing.T) { + tests := map[string]int{ + "fleeble-ignore-some.yaml": 2, + "fleeble-match-version.yaml": 2, + "fleeble-match-all.yaml": 2, + "plumbus-pr.yaml": 1, + "fleeble-multiple.yaml": 3, + "multiple-repos.yaml": 3, + } + for f, nExpected := range tests { + cfg, err := LoadFromFile(rulesDir + "/" + f) + if err != nil { + t.Error(err) + t.Fail() + } + if len(cfg.Rules) != nExpected { + t.Errorf("%s: expected %d rules loaded but found %d", f, nExpected, len(cfg.Rules)) + t.Fail() + } + t.Logf("Loaded %d rules\n", len(cfg.Rules)) + } +} diff --git a/pkg/graph/layer.go b/pkg/graph/layer.go deleted file mode 100644 index 1a2b89c..0000000 --- a/pkg/graph/layer.go +++ /dev/null @@ -1,9 +0,0 @@ -package graph - -import ( - g "gonum.org/v1/gonum/graph" -) - -type LayerNode struct { - Digest string -} diff --git a/pkg/graph/tag.go b/pkg/graph/tag.go deleted file mode 100644 index c607ba3..0000000 --- a/pkg/graph/tag.go +++ /dev/null @@ -1,10 +0,0 @@ -package graph - -import ( - g "gonum.org/v1/gonum/graph" -) - -type TagNode struct { - Tag string - Layers []LayerNode -} diff --git a/pkg/registry/docker_distribution_compat.go b/pkg/registry/docker_distribution_compat.go index 7f68029..1776bb4 100644 --- a/pkg/registry/docker_distribution_compat.go +++ b/pkg/registry/docker_distribution_compat.go @@ -14,33 +14,52 @@ import ( ) // internal struct used to extract lastmodified time from a V1 schema -type v1Compatibility struct { - Parent string `json:"parent,omitempty"` - Comment string `json:"comment,omitempty"` - Created time.Time `json:"created"` +type v1CompatibilityHistory struct { + Parent string `json:"parent,omitempty"` + Comment string `json:"comment,omitempty"` + Created time.Time `json:"created"` + Config struct { + Labels map[string]string `json:"Labels,omitempty"` + } `json:"config,omitempty"` ContainerConfig struct { Cmd []string } `json:"container_config,omitempty"` Author string `json:"author,omitempty"` } -// lastModified does some shady stuff to a v1 manifest to extract the latest time any history -// element was modified. This is used to tell us about an image, and do date-based expiry of images. -// NOTE: it appears this is not super supported by the docker/distribution API, and is not present -// in V2 schema! :shruggie: -// will need to figure out what we want to do for V2 manifests if we want to support date-based expirations -func lastModified(m *schema1.SignedManifest) time.Time { - var t time.Time - for _, h := range m.History { - v1c := v1Compatibility{} - err := json.Unmarshal([]byte(h.V1Compatibility), &v1c) - if err != nil { - // if we cant parse a v1compat struct, just skip - continue +func deserializeV1CompatibilityHistory(historySerialized string) (v1c v1CompatibilityHistory) { + // if we cant parse a v1compat struct, just skip suppress the error + json.Unmarshal([]byte(historySerialized), &v1c) + return +} + +// FromSignedManifest does some parsing and field extraction from the underlying SignedManifest +// and returns our sugar object +func FromSignedManifest(sm *schema1.SignedManifest) (*Manifest, error) { + // we do some shady stuff to a v1 manifest to extract the latest time any history + // element was modified. This is used to tell us about an image, and do date-based expiry of images. + // NOTE: it appears this is not super supported by the docker/distribution API, and is not present + // in V2 schema! :shruggie: + // will need to figure out what we want to do for V2 manifests if we want to support date-based expirations + + labels := map[string]string{} + var lastModified time.Time + + for _, h := range sm.History { + // keep deserializing the history blobs and extracting any interesting tidbit we can salvage from them + v1c := deserializeV1CompatibilityHistory(h.V1Compatibility) + // we care about the most recent Created field + if v1c.Created.After(lastModified) { + lastModified = v1c.Created } - if v1c.Created.After(t) { - t = v1c.Created + // merge all labels found, only adding those that are not already tracked + if v1c.Config.Labels != nil { + for k, v := range v1c.Config.Labels { + if _, ok := labels[k]; !ok { + labels[k] = v + } + } } } - return t + return NewManifest(sm.Name, sm.Tag, lastModified, labels) } diff --git a/pkg/registry/manifest.go b/pkg/registry/manifest.go index e8a2963..97713eb 100644 --- a/pkg/registry/manifest.go +++ b/pkg/registry/manifest.go @@ -3,12 +3,12 @@ package registry import ( "fmt" "regexp" - "sort" + //"sort" "time" - "github.com/docker/distribution/manifest/schema1" "github.com/hashicorp/go-version" - "github.com/tumblr/docker-registry-pruner/pkg/rules" + "go.uber.org/zap" + //"github.com/tumblr/docker-registry-pruner/pkg/rules" ) var ( @@ -16,6 +16,8 @@ var ( DefaultVersion = version.Must(version.NewVersion("0.0.0")) // GitShaRegex is the anchored regex that a pure commit sha matches GitShaRegex = regexp.MustCompile(`^[0-9a-f]{4,}$`) + logger, _ = zap.NewProduction() + log = logger.Sugar() ) // Manifest is a combined struct of a v1 manifest, as well as some interesting fields @@ -28,33 +30,17 @@ type Manifest struct { LastModified time.Time // Version is a sortable version field, derived from Tag Version *version.Version -} - -func must(m *Manifest, err error) *Manifest { - if err != nil { - panic(err) - } - return m -} - -// FromSignedManifest does some parsing and field extraction from the underlying SignedManifest -// and returns our sugar object -func FromSignedManifest(sm *schema1.SignedManifest) (*Manifest, error) { - return NewManifestWithLastModified(sm.Name, sm.Tag, lastModified(sm)) + Labels map[string]string } // NewManifest creates a new Manifest -func NewManifest(repo string, tag string) (*Manifest, error) { - return NewManifestWithLastModified(repo, tag, time.Unix(0, 0)) -} - -// NewManifestWithLastModified creates a new Manifest -func NewManifestWithLastModified(repo string, tag string, lm time.Time) (*Manifest, error) { +func NewManifest(repo string, tag string, lm time.Time, labels map[string]string) (*Manifest, error) { var err error mani := Manifest{ Name: repo, Tag: tag, LastModified: lm, + Labels: labels, } // do some version parsing of the tag, as well! @@ -75,106 +61,9 @@ func NewManifestWithLastModified(repo string, tag string, lm time.Time) (*Manife return &mani, nil } -// Match tells whether a Selector matches thsi manifest. It uses the ignore* and match* -// fields of the selector. -func (m *Manifest) Match(selector rules.Selector) bool { - return selector.Match(m.Name, m.Tag) -} - -// ApplyRules takes a list of rules, and applies them to a list of manifests. -// 2 stages: 1. matching selectors, 2. of those that match, apply retention logic in rule -// returns 2 slices; the manifests to keep, and those to delete -func ApplyRules(ruleset []*rules.Rule, manifests []*Manifest) (keep []*Manifest, delete []*Manifest) { - manifestsByRepo := map[string][]*Manifest{} - // group manifests by their repo, so we apply rule sets only over one repo's manifests at a time - for _, manifest := range manifests { - ms, ok := manifestsByRepo[manifest.Name] - if !ok { - ms = []*Manifest{} - } - manifestsByRepo[manifest.Name] = append(ms, manifest) - } - - // apply rules to manifests - for _, manifests := range manifestsByRepo { - k, d := applyRules(ruleset, manifests) - /* - for _, r := range ruleset { - fmt.Printf("rules: %+v\n", *r) - } - fmt.Printf("got keep=%v\n", manifestsAsTagList(k)) - fmt.Printf("got delete=%v\n", manifestsAsTagList(d)) - */ - keep = append(keep, k...) - delete = append(delete, d...) - } - - // 3. dedupe our keep/delete sets, because we definitely could have matched an image with multiple rules - // NOTE: delete supercedes any keep directive, because keep is a default. - // TODO: we will need to remove all the deletes from keeps - keep = removeItems(keep, delete) - return dedupeManifests(keep), dedupeManifests(delete) -} - -// applyRules returns a list of Manifests that match the set of rules -// assumes all manifests are for the same repo! -func applyRules(ruleset []*rules.Rule, manifests []*Manifest) (keep []*Manifest, delete []*Manifest) { - for _, rule := range ruleset { - // 1. for each rule, see if any manifests match our selector. - filteredManifests := []*Manifest{} - for _, manifest := range manifests { - // see if this rule's Selector matches any of these images for _, manifest := range manifests { - if manifest.Match(rule.Selector) { - filteredManifests = append(filteredManifests, manifest) - } - } - - // 2. For all manifests that were selected by this rule, apply retention logic to it - switch { - case rule.KeepVersions > 0: - // handle versions that arent parsable. We do not apply any retention rules to versions that didnt parse - validVersionManifests := []*Manifest{} - for _, manifest := range filteredManifests { - if manifest.Version != DefaultVersion { - validVersionManifests = append(validVersionManifests, manifest) - } - } - sort.Sort(ManifestVersionCollection(validVersionManifests)) - indexHigh := len(validVersionManifests) - indexLow := indexHigh - rule.KeepVersions - if indexLow < 0 { - indexLow = 0 - } - delete = append(delete, validVersionManifests[0:indexLow]...) - keep = append(keep, validVersionManifests[indexLow:indexHigh]...) - - case rule.KeepDays > 0: - tNow := time.Now() - sort.Sort(ManifestModifiedCollection(filteredManifests)) - for _, manifest := range filteredManifests { - if int64(tNow.Sub(manifest.LastModified).Minutes()) > int64(24*60*rule.KeepDays) { - delete = append(delete, manifest) - } else { - keep = append(keep, manifest) - } - } - case rule.KeepMostRecent > 0: - sort.Sort(ManifestModifiedCollection(filteredManifests)) - for i, manifest := range filteredManifests { - if i < len(filteredManifests)-rule.KeepMostRecent { - delete = append(delete, manifest) - } else { - keep = append(keep, manifest) - } - } - } - } - return -} - // removes all items in b from a, returning the list (a-b) // this is super shitty timecomplexity but i really dont care -func removeItems(a []*Manifest, b []*Manifest) []*Manifest { +func RemoveItems(a []*Manifest, b []*Manifest) []*Manifest { newa := make([]*Manifest, len(a)) for i, x := range a { newa[i] = x @@ -190,7 +79,8 @@ func removeItems(a []*Manifest, b []*Manifest) []*Manifest { return newa } -func dedupeManifests(s []*Manifest) []*Manifest { +// DedupeManifests will deduplicate a list of Manifests by name:tag +func DedupeManifests(s []*Manifest) []*Manifest { seen := make(map[string]struct{}, len(s)) j := 0 for _, v := range s { diff --git a/pkg/registry/rules_test.go b/pkg/registry/rules_test.go deleted file mode 100644 index 8c1f9c2..0000000 --- a/pkg/registry/rules_test.go +++ /dev/null @@ -1,240 +0,0 @@ -package registry - -import ( - "fmt" - "reflect" - "sort" - "testing" - "time" - - _ "github.com/tumblr/docker-registry-pruner/internal/pkg/testing" - "github.com/tumblr/docker-registry-pruner/pkg/config" -) - -// helper function to make a test fixture manifest -func mkmanifest(r, t string, daysOld int64) *Manifest { - return must(NewManifestWithLastModified(r, t, tNow.Add(time.Duration(-daysOld*24)*time.Hour))) -} - -var ( - rulesDir = "test/fixtures/rules" - - tNow = time.Now() - manifests = []*Manifest{ - mkmanifest("tumblr/plumbus", "v1.2.3", 4), - mkmanifest("tumblr/plumbus", "v1.0.3+metadata", 69), - mkmanifest("tumblr/plumbus", "pr-69420+13d", 13), - mkmanifest("tumblr/plumbus", "pr-69420+14d", 14), - mkmanifest("tumblr/plumbus", "master-v1.2.3-69", 14), - mkmanifest("tumblr/plumbus", "master-2019", 14), - mkmanifest("tumblr/plumbus", "pr-420", 1), - mkmanifest("tumblr/plumbus", "pr-69", 5), - mkmanifest("tumblr/plumbus", "pr-69421+15d", 15), - mkmanifest("tumblr/plumbus", "pr-69419+16d", 16), - mkmanifest("image/latest", "latest", 69420), - mkmanifest("tumblr/fleeble", "latest", 0), - mkmanifest("tumblr/fleeble", "garbage", 5), - mkmanifest("tumblr/fleeble", "v0.4.2-259-something", 5), - mkmanifest("tumblr/fleeble", "v0.5.0-260", 4), - mkmanifest("tumblr/fleeble", "v0.5.1-260", 4), - mkmanifest("tumblr/fleeble", "v0.5.23+test", 3), - mkmanifest("tumblr/fleeble", "v0.5.2", 3), - mkmanifest("tumblr/fleeble", "some-ignored-tag", 69), - mkmanifest("tumblr/fleeble", "oldtag-1", 69), - mkmanifest("tumblr/fleeble", "oldtag-2", 70), - mkmanifest("tumblr/fleeble", "v0.6.1-261-gbb41394", 0), - mkmanifest("tumblr/fleeble", "v0.5.3-nice", 1), - mkmanifest("tumblr/fleeble", "v0.69-6969", 1), - mkmanifest("tumblr/fleeble", "v0.5.5-420", 1), - mkmanifest("tumblr/fleeble", "v0.6.1-262", 0), - mkmanifest("tumblr/fleeble", "branch-v1.2.3-69", 14), - mkmanifest("tumblr/fleeble", "v0.69.1-262", 0), - mkmanifest("tumblr/fleeble", "abc123f", 0), - - mkmanifest("image/x", "v0.1.1+x", 0), - mkmanifest("image/x", "v0.6.9+x", 0), - mkmanifest("image/x", "v4.2.1+x", 0), - mkmanifest("image/x", "0.0.1+x", 0), - mkmanifest("image/x", "0.0.2+x", 0), - mkmanifest("image/y", "v0.1.0+y", 0), - mkmanifest("image/y", "v0.69.420+y", 0), - mkmanifest("image/y", "v4.2.0+y", 0), - mkmanifest("image/y", "0.0.1+y", 0), - mkmanifest("image/y", "0.0.2+y", 0), - } - tests = []testpayload{ - { - rulesFile: "multiple-repo-keep-latest.yaml", - input: manifests, - // should keep the latest 4 images from both fleeble and plumbus - keepImages: []string{ - "tumblr/fleeble:abc123f", // modified: 0 days ago - "tumblr/fleeble:v0.6.1-261-gbb41394", // modified: 0 days ago - "tumblr/fleeble:v0.6.1-262", // modified: 0 days ago - "tumblr/fleeble:v0.69.1-262", // modified: 0 days ago - "tumblr/plumbus:pr-420", // modified: 1 days ago - "tumblr/plumbus:v1.2.3", // modified: 4 days ago - "tumblr/plumbus:pr-69", // modified: 5 days ago - "tumblr/plumbus:pr-69420+13d", // modified: 13 days ago - }, - deleteImages: []string{ - "tumblr/fleeble:branch-v1.2.3-69", - "tumblr/fleeble:garbage", - "tumblr/fleeble:oldtag-1", - "tumblr/fleeble:oldtag-2", - "tumblr/fleeble:some-ignored-tag", - "tumblr/fleeble:v0.4.2-259-something", - "tumblr/fleeble:v0.5.0-260", - "tumblr/fleeble:v0.5.1-260", - "tumblr/fleeble:v0.5.2", - "tumblr/fleeble:v0.5.23+test", - "tumblr/fleeble:v0.5.3-nice", - "tumblr/fleeble:v0.5.5-420", - "tumblr/fleeble:v0.69-6969", - "tumblr/plumbus:master-2019", - "tumblr/plumbus:master-v1.2.3-69", - "tumblr/plumbus:pr-69419+16d", - "tumblr/plumbus:pr-69420+14d", - "tumblr/plumbus:pr-69421+15d", - "tumblr/plumbus:v1.0.3+metadata", - }, - }, - { - rulesFile: "onlylatest.yaml", - input: manifests, - keepImages: []string{}, - deleteImages: []string{}, // we dont want to see it in delete, cause its ignored - }, - { - rulesFile: "plumbus-pr.yaml", - input: manifests, - keepImages: []string{ - "tumblr/plumbus:pr-420", - "tumblr/plumbus:pr-69", - "tumblr/plumbus:pr-69420+13d", - "tumblr/plumbus:pr-69420+14d"}, - deleteImages: []string{ - "tumblr/plumbus:pr-69419+16d", - "tumblr/plumbus:pr-69421+15d"}, - }, - { - // ignore some tags that would otherwise get cleaned up by date predicates - rulesFile: "fleeble-ignore-some.yaml", - input: manifests, - keepImages: []string{"tumblr/fleeble:abc123f"}, - deleteImages: []string{"tumblr/fleeble:branch-v1.2.3-69", "tumblr/fleeble:garbage", "tumblr/fleeble:oldtag-1", "tumblr/fleeble:oldtag-2"}, - }, - { - rulesFile: "fleeble-match-version.yaml", - // this rule should retain only 5 latest version tags - // and will implicitly skip all versions taht dont parse correctly as a Version - // meaning there should be no deletedTags that arent correct semantic versions - input: manifests, - keepImages: []string{ - "tumblr/fleeble:v0.5.23+test", - "tumblr/fleeble:v0.6.1-261-gbb41394", - "tumblr/fleeble:v0.6.1-262", - "tumblr/fleeble:v0.69-6969", - "tumblr/fleeble:v0.69.1-262", - }, - deleteImages: []string{ - "tumblr/fleeble:v0.4.2-259-something", - "tumblr/fleeble:v0.5.0-260", - "tumblr/fleeble:v0.5.1-260", - "tumblr/fleeble:v0.5.2", - "tumblr/fleeble:v0.5.3-nice", - "tumblr/fleeble:v0.5.5-420", - }, - }, - { - // this should keep 2 latest version tags, and the last 2 days of all tags. - // this means there are some versions that would have been deleted, that are still retained - rulesFile: "fleeble-multiple.yaml", - input: manifests, - keepImages: []string{ - "tumblr/fleeble:abc123f", - "tumblr/fleeble:v0.69-6969", - "tumblr/fleeble:v0.69.1-262", - }, - deleteImages: []string{ - "tumblr/fleeble:branch-v1.2.3-69", - "tumblr/fleeble:garbage", - "tumblr/fleeble:oldtag-1", - "tumblr/fleeble:oldtag-2", - "tumblr/fleeble:some-ignored-tag", // ignored by 1 rule, but deleted by the nDays rule! - "tumblr/fleeble:v0.4.2-259-something", - "tumblr/fleeble:v0.5.0-260", - "tumblr/fleeble:v0.5.1-260", - "tumblr/fleeble:v0.5.2", - "tumblr/fleeble:v0.5.23+test", - "tumblr/fleeble:v0.5.3-nice", - "tumblr/fleeble:v0.5.5-420", - "tumblr/fleeble:v0.6.1-261-gbb41394", - "tumblr/fleeble:v0.6.1-262", - }, - }, - { - rulesFile: "multiple-repo-versions.yaml", - input: manifests, - keepImages: []string{ - "image/x:v0.1.1+x", - "image/x:v0.6.9+x", - "image/x:v4.2.1+x", - "image/y:v0.1.0+y", - "image/y:v0.69.420+y", - "image/y:v4.2.0+y", - }, - deleteImages: []string{ - "image/x:0.0.1+x", - "image/x:0.0.2+x", - "image/y:0.0.1+y", - "image/y:0.0.2+y", - }, - }, - } -) - -type testpayload struct { - rulesFile string - input []*Manifest - keepImages []string - deleteImages []string -} - -func TestApplyRules(t *testing.T) { - for _, testObj := range tests { - rulesfile := testObj.rulesFile - // map intent to a list of versions - cfg, err := config.LoadFromFile(rulesDir + "/" + rulesfile) - if err != nil { - t.Error(err) - t.Fail() - } - - keep, delete := ApplyRules(cfg.Rules, testObj.input) - keep_tags := manifestsAsImageList(keep) - delete_tags := manifestsAsImageList(delete) - expectedKeep := testObj.keepImages - expectedDelete := testObj.deleteImages - sort.Strings(expectedKeep) - sort.Strings(expectedDelete) - - if !reflect.DeepEqual(expectedKeep, keep_tags) { - t.Errorf("%s: expected keep images to be %v but was actually %v", rulesfile, expectedKeep, keep_tags) - t.Fail() - } - if !reflect.DeepEqual(expectedDelete, delete_tags) { - t.Errorf("%s: expected delete images tags to be %v but was actually %v", rulesfile, expectedDelete, delete_tags) - t.Fail() - } - } -} - -func manifestsAsImageList(ms []*Manifest) []string { - ts := []string{} - for _, m := range ms { - ts = append(ts, fmt.Sprintf("%s:%s", m.Name, m.Tag)) - } - sort.Strings(ts) - return ts -} diff --git a/pkg/rules/rule.go b/pkg/rules/rule.go index 64f4df9..e971847 100644 --- a/pkg/rules/rule.go +++ b/pkg/rules/rule.go @@ -2,18 +2,24 @@ package rules import ( "fmt" + "sort" "strings" + "time" + + "github.com/tumblr/docker-registry-pruner/pkg/registry" ) var ( + // ErrLabelsNil is returned when an initialization error creates a Selector with a nil Labels map + ErrLabelsNil = fmt.Errorf("labels must not be a nil map") // ErrKeepVersionsMustBePositive ErrKeepVersionsMustBePositive = fmt.Errorf("keep_versions must be positive") // ErrKeepDaysMustBePositive ErrKeepDaysMustBePositive = fmt.Errorf("keep_days must be positive") // ErrKeepMostRecentCountMustBePositive ErrKeepMostRecentCountMustBePositive = fmt.Errorf("keep_recent must be positive") - // ErrMissingRepos - ErrMissingRepos = fmt.Errorf("repos field missing") + // ErrMissingReposOrLabels + ErrMissingReposOrLabels = fmt.Errorf("repos or labels selector is required") // ErrActionMustBeSpecified ErrActionMustBeSpecified = fmt.Errorf("one of keep_versions, keep_days, or keep_recent must be specified as an action") ErrMultipleActionVersionsDays = fmt.Errorf("both keep_versions and keep_days specified, but are mutually exclusive") @@ -53,13 +59,15 @@ func (r *Rule) String() string { if r.KeepVersions != 0 { action = fmt.Sprintf("keep latest %d versions", r.KeepVersions) } - return fmt.Sprintf("Repos:%s Selector{%s} Action{%s}", strings.Join(r.Repos, ","), selector, action) + return fmt.Sprintf("Repos:%s Labels:%v Selector{%s} Action{%s}", strings.Join(r.Repos, ","), r.Labels, selector, action) } func (r *Rule) Validate() error { switch { - case len(r.Repos) == 0: - return ErrMissingRepos + case r.Labels == nil: + return ErrLabelsNil + case len(r.Repos) == 0 && len(r.Labels) == 0: + return ErrMissingReposOrLabels case r.KeepDays != 0 && r.KeepVersions != 0: return ErrMultipleActionVersionsDays case r.KeepDays != 0 && r.KeepMostRecent != 0: @@ -78,3 +86,87 @@ func (r *Rule) Validate() error { return nil } } + +// ApplyRules takes a list of rules, and applies them to a list of manifests. +// 2 stages: 1. matching selectors, 2. of those that match, apply retention logic in rule +// returns 2 slices; the manifests to keep, and those to delete +func ApplyRules(ruleset []*Rule, manifests []*registry.Manifest) (keep []*registry.Manifest, delete []*registry.Manifest) { + manifestsByRepo := map[string][]*registry.Manifest{} + // group manifests by their repo, so we apply rule sets only over one repo's manifests at a time + for _, manifest := range manifests { + ms, ok := manifestsByRepo[manifest.Name] + if !ok { + ms = []*registry.Manifest{} + } + manifestsByRepo[manifest.Name] = append(ms, manifest) + } + + // apply rules to manifests + for _, manifests := range manifestsByRepo { + k, d := applyRules(ruleset, manifests) + keep = append(keep, k...) + delete = append(delete, d...) + } + + // 3. dedupe our keep/delete sets, because we definitely could have matched an image with multiple rules + // NOTE: delete supercedes any keep directive, because keep is a default. + // TODO: we will need to remove all the deletes from keeps + keep = registry.RemoveItems(keep, delete) + return registry.DedupeManifests(keep), registry.DedupeManifests(delete) +} + +// applyRules returns a list of Manifests that match the set of rules +// assumes all manifests are for the same repo! +func applyRules(ruleset []*Rule, manifests []*registry.Manifest) (keep []*registry.Manifest, delete []*registry.Manifest) { + for _, rule := range ruleset { + // 1. for each rule, see if any manifests match our selector. + filteredManifests := []*registry.Manifest{} + for _, manifest := range manifests { + // see if this rule's Selector matches any of these manifests + if rule.Match(manifest) { + filteredManifests = append(filteredManifests, manifest) + } + } + + // 2. For all manifests that were selected by this rule, apply retention logic to it + switch { + case rule.KeepVersions > 0: + // handle versions that arent parsable. We do not apply any retention rules to versions that didnt parse + validVersionManifests := []*registry.Manifest{} + for _, manifest := range filteredManifests { + if manifest.Version != registry.DefaultVersion { + validVersionManifests = append(validVersionManifests, manifest) + } + } + sort.Sort(registry.ManifestVersionCollection(validVersionManifests)) + indexHigh := len(validVersionManifests) + indexLow := indexHigh - rule.KeepVersions + if indexLow < 0 { + indexLow = 0 + } + delete = append(delete, validVersionManifests[0:indexLow]...) + keep = append(keep, validVersionManifests[indexLow:indexHigh]...) + + case rule.KeepDays > 0: + tNow := time.Now() + sort.Sort(registry.ManifestModifiedCollection(filteredManifests)) + for _, manifest := range filteredManifests { + if int64(tNow.Sub(manifest.LastModified).Minutes()) > int64(24*60*rule.KeepDays) { + delete = append(delete, manifest) + } else { + keep = append(keep, manifest) + } + } + case rule.KeepMostRecent > 0: + sort.Sort(registry.ManifestModifiedCollection(filteredManifests)) + for i, manifest := range filteredManifests { + if i < len(filteredManifests)-rule.KeepMostRecent { + delete = append(delete, manifest) + } else { + keep = append(keep, manifest) + } + } + } + } + return +} diff --git a/pkg/rules/selector.go b/pkg/rules/selector.go index 358135e..a9fc3f7 100644 --- a/pkg/rules/selector.go +++ b/pkg/rules/selector.go @@ -3,27 +3,45 @@ package rules import ( "regexp" "sort" + + "github.com/tumblr/docker-registry-pruner/pkg/registry" ) type Selector struct { + // Repos are a list of repo literal strings that the selector will match Repos []string + // Labels are a map of docker labels that are required to be present on an image to be matched by this selector + Labels map[string]string // IgnoreTags will ignore all manifests with the matching tags (regex) IgnoreTags []*regexp.Regexp // MatchTags will restrict the rule to only apply to manifests matching the regex tag MatchTags []*regexp.Regexp } -func (r *Selector) Match(repo, tag string) bool { - anyRepoMatch := false +//func (r *Selector) Match(repo, tag string, labels map[string]string) bool { +func (r *Selector) Match(m *registry.Manifest) bool { + + anyRepoMatch := len(r.Repos) == 0 // if r.Repos is empty, assume we have a Repos predicate match for _, r := range r.Repos { - anyRepoMatch = (r == repo) || anyRepoMatch + anyRepoMatch = (r == m.Name) || anyRepoMatch + } + allLabelsMatch := true // default is that we "match" labels, because empty set is a match + if len(r.Labels) > 0 { + // shortcircuit matching if we have a Labels and the image is missing + // one of our required label keys or values + // require _all_ labels to match + for k, v := range r.Labels { + foundValue, ok := m.Labels[k] + allLabelsMatch = ok && (foundValue == v) && allLabelsMatch + } } - if !anyRepoMatch { - // always return false when this rule does not apply to any listed repos + if !anyRepoMatch || !allLabelsMatch { + // require that a Selector match must match any Repos, and if present, all Labels + // if either of these predicates are not true, bail! return false } for _, re := range r.IgnoreTags { - if re.MatchString(tag) { + if re.MatchString(m.Tag) { // always respect ignored tag patterns return false } @@ -34,15 +52,15 @@ func (r *Selector) Match(repo, tag string) bool { } matchAnyTag := false for _, re := range r.MatchTags { - matchAnyTag = re.MatchString(tag) || matchAnyTag + matchAnyTag = re.MatchString(m.Tag) || matchAnyTag } return matchAnyTag } -func MatchAny(selectors []*Selector, repo, tag string) bool { +func MatchAny(selectors []*Selector, m *registry.Manifest) bool { anyMatch := false for _, selector := range selectors { - anyMatch = selector.Match(repo, tag) || anyMatch + anyMatch = selector.Match(m) || anyMatch } return anyMatch } @@ -55,19 +73,22 @@ func RulesToSelectors(ruleset []*Rule) []*Selector { return ss } -func FilterRepoTags(repoTags map[string][]string, selectors []*Selector) map[string][]string { - matchingRepoTags := map[string][]string{} - for repo, tags := range repoTags { - matchingTags := []string{} - for _, tag := range tags { - if MatchAny(selectors, repo, tag) { - matchingTags = append(matchingTags, tag) +// FilterManifests will apply a set of Selectors over a slice of Manifests, +// and return the map mapping from repo name to list of matching Manifests. +func FilterManifests(manifests []*registry.Manifest, selectors []*Selector) map[string][]*registry.Manifest { + matchingManifests := map[string][]*registry.Manifest{} + for _, manifest := range manifests { + if MatchAny(selectors, manifest) { + if _, ok := matchingManifests[manifest.Name]; !ok { + matchingManifests[manifest.Name] = []*registry.Manifest{} } + matchingManifests[manifest.Name] = append(matchingManifests[manifest.Name], manifest) } - if len(matchingTags) > 0 { - sort.Strings(matchingTags) - matchingRepoTags[repo] = matchingTags - } } - return matchingRepoTags + for _, ms := range matchingManifests { + //sortable := registry.ManifestModifiedCollection(ms) + sort.Sort(registry.ManifestModifiedCollection(ms)) + //matchingManifests[repo] = sortable + } + return matchingManifests } diff --git a/test/fixtures/config/invalid-rule-missing-repos.yaml b/test/fixtures/config/invalid-rule-missing-repos-and-labels.yaml similarity index 100% rename from test/fixtures/config/invalid-rule-missing-repos.yaml rename to test/fixtures/config/invalid-rule-missing-repos-and-labels.yaml diff --git a/test/fixtures/manifest_tests/apply-rules.yaml b/test/fixtures/manifest_tests/apply-rules.yaml new file mode 100644 index 0000000..57f08df --- /dev/null +++ b/test/fixtures/manifest_tests/apply-rules.yaml @@ -0,0 +1,398 @@ +--- +source_manifests: +- name: tumblr/plumbus + tag: v1.2.3 + days_old: 4 + labels: {} +- name: tumblr/plumbus + tag: v1.0.3+metadata + days_old: 69 + labels: {} +- name: tumblr/plumbus + tag: pr-69420+13d + days_old: 13 + labels: {} +- name: tumblr/plumbus + tag: pr-69420+14d + days_old: 14 + labels: {} +- name: tumblr/plumbus + tag: master-v1.2.3-69 + days_old: 14 + labels: {} +- name: tumblr/plumbus + tag: master-2019 + days_old: 14 + labels: {} +- name: tumblr/plumbus + tag: pr-420 + days_old: 1 + labels: {} +- name: tumblr/plumbus + tag: pr-69 + days_old: 5 + labels: {} +- name: tumblr/plumbus + tag: pr-69421+15d + days_old: 15 + labels: {} +- name: tumblr/plumbus + tag: pr-69419+16d + days_old: 16 + labels: {} +- name: image/latest + tag: latest + days_old: 69420 + labels: {} +- name: tumblr/fleeble + tag: latest + days_old: 0 + labels: {} +- name: tumblr/fleeble + tag: garbage + days_old: 5 + labels: {} +- name: tumblr/fleeble + tag: v0.4.2-259-something + days_old: 5 + labels: {} +- name: tumblr/fleeble + tag: v0.5.0-260 + days_old: 4 + labels: {} +- name: tumblr/fleeble + tag: v0.5.1-260 + days_old: 4 + labels: {} +- name: tumblr/fleeble + tag: v0.5.23+test + days_old: 3 + labels: {} +- name: tumblr/fleeble + tag: v0.5.2 + days_old: 3 + labels: {} +- name: tumblr/fleeble + tag: some-ignored-tag + days_old: 69 + labels: {} +- name: tumblr/fleeble + tag: oldtag-1 + days_old: 69 + labels: {} +- name: tumblr/fleeble + tag: oldtag-2 + days_old: 70 + labels: {} +- name: tumblr/fleeble + tag: v0.6.1-261-gbb41394 + days_old: 0 + labels: {} +- name: tumblr/fleeble + tag: v0.5.3-nice + days_old: 1 + labels: {} +- name: tumblr/fleeble + tag: v0.69-6969 + days_old: 1 + labels: {} +- name: tumblr/fleeble + tag: v0.5.5-420 + days_old: 1 + labels: {} +- name: tumblr/fleeble + tag: v0.6.1-262 + days_old: 0 + labels: {} +- name: tumblr/fleeble + tag: branch-v1.2.3-69 + days_old: 14 + labels: {} +- name: tumblr/fleeble + tag: v0.69.1-262 + days_old: 0 + labels: {} +- name: tumblr/fleeble + tag: abc123f + days_old: 0 + labels: {} +- name: image/x + tag: v0.1.1+x + days_old: 0 + labels: {} +- name: image/x + tag: v0.6.9+x + days_old: 0 + labels: {} +- name: image/x + tag: v4.2.1+x + days_old: 0 + labels: {} +- name: image/x + tag: 0.0.1+x + days_old: 0 + labels: {} +- name: image/x + tag: 0.0.2+x + days_old: 0 + labels: {} +- name: image/y + tag: v0.1.0+y + days_old: 0 + labels: {} +- name: image/y + tag: v0.69.420+y + days_old: 0 + labels: {} +- name: image/y + tag: v4.2.0+y + days_old: 0 + labels: {} +- name: image/y + tag: 0.0.1+y + days_old: 0 + labels: {} +- name: image/y + tag: 0.0.2+y + days_old: 0 + labels: {} +- name: image/labeled-x + tag: "d0" + days_old: 0 + labels: + prune: "true" + type: prod +- name: image/labeled-x + tag: "d5" + days_old: 5 + labels: + prune: "true" + type: prod +- name: image/labeled-x + tag: "d4" + days_old: 4 + labels: + prune: "true" + type: prod +- name: image/labeled-x + tag: "d3" + days_old: 3 + labels: + prune: "true" + type: prod +- name: image/labeled-x + tag: "d2" + days_old: 2 + labels: + prune: "true" + type: prod +- name: image/labeled-x + tag: "d1" + days_old: 1 + labels: + prune: "true" + type: prod +- name: image/labeled-y + tag: "420.69" + days_old: 4 + labels: + prune: "true" + type: devel +- name: image/labeled-y + tag: "69.69" + days_old: 15 + labels: + prune: "true" + type: devel +- name: image/labeled-x + tag: "1.2.3" + days_old: 4 + labels: + prune: "true" + type: devel +- name: image/labeled-x + tag: "1.3" + labels: + prune: "true" + type: devel +- name: image/labeled-x + tag: "1.5" + labels: + prune: "true" + type: devel +- name: image/labeled-x + tag: "2.6.9" + labels: + prune: "true" + type: devel +- name: image/labeled-x + tag: "0.0.1+notlabeled" +- name: image/labeled-x + tag: "0.2.1+differentlabels" + labels: + something: notmatching +tests: +tests: + - config: test/fixtures/rules/multiple-repo-keep-latest.yaml + expected: + keep: + tumblr/fleeble: + - abc123f + - v0.6.1-261-gbb41394 + - v0.6.1-262 + - v0.69.1-262 + tumblr/plumbus: + - pr-420 + - v1.2.3 + - pr-69 + - pr-69420+13d + delete: + tumblr/fleeble: + - branch-v1.2.3-69 + - garbage + - oldtag-1 + - oldtag-2 + - some-ignored-tag + - v0.4.2-259-something + - v0.5.0-260 + - v0.5.1-260 + - v0.5.2 + - v0.5.23+test + - v0.5.3-nice + - v0.5.5-420 + - v0.69-6969 + tumblr/plumbus: + - master-2019 + - master-v1.2.3-69 + - pr-69419+16d + - pr-69420+14d + - pr-69421+15d + - v1.0.3+metadata + - config: test/fixtures/rules/onlylatest.yaml + expected: + keep: {} + delete: {} + - config: test/fixtures/rules/plumbus-pr.yaml + expected: + keep: + tumblr/plumbus: + - pr-420 + - pr-69 + - pr-69420+13d + - pr-69420+14d + delete: + tumblr/plumbus: + - pr-69419+16d + - pr-69421+15d + - config: test/fixtures/rules/fleeble-ignore-some.yaml + expected: + keep: + tumblr/fleeble: + - abc123f + delete: + tumblr/fleeble: + - branch-v1.2.3-69 + - garbage + - oldtag-1 + - oldtag-2 + # this rule should retain only 5 latest version tags + # and will implicitly skip all versions taht dont parse correctly as a Version + # meaning there should be no deletedTags that arent correct semantic versions + - config: test/fixtures/rules/fleeble-match-version.yaml + expected: + keep: + tumblr/fleeble: + - v0.5.23+test + - v0.6.1-261-gbb41394 + - v0.6.1-262 + - v0.69-6969 + - v0.69.1-262 + delete: + tumblr/fleeble: + - v0.4.2-259-something + - v0.5.0-260 + - v0.5.1-260 + - v0.5.2 + - v0.5.3-nice + - v0.5.5-420 + # this should keep 2 latest version tags, and the last 2 days of all tags. + # this means there are some versions that would have been deleted, that are still retained + - config: test/fixtures/rules/fleeble-multiple.yaml + expected: + keep: + tumblr/fleeble: + - abc123f + - v0.69-6969 + - v0.69.1-262 + delete: + tumblr/fleeble: + - branch-v1.2.3-69 + - garbage + - oldtag-1 + - oldtag-2 + - some-ignored-tag # ignored by 1 rule, but deleted by the nDays rule! + - v0.4.2-259-something + - v0.5.0-260 + - v0.5.1-260 + - v0.5.2 + - v0.5.23+test + - v0.5.3-nice + - v0.5.5-420 + - v0.6.1-261-gbb41394 + - v0.6.1-262 + - config: test/fixtures/rules/multiple-repo-versions.yaml + expected: + keep: + image/x: + - v0.1.1+x + - v0.6.9+x + - v4.2.1+x + image/y: + - v0.1.0+y + - v0.69.420+y + - v4.2.0+y + delete: + image/x: + - 0.0.1+x + - 0.0.2+x + image/y: + - 0.0.1+y + - 0.0.2+y + # test that matching based on labels works with a keep_versions action + - config: test/fixtures/rules/labels-devel-3-versions.yaml + expected: + keep: + image/labeled-x: + - "2.6.9" + - "1.5" + - "1.3" + image/labeled-y: + - "420.69" + - "69.69" + delete: + image/labeled-x: + - "1.2.3" + # test that matching based on labels works with a keep_latest action + - config: test/fixtures/rules/labels-prod-3-latest.yaml + expected: + keep: + image/labeled-x: + - "d0" + - "d1" + - "d2" + delete: + image/labeled-x: + - "d3" + - "d4" + - "d5" + # test that matching based on labels works when we constrain matches to specific repos + - config: test/fixtures/rules/repo-and-labels-devel-3-versions.yaml + expected: + keep: + image/labeled-x: + - "2.6.9" + - "1.5" + - "1.3" + delete: + image/labeled-x: + - "1.2.3" diff --git a/test/fixtures/manifest_tests/filter_repo_tags.yaml b/test/fixtures/manifest_tests/filter_repo_tags.yaml new file mode 100644 index 0000000..44adf87 --- /dev/null +++ b/test/fixtures/manifest_tests/filter_repo_tags.yaml @@ -0,0 +1,133 @@ +source_manifests: + - name: tumblr/fleeble + tag: v0.6.0-480-g5d09186 + - name: tumblr/fleeble + tag: v0.6.0-486-g77397a0 + - name: tumblr/fleeble + tag: v0.6.0-413-g463a787 + - name: tumblr/fleeble + tag: latest + - name: tumblr/fleeble + tag: v4.2.0 + - name: tumblr/fleeble + tag: v4.2.1 + - name: tumblr/fleeble + tag: some-ignored-tag + - name: tumblr/fleeble + tag: anothertag + - name: tumblr/fleeble + tag: 0.1.2+notignored + - name: foo/bar + tag: 1.2.3 + - name: foo/bar + tag: abf273 + - name: foo/bar + tag: henlo + - name: image/x + tag: v0.1.1+x + - name: image/x + tag: v0.6.9+x + - name: image/x + tag: v4.2.1+x + - name: image/x + tag: 0.0.1+x + - name: image/x + tag: 0.0.2+x + - name: image/y + tag: v0.1.0+y + - name: image/y + tag: v0.69.420+y + - name: image/y + tag: v4.2.0+y + - name: image/y + tag: 0.0.1+y + - name: image/y + tag: 0.0.2+y + - name: tumblr/plumbus + tag: abcdef123 + - name: tumblr/plumbus + tag: v1.2.3 + - name: tumblr/plumbus + tag: v1.2.4 + - name: tumblr/plumbus + tag: v1.2.5 + - name: tumblr/plumbus + tag: v2.0+hello + - name: tumblr/plumbus + tag: pr-123 + - name: tumblr/plumbus + tag: pr-124 + - name: tumblr/plumbus + tag: pr-2345 +tests: + - config: test/fixtures/rules/fleeble-ignore-some.yaml + expected: + keep: + tumblr/fleeble: + - 0.1.2+notignored + - anothertag + - config: test/fixtures/rules/fleeble-match-all.yaml + expected: + keep: + tumblr/fleeble: + - 0.1.2+notignored + - anothertag + - some-ignored-tag + - v0.6.0-413-g463a787 + - v0.6.0-480-g5d09186 + - v0.6.0-486-g77397a0 + - v4.2.0 + - v4.2.1 + - config: test/fixtures/rules/fleeble-match-version.yaml + expected: + keep: + tumblr/fleeble: + - v0.6.0-413-g463a787 + - v0.6.0-480-g5d09186 + - v0.6.0-486-g77397a0 + - v4.2.0 + - v4.2.1 + - config: test/fixtures/rules/plumbus-pr.yaml + expected: + keep: + tumblr/plumbus: + - pr-123 + - pr-124 + - pr-2345 + - config: test/fixtures/rules/fleeble-tagselectors.yaml + expected: + keep: + tumblr/fleeble: + - v0.6.0-413-g463a787 + - v0.6.0-480-g5d09186 + - v0.6.0-486-g77397a0 + - v4.2.0 + - v4.2.1 + - config: test/fixtures/rules/multiple-repos.yaml + expected: + keep: + tumblr/fleeble: + - v0.6.0-413-g463a787 + - v0.6.0-480-g5d09186 + - v0.6.0-486-g77397a0 + - v4.2.0 + - v4.2.1 + tumblr/plumbus: + - pr-123 + - pr-124 + - pr-2345 + - config: test/fixtures/rules/multiple-repo-versions.yaml + expected: + keep: + image/x: + - 0.0.1+x + - 0.0.2+x + - v0.1.1+x + - v0.6.9+x + - v4.2.1+x + image/y: + - 0.0.1+y + - 0.0.2+y + - v0.1.0+y + - v0.69.420+y + - v4.2.0+y diff --git a/test/fixtures/manifest_tests/manifest_matching_1.yaml b/test/fixtures/manifest_tests/manifest_matching_1.yaml new file mode 100644 index 0000000..02aff6b --- /dev/null +++ b/test/fixtures/manifest_tests/manifest_matching_1.yaml @@ -0,0 +1,90 @@ +--- +source_manifests: + - name: tumblr/fleeble + tag: v0.6.0-480-g5d09186 + - name: tumblr/fleeble + tag: v0.6.0-486-g77397a0 + - name: tumblr/fleeble + tag: v0.6.0-413-g463a787 + - name: tumblr/fleeble + tag: latest + - name: tumblr/fleeble + tag: v4.2.0 + - name: tumblr/fleeble + tag: v4.2.1 + - name: tumblr/fleeble + tag: some-ignored-tag + - name: tumblr/fleeble + tag: anothertag + - name: tumblr/fleeble + tag: 0.1.2+notignored + - name: foo/bar + tag: 1.2.3 + - name: foo/bar + tag: abf273 + - name: foo/bar + tag: henlo + - name: image/x + tag: v0.1.1+x + - name: image/x + tag: v0.6.9+x + - name: image/x + tag: v4.2.1+x + - name: image/x + tag: 0.0.1+x + - name: image/x + tag: 0.0.2+x + - name: image/y + tag: v0.1.0+y + - name: image/y + tag: v0.69.420+y + - name: image/y + tag: v4.2.0+y + - name: image/y + tag: 0.0.1+y + - name: image/y + tag: 0.0.2+y + - name: tumblr/plumbus + tag: abcdef123 + - name: tumblr/plumbus + tag: v1.2.3 + - name: tumblr/plumbus + tag: v1.2.4 + - name: tumblr/plumbus + tag: v1.2.5 + - name: tumblr/plumbus + tag: v2.0+hello + - name: tumblr/plumbus + tag: pr-123 + - name: tumblr/plumbus + tag: pr-124 + - name: tumblr/plumbus + tag: pr-2345 +tests: + - config: test/fixtures/rules/fleeble-match-all.yaml + expected: + keep: + tumblr/fleeble: + - v0.6.0-480-g5d09186 + - v0.6.0-486-g77397a0 + - v0.6.0-413-g463a787 + - v4.2.0 + - v4.2.1 + - some-ignored-tag + - 0.1.2+notignored + - anothertag + - config: test/fixtures/rules/fleeble-match-version.yaml + expected: + keep: + tumblr/fleeble: + - v0.6.0-480-g5d09186 + - v0.6.0-486-g77397a0 + - v0.6.0-413-g463a787 + - v4.2.0 + - v4.2.1 + - config: test/fixtures/rules/fleeble-ignore-some.yaml + expected: + keep: + tumblr/fleeble: + - 0.1.2+notignored + - anothertag diff --git a/test/fixtures/rules/labels-devel-3-versions.yaml b/test/fixtures/rules/labels-devel-3-versions.yaml new file mode 100644 index 0000000..5ef3cd9 --- /dev/null +++ b/test/fixtures/rules/labels-devel-3-versions.yaml @@ -0,0 +1,8 @@ +--- +registry: https://foo.bar +rules: + # match any image that has the following labels, and only keep 3 versions + - labels: + prune: "true" + type: "devel" + keep_versions: 3 diff --git a/test/fixtures/rules/labels-prod-3-latest.yaml b/test/fixtures/rules/labels-prod-3-latest.yaml new file mode 100644 index 0000000..733514c --- /dev/null +++ b/test/fixtures/rules/labels-prod-3-latest.yaml @@ -0,0 +1,8 @@ +--- +registry: https://foo.bar +rules: + # match any image that has the following labels, and only keep 3 latest images + - labels: + prune: "true" + type: "prod" + keep_recent: 3 diff --git a/test/fixtures/rules/redpop-pr.yaml b/test/fixtures/rules/redpop-pr.yaml deleted file mode 100644 index 5c9b1e4..0000000 --- a/test/fixtures/rules/redpop-pr.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -registry: https://foo.bar -rules: - - repos: - - tumblr/plumbus - match_tags: - - pr-.* - keep_days: 14 diff --git a/test/fixtures/rules/repo-and-labels-devel-3-versions.yaml b/test/fixtures/rules/repo-and-labels-devel-3-versions.yaml new file mode 100644 index 0000000..1cf8f1e --- /dev/null +++ b/test/fixtures/rules/repo-and-labels-devel-3-versions.yaml @@ -0,0 +1,10 @@ +--- +registry: https://foo.bar +rules: + # match only image/labeled-x manifests that has the following labels, and only keep 3 versions + - repos: + - image/labeled-x + labels: + prune: "true" + type: "devel" + keep_versions: 3 From d4774b33465adaf70381f0f1e1811ba123c479b1 Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Wed, 10 Jul 2019 14:13:49 -0400 Subject: [PATCH 2/3] remove commented out tests, as they live in yaml configs now --- internal/pkg/rules/rules2_test.go | 178 ------------------------------ 1 file changed, 178 deletions(-) diff --git a/internal/pkg/rules/rules2_test.go b/internal/pkg/rules/rules2_test.go index c3ab7ab..e67e2b5 100644 --- a/internal/pkg/rules/rules2_test.go +++ b/internal/pkg/rules/rules2_test.go @@ -28,185 +28,7 @@ func must(m *registry.Manifest, err error) *registry.Manifest { } var ( - rulesDir = "test/fixtures/rules" - tNow = time.Now() - /* - manifests = []*registry.Manifest{ - mkmanifest("tumblr/plumbus", "v1.2.3", 4, nil), - mkmanifest("tumblr/plumbus", "v1.0.3+metadata", 69, nil), - mkmanifest("tumblr/plumbus", "pr-69420+13d", 13, nil), - mkmanifest("tumblr/plumbus", "pr-69420+14d", 14, nil), - mkmanifest("tumblr/plumbus", "master-v1.2.3-69", 14, nil), - mkmanifest("tumblr/plumbus", "master-2019", 14, nil), - mkmanifest("tumblr/plumbus", "pr-420", 1, nil), - mkmanifest("tumblr/plumbus", "pr-69", 5, nil), - mkmanifest("tumblr/plumbus", "pr-69421+15d", 15, nil), - mkmanifest("tumblr/plumbus", "pr-69419+16d", 16, nil), - mkmanifest("image/latest", "latest", 69420, nil), - mkmanifest("tumblr/fleeble", "latest", 0, nil), - mkmanifest("tumblr/fleeble", "garbage", 5, nil), - mkmanifest("tumblr/fleeble", "v0.4.2-259-something", 5, nil), - mkmanifest("tumblr/fleeble", "v0.5.0-260", 4, nil), - mkmanifest("tumblr/fleeble", "v0.5.1-260", 4, nil), - mkmanifest("tumblr/fleeble", "v0.5.23+test", 3, nil), - mkmanifest("tumblr/fleeble", "v0.5.2", 3, nil), - mkmanifest("tumblr/fleeble", "some-ignored-tag", 69, nil), - mkmanifest("tumblr/fleeble", "oldtag-1", 69, nil), - mkmanifest("tumblr/fleeble", "oldtag-2", 70, nil), - mkmanifest("tumblr/fleeble", "v0.6.1-261-gbb41394", 0, nil), - mkmanifest("tumblr/fleeble", "v0.5.3-nice", 1, nil), - mkmanifest("tumblr/fleeble", "v0.69-6969", 1, nil), - mkmanifest("tumblr/fleeble", "v0.5.5-420", 1, nil), - mkmanifest("tumblr/fleeble", "v0.6.1-262", 0, nil), - mkmanifest("tumblr/fleeble", "branch-v1.2.3-69", 14, nil), - mkmanifest("tumblr/fleeble", "v0.69.1-262", 0, nil), - mkmanifest("tumblr/fleeble", "abc123f", 0, nil), - - mkmanifest("image/x", "v0.1.1+x", 0, nil), - mkmanifest("image/x", "v0.6.9+x", 0, nil), - mkmanifest("image/x", "v4.2.1+x", 0, nil), - mkmanifest("image/x", "0.0.1+x", 0, nil), - mkmanifest("image/x", "0.0.2+x", 0, nil), - mkmanifest("image/y", "v0.1.0+y", 0, nil), - mkmanifest("image/y", "v0.69.420+y", 0, nil), - mkmanifest("image/y", "v4.2.0+y", 0, nil), - mkmanifest("image/y", "0.0.1+y", 0, nil), - mkmanifest("image/y", "0.0.2+y", 0, nil), - } - */ - /* - tests = []testpayload{ - { - rulesFile: "multiple-repo-keep-latest.yaml", - input: manifests, - // should keep the latest 4 images from both fleeble and plumbus - keepImages: []string{ - "tumblr/fleeble:abc123f", // modified: 0 days ago - "tumblr/fleeble:v0.6.1-261-gbb41394", // modified: 0 days ago - "tumblr/fleeble:v0.6.1-262", // modified: 0 days ago - "tumblr/fleeble:v0.69.1-262", // modified: 0 days ago - "tumblr/plumbus:pr-420", // modified: 1 days ago - "tumblr/plumbus:v1.2.3", // modified: 4 days ago - "tumblr/plumbus:pr-69", // modified: 5 days ago - "tumblr/plumbus:pr-69420+13d", // modified: 13 days ago - }, - deleteImages: []string{ - "tumblr/fleeble:branch-v1.2.3-69", - "tumblr/fleeble:garbage", - "tumblr/fleeble:oldtag-1", - "tumblr/fleeble:oldtag-2", - "tumblr/fleeble:some-ignored-tag", - "tumblr/fleeble:v0.4.2-259-something", - "tumblr/fleeble:v0.5.0-260", - "tumblr/fleeble:v0.5.1-260", - "tumblr/fleeble:v0.5.2", - "tumblr/fleeble:v0.5.23+test", - "tumblr/fleeble:v0.5.3-nice", - "tumblr/fleeble:v0.5.5-420", - "tumblr/fleeble:v0.69-6969", - "tumblr/plumbus:master-2019", - "tumblr/plumbus:master-v1.2.3-69", - "tumblr/plumbus:pr-69419+16d", - "tumblr/plumbus:pr-69420+14d", - "tumblr/plumbus:pr-69421+15d", - "tumblr/plumbus:v1.0.3+metadata", - }, - }, - { - rulesFile: "onlylatest.yaml", - input: manifests, - keepImages: []string{}, - deleteImages: []string{}, // we dont want to see it in delete, cause its ignored - }, - { - rulesFile: "plumbus-pr.yaml", - input: manifests, - keepImages: []string{ - "tumblr/plumbus:pr-420", - "tumblr/plumbus:pr-69", - "tumblr/plumbus:pr-69420+13d", - "tumblr/plumbus:pr-69420+14d"}, - deleteImages: []string{ - "tumblr/plumbus:pr-69419+16d", - "tumblr/plumbus:pr-69421+15d"}, - }, - { - // ignore some tags that would otherwise get cleaned up by date predicates - rulesFile: "fleeble-ignore-some.yaml", - input: manifests, - keepImages: []string{"tumblr/fleeble:abc123f"}, - deleteImages: []string{"tumblr/fleeble:branch-v1.2.3-69", "tumblr/fleeble:garbage", "tumblr/fleeble:oldtag-1", "tumblr/fleeble:oldtag-2"}, - }, - { - rulesFile: "fleeble-match-version.yaml", - // this rule should retain only 5 latest version tags - // and will implicitly skip all versions taht dont parse correctly as a Version - // meaning there should be no deletedTags that arent correct semantic versions - input: manifests, - keepImages: []string{ - "tumblr/fleeble:v0.5.23+test", - "tumblr/fleeble:v0.6.1-261-gbb41394", - "tumblr/fleeble:v0.6.1-262", - "tumblr/fleeble:v0.69-6969", - "tumblr/fleeble:v0.69.1-262", - }, - deleteImages: []string{ - "tumblr/fleeble:v0.4.2-259-something", - "tumblr/fleeble:v0.5.0-260", - "tumblr/fleeble:v0.5.1-260", - "tumblr/fleeble:v0.5.2", - "tumblr/fleeble:v0.5.3-nice", - "tumblr/fleeble:v0.5.5-420", - }, - }, - { - // this should keep 2 latest version tags, and the last 2 days of all tags. - // this means there are some versions that would have been deleted, that are still retained - rulesFile: "fleeble-multiple.yaml", - input: manifests, - keepImages: []string{ - "tumblr/fleeble:abc123f", - "tumblr/fleeble:v0.69-6969", - "tumblr/fleeble:v0.69.1-262", - }, - deleteImages: []string{ - "tumblr/fleeble:branch-v1.2.3-69", - "tumblr/fleeble:garbage", - "tumblr/fleeble:oldtag-1", - "tumblr/fleeble:oldtag-2", - "tumblr/fleeble:some-ignored-tag", // ignored by 1 rule, but deleted by the nDays rule! - "tumblr/fleeble:v0.4.2-259-something", - "tumblr/fleeble:v0.5.0-260", - "tumblr/fleeble:v0.5.1-260", - "tumblr/fleeble:v0.5.2", - "tumblr/fleeble:v0.5.23+test", - "tumblr/fleeble:v0.5.3-nice", - "tumblr/fleeble:v0.5.5-420", - "tumblr/fleeble:v0.6.1-261-gbb41394", - "tumblr/fleeble:v0.6.1-262", - }, - }, - { - rulesFile: "multiple-repo-versions.yaml", - input: manifests, - keepImages: []string{ - "image/x:v0.1.1+x", - "image/x:v0.6.9+x", - "image/x:v4.2.1+x", - "image/y:v0.1.0+y", - "image/y:v0.69.420+y", - "image/y:v4.2.0+y", - }, - deleteImages: []string{ - "image/x:0.0.1+x", - "image/x:0.0.2+x", - "image/y:0.0.1+y", - "image/y:0.0.2+y", - }, - }, - } - */ ) type testpayload struct { From e4c025bcf0104c69cdd3ac9cfb3584fbf42d5187 Mon Sep 17 00:00:00 2001 From: Gabe Conradi Date: Wed, 10 Jul 2019 14:19:57 -0400 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=90=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8652c7d..8a757b9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# docker-registry-pruner 🌱✂️ +# docker-registry-pruner 🐳✂️ `docker-registry-pruner` is a rules-based tool that applies business logic to docker images in a Docker Registry storage system for retention.