Skip to content

Commit

Permalink
feat: Allow poll trigger to work with glob and regexp (#745)
Browse files Browse the repository at this point in the history
  • Loading branch information
imranismail authored Nov 6, 2024
1 parent 11c1b68 commit 611ff29
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 62 deletions.
4 changes: 4 additions & 0 deletions internal/policy/force.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
18 changes: 18 additions & 0 deletions internal/policy/glob.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package policy

import (
"fmt"
"sort"
"strings"

"github.com/ryanuber/go-glob"
Expand Down Expand Up @@ -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 }
2 changes: 2 additions & 0 deletions internal/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ type Policy interface {
ShouldUpdate(current, new string) (bool, error)
Name() string
Type() PolicyType
Filter(tags []string) []string
}

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 {
Expand Down
24 changes: 24 additions & 0 deletions internal/policy/regexp.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package policy
import (
"fmt"
"regexp"
"sort"
"strings"
)

Expand Down Expand Up @@ -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 }
27 changes: 27 additions & 0 deletions internal/policy/semver.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package policy
import (
"errors"
"fmt"
"sort"
"strings"

"github.com/Masterminds/semver"
Expand Down Expand Up @@ -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
}
55 changes: 9 additions & 46 deletions trigger/poll/multi_tags_watcher.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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(),
Expand All @@ -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 {
Expand Down
42 changes: 26 additions & 16 deletions trigger/poll/multi_tags_watcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<compare>.+)$")
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 {
Expand Down
1 change: 1 addition & 0 deletions types/tracked_images.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 611ff29

Please sign in to comment.