Merge pull request #318 from keel-hq/feature/polling_revamp

Feature/polling revamp
pull/322/head
Karolis 2018-12-01 20:08:16 +00:00 committed by GitHub
commit 2262a400ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 465 additions and 507 deletions

View File

@ -17,8 +17,14 @@ var ErrKeelConfigNotFound = errors.New("keel configuration not found")
// getImages - get images from chart values
func getImages(vals chartutil.Values) ([]*types.TrackedImage, error) {
var images []*types.TrackedImage
keelCfg, err := getKeelConfig(vals)
if err != nil {
if err == ErrPolicyNotSpecified {
// nothing to do
return images, nil
}
log.WithFields(log.Fields{
"error": err,
}).Error("provider.helm: failed to get keel configuration for release")
@ -26,8 +32,6 @@ func getImages(vals chartutil.Values) ([]*types.TrackedImage, error) {
return nil, ErrKeelConfigNotFound
}
var images []*types.TrackedImage
for _, imageDetails := range keelCfg.Images {
imageRef, err := parseImage(vals, &imageDetails)
if err != nil {
@ -40,10 +44,10 @@ func getImages(vals chartutil.Values) ([]*types.TrackedImage, error) {
}
trackedImage := &types.TrackedImage{
Image: imageRef,
PollSchedule: keelCfg.PollSchedule,
Trigger: keelCfg.Trigger,
SemverPreReleaseTags: make(map[string]string),
Image: imageRef,
PollSchedule: keelCfg.PollSchedule,
Trigger: keelCfg.Trigger,
Policy: keelCfg.Plc,
}
images = append(images, trackedImage)

View File

@ -4,6 +4,7 @@ import (
"reflect"
"testing"
"github.com/keel-hq/keel/internal/policy"
"github.com/keel-hq/keel/types"
"github.com/keel-hq/keel/util/image"
"k8s.io/helm/pkg/chartutil"
@ -47,9 +48,9 @@ func Test_getImages(t *testing.T) {
},
want: []*types.TrackedImage{
&types.TrackedImage{
Image: img,
Trigger: types.TriggerTypePoll,
SemverPreReleaseTags: make(map[string]string),
Image: img,
Trigger: types.TriggerTypePoll,
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeAll),
},
},
wantErr: false,

View File

@ -1,6 +1,7 @@
package helm
import (
"errors"
"fmt"
"strings"
"time"
@ -44,6 +45,11 @@ func init() {
prometheus.MustRegister(helmUnversionedUpdatesCounter)
}
// ErrPolicyNotSpecified helm related errors
var (
ErrPolicyNotSpecified = errors.New("policy not specified")
)
// Manager - high level interface into helm provider related data used by
// triggers
type Manager interface {
@ -426,7 +432,7 @@ func getKeelConfig(vals chartutil.Values) (*KeelChartConfig, error) {
}
if r.Keel.Policy == "" {
return nil, fmt.Errorf("policy not specified")
return nil, ErrPolicyNotSpecified
}
cfg := r.Keel

View File

@ -39,6 +39,10 @@ func checkRelease(repo *types.Repository, namespace, name string, chart *hapi_ch
keelCfg, err := getKeelConfig(vals)
if err != nil {
if err == ErrPolicyNotSpecified {
// nothing to do
return plan, false, nil
}
log.WithFields(log.Fields{
"error": err,
}).Error("provider.helm: failed to get keel configuration for release")

View File

@ -181,14 +181,14 @@ func (p *Provider) TrackedImages() ([]*types.TrackedImage, error) {
}
trackedImages = append(trackedImages, &types.TrackedImage{
Image: ref,
PollSchedule: schedule,
Trigger: trigger,
Provider: ProviderName,
Namespace: gr.Namespace,
Secrets: secrets,
Meta: make(map[string]string),
SemverPreReleaseTags: svp,
Image: ref,
PollSchedule: schedule,
Trigger: trigger,
Provider: ProviderName,
Namespace: gr.Namespace,
Secrets: secrets,
Meta: make(map[string]string),
Policy: plc,
})
}
}

View File

@ -3,7 +3,6 @@ package provider
import (
"context"
"github.com/Masterminds/semver"
"github.com/keel-hq/keel/approvals"
"github.com/keel-hq/keel/types"
@ -116,91 +115,6 @@ func (p *DefaultProviders) TrackedImages() ([]*types.TrackedImage, error) {
return trackedImages, nil
}
func appendIfDoesntExist(tags []string, tag string) []string {
found := false
for _, t := range tags {
if t == tag {
found = true
}
}
if !found {
return append(tags, tag)
}
return tags
}
func appendImage(images []*types.TrackedImage, new *types.TrackedImage) []*types.TrackedImage {
newSemverTag, err := semver.NewVersion(new.Image.Tag())
if err != nil {
// not semver, just appending as a new image
return append(images, new)
}
new.Tags = appendIfDoesntExist(new.Tags, new.Image.Tag())
// looking for a semver image
idx, ok := lookupSemverImageIdx(images, new)
if !ok || len(images) == 0 {
if newSemverTag.Prerelease() != "" {
new.SemverPreReleaseTags[newSemverTag.Prerelease()] = new.Image.Tag()
// new.SemverPreReleaseTags = append(new.SemverPreReleaseTags, newSemverTag.Prerelease())
}
return append(images, new)
}
existingSemverTag, err := semver.NewVersion(images[idx].Image.Tag())
if err != nil {
// existing tag not semver, just appending as new image
if newSemverTag.Prerelease() != "" {
new.SemverPreReleaseTags[newSemverTag.Prerelease()] = new.Image.Tag()
// new.SemverPreReleaseTags = append(new.SemverPreReleaseTags, newSemverTag.Prerelease())
}
return append(images, new)
}
// semver, checking for prerelease tags
if newSemverTag.Prerelease() != "" {
_, ok := images[idx].SemverPreReleaseTags[newSemverTag.Prerelease()]
if ok {
// checking which is higher and setting higher
if newSemverTag.GreaterThan(existingSemverTag) {
images[idx].SemverPreReleaseTags[newSemverTag.Prerelease()] = new.Image.Tag()
return images
}
}
images[idx].SemverPreReleaseTags[newSemverTag.Prerelease()] = new.Image.Tag()
}
// if new semver tag is a higher version, updating it as well
if newSemverTag.GreaterThan(existingSemverTag) {
images[idx].Image = new.Image
}
images[idx].Tags = appendIfDoesntExist(images[idx].Tags, new.Image.Tag())
return images
}
func lookupSemverImageIdx(images []*types.TrackedImage, new *types.TrackedImage) (int, bool) {
_, err := semver.NewVersion(new.Image.Tag())
if err != nil {
// looking for a non semver
return 0, false
}
for idx, existing := range images {
if existing.Image.Repository() == new.Image.Repository() {
_, err = semver.NewVersion(existing.Image.Tag())
if err != nil {
continue
}
return idx, true
}
}
return 0, false
}
// List - list available providers
func (p *DefaultProviders) List() []string {
list := []string{}

View File

@ -1,254 +0,0 @@
package provider
import (
"reflect"
"testing"
"github.com/keel-hq/keel/types"
"github.com/keel-hq/keel/util/image"
testingUtils "github.com/keel-hq/keel/util/testing"
)
func mustParse(i string) *image.Reference {
ref, err := image.Parse(i)
if err != nil {
panic(err)
}
return ref
}
func Test_appendImage(t *testing.T) {
type args struct {
images []*types.TrackedImage
new *types.TrackedImage
}
tests := []struct {
name string
args args
want []*types.TrackedImage
}{
{
name: "new image",
args: args{
images: []*types.TrackedImage{},
new: testingUtils.GetTrackedImage("karolisr/webhook-demo:latest"),
},
want: []*types.TrackedImage{
testingUtils.GetTrackedImage("karolisr/webhook-demo:latest"),
},
},
{
name: "new semver",
args: args{
images: []*types.TrackedImage{},
new: testingUtils.GetTrackedImage("karolisr/webhook-demo:1.2.3"),
},
want: []*types.TrackedImage{
testingUtils.GetTrackedImage("karolisr/webhook-demo:1.2.3"),
},
},
{
name: "new semver with prerelease",
args: args{
images: []*types.TrackedImage{
testingUtils.GetTrackedImage("karolisr/webhook-demo:1.2.3"),
},
new: testingUtils.GetTrackedImage("karolisr/webhook-demo:1.5.0-dev"),
},
want: []*types.TrackedImage{
&types.TrackedImage{
Image: mustParse("karolisr/webhook-demo:1.5.0-dev"),
SemverPreReleaseTags: map[string]string{
"dev": "1.5.0-dev",
},
Meta: make(map[string]string),
Tags: []string{"1.2.3", "1.5.0-dev"},
},
},
},
{
name: "new semver with both prereleases",
args: args{
images: []*types.TrackedImage{
&types.TrackedImage{
Image: mustParse("karolisr/webhook-demo:1.2.3-prod"),
SemverPreReleaseTags: map[string]string{
"prod": "1.2.3-prod",
},
Meta: make(map[string]string),
Tags: []string{"1.2.3-prod"},
},
},
new: testingUtils.GetTrackedImage("karolisr/webhook-demo:1.5.0-dev"),
},
want: []*types.TrackedImage{
&types.TrackedImage{
Image: mustParse("karolisr/webhook-demo:1.5.0-dev"),
SemverPreReleaseTags: map[string]string{
"dev": "1.5.0-dev",
"prod": "1.2.3-prod",
},
Meta: make(map[string]string),
Tags: []string{"1.2.3-prod", "1.5.0-dev"},
},
},
},
{
name: "semver prerelease",
args: args{
images: []*types.TrackedImage{},
new: testingUtils.GetTrackedImage("karolisr/webhook-demo:1.5.0-dev"),
},
want: []*types.TrackedImage{
&types.TrackedImage{
Image: mustParse("karolisr/webhook-demo:1.5.0-dev"),
SemverPreReleaseTags: map[string]string{
"dev": "1.5.0-dev",
},
Meta: make(map[string]string),
Tags: []string{"1.5.0-dev"},
},
},
},
{
name: "new semver with previous non-semver tag",
args: args{
images: []*types.TrackedImage{
&types.TrackedImage{
Image: mustParse("karolisr/webhook-demo:build-xx"),
SemverPreReleaseTags: make(map[string]string),
Meta: make(map[string]string),
},
},
new: testingUtils.GetTrackedImage("karolisr/webhook-demo:1.5.0-dev"),
},
want: []*types.TrackedImage{
&types.TrackedImage{
Image: mustParse("karolisr/webhook-demo:build-xx"),
SemverPreReleaseTags: make(map[string]string),
Meta: make(map[string]string),
},
&types.TrackedImage{
Image: mustParse("karolisr/webhook-demo:1.5.0-dev"),
SemverPreReleaseTags: map[string]string{
"dev": "1.5.0-dev",
},
Meta: make(map[string]string),
Tags: []string{"1.5.0-dev"},
},
},
},
{
name: "mixed versions",
args: args{
images: []*types.TrackedImage{
&types.TrackedImage{
Image: mustParse("karolisr/webhook-demo:latest"),
SemverPreReleaseTags: make(map[string]string),
Meta: make(map[string]string),
},
&types.TrackedImage{
Image: mustParse("karolisr/webhook-demo:build-foo"),
SemverPreReleaseTags: make(map[string]string),
Meta: make(map[string]string),
},
&types.TrackedImage{
Image: mustParse("karolisr/webhook-demo:1.5.0-prod"),
SemverPreReleaseTags: map[string]string{
"prod": "1.5.0-prod",
},
Meta: make(map[string]string),
Tags: []string{"1.5.0-prod"},
},
},
new: testingUtils.GetTrackedImage("karolisr/webhook-demo:1.7.0-dev"),
},
want: []*types.TrackedImage{
&types.TrackedImage{
Image: mustParse("karolisr/webhook-demo:latest"),
SemverPreReleaseTags: make(map[string]string),
Meta: make(map[string]string),
},
&types.TrackedImage{
Image: mustParse("karolisr/webhook-demo:build-foo"),
SemverPreReleaseTags: make(map[string]string),
Meta: make(map[string]string),
},
&types.TrackedImage{
Image: mustParse("karolisr/webhook-demo:1.7.0-dev"),
SemverPreReleaseTags: map[string]string{
"prod": "1.5.0-prod",
"dev": "1.7.0-dev",
},
Meta: make(map[string]string),
Tags: []string{"1.5.0-prod", "1.7.0-dev"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := appendImage(tt.args.images, tt.args.new); !reflect.DeepEqual(got, tt.want) {
t.Errorf("appendImage() = %v, want %v", got, tt.want)
}
})
}
}
func Test_lookupSemverImageIdx(t *testing.T) {
type args struct {
images []*types.TrackedImage
new *types.TrackedImage
}
tests := []struct {
name string
args args
want int
want1 bool
}{
{
name: "different image",
args: args{
images: []*types.TrackedImage{
testingUtils.GetTrackedImage("karolisr/webhook-demo:1.7.0-dev"),
},
new: testingUtils.GetTrackedImage("karolisr/foo:latest"),
},
want: 0,
want1: false,
},
{
name: "empty",
args: args{
images: []*types.TrackedImage{},
new: testingUtils.GetTrackedImage("karolisr/foo:latest"),
},
want: 0,
want1: false,
},
{
name: "semver second",
args: args{
images: []*types.TrackedImage{
testingUtils.GetTrackedImage("karolisr/webhook-demo:dev"),
testingUtils.GetTrackedImage("karolisr/webhook-demo:1.7.0-dev"),
testingUtils.GetTrackedImage("karolisr/webhook-demo:test"),
},
new: testingUtils.GetTrackedImage("karolisr/webhook-demo:1.5.0"),
},
want: 1,
want1: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1 := lookupSemverImageIdx(tt.args.images, tt.args.new)
if got != tt.want {
t.Errorf("lookupSemverImageIdx() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("lookupSemverImageIdx() got1 = %v, want %v", got1, tt.want1)
}
})
}
}

View File

@ -163,6 +163,63 @@ func TestPollingSemverUpdate(t *testing.T) {
t.Errorf("update failed: %s", err)
}
})
t.Run("UpdateThroughDockerHubPollingC", func(t *testing.T) {
testNamespace := createNamespaceForTest()
defer deleteTestNamespace(testNamespace)
dep := &apps_v1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "deployment-2",
Namespace: testNamespace,
Labels: map[string]string{
types.KeelPolicyLabel: "major",
types.KeelTriggerLabel: "poll",
},
Annotations: map[string]string{
types.KeelPollScheduleAnnotation: "@every 2s",
},
},
apps_v1.DeploymentSpec{
Selector: &meta_v1.LabelSelector{
MatchLabels: map[string]string{
"app": "wd-1",
},
},
Template: v1.PodTemplateSpec{
ObjectMeta: meta_v1.ObjectMeta{
Labels: map[string]string{
"app": "wd-1",
"release": "1",
},
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Name: "wd-1",
Image: "keelhq/push-workflow-example:0.3.0-alpha",
},
},
},
},
},
apps_v1.DeploymentStatus{},
}
_, err := kcs.AppsV1().Deployments(testNamespace).Create(dep)
if err != nil {
t.Fatalf("failed to create deployment: %s", err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
err = waitFor(ctx, kcs, testNamespace, dep.ObjectMeta.Name, "keelhq/push-workflow-example:0.11.0-alpha")
if err != nil {
t.Errorf("update failed: %s", err)
}
})
}
func TestPollingPrivateRegistry(t *testing.T) {

View File

@ -1,12 +1,10 @@
package poll
import (
"fmt"
"strings"
"github.com/Masterminds/semver"
"sort"
"github.com/keel-hq/keel/extension/credentialshelper"
"github.com/keel-hq/keel/internal/policy"
"github.com/keel-hq/keel/provider"
"github.com/keel-hq/keel/registry"
"github.com/keel-hq/keel/types"
@ -23,7 +21,7 @@ type WatchRepositoryTagsJob struct {
registryClient registry.Client
details *watchDetails
latests map[string]string // a map of prerelease tags and their corresponding latest versions
// latests map[string]string // a map of prerelease tags and their corresponding latest versions
}
// NewWatchRepositoryTagsJob - new tags watcher job
@ -32,7 +30,7 @@ func NewWatchRepositoryTagsJob(providers provider.Providers, registryClient regi
providers: providers,
registryClient: registryClient,
details: details,
latests: details.trackedImage.SemverPreReleaseTags,
// latests: details.trackedImage.SemverPreReleaseTags,
}
}
@ -83,104 +81,116 @@ func (j *WatchRepositoryTagsJob) Run() {
}
}
func (j *WatchRepositoryTagsJob) processTags(tags []string) error {
var errors []string
log.WithFields(log.Fields{
"latest": j.details.latest,
"all_tags": tags,
}).Debug("trigger.poll.WatchRepositoryTagsJob: checking for new version")
latestVersion, newAvailable, err := version.NewAvailable(j.details.latest, tags, false)
func (j *WatchRepositoryTagsJob) computeEvents(tags []string) ([]types.Event, error) {
trackedImages, err := j.providers.TrackedImages()
if err != nil {
errors = append(errors, fmt.Sprintf("new available version func returned an error: %s", err))
return nil, err
}
if newAvailable {
log.Debugf("new tag '%s' available, submitting event and assigning it to be the latest", latestVersion)
// updating current latest
j.details.latest = latestVersion
event := types.Event{
Repository: types.Repository{
Name: j.details.trackedImage.Image.Repository(),
Tag: latestVersion,
},
TriggerName: types.TriggerTypePoll.String(),
events := []types.Event{}
// collapse removes all non-semver tags and only takes
// the highest versions of each prerelease + the main version that doesn't have
// any prereleases
tags = collapse(tags)
for _, trackedImage := range getRelatedTrackedImages(j.details.trackedImage, trackedImages) {
// matches, going through tags
for _, tag := range tags {
update, err := trackedImage.Policy.ShouldUpdate(trackedImage.Image.Tag(), tag)
if err != nil {
continue
}
if update && !exists(tag, events) {
event := types.Event{
Repository: types.Repository{
Name: j.details.trackedImage.Image.Repository(),
Tag: tag,
},
TriggerName: types.TriggerTypePoll.String(),
}
events = append(events, event)
}
}
log.WithFields(log.Fields{
"repository": j.details.trackedImage.Image.Repository(),
"new_tag": latestVersion,
}).Debug("trigger.poll.WatchRepositoryTagsJob: submiting event to providers")
err := j.providers.Submit(event)
}
return events, nil
}
func exists(tag string, events []types.Event) bool {
for _, e := range events {
if tag == e.Repository.Tag {
return true
}
}
return false
}
// collapse gets latest available tags for main version and pre-releases
// example:
// [1.0.0, 1.5.0, 1.3.0-dev, 1.4.5-dev] would become [1.5.0, 1.4.5-dev]
func collapse(tags []string) []string {
r := map[string]string{}
p := policy.NewSemverPolicy(policy.SemverPolicyTypeAll)
for _, t := range tags {
v, err := version.GetVersion(t)
// v, err := semver.NewVersion(tag)
if err != nil {
continue
}
stored, ok := r[v.PreRelease]
if !ok {
r[v.PreRelease] = t
continue
}
higher, err := p.ShouldUpdate(stored, t)
if err != nil {
continue
}
if higher {
r[v.PreRelease] = t
}
}
result := []string{}
for _, tag := range r {
result = append(result, tag)
}
// always sort, for test purposes
sort.Strings(result)
return result
}
func getRelatedTrackedImages(ours *types.TrackedImage, all []*types.TrackedImage) []*types.TrackedImage {
b := all[:0]
for _, x := range all {
if x.Image.Repository() == ours.Image.Repository() {
b = append(b, x)
}
}
return b
}
func (j *WatchRepositoryTagsJob) processTags(tags []string) error {
events, err := j.computeEvents(tags)
if err != nil {
return err
}
for _, e := range events {
err = j.providers.Submit(e)
if err != nil {
log.WithFields(log.Fields{
"repository": j.details.trackedImage.Image.Repository(),
"new_tag": latestVersion,
"new_tag": e.Repository.Tag,
"error": err,
}).Error("trigger.poll.WatchRepositoryTagsJob: error while submitting an event")
}
}
log.Debugf("trigger.poll.WatchRepositoryTagsJob: processing tags %s", tags)
for _, tag := range tags {
sv, err := semver.NewVersion(tag)
if err != nil {
continue
}
if sv.Prerelease() == "" {
continue
}
trackedVersion, ok := j.details.trackedImage.SemverPreReleaseTags[sv.Prerelease()]
log.WithFields(log.Fields{
"all": tags,
"pre_release": sv.Prerelease(),
"tracked": trackedVersion,
"tracked_all": j.details.trackedImage.SemverPreReleaseTags,
}).Debug("looking for a pre-release")
if ok {
latestVersion, newAvailable, err := version.NewAvailable(trackedVersion, tags, true)
if err != nil {
errors = append(errors, fmt.Sprintf("new available version func for tag %s returned an error: %s", trackedVersion, err))
continue
}
if newAvailable {
// log.Debugf("new tag with prerelease '%s' available", latestVersion)
// updating current latest
// j.details.latest = latestVersion
j.details.trackedImage.SemverPreReleaseTags[sv.Prerelease()] = latestVersion
event := types.Event{
Repository: types.Repository{
Name: j.details.trackedImage.Image.Repository(),
Tag: latestVersion,
},
TriggerName: types.TriggerTypePoll.String(),
}
log.WithFields(log.Fields{
"repository": j.details.trackedImage.Image.Repository(),
"new_tag": latestVersion,
}).Debug("trigger.poll.WatchRepositoryTagsJob: submiting event to providers")
err := j.providers.Submit(event)
if err != nil {
log.WithFields(log.Fields{
"repository": j.details.trackedImage.Image.Repository(),
"new_tag": latestVersion,
"error": err,
}).Error("trigger.poll.WatchRepositoryTagsJob: error while submitting an event")
}
}
if err != nil {
errors = append(errors, fmt.Sprintf("new available version func returned an error: %s", err))
}
}
// nothing to do
}
if len(errors) > 0 {
return fmt.Errorf("errors while processing repository tags: %s", strings.Join(errors, ","))
}
return nil
}

View File

@ -1,10 +1,13 @@
package poll
import (
"reflect"
"strings"
"testing"
"github.com/keel-hq/keel/approvals"
"github.com/keel-hq/keel/cache/memory"
"github.com/keel-hq/keel/internal/policy"
"github.com/keel-hq/keel/provider"
"github.com/keel-hq/keel/types"
"github.com/keel-hq/keel/util/image"
@ -20,10 +23,7 @@ func TestWatchMultipleTagsWithSemver(t *testing.T) {
Trigger: types.TriggerTypePoll,
Provider: "fp",
PollSchedule: types.KeelPollDefaultSchedule,
SemverPreReleaseTags: map[string]string{
"dev": "1.0.0-dev",
"prod": "1.5.0-prod",
},
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeAll),
},
},
}
@ -49,19 +49,21 @@ func TestWatchMultipleTagsWithSemver(t *testing.T) {
}
if len(watcher.watched) != 1 {
t.Errorf("expected to find watching 4 entries, found: %d", len(watcher.watched))
}
if det, ok := watcher.watched["gcr.io/v2-namespace/hello-world"]; ok != true {
t.Errorf("alpha watcher not found")
if det.latest != "1.5.0" {
t.Errorf("expected to find a tag set for multiple tags watch job")
}
t.Errorf("expected to find watching 1 entries, found: %d", len(watcher.watched))
}
}
func TestWatchAllTagsJobWithSemver(t *testing.T) {
fp := &fakeProvider{}
reference, _ := image.Parse("foo/bar:1.1.0")
fp := &fakeProvider{
images: []*types.TrackedImage{
&types.TrackedImage{
Image: reference,
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeMajor),
},
},
}
mem := memory.NewMemoryCache()
am := approvals.New(mem)
providers := provider.New([]provider.Provider{fp}, am)
@ -70,16 +72,113 @@ func TestWatchAllTagsJobWithSemver(t *testing.T) {
tagsToReturn: []string{"1.3.0-dev", "1.5.0", "1.8.0-alpha"},
}
reference, _ := image.Parse("foo/bar:1.1.0")
details := &watchDetails{
trackedImage: &types.TrackedImage{
Image: reference,
SemverPreReleaseTags: map[string]string{
"dev": "1.2.0-dev",
trackedImage: fp.images[0],
}
job := NewWatchRepositoryTagsJob(providers, frc, details)
job.Run()
// checking whether new job was submitted
if len(fp.submitted) != 1 {
tags := []string{}
for _, s := range fp.submitted {
tags = append(tags, s.Repository.Tag)
}
t.Errorf("expected 1 events, got: %d [%s]", len(fp.submitted), strings.Join(tags, ", "))
}
submitted := fp.submitted[0]
if submitted.Repository.Name != "index.docker.io/foo/bar" {
t.Errorf("unexpected event repository name: %s", submitted.Repository.Name)
}
if submitted.Repository.Tag != "1.5.0" {
t.Errorf("expected event repository tag 1.5.0, but got: %s", submitted.Repository.Tag)
}
}
func TestWatchAllTagsPrerelease(t *testing.T) {
referenceB, _ := image.Parse("foo/bar:1.2.0-dev")
fp := &fakeProvider{
images: []*types.TrackedImage{
&types.TrackedImage{
Image: referenceB,
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeMajor),
},
},
}
mem := memory.NewMemoryCache()
am := approvals.New(mem)
providers := provider.New([]provider.Provider{fp}, am)
frc := &fakeRegistryClient{
tagsToReturn: []string{"1.3.0-dev", "1.5.0", "1.8.0-alpha"},
}
details := &watchDetails{
trackedImage: fp.images[0],
}
job := NewWatchRepositoryTagsJob(providers, frc, details)
job.Run()
// checking whether new job was submitted
if len(fp.submitted) != 1 {
tags := []string{}
for _, s := range fp.submitted {
tags = append(tags, s.Repository.Tag)
}
t.Errorf("expected 1 events, got: %d [%s]", len(fp.submitted), strings.Join(tags, ", "))
}
submitted := fp.submitted[0]
if submitted.Repository.Name != "index.docker.io/foo/bar" {
t.Errorf("unexpected event repository name: %s", submitted.Repository.Name)
}
if submitted.Repository.Tag != "1.3.0-dev" {
t.Errorf("expected event repository tag 1.3.0-dev, but got: %s", submitted.Repository.Tag)
}
}
func TestWatchAllTagsMixed(t *testing.T) {
referenceA, _ := image.Parse("foo/bar:1.0.0")
referenceB, _ := image.Parse("foo/bar:1.2.0-dev")
fp := &fakeProvider{
images: []*types.TrackedImage{
&types.TrackedImage{
Image: referenceB,
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeMajor),
},
&types.TrackedImage{
Image: referenceA,
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeMajor),
},
},
}
mem := memory.NewMemoryCache()
am := approvals.New(mem)
providers := provider.New([]provider.Provider{fp}, am)
frc := &fakeRegistryClient{
tagsToReturn: []string{"1.3.0-dev", "1.5.0", "1.8.0-alpha"},
}
details := &watchDetails{
trackedImage: fp.images[0],
}
job := NewWatchRepositoryTagsJob(providers, frc, details)
@ -88,10 +187,74 @@ func TestWatchAllTagsJobWithSemver(t *testing.T) {
// checking whether new job was submitted
if len(fp.submitted) != 2 {
t.Errorf("expected 2 events, got: %d", len(fp.submitted))
tags := []string{}
for _, s := range fp.submitted {
tags = append(tags, s.Repository.Tag)
}
t.Errorf("expected 1 events, got: %d [%s]", len(fp.submitted), strings.Join(tags, ", "))
}
submitted := fp.submitted[0]
submitted2 := fp.submitted[1]
if submitted.Repository.Name != "index.docker.io/foo/bar" {
t.Errorf("unexpected event repository name: %s", submitted.Repository.Name)
}
if submitted.Repository.Tag != "1.3.0-dev" {
t.Errorf("expected event repository tag 1.3.0-dev, but got: %s", submitted.Repository.Tag)
}
if submitted2.Repository.Tag != "1.5.0" {
t.Errorf("expected event repository tag 1.5.0, but got: %s", submitted2.Repository.Tag)
}
}
func TestWatchAllTagsMixedPolicyAll(t *testing.T) {
referenceA, _ := image.Parse("foo/bar:1.0.0")
referenceB, _ := image.Parse("foo/bar:1.6.0-alpha")
fp := &fakeProvider{
images: []*types.TrackedImage{
&types.TrackedImage{
Image: referenceB,
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeAll),
},
&types.TrackedImage{
Image: referenceA,
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeMajor),
},
},
}
mem := memory.NewMemoryCache()
am := approvals.New(mem)
providers := provider.New([]provider.Provider{fp}, am)
frc := &fakeRegistryClient{
tagsToReturn: []string{"1.3.0-dev", "1.5.0", "1.8.0-alpha"},
}
details := &watchDetails{
trackedImage: fp.images[0],
}
job := NewWatchRepositoryTagsJob(providers, frc, details)
job.Run()
// checking whether new job was submitted
if len(fp.submitted) != 2 {
tags := []string{}
for _, s := range fp.submitted {
tags = append(tags, s.Repository.Tag)
}
t.Errorf("expected 1 events, got: %d [%s]", len(fp.submitted), strings.Join(tags, ", "))
}
submitted := fp.submitted[0]
submitted2 := fp.submitted[1]
if submitted.Repository.Name != "index.docker.io/foo/bar" {
t.Errorf("unexpected event repository name: %s", submitted.Repository.Name)
@ -101,13 +264,51 @@ func TestWatchAllTagsJobWithSemver(t *testing.T) {
t.Errorf("expected event repository tag 1.8.0-alpha, but got: %s", submitted.Repository.Tag)
}
submitted2 := fp.submitted[1]
if submitted2.Repository.Name != "index.docker.io/foo/bar" {
t.Errorf("unexpected event repository name: %s", submitted.Repository.Name)
}
if submitted2.Repository.Tag != "1.3.0-dev" {
t.Errorf("expected event repository tag 1.3.0-dev, but got: %s", submitted.Repository.Tag)
if submitted2.Repository.Tag != "1.5.0" {
t.Errorf("expected event repository tag 1.5.0, but got: %s", submitted2.Repository.Tag)
}
}
func Test_collapse(t *testing.T) {
type args struct {
tags []string
}
tests := []struct {
name string
args args
want []string
}{
{
name: "single version",
args: args{tags: []string{"1.0.0"}},
want: []string{"1.0.0"},
},
{
name: "multi",
args: args{tags: []string{"1.0.0", "1.4.0"}},
want: []string{"1.4.0"},
},
{
name: "prerelease",
args: args{tags: []string{"1.0.0-dev", "1.4.0-dev"}},
want: []string{"1.4.0-dev"},
},
{
name: "prerelease multi",
args: args{tags: []string{"1.3.0-bb", "1.0.0-dev", "1.4.0-dev"}},
want: []string{"1.3.0-bb", "1.4.0-dev"},
},
{
name: "prerelease multi, mixed",
args: args{tags: []string{"1.2.0", "1.3.0-bb", "1.0.0-dev", "1.4.0-dev"}},
want: []string{"1.2.0", "1.3.0-bb", "1.4.0-dev"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := collapse(tt.args.tags); !reflect.DeepEqual(got, tt.want) {
t.Errorf("collapse() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -8,6 +8,7 @@ import (
"github.com/keel-hq/keel/approvals"
"github.com/keel-hq/keel/cache/memory"
"github.com/keel-hq/keel/extension/credentialshelper"
"github.com/keel-hq/keel/internal/policy"
"github.com/keel-hq/keel/provider"
"github.com/keel-hq/keel/registry"
"github.com/keel-hq/keel/types"
@ -20,10 +21,9 @@ func mustParse(img string, schedule string) *types.TrackedImage {
panic(err)
}
return &types.TrackedImage{
Image: ref,
PollSchedule: schedule,
Trigger: types.TriggerTypePoll,
SemverPreReleaseTags: make(map[string]string),
Image: ref,
PollSchedule: schedule,
Trigger: types.TriggerTypePoll,
}
}
@ -166,7 +166,15 @@ func TestWatchTagJobLatest(t *testing.T) {
func TestWatchAllTagsJob(t *testing.T) {
fp := &fakeProvider{}
reference, _ := image.Parse("foo/bar:1.1.0")
fp := &fakeProvider{
images: []*types.TrackedImage{
&types.TrackedImage{
Image: reference,
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeAll),
},
},
}
mem := memory.NewMemoryCache()
am := approvals.New(mem)
providers := provider.New([]provider.Provider{fp}, am)
@ -175,12 +183,8 @@ func TestWatchAllTagsJob(t *testing.T) {
tagsToReturn: []string{"1.1.2", "1.1.3", "0.9.1"},
}
reference, _ := image.Parse("foo/bar:1.1.0")
details := &watchDetails{
trackedImage: &types.TrackedImage{
Image: reference,
},
trackedImage: fp.images[0],
}
job := NewWatchRepositoryTagsJob(providers, frc, details)
@ -202,7 +206,15 @@ func TestWatchAllTagsJob(t *testing.T) {
func TestWatchAllTagsJobCurrentLatest(t *testing.T) {
fp := &fakeProvider{}
reference, _ := image.Parse("foo/bar:latest")
fp := &fakeProvider{
images: []*types.TrackedImage{
&types.TrackedImage{
Image: reference,
Policy: policy.NewForcePolicy(true),
},
},
}
mem := memory.NewMemoryCache()
am := approvals.New(mem)
providers := provider.New([]provider.Provider{fp}, am)
@ -211,12 +223,8 @@ func TestWatchAllTagsJobCurrentLatest(t *testing.T) {
tagsToReturn: []string{"1.1.2", "1.1.3", "0.9.1"},
}
reference, _ := image.Parse("foo/bar:latest")
details := &watchDetails{
trackedImage: &types.TrackedImage{
Image: reference,
},
trackedImage: fp.images[0],
}
job := NewWatchRepositoryTagsJob(providers, frc, details)
@ -245,6 +253,8 @@ func TestWatchMultipleTags(t *testing.T) {
Trigger: types.TriggerTypePoll,
Provider: "fp",
PollSchedule: types.KeelPollDefaultSchedule,
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeMajor),
},
&types.TrackedImage{
@ -252,6 +262,7 @@ func TestWatchMultipleTags(t *testing.T) {
Image: imgB,
Provider: "fp",
PollSchedule: types.KeelPollDefaultSchedule,
Policy: policy.NewSemverPolicy(policy.SemverPolicyTypeMajor),
},
&types.TrackedImage{
@ -259,6 +270,7 @@ func TestWatchMultipleTags(t *testing.T) {
Image: imgC,
Provider: "fp",
PollSchedule: types.KeelPollDefaultSchedule,
Policy: policy.NewForcePolicy(true),
},
&types.TrackedImage{
@ -266,6 +278,7 @@ func TestWatchMultipleTags(t *testing.T) {
Image: imgD,
Provider: "fp",
PollSchedule: types.KeelPollDefaultSchedule,
Policy: policy.NewForcePolicy(true),
},
},
}

View File

@ -22,12 +22,15 @@ type TrackedImage struct {
Meta map[string]string // metadata supplied by providers
// a list of pre-release tags, ie: 1.0.0-dev, 1.5.0-prod get translated into
// dev, prod
// SemverPreReleaseTags []string
SemverPreReleaseTags map[string]string
// combined semver tags
Tags []string
Tags []string
Policy Policy
}
type Policy interface {
ShouldUpdate(current, new string) (bool, error)
}
func (i TrackedImage) String() string {
return fmt.Sprintf("namespace:%s,image:%s:%s,provider:%s,trigger:%s,sched:%s,secrets:%s,semver:%v,tags:%v", i.Namespace, i.Image.Repository(), i.Image.Tag(), i.Provider, i.Trigger, i.PollSchedule, i.Secrets, i.SemverPreReleaseTags, i.Tags)
return fmt.Sprintf("namespace:%s,image:%s:%s,provider:%s,trigger:%s,sched:%s,secrets:%s", i.Namespace, i.Image.Repository(), i.Image.Tag(), i.Provider, i.Trigger, i.PollSchedule, i.Secrets)
}

View File

@ -88,13 +88,12 @@ func GetTrackedImage(i string) *types.TrackedImage {
panic(err)
}
return &types.TrackedImage{
Image: ref,
PollSchedule: "",
Trigger: types.TriggerTypeDefault,
Provider: "",
Namespace: "",
Meta: make(map[string]string),
SemverPreReleaseTags: make(map[string]string),
Tags: []string{ref.Tag()},
Image: ref,
PollSchedule: "",
Trigger: types.TriggerTypeDefault,
Provider: "",
Namespace: "",
Meta: make(map[string]string),
Tags: []string{ref.Tag()},
}
}