Merge branch 'develop' into feature/helm_provider
commit
a2aef71ae0
|
@ -0,0 +1,21 @@
|
|||
# Golang CircleCI 2.0 configuration file
|
||||
#
|
||||
# Check https://circleci.com/docs/2.0/language-go/ for more details
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
# specify the version
|
||||
- image: circleci/golang:1.8.3
|
||||
|
||||
#### TEMPLATE_NOTE: go expects specific checkout path representing url
|
||||
#### expecting it in the form of
|
||||
#### /go/src/github.com/circleci/go-tool
|
||||
#### /go/src/bitbucket.org/circleci/go-tool
|
||||
working_directory: /go/src/github.com/rusenask/keel
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
# specify any bash command here prefixed with `run: `
|
||||
- run: go get
|
||||
- run: make test
|
|
@ -1,7 +1,9 @@
|
|||
[](https://circleci.com/gh/rusenask/keel/tree/master)
|
||||
|
||||
# Keel - automated Kubernetes deployments for the rest of us
|
||||
|
||||
* Website [https://keel.sh](https://keel.sh)
|
||||
* Slack - [kubernetes.slack.com](kubernetes.slack.com) look for @karolis
|
||||
* Slack - [kubernetes.slack.com](https://kubernetes.slack.com) look for @karolis
|
||||
|
||||
Keel is a tool for automating [Kubernetes](https://kubernetes.io/) deployment updates. Keel is stateless, robust and lightweight.
|
||||
|
||||
|
@ -13,7 +15,7 @@ Keel provides several key features:
|
|||
|
||||
* __[DockerHub Webhooks](https://docs.docker.com/docker-hub/webhooks/) support__ - Keel accepts dockerhub style webhooks on `/v1/webhooks/dockerhub` endpoint. Impacted deployments will be identified and updated.
|
||||
|
||||
* __[Polling](https://keel.sh/user-guide/#polling-deployment-example)__ - when webhooks and pubsub aren't available - Keel can still be useful by checking Docker Registry for changed SHA digest.
|
||||
* __[Polling](https://keel.sh/user-guide/#polling-deployment-example)__ - when webhooks and pubsub aren't available - Keel can still be useful by checking Docker Registry for new tags (if current tag is semver) or same tag SHA digest change (ie: `latest`).
|
||||
|
||||
* __Notifications__ - out of the box Keel has Slack and standard webhook notifications, more info [here](https://keel.sh/user-guide/#notifications)
|
||||
|
||||
|
|
|
@ -36,6 +36,11 @@ type Opts struct {
|
|||
Username, Password string // if "" - anonymous
|
||||
}
|
||||
|
||||
// LogFormatter - formatter callback passed into registry client
|
||||
func LogFormatter(format string, args ...interface{}) {
|
||||
log.Debugf(format, args...)
|
||||
}
|
||||
|
||||
// Get - get repository
|
||||
func (c *DefaultClient) Get(opts Opts) (*Repository, error) {
|
||||
|
||||
|
@ -44,6 +49,7 @@ func (c *DefaultClient) Get(opts Opts) (*Repository, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hub.Logf = LogFormatter
|
||||
|
||||
tags, err := hub.Tags(opts.Name)
|
||||
if err != nil {
|
||||
|
@ -70,6 +76,7 @@ func (c *DefaultClient) Digest(opts Opts) (digest string, err error) {
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
hub.Logf = LogFormatter
|
||||
|
||||
manifestDigest, err := hub.ManifestDigest(opts.Name, opts.Tag)
|
||||
if err != nil {
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/rusenask/keel/registry"
|
||||
"github.com/rusenask/keel/types"
|
||||
"github.com/rusenask/keel/util/image"
|
||||
"github.com/rusenask/keel/util/version"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
@ -22,6 +23,7 @@ type watchDetails struct {
|
|||
registryUsername string // "" for anonymous
|
||||
registryPassword string // "" for anonymous
|
||||
digest string // image digest
|
||||
latest string // latest tag
|
||||
schedule string
|
||||
}
|
||||
|
||||
|
@ -51,6 +53,7 @@ func NewRepositoryWatcher(providers provider.Providers, registryClient registry.
|
|||
}
|
||||
}
|
||||
|
||||
// Start - starts repository watcher
|
||||
func (w *RepositoryWatcher) Start(ctx context.Context) {
|
||||
// starting cron job
|
||||
w.cron.Start()
|
||||
|
@ -164,6 +167,7 @@ func (w *RepositoryWatcher) addJob(ref *image.Reference, registryUsername, regis
|
|||
details := &watchDetails{
|
||||
imageRef: ref,
|
||||
digest: digest, // current image digest
|
||||
latest: ref.Tag(),
|
||||
registryUsername: registryUsername,
|
||||
registryPassword: registryPassword,
|
||||
schedule: schedule,
|
||||
|
@ -172,14 +176,30 @@ func (w *RepositoryWatcher) addJob(ref *image.Reference, registryUsername, regis
|
|||
// adding job to internal map
|
||||
w.watched[key] = details
|
||||
|
||||
// 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
|
||||
// checks digest
|
||||
_, err = version.GetVersion(ref.Tag())
|
||||
if err != nil {
|
||||
// adding new job
|
||||
job := NewWatchTagJob(w.providers, w.registryClient, details)
|
||||
log.WithFields(log.Fields{
|
||||
"job_name": key,
|
||||
"image": ref.Remote(),
|
||||
"digest": digest,
|
||||
"schedule": schedule,
|
||||
}).Info("trigger.poll.RepositoryWatcher: new watch tag digest job added")
|
||||
return w.cron.AddJob(key, schedule, job)
|
||||
}
|
||||
|
||||
// adding new job
|
||||
job := NewWatchTagJob(w.providers, w.registryClient, details)
|
||||
job := NewWatchRepositoryTagsJob(w.providers, w.registryClient, details)
|
||||
log.WithFields(log.Fields{
|
||||
"job_name": key,
|
||||
"image": ref.Remote(),
|
||||
"digest": digest,
|
||||
"schedule": schedule,
|
||||
}).Info("trigger.poll.RepositoryWatcher: new job added")
|
||||
}).Info("trigger.poll.RepositoryWatcher: new watch repository tags job added")
|
||||
return w.cron.AddJob(key, schedule, job)
|
||||
|
||||
}
|
||||
|
@ -222,7 +242,7 @@ func (j *WatchTagJob) Run() {
|
|||
"current_digest": j.details.digest,
|
||||
"new_digest": currentDigest,
|
||||
"image_name": j.details.imageRef.Remote(),
|
||||
}).Info("trigger.poll.WatchTagJob: checking digest")
|
||||
}).Debug("trigger.poll.WatchTagJob: checking digest")
|
||||
|
||||
// checking whether image digest has changed
|
||||
if j.details.digest != currentDigest {
|
||||
|
@ -231,15 +251,92 @@ func (j *WatchTagJob) Run() {
|
|||
|
||||
event := types.Event{
|
||||
Repository: types.Repository{
|
||||
Name: j.details.imageRef.Remote(),
|
||||
Name: j.details.imageRef.Repository(),
|
||||
Tag: j.details.imageRef.Tag(),
|
||||
Digest: currentDigest,
|
||||
},
|
||||
TriggerName: types.TriggerTypePoll.String(),
|
||||
}
|
||||
log.Info("trigger.poll.WatchTagJob: digest change detected, submiting event to providers")
|
||||
log.WithFields(log.Fields{
|
||||
"repository": j.details.imageRef.Repository(),
|
||||
"new_digest": currentDigest,
|
||||
}).Info("trigger.poll.WatchTagJob: digest change detected, submiting event to providers")
|
||||
|
||||
j.providers.Submit(event)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// WatchRepositoryTagsJob - watch all tags
|
||||
type WatchRepositoryTagsJob struct {
|
||||
providers provider.Providers
|
||||
registryClient registry.Client
|
||||
details *watchDetails
|
||||
}
|
||||
|
||||
// NewWatchRepositoryTagsJob - new tags watcher job
|
||||
func NewWatchRepositoryTagsJob(providers provider.Providers, registryClient registry.Client, details *watchDetails) *WatchRepositoryTagsJob {
|
||||
return &WatchRepositoryTagsJob{
|
||||
providers: providers,
|
||||
registryClient: registryClient,
|
||||
details: details,
|
||||
}
|
||||
}
|
||||
|
||||
// Run - main function to check schedule
|
||||
func (j *WatchRepositoryTagsJob) Run() {
|
||||
reg := j.details.imageRef.Scheme() + "://" + j.details.imageRef.Registry()
|
||||
|
||||
if j.details.latest == "" {
|
||||
j.details.latest = j.details.imageRef.Tag()
|
||||
}
|
||||
|
||||
repository, err := j.registryClient.Get(registry.Opts{
|
||||
Registry: reg,
|
||||
Name: j.details.imageRef.ShortName(),
|
||||
Tag: j.details.latest,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"image": j.details.imageRef.Remote(),
|
||||
}).Error("trigger.poll.WatchRepositoryTagsJob: failed to get repository")
|
||||
return
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"current_tag": j.details.imageRef.Tag(),
|
||||
"repository_tags": repository.Tags,
|
||||
"image_name": j.details.imageRef.Remote(),
|
||||
}).Debug("trigger.poll.WatchRepositoryTagsJob: checking tags")
|
||||
|
||||
latestVersion, newAvailable, err := version.NewAvailable(j.details.latest, repository.Tags)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"repository_tags": repository.Tags,
|
||||
"image": j.details.imageRef.Remote(),
|
||||
}).Error("trigger.poll.WatchRepositoryTagsJob: failed to get latest version from tags")
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("new tag '%s' available", latestVersion)
|
||||
|
||||
if newAvailable {
|
||||
// updating current latest
|
||||
j.details.latest = latestVersion
|
||||
event := types.Event{
|
||||
Repository: types.Repository{
|
||||
Name: j.details.imageRef.Repository(),
|
||||
Tag: latestVersion,
|
||||
},
|
||||
TriggerName: types.TriggerTypePoll.String(),
|
||||
}
|
||||
log.WithFields(log.Fields{
|
||||
"repository": j.details.imageRef.Repository(),
|
||||
"new_tag": latestVersion,
|
||||
}).Info("trigger.poll.WatchRepositoryTagsJob: submiting event to providers")
|
||||
j.providers.Submit(event)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,10 +14,15 @@ type fakeRegistryClient struct {
|
|||
opts registry.Opts // opts set if anything called Digest(opts Opts)
|
||||
|
||||
digestToReturn string
|
||||
|
||||
tagsToReturn []string
|
||||
}
|
||||
|
||||
func (c *fakeRegistryClient) Get(opts registry.Opts) (*registry.Repository, error) {
|
||||
return nil, nil
|
||||
return ®istry.Repository{
|
||||
Name: opts.Name,
|
||||
Tags: c.tagsToReturn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *fakeRegistryClient) Digest(opts registry.Opts) (digest string, err error) {
|
||||
|
@ -62,7 +67,7 @@ func TestWatchTagJob(t *testing.T) {
|
|||
|
||||
submitted := fp.submitted[0]
|
||||
|
||||
if submitted.Repository.Name != "index.docker.io/foo/bar:1.1" {
|
||||
if submitted.Repository.Name != "index.docker.io/foo/bar" {
|
||||
t.Errorf("unexpected event repository name: %s", submitted.Repository.Name)
|
||||
}
|
||||
|
||||
|
@ -105,7 +110,7 @@ func TestWatchTagJobLatest(t *testing.T) {
|
|||
|
||||
submitted := fp.submitted[0]
|
||||
|
||||
if submitted.Repository.Name != "index.docker.io/foo/bar:latest" {
|
||||
if submitted.Repository.Name != "index.docker.io/foo/bar" {
|
||||
t.Errorf("unexpected event repository name: %s", submitted.Repository.Name)
|
||||
}
|
||||
|
||||
|
@ -123,3 +128,62 @@ func TestWatchTagJobLatest(t *testing.T) {
|
|||
t.Errorf("job details digest wasn't updated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchAllTagsJob(t *testing.T) {
|
||||
|
||||
fp := &fakeProvider{}
|
||||
providers := provider.New([]provider.Provider{fp})
|
||||
|
||||
frc := &fakeRegistryClient{
|
||||
tagsToReturn: []string{"1.1.2", "1.1.3", "0.9.1"},
|
||||
}
|
||||
|
||||
reference, _ := image.Parse("foo/bar:1.1.0")
|
||||
|
||||
details := &watchDetails{
|
||||
imageRef: reference,
|
||||
}
|
||||
|
||||
job := NewWatchRepositoryTagsJob(providers, frc, details)
|
||||
|
||||
job.Run()
|
||||
|
||||
// checking whether new job was submitted
|
||||
|
||||
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.1.3" {
|
||||
t.Errorf("expected event repository tag 1.1.3, but got: %s", submitted.Repository.Tag)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchAllTagsJobCurrentLatest(t *testing.T) {
|
||||
|
||||
fp := &fakeProvider{}
|
||||
providers := provider.New([]provider.Provider{fp})
|
||||
|
||||
frc := &fakeRegistryClient{
|
||||
tagsToReturn: []string{"1.1.2", "1.1.3", "0.9.1"},
|
||||
}
|
||||
|
||||
reference, _ := image.Parse("foo/bar:latest")
|
||||
|
||||
details := &watchDetails{
|
||||
imageRef: reference,
|
||||
}
|
||||
|
||||
job := NewWatchRepositoryTagsJob(providers, frc, details)
|
||||
|
||||
job.Run()
|
||||
|
||||
// checking whether new job was submitted
|
||||
|
||||
if len(fp.submitted) != 0 {
|
||||
t.Errorf("expected 0 submitted events but got something: %s", fp.submitted[0].Repository)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,20 +3,30 @@ package version
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver"
|
||||
"github.com/rusenask/keel/types"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ErrVersionTagMissing - tag missing error
|
||||
var ErrVersionTagMissing = errors.New("version tag is missing")
|
||||
|
||||
// ErrInvalidSemVer is returned a version is found to be invalid when
|
||||
// being parsed.
|
||||
var ErrInvalidSemVer = errors.New("invalid semantic version")
|
||||
|
||||
// GetVersion - parse version
|
||||
func GetVersion(version string) (*types.Version, error) {
|
||||
|
||||
v, err := semver.NewVersion(version)
|
||||
if err != nil {
|
||||
if err == semver.ErrInvalidSemVer {
|
||||
return nil, ErrInvalidSemVer
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
// TODO: probably make it customazible
|
||||
|
@ -60,6 +70,47 @@ func GetImageNameAndVersion(name string) (string, *types.Version, error) {
|
|||
return "", nil, ErrVersionTagMissing
|
||||
}
|
||||
|
||||
// NewAvailable - takes version and current tags. Checks whether there is a new version in the list of tags
|
||||
// and returns it as well as newAvailable bool
|
||||
func NewAvailable(current string, tags []string) (newVersion string, newAvailable bool, err error) {
|
||||
|
||||
currentVersion, err := semver.NewVersion(current)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
if len(tags) == 0 {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
var vs []*semver.Version
|
||||
for _, r := range tags {
|
||||
v, err := semver.NewVersion(r)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"tag": r,
|
||||
}).Debug("failed to parse tag")
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
vs = append(vs, v)
|
||||
}
|
||||
|
||||
if len(vs) == 0 {
|
||||
log.Debug("no versions available")
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
sort.Sort(sort.Reverse(semver.Collection(vs)))
|
||||
|
||||
if currentVersion.LessThan(vs[0]) {
|
||||
return vs[0].String(), true, nil
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
// ShouldUpdate - checks whether update is needed
|
||||
func ShouldUpdate(current *types.Version, new *types.Version, policy types.PolicyType) (bool, error) {
|
||||
if policy == types.PolicyTypeForce {
|
||||
|
|
|
@ -176,3 +176,82 @@ func TestShouldUpdate(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAvailable(t *testing.T) {
|
||||
type args struct {
|
||||
current string
|
||||
tags []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNewVersion string
|
||||
wantNewAvailable bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "new semver",
|
||||
args: args{current: "1.1.1", tags: []string{"1.1.1", "1.1.2"}},
|
||||
wantNewVersion: "1.1.2",
|
||||
wantNewAvailable: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "no new semver",
|
||||
args: args{current: "1.1.1", tags: []string{"1.1.0", "1.1.1"}},
|
||||
wantNewVersion: "",
|
||||
wantNewAvailable: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "no semvers in tag list",
|
||||
args: args{current: "1.1.1", tags: []string{"latest", "alpha"}},
|
||||
wantNewVersion: "",
|
||||
wantNewAvailable: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "mixed tag list",
|
||||
args: args{current: "1.1.1", tags: []string{"latest", "alpha", "1.1.2"}},
|
||||
wantNewVersion: "1.1.2",
|
||||
wantNewAvailable: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "mixed tag list",
|
||||
args: args{current: "1.1.1", tags: []string{"1.1.0", "alpha", "1.1.2", "latest"}},
|
||||
wantNewVersion: "1.1.2",
|
||||
wantNewAvailable: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty tags list",
|
||||
args: args{current: "1.1.1", tags: []string{}},
|
||||
wantNewVersion: "",
|
||||
wantNewAvailable: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "not semver current tag",
|
||||
args: args{current: "latest", tags: []string{"1.1.1"}},
|
||||
wantNewVersion: "",
|
||||
wantNewAvailable: false,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotNewVersion, gotNewAvailable, err := NewAvailable(tt.args.current, tt.args.tags)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NewAvailable() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotNewVersion != tt.wantNewVersion {
|
||||
t.Errorf("NewAvailable() gotNewVersion = %v, want %v", gotNewVersion, tt.wantNewVersion)
|
||||
}
|
||||
if gotNewAvailable != tt.wantNewAvailable {
|
||||
t.Errorf("NewAvailable() gotNewAvailable = %v, want %v", gotNewAvailable, tt.wantNewAvailable)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue