using cred helper from watcher

feature/ecr_registry_auth
Karolis Rusenas 2018-04-28 22:01:44 +01:00
parent 286508d60d
commit 52c5101646
5 changed files with 181 additions and 154 deletions

View File

@ -40,6 +40,7 @@ import (
// credentials helpers // credentials helpers
_ "github.com/keel-hq/keel/extension/credentialshelper/aws" _ "github.com/keel-hq/keel/extension/credentialshelper/aws"
secretsCredentialsHelper "github.com/keel-hq/keel/extension/credentialshelper/secrets"
// bots // bots
_ "github.com/keel-hq/keel/bot/hipchat" _ "github.com/keel-hq/keel/bot/hipchat"
@ -171,8 +172,14 @@ func main() {
// setting up providers // setting up providers
providers := setupProviders(implementer, sender, approvalsManager, &t.GenericResourceCache) providers := setupProviders(implementer, sender, approvalsManager, &t.GenericResourceCache)
// registering secrets based credentials helper
secretsGetter := secrets.NewGetter(implementer) secretsGetter := secrets.NewGetter(implementer)
teardownTriggers := setupTriggers(ctx, providers, secretsGetter, approvalsManager) ch := secretsCredentialsHelper.New(secretsGetter)
credentialshelper.RegisterCredentialsHelper("secrets", ch)
// trigger setup
teardownTriggers := setupTriggers(ctx, providers, approvalsManager)
bot.Run(implementer, approvalsManager) bot.Run(implementer, approvalsManager)
@ -235,7 +242,7 @@ func setupProviders(k8sImplementer kubernetes.Implementer, sender notification.S
// setupTriggers - setting up triggers. New triggers should be added to this function. Each trigger // setupTriggers - setting up triggers. New triggers should be added to this function. Each trigger
// should go through all providers (or not if there is a reason) and submit events) // should go through all providers (or not if there is a reason) and submit events)
func setupTriggers(ctx context.Context, providers provider.Providers, secretsGetter secrets.Getter, approvalsManager approvals.Manager) (teardown func()) { func setupTriggers(ctx context.Context, providers provider.Providers, approvalsManager approvals.Manager) (teardown func()) {
// setting up generic http webhook server // setting up generic http webhook server
whs := http.NewTriggerServer(&http.Opts{ whs := http.NewTriggerServer(&http.Opts{
@ -273,7 +280,7 @@ func setupTriggers(ctx context.Context, providers provider.Providers, secretsGet
registryClient := registry.New() registryClient := registry.New()
watcher := poll.NewRepositoryWatcher(providers, registryClient) watcher := poll.NewRepositoryWatcher(providers, registryClient)
pollManager := poll.NewPollManager(providers, watcher, secretsGetter, credentialshelper.New()) pollManager := poll.NewPollManager(providers, watcher)
// start poll manager, will finish with ctx // start poll manager, will finish with ctx
go watcher.Start(ctx) go watcher.Start(ctx)

View File

@ -5,9 +5,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/keel-hq/keel/extension/credentialshelper"
"github.com/keel-hq/keel/provider" "github.com/keel-hq/keel/provider"
"github.com/keel-hq/keel/secrets"
"github.com/keel-hq/keel/types" "github.com/keel-hq/keel/types"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
@ -31,10 +29,6 @@ func init() {
type DefaultManager struct { type DefaultManager struct {
providers provider.Providers providers provider.Providers
secretsGetter secrets.Getter
credentialsHelper credentialshelper.CredentialsHelper
// repository watcher // repository watcher
watcher Watcher watcher Watcher
@ -48,11 +42,9 @@ type DefaultManager struct {
} }
// NewPollManager - new default poller // NewPollManager - new default poller
func NewPollManager(providers provider.Providers, watcher Watcher, secretsGetter secrets.Getter, credentialsHelper credentialshelper.CredentialsHelper) *DefaultManager { func NewPollManager(providers provider.Providers, watcher Watcher) *DefaultManager {
return &DefaultManager{ return &DefaultManager{
providers: providers, providers: providers,
secretsGetter: secretsGetter,
credentialsHelper: credentialsHelper,
watcher: watcher, watcher: watcher,
mu: &sync.Mutex{}, mu: &sync.Mutex{},
scanTick: 1, scanTick: 1,
@ -93,59 +85,25 @@ func (s *DefaultManager) Start(ctx context.Context) error {
} }
func (s *DefaultManager) scan(ctx context.Context) error { func (s *DefaultManager) scan(ctx context.Context) error {
log.Info("performing scan") log.Debug("trigger.poll.manager: performing scan")
trackedImages, err := s.providers.TrackedImages() trackedImages, err := s.providers.TrackedImages()
if err != nil { if err != nil {
return err return err
} }
var tracked float64 var tracked float64
for _, trackedImage := range trackedImages { for _, trackedImage := range trackedImages {
if trackedImage.Trigger != types.TriggerTypePoll { if trackedImage.Trigger != types.TriggerTypePoll {
continue continue
} }
tracked++ tracked++
err = s.watcher.Watch(trackedImage, trackedImage.PollSchedule)
var imageCreds *types.Credentials
// anonymous credentials
creds := &types.Credentials{}
imageCreds, err = s.secretsGetter.Get(trackedImage)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"secrets": trackedImage.Secrets,
"image": trackedImage.Image.Remote(),
}).Error("trigger.poll.manager: failed to get authentication credentials")
} else {
creds = imageCreds
}
// TODO: refactor to either recreate it every 10 hours (12 hours expiration) or better to retrieve creds
// just before quering the registry
if imageCreds.Username == "" && imageCreds.Password == "" {
registryCreds, err := s.credentialsHelper.GetCredentials(trackedImage.Image.Registry())
if err != nil {
log.WithFields(log.Fields{
"error": err,
"registry": trackedImage.Image.Registry(),
"image": trackedImage.Image.Remote(),
}).Error("trigger.poll.manager: failed to get registry credentials")
} else {
creds = registryCreds
}
}
err = s.watcher.Watch(trackedImage.Image.Remote(), trackedImage.PollSchedule, creds.Username, creds.Password)
if err != nil { if err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"error": err, "error": err,
"schedule": trackedImage.PollSchedule, "schedule": trackedImage.PollSchedule,
"image": trackedImage.Image.Remote(), "image": trackedImage.Image.Remote(),
}).Error("trigger.poll.manager: failed to start watching repository") }).Error("trigger.poll.manager: failed to start watching repository")
// continue processing other images
} }
} }

View File

@ -13,7 +13,7 @@ import (
"github.com/keel-hq/keel/util/codecs" "github.com/keel-hq/keel/util/codecs"
"github.com/keel-hq/keel/util/image" "github.com/keel-hq/keel/util/image"
"github.com/keel-hq/keel/extension/credentialshelper" // "github.com/keel-hq/keel/extension/credentialshelper"
_ "github.com/keel-hq/keel/extension/credentialshelper/aws" _ "github.com/keel-hq/keel/extension/credentialshelper/aws"
"testing" "testing"
@ -59,7 +59,7 @@ func TestCheckDeployment(t *testing.T) {
watcher := NewRepositoryWatcher(providers, frc) watcher := NewRepositoryWatcher(providers, frc)
pm := NewPollManager(providers, watcher, &FakeSecretsGetter{}, credentialshelper.New()) pm := NewPollManager(providers, watcher)
imageA := "gcr.io/v2-namespace/hello-world:1.1.1" imageA := "gcr.io/v2-namespace/hello-world:1.1.1"
imageB := "gcr.io/v2-namespace/greetings-world:1.1.1" imageB := "gcr.io/v2-namespace/greetings-world:1.1.1"
@ -80,11 +80,11 @@ func TestCheckDeployment(t *testing.T) {
if watcher.watched[keyA].schedule != types.KeelPollDefaultSchedule { if watcher.watched[keyA].schedule != types.KeelPollDefaultSchedule {
t.Errorf("unexpected schedule: %s", watcher.watched[keyA].schedule) t.Errorf("unexpected schedule: %s", watcher.watched[keyA].schedule)
} }
if watcher.watched[keyA].imageRef.Remote() != ref.Remote() { if watcher.watched[keyA].trackedImage.Image.Remote() != ref.Remote() {
t.Errorf("unexpected remote remote: %s", watcher.watched[keyA].imageRef.Remote()) t.Errorf("unexpected remote remote: %s", watcher.watched[keyA].trackedImage.Image.Remote())
} }
if watcher.watched[keyA].imageRef.Tag() != ref.Tag() { if watcher.watched[keyA].trackedImage.Image.Tag() != ref.Tag() {
t.Errorf("unexpected tag: %s", watcher.watched[keyA].imageRef.Tag()) t.Errorf("unexpected tag: %s", watcher.watched[keyA].trackedImage.Image.Tag())
} }
refB, _ := image.Parse(imageB) refB, _ := image.Parse(imageB)
@ -95,11 +95,11 @@ func TestCheckDeployment(t *testing.T) {
if watcher.watched[keyB].schedule != types.KeelPollDefaultSchedule { if watcher.watched[keyB].schedule != types.KeelPollDefaultSchedule {
t.Errorf("unexpected schedule: %s", watcher.watched[keyB].schedule) t.Errorf("unexpected schedule: %s", watcher.watched[keyB].schedule)
} }
if watcher.watched[keyB].imageRef.Remote() != refB.Remote() { if watcher.watched[keyB].trackedImage.Image.Remote() != refB.Remote() {
t.Errorf("unexpected remote remote: %s", watcher.watched[keyB].imageRef.Remote()) t.Errorf("unexpected remote remote: %s", watcher.watched[keyB].trackedImage.Image.Remote())
} }
if watcher.watched[keyB].imageRef.Tag() != refB.Tag() { if watcher.watched[keyB].trackedImage.Image.Tag() != refB.Tag() {
t.Errorf("unexpected tag: %s", watcher.watched[keyB].imageRef.Tag()) t.Errorf("unexpected tag: %s", watcher.watched[keyB].trackedImage.Image.Tag())
} }
} }
@ -132,7 +132,7 @@ func TestCheckECRDeployment(t *testing.T) {
watcher := NewRepositoryWatcher(providers, rc) watcher := NewRepositoryWatcher(providers, rc)
pm := NewPollManager(providers, watcher, &FakeSecretsGetter{}, credentialshelper.New()) pm := NewPollManager(providers, watcher)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@ -157,10 +157,10 @@ func TestCheckECRDeployment(t *testing.T) {
if watcher.watched[keyA].schedule != types.KeelPollDefaultSchedule { if watcher.watched[keyA].schedule != types.KeelPollDefaultSchedule {
t.Errorf("unexpected schedule: %s", watcher.watched[keyA].schedule) t.Errorf("unexpected schedule: %s", watcher.watched[keyA].schedule)
} }
if watcher.watched[keyA].imageRef.Remote() != imgA.Remote() { if watcher.watched[keyA].trackedImage.Image.Remote() != imgA.Remote() {
t.Errorf("unexpected remote remote: %s", watcher.watched[keyA].imageRef.Remote()) t.Errorf("unexpected remote remote: %s", watcher.watched[keyA].trackedImage.Image.Remote())
} }
if watcher.watched[keyA].imageRef.Tag() != imgA.Tag() { if watcher.watched[keyA].trackedImage.Image.Tag() != imgA.Tag() {
t.Errorf("unexpected tag: %s", watcher.watched[keyA].imageRef.Tag()) t.Errorf("unexpected tag: %s", watcher.watched[keyA].trackedImage.Image.Tag())
} }
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/keel-hq/keel/extension/credentialshelper"
"github.com/keel-hq/keel/provider" "github.com/keel-hq/keel/provider"
"github.com/keel-hq/keel/registry" "github.com/keel-hq/keel/registry"
"github.com/keel-hq/keel/types" "github.com/keel-hq/keel/types"
@ -30,14 +31,12 @@ func init() {
// Watcher - generic watcher interface // Watcher - generic watcher interface
type Watcher interface { type Watcher interface {
Watch(imageName, registryUsername, registryPassword, schedule string) error Watch(image *types.TrackedImage, schedule string) error
Unwatch(image string) error Unwatch(image string) error
} }
type watchDetails struct { type watchDetails struct {
imageRef *image.Reference trackedImage *types.TrackedImage
registryUsername string // "" for anonymous
registryPassword string // "" for anonymous
digest string // image digest digest string // image digest
latest string // latest tag latest string // latest tag
schedule string schedule string
@ -115,7 +114,7 @@ func (w *RepositoryWatcher) Unwatch(imageName string) error {
// Watch - starts watching repository for changes, if it's already watching - ignores, // Watch - starts watching repository for changes, if it's already watching - ignores,
// if details changed - updates details // if details changed - updates details
func (w *RepositoryWatcher) Watch(imageName, schedule, registryUsername, registryPassword string) error { func (w *RepositoryWatcher) Watch(image *types.TrackedImage, schedule string) error {
if schedule == "" { if schedule == "" {
return fmt.Errorf("cron schedule cannot be empty") return fmt.Errorf("cron schedule cannot be empty")
@ -125,32 +124,23 @@ func (w *RepositoryWatcher) Watch(imageName, schedule, registryUsername, registr
if err != nil { if err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"error": err, "error": err,
"image": imageName, "image": image.String(),
"schedule": schedule, "schedule": schedule,
}).Error("trigger.poll.RepositoryWatcher.addJob: invalid cron schedule") }).Error("trigger.poll.RepositoryWatcher.addJob: invalid cron schedule")
return fmt.Errorf("invalid cron schedule: %s", err) return fmt.Errorf("invalid cron schedule: %s", err)
} }
imageRef, err := image.Parse(imageName) key := getImageIdentifier(image.Image)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"image_name": imageName,
}).Error("trigger.poll.RepositoryWatcher.Watch: failed to parse image")
return err
}
key := getImageIdentifier(imageRef)
// checking whether it's already being watched // checking whether it's already being watched
details, ok := w.watched[key] details, ok := w.watched[key]
if !ok { if !ok {
err = w.addJob(imageRef, registryUsername, registryPassword, schedule) // err = w.addJob(imageRef, registryUsername, registryPassword, schedule)
err = w.addJob(image, schedule)
if err != nil { if err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"error": err, "error": err,
"image_name": imageName, "image": image.String(),
"registry_username": registryUsername,
}).Error("trigger.poll.RepositoryWatcher.Watch: failed to add image watch job") }).Error("trigger.poll.RepositoryWatcher.Watch: failed to add image watch job")
} }
@ -162,52 +152,37 @@ func (w *RepositoryWatcher) Watch(imageName, schedule, registryUsername, registr
w.cron.UpdateJob(key, schedule) w.cron.UpdateJob(key, schedule)
} }
// checking auth details, if changed - need to update
if details.registryPassword != registryPassword || details.registryUsername != registryUsername {
// recreating job
w.cron.DeleteJob(key)
err = w.addJob(imageRef, registryUsername, registryPassword, schedule)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"image_name": imageName,
"registry_username": registryUsername,
}).Error("trigger.poll.RepositoryWatcher.Watch: failed to add image watch job")
}
return err
}
// nothing to do // nothing to do
return nil return nil
} }
func (w *RepositoryWatcher) addJob(ref *image.Reference, registryUsername, registryPassword, schedule string) error { func (w *RepositoryWatcher) addJob(ti *types.TrackedImage, schedule string) error {
// getting initial digest // getting initial digest
reg := ref.Scheme() + "://" + ref.Registry() reg := ti.Image.Scheme() + "://" + ti.Image.Registry()
creds := credentialshelper.GetCredentials(ti)
digest, err := w.registryClient.Digest(registry.Opts{ digest, err := w.registryClient.Digest(registry.Opts{
Registry: reg, Registry: reg,
Name: ref.ShortName(), Name: ti.Image.ShortName(),
Tag: ref.Tag(), Tag: ti.Image.Tag(),
Username: registryUsername, Username: creds.Username,
Password: registryPassword, Password: creds.Password,
}) })
if err != nil { if err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"error": err, "error": err,
"image": ref.Remote(), "image": ti.Image.String(),
}).Error("trigger.poll.RepositoryWatcher.addJob: failed to get image digest") }).Error("trigger.poll.RepositoryWatcher.addJob: failed to get image digest")
return err return err
} }
key := getImageIdentifier(ref) key := getImageIdentifier(ti.Image)
details := &watchDetails{ details := &watchDetails{
imageRef: ref, trackedImage: ti,
digest: digest, // current image digest digest: digest, // current image digest
latest: ref.Tag(), latest: ti.Image.Tag(),
registryUsername: registryUsername,
registryPassword: registryPassword,
schedule: schedule, schedule: schedule,
} }
@ -217,13 +192,13 @@ func (w *RepositoryWatcher) addJob(ref *image.Reference, registryUsername, regis
// checking tag type, for versioned (semver) tags we setup a watch all tags job // checking tag type, for versioned (semver) tags we setup a watch all tags job
// and for non-semver types we create a single tag watcher which // and for non-semver types we create a single tag watcher which
// checks digest // checks digest
_, err = version.GetVersion(ref.Tag()) _, err = version.GetVersion(ti.Image.Tag())
if err != nil { if err != nil {
// adding new job // adding new job
job := NewWatchTagJob(w.providers, w.registryClient, details) job := NewWatchTagJob(w.providers, w.registryClient, details)
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"job_name": key, "job_name": key,
"image": ref.Remote(), "image": ti.Image.String(),
"digest": digest, "digest": digest,
"schedule": schedule, "schedule": schedule,
}).Info("trigger.poll.RepositoryWatcher: new watch tag digest job added") }).Info("trigger.poll.RepositoryWatcher: new watch tag digest job added")
@ -234,7 +209,7 @@ func (w *RepositoryWatcher) addJob(ref *image.Reference, registryUsername, regis
job := NewWatchRepositoryTagsJob(w.providers, w.registryClient, details) job := NewWatchRepositoryTagsJob(w.providers, w.registryClient, details)
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"job_name": key, "job_name": key,
"image": ref.Remote(), "image": ti.Image.String(),
"digest": digest, "digest": digest,
"schedule": schedule, "schedule": schedule,
}).Info("trigger.poll.RepositoryWatcher: new watch repository tags job added") }).Info("trigger.poll.RepositoryWatcher: new watch repository tags job added")
@ -261,21 +236,22 @@ func NewWatchTagJob(providers provider.Providers, registryClient registry.Client
// Run - main function to check schedule // Run - main function to check schedule
func (j *WatchTagJob) Run() { func (j *WatchTagJob) Run() {
reg := j.details.imageRef.Scheme() + "://" + j.details.imageRef.Registry() creds := credentialshelper.GetCredentials(j.details.trackedImage)
reg := j.details.trackedImage.Image.Scheme() + "://" + j.details.trackedImage.Image.Registry()
currentDigest, err := j.registryClient.Digest(registry.Opts{ currentDigest, err := j.registryClient.Digest(registry.Opts{
Registry: reg, Registry: reg,
Name: j.details.imageRef.ShortName(), Name: j.details.trackedImage.Image.ShortName(),
Tag: j.details.imageRef.Tag(), Tag: j.details.trackedImage.Image.Tag(),
Username: j.details.registryUsername, Username: creds.Username,
Password: j.details.registryPassword, Password: creds.Password,
}) })
registriesScannedCounter.With(prometheus.Labels{"registry": j.details.imageRef.Registry(), "image": j.details.imageRef.Name()}).Inc() registriesScannedCounter.With(prometheus.Labels{"registry": j.details.trackedImage.Image.Registry(), "image": j.details.trackedImage.Image.Name()}).Inc()
if err != nil { if err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"error": err, "error": err,
"image": j.details.imageRef.Remote(), "image": j.details.trackedImage.Image.String(),
}).Error("trigger.poll.WatchTagJob: failed to check digest") }).Error("trigger.poll.WatchTagJob: failed to check digest")
return return
} }
@ -283,7 +259,7 @@ func (j *WatchTagJob) Run() {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"current_digest": j.details.digest, "current_digest": j.details.digest,
"new_digest": currentDigest, "new_digest": currentDigest,
"image_name": j.details.imageRef.Remote(), "image": j.details.trackedImage.Image.String(),
}).Debug("trigger.poll.WatchTagJob: checking digest") }).Debug("trigger.poll.WatchTagJob: checking digest")
// checking whether image digest has changed // checking whether image digest has changed
@ -293,14 +269,14 @@ func (j *WatchTagJob) Run() {
event := types.Event{ event := types.Event{
Repository: types.Repository{ Repository: types.Repository{
Name: j.details.imageRef.Repository(), Name: j.details.trackedImage.Image.Repository(),
Tag: j.details.imageRef.Tag(), Tag: j.details.trackedImage.Image.Tag(),
Digest: currentDigest, Digest: currentDigest,
}, },
TriggerName: types.TriggerTypePoll.String(), TriggerName: types.TriggerTypePoll.String(),
} }
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"repository": j.details.imageRef.Repository(), "image": j.details.trackedImage.Image.String(),
"new_digest": currentDigest, "new_digest": currentDigest,
}).Info("trigger.poll.WatchTagJob: digest change detected, submiting event to providers") }).Info("trigger.poll.WatchTagJob: digest change detected, submiting event to providers")
@ -327,32 +303,35 @@ func NewWatchRepositoryTagsJob(providers provider.Providers, registryClient regi
// Run - main function to check schedule // Run - main function to check schedule
func (j *WatchRepositoryTagsJob) Run() { func (j *WatchRepositoryTagsJob) Run() {
reg := j.details.imageRef.Scheme() + "://" + j.details.imageRef.Registry()
creds := credentialshelper.GetCredentials(j.details.trackedImage)
// reg := j.details.imageRef.Scheme() + "://" + j.details.imageRef.Registry()
reg := j.details.trackedImage.Image.Scheme() + "://" + j.details.trackedImage.Image.Registry()
if j.details.latest == "" { if j.details.latest == "" {
j.details.latest = j.details.imageRef.Tag() j.details.latest = j.details.trackedImage.Image.Tag()
} }
repository, err := j.registryClient.Get(registry.Opts{ repository, err := j.registryClient.Get(registry.Opts{
Registry: reg, Registry: reg,
Name: j.details.imageRef.ShortName(), Name: j.details.trackedImage.Image.ShortName(),
Tag: j.details.latest, Tag: j.details.latest,
Username: j.details.registryUsername, Username: creds.Username,
Password: j.details.registryPassword, Password: creds.Password,
}) })
if err != nil { if err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"error": err, "error": err,
"image": j.details.imageRef.Remote(), "image": j.details.trackedImage.Image.String(),
}).Error("trigger.poll.WatchRepositoryTagsJob: failed to get repository") }).Error("trigger.poll.WatchRepositoryTagsJob: failed to get repository")
return return
} }
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"current_tag": j.details.imageRef.Tag(), "current_tag": j.details.trackedImage.Image.Tag(),
"repository_tags": repository.Tags, "repository_tags": repository.Tags,
"image_name": j.details.imageRef.Remote(), "image_name": j.details.trackedImage.Image.Remote(),
}).Debug("trigger.poll.WatchRepositoryTagsJob: checking tags") }).Debug("trigger.poll.WatchRepositoryTagsJob: checking tags")
latestVersion, newAvailable, err := version.NewAvailable(j.details.latest, repository.Tags) latestVersion, newAvailable, err := version.NewAvailable(j.details.latest, repository.Tags)
@ -360,7 +339,8 @@ func (j *WatchRepositoryTagsJob) Run() {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"error": err, "error": err,
"repository_tags": repository.Tags, "repository_tags": repository.Tags,
"image": j.details.imageRef.Remote(), // "image": j.details.imageRef.Remote(),
"image": j.details.trackedImage.Image.String(),
}).Error("trigger.poll.WatchRepositoryTagsJob: failed to get latest version from tags") }).Error("trigger.poll.WatchRepositoryTagsJob: failed to get latest version from tags")
return return
} }
@ -372,13 +352,13 @@ func (j *WatchRepositoryTagsJob) Run() {
j.details.latest = latestVersion j.details.latest = latestVersion
event := types.Event{ event := types.Event{
Repository: types.Repository{ Repository: types.Repository{
Name: j.details.imageRef.Repository(), Name: j.details.trackedImage.Image.Repository(),
Tag: latestVersion, Tag: latestVersion,
}, },
TriggerName: types.TriggerTypePoll.String(), TriggerName: types.TriggerTypePoll.String(),
} }
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"repository": j.details.imageRef.Repository(), "repository": j.details.trackedImage.Image.Repository(),
"new_tag": latestVersion, "new_tag": latestVersion,
}).Info("trigger.poll.WatchRepositoryTagsJob: submiting event to providers") }).Info("trigger.poll.WatchRepositoryTagsJob: submiting event to providers")
j.providers.Submit(event) j.providers.Submit(event)

View File

@ -6,6 +6,7 @@ import (
"github.com/keel-hq/keel/approvals" "github.com/keel-hq/keel/approvals"
"github.com/keel-hq/keel/cache/memory" "github.com/keel-hq/keel/cache/memory"
"github.com/keel-hq/keel/extension/credentialshelper"
"github.com/keel-hq/keel/provider" "github.com/keel-hq/keel/provider"
"github.com/keel-hq/keel/registry" "github.com/keel-hq/keel/registry"
"github.com/keel-hq/keel/types" "github.com/keel-hq/keel/types"
@ -13,6 +14,16 @@ import (
"github.com/keel-hq/keel/util/image" "github.com/keel-hq/keel/util/image"
) )
func mustParse(img string) *types.TrackedImage {
ref, err := image.Parse(img)
if err != nil {
panic(err)
}
return &types.TrackedImage{
Image: ref,
}
}
// ======== fake registry client for testing ======= // ======== fake registry client for testing =======
type fakeRegistryClient struct { type fakeRegistryClient struct {
opts registry.Opts // opts set if anything called Digest(opts Opts) opts registry.Opts // opts set if anything called Digest(opts Opts)
@ -23,6 +34,7 @@ type fakeRegistryClient struct {
} }
func (c *fakeRegistryClient) Get(opts registry.Opts) (*registry.Repository, error) { func (c *fakeRegistryClient) Get(opts registry.Opts) (*registry.Repository, error) {
c.opts = opts
return &registry.Repository{ return &registry.Repository{
Name: opts.Name, Name: opts.Name,
Tags: c.tagsToReturn, Tags: c.tagsToReturn,
@ -30,6 +42,7 @@ func (c *fakeRegistryClient) Get(opts registry.Opts) (*registry.Repository, erro
} }
func (c *fakeRegistryClient) Digest(opts registry.Opts) (digest string, err error) { func (c *fakeRegistryClient) Digest(opts registry.Opts) (digest string, err error) {
c.opts = opts
return c.digestToReturn, nil return c.digestToReturn, nil
} }
@ -68,7 +81,9 @@ func TestWatchTagJob(t *testing.T) {
reference, _ := image.Parse("foo/bar:1.1") reference, _ := image.Parse("foo/bar:1.1")
details := &watchDetails{ details := &watchDetails{
imageRef: reference, trackedImage: &types.TrackedImage{
Image: reference,
},
digest: "sha256:123123123", digest: "sha256:123123123",
} }
@ -113,7 +128,9 @@ func TestWatchTagJobLatest(t *testing.T) {
reference, _ := image.Parse("foo/bar:latest") reference, _ := image.Parse("foo/bar:latest")
details := &watchDetails{ details := &watchDetails{
imageRef: reference, trackedImage: &types.TrackedImage{
Image: reference,
},
digest: "sha256:123123123", digest: "sha256:123123123",
} }
@ -158,7 +175,9 @@ func TestWatchAllTagsJob(t *testing.T) {
reference, _ := image.Parse("foo/bar:1.1.0") reference, _ := image.Parse("foo/bar:1.1.0")
details := &watchDetails{ details := &watchDetails{
imageRef: reference, trackedImage: &types.TrackedImage{
Image: reference,
},
} }
job := NewWatchRepositoryTagsJob(providers, frc, details) job := NewWatchRepositoryTagsJob(providers, frc, details)
@ -192,7 +211,9 @@ func TestWatchAllTagsJobCurrentLatest(t *testing.T) {
reference, _ := image.Parse("foo/bar:latest") reference, _ := image.Parse("foo/bar:latest")
details := &watchDetails{ details := &watchDetails{
imageRef: reference, trackedImage: &types.TrackedImage{
Image: reference,
},
} }
job := NewWatchRepositoryTagsJob(providers, frc, details) job := NewWatchRepositoryTagsJob(providers, frc, details)
@ -257,10 +278,10 @@ func TestWatchMultipleTags(t *testing.T) {
watcher := NewRepositoryWatcher(providers, frc) watcher := NewRepositoryWatcher(providers, frc)
watcher.Watch("gcr.io/v2-namespace/hello-world:1.1.1", "@every 10m", "", "") watcher.Watch(mustParse("gcr.io/v2-namespace/hello-world:1.1.1"), "@every 10m")
watcher.Watch("gcr.io/v2-namespace/greetings-world:1.1.1", "@every 10m", "", "") watcher.Watch(mustParse("gcr.io/v2-namespace/greetings-world:1.1.1"), "@every 10m")
watcher.Watch("gcr.io/v2-namespace/greetings-world:alpha", "@every 10m", "", "") watcher.Watch(mustParse("gcr.io/v2-namespace/greetings-world:alpha"), "@every 10m")
watcher.Watch("gcr.io/v2-namespace/greetings-world:master", "@every 10m", "", "") watcher.Watch(mustParse("gcr.io/v2-namespace/greetings-world:master"), "@every 10m")
if len(watcher.watched) != 4 { if len(watcher.watched) != 4 {
t.Errorf("expected to find watching 4 entries, found: %d", len(watcher.watched)) t.Errorf("expected to find watching 4 entries, found: %d", len(watcher.watched))
@ -287,3 +308,64 @@ func TestWatchMultipleTags(t *testing.T) {
} }
} }
} }
type fakeCredentialsHelper struct {
// set by the caller
getImageRequest *types.TrackedImage
// credentials to return
creds *types.Credentials
}
func (fch *fakeCredentialsHelper) GetCredentials(image *types.TrackedImage) (*types.Credentials, error) {
fch.getImageRequest = image
return fch.creds, nil
}
func (fch *fakeCredentialsHelper) IsEnabled() bool { return true }
func TestWatchTagJobCheckCredentials(t *testing.T) {
fakeHelper := &fakeCredentialsHelper{
creds: &types.Credentials{
Username: "user-xx",
Password: "pass-xx",
},
}
credentialshelper.RegisterCredentialsHelper("fake", fakeHelper)
defer credentialshelper.UnregisterCredentialsHelper("fake")
fp := &fakeProvider{}
mem := memory.NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
am := approvals.New(mem, codecs.DefaultSerializer())
providers := provider.New([]provider.Provider{fp}, am)
frc := &fakeRegistryClient{
digestToReturn: "sha256:0604af35299dd37ff23937d115d103532948b568a9dd8197d14c256a8ab8b0bb",
}
reference, _ := image.Parse("foo/bar:1.1")
details := &watchDetails{
trackedImage: &types.TrackedImage{
Image: reference,
},
digest: "sha256:123123123",
}
job := NewWatchTagJob(providers, frc, details)
job.Run()
// checking whether new job was submitted
if frc.opts.Password != "pass-xx" {
t.Errorf("unexpected password for registry: %s", frc.opts.Password)
}
if frc.opts.Username != "user-xx" {
t.Errorf("unexpected username for registry: %s", frc.opts.Username)
}
}