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/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. 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..e67e2b5 --- /dev/null +++ b/internal/pkg/rules/rules2_test.go @@ -0,0 +1,96 @@ +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 ( + tNow = time.Now() +) + +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