feat: Allow poll trigger to work with glob and regexp (#745)
parent
11c1b68cfe
commit
611ff29997
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,6 +183,33 @@ func TestWatchAllTagsMixed(t *testing.T) {
|
|||
testRunHelper(testCases, availableTags, t)
|
||||
}
|
||||
|
||||
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", "build-1696801785", policy},
|
||||
}
|
||||
testRunHelper(testCases, availableTags, t)
|
||||
}
|
||||
|
||||
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},
|
||||
}
|
||||
testRunHelper(testCases, availableTags, t)
|
||||
}
|
||||
|
||||
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{
|
||||
|
@ -193,21 +218,6 @@ func TestWatchAllTagsMixedPolicyAll(t *testing.T) {
|
|||
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
|
||||
}
|
||||
sortedTags := semverSort(tags)
|
||||
|
||||
if !reflect.DeepEqual(sortedTags, expectedVersions) {
|
||||
t.Errorf("Invalid sorted tags; expected: %s; got: %s", expectedVersions, sortedTags)
|
||||
}
|
||||
}
|
||||
|
||||
type testingCredsHelper struct {
|
||||
err error // err to return
|
||||
credentials *types.Credentials // creds to return
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue