diff --git a/internal/policy/force.go b/internal/policy/force.go index 1a2ff5b8..77d15558 100644 --- a/internal/policy/force.go +++ b/internal/policy/force.go @@ -17,6 +17,10 @@ func (fp *ForcePolicy) ShouldUpdate(current, new string) (bool, error) { return true, nil } +func (fp *ForcePolicy) Filter(tags []string) []string { + return append([]string{}, tags...) +} + func (fp *ForcePolicy) Name() string { return "force" } diff --git a/internal/policy/glob.go b/internal/policy/glob.go index 96279a15..d0f5ec7c 100644 --- a/internal/policy/glob.go +++ b/internal/policy/glob.go @@ -2,6 +2,7 @@ package policy import ( "fmt" + "sort" "strings" "github.com/ryanuber/go-glob" @@ -30,5 +31,22 @@ func (p *GlobPolicy) ShouldUpdate(current, new string) (bool, error) { return glob.Glob(p.pattern, new), nil } +func (p *GlobPolicy) Filter(tags []string) []string { + filtered := []string{} + + for _, tag := range tags { + if glob.Glob(p.pattern, tag) { + filtered = append(filtered, tag) + } + } + + // sort desc alphabetically + sort.Slice(filtered, func(i, j int) bool { + return filtered[i] > filtered[j] + }) + + return filtered +} + func (p *GlobPolicy) Name() string { return p.policy } func (p *GlobPolicy) Type() PolicyType { return PolicyTypeGlob } diff --git a/internal/policy/policy.go b/internal/policy/policy.go index eacfd240..fb77bb29 100644 --- a/internal/policy/policy.go +++ b/internal/policy/policy.go @@ -22,6 +22,7 @@ type Policy interface { ShouldUpdate(current, new string) (bool, error) Name() string Type() PolicyType + Filter(tags []string) []string } type NilPolicy struct{} @@ -29,6 +30,7 @@ type NilPolicy struct{} func (np *NilPolicy) ShouldUpdate(c, n string) (bool, error) { return false, nil } func (np *NilPolicy) Name() string { return "nil policy" } func (np *NilPolicy) Type() PolicyType { return PolicyTypeNone } +func (np *NilPolicy) Filter(tags []string) []string { return append([]string{}, tags...) } // GetPolicyFromLabelsOrAnnotations - gets policy from k8s labels or annotations func GetPolicyFromLabelsOrAnnotations(labels map[string]string, annotations map[string]string) Policy { diff --git a/internal/policy/regexp.go b/internal/policy/regexp.go index 829a7e8b..e28c8549 100644 --- a/internal/policy/regexp.go +++ b/internal/policy/regexp.go @@ -3,6 +3,7 @@ package policy import ( "fmt" "regexp" + "sort" "strings" ) @@ -36,5 +37,28 @@ func (p *RegexpPolicy) ShouldUpdate(current, new string) (bool, error) { return p.regexp.MatchString(new), nil } +func (p *RegexpPolicy) Filter(tags []string) []string { + filtered := []string{} + compare := p.regexp.SubexpIndex("compare") + + for _, tag := range tags { + if p.regexp.MatchString(tag) { + filtered = append(filtered, tag) + } + } + + sort.Slice(filtered, func(i, j int) bool { + if compare != -1 { + mi := p.regexp.FindStringSubmatch(filtered[i]) + mj := p.regexp.FindStringSubmatch(filtered[j]) + return mi[compare] > mj[compare] + } else { + return filtered[i] > filtered[j] + } + }) + + return filtered +} + func (p *RegexpPolicy) Name() string { return p.policy } func (p *RegexpPolicy) Type() PolicyType { return PolicyTypeRegexp } diff --git a/internal/policy/semver.go b/internal/policy/semver.go index d7bf2952..c0d7faa4 100644 --- a/internal/policy/semver.go +++ b/internal/policy/semver.go @@ -3,6 +3,7 @@ package policy import ( "errors" "fmt" + "sort" "strings" "github.com/Masterminds/semver" @@ -105,3 +106,29 @@ func shouldUpdate(spt SemverPolicyType, matchPreRelease bool, current, new strin } return false, nil } + +func (sp *SemverPolicy) Filter(tags []string) []string { + var versions []*semver.Version + var filtered []string + + for _, t := range tags { + if len(strings.SplitN(t, ".", 3)) < 2 { + // Keep only X.Y.Z+ semver + continue + } + v, err := semver.NewVersion(t) + // Filter out non semver tags + if err != nil { + continue + } + versions = append(versions, v) + } + + sort.Slice(versions, func(i, j int) bool { return versions[j].LessThan(versions[i]) }) + + for _, version := range versions { + filtered = append(filtered, version.Original()) + } + + return filtered +} diff --git a/trigger/poll/multi_tags_watcher.go b/trigger/poll/multi_tags_watcher.go index 42c77f48..542bdd8d 100644 --- a/trigger/poll/multi_tags_watcher.go +++ b/trigger/poll/multi_tags_watcher.go @@ -1,10 +1,6 @@ package poll import ( - "sort" - "strings" - - "github.com/Masterminds/semver" "github.com/keel-hq/keel/extension/credentialshelper" "github.com/keel-hq/keel/provider" "github.com/keel-hq/keel/registry" @@ -94,43 +90,30 @@ func (j *WatchRepositoryTagsJob) computeEvents(tags []string) ([]types.Event, er events := []types.Event{} - // Keep only semver tags, sorted desc (to optimize process) - versions := semverSort(tags) + if j.details.trackedImage.Policy != nil { + tags = j.details.trackedImage.Policy.Filter(tags) + } for _, trackedImage := range getRelatedTrackedImages(j.details.trackedImage, trackedImages) { - // Current version tag might not be a valid semver one - currentVersion, invalidCurrentVersion := semver.NewVersion(trackedImage.Image.Tag()) - // matches, going through tags - for _, version := range versions { - if invalidCurrentVersion == nil && (currentVersion.GreaterThan(version) || currentVersion.Equal(version)) { - // Current tag is a valid semver, and is bigger than currently tested one - // -> we can stop now, nothing will be worth upgrading in the rest of the sorted list - break - } - update, err := trackedImage.Policy.ShouldUpdate(trackedImage.Image.Tag(), version.Original()) - // log.WithFields(log.Fields{ - // "current_tag": j.details.trackedImage.Image.Tag(), - // "image_name": j.details.trackedImage.Image.Remote(), - // }).Debug("trigger.poll.WatchRepositoryTagsJob: tag: ", version.Original(), "; update: ", update, "; err:", err) + for _, tag := range tags { + update, err := trackedImage.Policy.ShouldUpdate(trackedImage.Image.Tag(), tag) if err != nil { continue } - if update && !exists(version.Original(), events) { + if update && !exists(tag, events) { event := types.Event{ Repository: types.Repository{ - Name: j.details.trackedImage.Image.Repository(), - Tag: version.Original(), + Name: trackedImage.Image.Repository(), + Tag: tag, }, TriggerName: types.TriggerTypePoll.String(), } events = append(events, event) - // Only keep first match per image (should be the highest usable version) break } - } - } + log.WithFields(log.Fields{ "current_tag": j.details.trackedImage.Image.Tag(), "image_name": j.details.trackedImage.Image.Remote(), @@ -148,26 +131,6 @@ func exists(tag string, events []types.Event) bool { return false } -// Filter and sort tags according to semver, desc -func semverSort(tags []string) []*semver.Version { - var versions []*semver.Version - for _, t := range tags { - if len(strings.SplitN(t, ".", 3)) < 2 { - // Keep only X.Y.Z+ semver - continue - } - v, err := semver.NewVersion(t) - // Filter out non semver tags - if err != nil { - continue - } - versions = append(versions, v) - } - // Sort desc, following semver - sort.Slice(versions, func(i, j int) bool { return versions[j].LessThan(versions[i]) }) - return versions -} - func getRelatedTrackedImages(ours *types.TrackedImage, all []*types.TrackedImage) []*types.TrackedImage { b := all[:0] for _, x := range all { diff --git a/trigger/poll/multi_tags_watcher_test.go b/trigger/poll/multi_tags_watcher_test.go index 97a82523..e254c2d3 100644 --- a/trigger/poll/multi_tags_watcher_test.go +++ b/trigger/poll/multi_tags_watcher_test.go @@ -2,12 +2,10 @@ package poll import ( "errors" - "reflect" "strconv" "strings" "testing" - "github.com/Masterminds/semver" "github.com/stretchr/testify/assert" "github.com/keel-hq/keel/approvals" @@ -185,27 +183,39 @@ func TestWatchAllTagsMixed(t *testing.T) { testRunHelper(testCases, availableTags, t) } -func TestWatchAllTagsMixedPolicyAll(t *testing.T) { - availableTags := []string{"1.3.0-dev", "1.5.0", "1.8.0-alpha"} +func TestWatchGlobTagsMixed(t *testing.T) { + availableTags := []string{"1.3.0-dev", "build-1694132169", "build-1696801785", "build-1695801785"} + policy, _ := policy.NewGlobPolicy("glob:build-*") testCases := []runTestCase{ - {"1.0.0", "1.5.0", policy.NewSemverPolicy(policy.SemverPolicyTypeMajor, true)}, - {"1.6.0-alpha", "1.8.0-alpha", policy.NewSemverPolicy(policy.SemverPolicyTypeAll, true)}} + {"1.0.0", "build-1696801785", policy}, + } testRunHelper(testCases, availableTags, t) } -func Test_semverSort(t *testing.T) { - tags := []string{"1.3.0", "aa1.0.0", "zzz", "1.3.0-dev", "1.5.0", "2.0.0-alpha", "1.3.0-dev1", "1.8.0-alpha", "1.3.1-dev", "123", "1.2.3-rc.1.2+meta"} - expectedTags := []string{"2.0.0-alpha", "1.8.0-alpha", "1.5.0", "1.3.1-dev", "1.3.0", "1.3.0-dev1", "1.3.0-dev", "1.2.3-rc.1.2+meta"} - expectedVersions := make([]*semver.Version, len(expectedTags)) - for i, tag := range expectedTags { - v, _ := semver.NewVersion(tag) - expectedVersions[i] = v +func TestWatchRegexpTagsCompareMixed(t *testing.T) { + availableTags := []string{"1.3.0-dev", "build-2a3560ef-1694132169", "build-1a3560ef-1696801785", "build-3a3560ef-1695801785"} + policy, _ := policy.NewRegexpPolicy("regexp:^build-.*-(?P.+)$") + testCases := []runTestCase{ + {"1.0.0", "build-1a3560ef-1696801785", policy}, } - sortedTags := semverSort(tags) + testRunHelper(testCases, availableTags, t) +} - if !reflect.DeepEqual(sortedTags, expectedVersions) { - t.Errorf("Invalid sorted tags; expected: %s; got: %s", expectedVersions, sortedTags) +func TestWatchRegexpTagsMixed(t *testing.T) { + availableTags := []string{"1.3.0-dev", "build-2a3560ef-1694132169", "build-1a3560ef-1696801785", "build-3a3560ef-1695801785"} + policy, _ := policy.NewRegexpPolicy("regexp:^build-.*$") + testCases := []runTestCase{ + {"1.0.0", "build-3a3560ef-1695801785", policy}, } + testRunHelper(testCases, availableTags, t) +} + +func TestWatchAllTagsMixedPolicyAll(t *testing.T) { + availableTags := []string{"1.3.0-dev", "1.5.0", "1.8.0-alpha"} + testCases := []runTestCase{ + {"1.0.0", "1.5.0", policy.NewSemverPolicy(policy.SemverPolicyTypeMajor, true)}, + {"1.6.0-alpha", "1.8.0-alpha", policy.NewSemverPolicy(policy.SemverPolicyTypeAll, true)}} + testRunHelper(testCases, availableTags, t) } type testingCredsHelper struct { diff --git a/types/tracked_images.go b/types/tracked_images.go index bf848fa3..0dc65f04 100644 --- a/types/tracked_images.go +++ b/types/tracked_images.go @@ -30,6 +30,7 @@ type TrackedImage struct { type Policy interface { ShouldUpdate(current, new string) (bool, error) Name() string + Filter(tags []string) []string } func (i TrackedImage) String() string {