Merge branch 'develop' into feature/helm_provider

pull/39/head
Karolis Rusenas 2017-07-18 16:54:14 +01:00
commit a2aef71ae0
7 changed files with 331 additions and 10 deletions

21
.circleci/config.yml Normal file
View File

@ -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

View File

@ -1,7 +1,9 @@
[![CircleCI](https://circleci.com/gh/rusenask/keel/tree/master.svg?style=shield&circle-token=0239846a42cfa188de531058b9a2116a4b8600d8)](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)

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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 &registry.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)
}
}

View File

@ -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 {

View File

@ -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)
}
})
}
}