From 71acf534efabbdb0c807ce4468bb548df68e3baf Mon Sep 17 00:00:00 2001 From: Ivan Ilves Date: Sat, 23 Sep 2017 10:48:02 +0200 Subject: [PATCH] Filter images by tag with regular expressions :tada: --- README.md | 14 +++++++- main.go | 49 ++++++++++++++++++++++++++-- main_test.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2ed1ba6..bff15b2 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,23 @@ # lstags * *Compare local Docker images with ones present in registry.* -* *Get insights on changes in watched Docker registries, easily.* +* *Get insights on changes in watched Docker registries, easily.* * *Facilitate maintenance of your own local "proxy" registries.* **NB!** [Issues](https://github.com/ivanilves/lstags/issues) are welcome, [pull requests](https://github.com/ivanilves/lstags/pulls) are even more welcome! :smile: +### Example invocation +``` +$ lstags alpine~/^3\\./ + <(local) ID> +ABSENT sha256:9363d03ef12c8c25a2def8551e609f146 n/a 2017-09-13T16:32:00 alpine:3.1 +CHANGED sha256:9866438860a1b28cd9f0c944e42d3f6cd 39be345c901f 2017-09-13T16:32:05 alpine:3.2 +ABSENT sha256:ae4d16d132e3c93dd09aec45e4c13e9d7 n/a 2017-09-13T16:32:10 alpine:3.3 +CHANGED sha256:0d82f2f4b464452aac758c77debfff138 f64255f97787 2017-09-13T16:32:15 alpine:3.4 +PRESENT sha256:129a7f8c0fae8c3251a8df9370577d9d6 074d602a59d7 2017-09-13T16:32:20 alpine:3.5 +PRESENT sha256:f006ecbb824d87947d0b51ab8488634bf 76da55c8019d 2017-09-13T16:32:26 alpine:3.6 +``` + ## Why would someone use this? You could use `lstags`, if you ... * ... continuously pull Docker images from some public or private registry to speed-up Docker run. diff --git a/main.go b/main.go index 20da5b4..f6eadbd 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "regexp" "strings" "github.com/jessevdk/go-flags" @@ -37,6 +38,39 @@ func getVersion() string { return VERSION } +func trimFilter(repoWithFilter string) (string, string, error) { + parts := strings.Split(repoWithFilter, "~") + + repository := parts[0] + + if len(parts) < 2 { + return repository, ".*", nil + } + + if len(parts) > 2 { + return "", "", errors.New("Unable to trim filter from repository (too many '~'!): " + repoWithFilter) + } + + f := parts[1] + + if !strings.HasPrefix(f, "/") || !strings.HasSuffix(f, "/") { + return "", "", errors.New("Filter should be passed in a form: /REGEXP/") + } + + filter := f[1 : len(f)-1] + + return repository, filter, nil +} + +func matchesFilter(s, filter string) bool { + matched, err := regexp.MatchString(filter, s) + if err != nil { + return false + } + + return matched +} + func isHostname(s string) bool { if strings.Contains(s, ".") { return true @@ -105,10 +139,15 @@ func main() { registry.TraceRequests = o.TraceRequests - registryName := getRegistryName(o.Positional.Repository, o.DefaultRegistry) + repository, filter, err := trimFilter(o.Positional.Repository) + if err != nil { + suicide(err) + } + + registryName := getRegistryName(repository, o.DefaultRegistry) - repoRegistryName := registry.FormatRepoName(o.Positional.Repository, registryName) - repoLocalName := local.FormatRepoName(o.Positional.Repository, registryName) + repoRegistryName := registry.FormatRepoName(repository, registryName) + repoLocalName := local.FormatRepoName(repository, registryName) username, password, err := assignCredentials(registryName, o.Username, o.Password, o.DockerJSON) if err != nil { @@ -140,6 +179,10 @@ func main() { tg := joinedTags[name] + if !matchesFilter(tg.GetName(), filter) { + continue + } + fmt.Printf( format, tg.GetState(), diff --git a/main_test.go b/main_test.go index 492e88b..2fc4e75 100644 --- a/main_test.go +++ b/main_test.go @@ -69,7 +69,97 @@ func TestGetAuthorization(t *testing.T) { } } +func TestTrimFilter(t *testing.T) { + flag.Parse() + if *runIntegrationTests { + t.SkipNow() + } + + expected := []struct { + repoWithFilter string + repo string + filter string + iserr bool + }{ + {"nginx", "nginx", ".*", false}, + {"registry.hipster.io/hype/sdn", "registry.hipster.io/hype/sdn", ".*", false}, + {"mesosphere/mesos~/^1\\.[0-9]+\\.[0-9]+$/", "mesosphere/mesos", "^1\\.[0-9]+\\.[0-9]+$", false}, + {"registry.hipster.io/hype/drone~/v[0-9]+$/", "registry.hipster.io/hype/drone", "v[0-9]+$", false}, + {"bogohost:5000/hype/drone~/v[0-9]+$/", "bogohost:5000/hype/drone", "v[0-9]+$", false}, + {"registry.clown.bad/cache/merd~x[0-9]", "", "", true}, + {"cabron/~plla~x~", "", "", true}, + } + + for _, e := range expected { + repo, filter, err := trimFilter(e.repoWithFilter) + + if repo != e.repo { + t.Fatalf( + "Unexpected repository name '%s' trimmed from '%s' (expected: '%s')", + repo, + e.repoWithFilter, + e.repo, + ) + } + + if filter != e.filter { + t.Fatalf( + "Unexpected repository filter '%s' trimmed from '%s' (expected: '%s')", + filter, + e.repoWithFilter, + e.filter, + ) + } + + iserr := err != nil + if iserr != e.iserr { + t.Fatalf("Passing badly formatted repository '%s' should trigger an error", e.repoWithFilter) + } + } +} + +func TestMatchesFilter(t *testing.T) { + flag.Parse() + if *runIntegrationTests { + t.SkipNow() + } + + expected := []struct { + s string + pattern string + matched bool + }{ + {"latest", "^latest$", true}, + {"v1.0.1", "^v1\\.0\\.1$", true}, + {"barbos", ".*", true}, + {"3.4", "*", false}, + } + + for _, e := range expected { + matched := matchesFilter(e.s, e.pattern) + + action := "should" + if !e.matched { + action = "should not" + } + + if matched != e.matched { + t.Fatalf( + "String '%s' %s match pattern '%s'", + e.s, + action, + e.pattern, + ) + } + } +} + func TestGetRegistryName(t *testing.T) { + flag.Parse() + if *runIntegrationTests { + t.SkipNow() + } + expected := map[string]string{ "mesosphere/marathon": dockerHub, "bogohost/my/inner/troll": dockerHub,