notifications, testing

pull/39/head
Karolis Rusenas 2017-07-20 18:56:03 +01:00
parent 72d7fc6092
commit 76bc42c8af
2 changed files with 298 additions and 62 deletions

View File

@ -2,13 +2,16 @@ package helm
import ( import (
"fmt" "fmt"
"strings"
"time"
"github.com/rusenask/keel/types" "github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/image" "github.com/rusenask/keel/util/image"
"github.com/rusenask/keel/util/version" "github.com/rusenask/keel/util/version"
hapi_chart "k8s.io/helm/pkg/proto/hapi/chart" hapi_chart "k8s.io/helm/pkg/proto/hapi/chart"
// rls "k8s.io/helm/pkg/proto/hapi/services"
"github.com/rusenask/keel/extension/notification"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
@ -17,15 +20,15 @@ import (
"k8s.io/helm/pkg/strvals" "k8s.io/helm/pkg/strvals"
) )
// Manager - high level interface into helm provider related data used by
// triggers
type Manager interface {
Images() ([]*image.Reference, error)
}
// ProviderName - helm provider name // ProviderName - helm provider name
const ProviderName = "helm" const ProviderName = "helm"
// keel paths
const (
policyPath = "keel.policy"
imagesPath = "keel.images"
)
// UpdatePlan - release update plan // UpdatePlan - release update plan
type UpdatePlan struct { type UpdatePlan struct {
Namespace string Namespace string
@ -43,6 +46,7 @@ type UpdatePlan struct {
// policy: all // policy: all
// # trigger type, defaults to events such as pubsub, webhooks // # trigger type, defaults to events such as pubsub, webhooks
// trigger: poll // trigger: poll
// pollSchedule: "@every 2m"
// # images to track and update // # images to track and update
// images: // images:
// - repository: image.repository // - repository: image.repository
@ -55,9 +59,10 @@ type Root struct {
// KeelChartConfig - keel related configuration taken from values.yaml // KeelChartConfig - keel related configuration taken from values.yaml
type KeelChartConfig struct { type KeelChartConfig struct {
Policy types.PolicyType `json:"policy"` Policy types.PolicyType `json:"policy"`
Trigger string `json:"trigger"` Trigger types.TriggerType `json:"trigger"`
Images []ImageDetails `json:"images"` PollSchedule string `json:"pollSchedule"`
Images []ImageDetails `json:"images"`
} }
// ImageDetails - image details // ImageDetails - image details
@ -70,18 +75,23 @@ type ImageDetails struct {
type Provider struct { type Provider struct {
implementer Implementer implementer Implementer
sender notification.Sender
events chan *types.Event events chan *types.Event
stop chan struct{} stop chan struct{}
} }
func NewProvider(implementer Implementer) *Provider { // NewProvider - create new Helm provider
func NewProvider(implementer Implementer, sender notification.Sender) *Provider {
return &Provider{ return &Provider{
implementer: implementer, implementer: implementer,
sender: sender,
events: make(chan *types.Event, 100), events: make(chan *types.Event, 100),
stop: make(chan struct{}), stop: make(chan struct{}),
} }
} }
// GetName - get provider name
func (p *Provider) GetName() string { func (p *Provider) GetName() string {
return ProviderName return ProviderName
} }
@ -102,8 +112,9 @@ func (p *Provider) Stop() {
close(p.stop) close(p.stop)
} }
func (p *Provider) Releases() ([]*types.HelmRelease, error) { // TrackedImages - returns tracked images from all releases that have keel configuration
releases := []*types.HelmRelease{} func (p *Provider) TrackedImages() ([]*types.TrackedImage, error) {
var trackedImages []*types.TrackedImage
releaseList, err := p.implementer.ListReleases() releaseList, err := p.implementer.ListReleases()
if err != nil { if err != nil {
@ -111,10 +122,53 @@ func (p *Provider) Releases() ([]*types.HelmRelease, error) {
} }
for _, release := range releaseList.Releases { for _, release := range releaseList.Releases {
// getting configuration
vals, err := values(release.Chart, release.Config)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"release": release.Name,
"namespace": release.Namespace,
}).Error("provider.helm: failed to get values.yaml for release")
continue
}
cfg, err := getKeelConfig(vals)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"release": release.Name,
"namespace": release.Namespace,
}).Error("provider.helm: failed to get config for release")
continue
}
if cfg.PollSchedule == "" {
cfg.PollSchedule = types.KeelPollDefaultSchedule
}
releaseImages, err := getImages(vals)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"release": release.Name,
"namespace": release.Namespace,
}).Error("provider.helm: failed to get images for release")
continue
}
for _, img := range releaseImages {
trackedImages = append(trackedImages, &types.TrackedImage{
Image: img,
PollSchedule: cfg.PollSchedule,
Trigger: cfg.Trigger,
Provider: ProviderName,
})
}
} }
return releases, nil return trackedImages, nil
} }
func (p *Provider) startInternal() error { func (p *Provider) startInternal() error {
@ -162,6 +216,22 @@ func (p *Provider) createUpdatePlans(event *types.Event) ([]*UpdatePlan, error)
newVersion, err := version.GetVersion(event.Repository.Tag) newVersion, err := version.GetVersion(event.Repository.Tag)
if err != nil { if err != nil {
plan, update, errCheck := checkUnversionedRelease(&event.Repository, release.Namespace, release.Name, release.Chart, release.Config)
if errCheck != nil {
log.WithFields(log.Fields{
"error": err,
"deployment": release.Name,
"namespace": release.Namespace,
}).Error("provider.kubernetes: got error while checking unversioned release")
continue
}
if update {
plans = append(plans, plan)
continue
}
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"error": err, "error": err,
}).Error("provider.helm: failed to parse version") }).Error("provider.helm: failed to parse version")
@ -187,6 +257,15 @@ func (p *Provider) createUpdatePlans(event *types.Event) ([]*UpdatePlan, error)
func (p *Provider) applyPlans(plans []*UpdatePlan) error { func (p *Provider) applyPlans(plans []*UpdatePlan) error {
for _, plan := range plans { for _, plan := range plans {
p.sender.Send(types.EventNotification{
Name: "update release",
Message: fmt.Sprintf("Preparing to update release %s/%s (%s)", plan.Namespace, plan.Name, strings.Join(mapToSlice(plan.Values), ", ")),
CreatedAt: time.Now(),
Type: types.NotificationPreReleaseUpdate,
Level: types.LevelDebug,
})
err := updateHelmRelease(p.implementer, plan.Name, plan.Chart, plan.Values) err := updateHelmRelease(p.implementer, plan.Name, plan.Chart, plan.Values)
if err != nil { if err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
@ -194,8 +273,25 @@ func (p *Provider) applyPlans(plans []*UpdatePlan) error {
"name": plan.Name, "name": plan.Name,
"namespace": plan.Namespace, "namespace": plan.Namespace,
}).Error("provider.helm: failed to apply plan") }).Error("provider.helm: failed to apply plan")
p.sender.Send(types.EventNotification{
Name: "update release",
Message: fmt.Sprintf("Release update feailed %s/%s (%s), error: %s", plan.Namespace, plan.Name, strings.Join(mapToSlice(plan.Values), ", "), err),
CreatedAt: time.Now(),
Type: types.NotificationReleaseUpdate,
Level: types.LevelError,
})
continue continue
} }
p.sender.Send(types.EventNotification{
Name: "update release",
Message: fmt.Sprintf("Successfully updated release %s/%s (%s)", plan.Namespace, plan.Name, strings.Join(mapToSlice(plan.Values), ", ")),
CreatedAt: time.Now(),
Type: types.NotificationReleaseUpdate,
Level: types.LevelSuccess,
})
} }
return nil return nil

View File

@ -4,13 +4,57 @@ import (
"fmt" "fmt"
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
"github.com/rusenask/keel/extension/notification"
"github.com/rusenask/keel/types" "github.com/rusenask/keel/types"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/proto/hapi/chart"
hapi_release5 "k8s.io/helm/pkg/proto/hapi/release"
rls "k8s.io/helm/pkg/proto/hapi/services"
"testing" "testing"
) )
type fakeSender struct {
sentEvent types.EventNotification
}
func (s *fakeSender) Configure(cfg *notification.Config) (bool, error) {
return true, nil
}
func (s *fakeSender) Send(event types.EventNotification) error {
s.sentEvent = event
return nil
}
type fakeImplementer struct {
listReleasesResponse *rls.ListReleasesResponse
// updated info
updatedRlsName string
updatedChart *chart.Chart
updatedOptions []helm.UpdateOption
}
func (i *fakeImplementer) ListReleases(opts ...helm.ReleaseListOption) (*rls.ListReleasesResponse, error) {
return i.listReleasesResponse, nil
}
func (i *fakeImplementer) UpdateReleaseFromChart(rlsName string, chart *chart.Chart, opts ...helm.UpdateOption) (*rls.UpdateReleaseResponse, error) {
i.updatedRlsName = rlsName
i.updatedChart = chart
i.updatedOptions = opts
return &rls.UpdateReleaseResponse{
Release: &hapi_release5.Release{
Version: 2,
},
}, nil
}
// helper function to generate keel configuration // helper function to generate keel configuration
func testingConfigYaml(cfg *KeelChartConfig) (vals chartutil.Values, err error) { func testingConfigYaml(cfg *KeelChartConfig) (vals chartutil.Values, err error) {
root := &Root{Keel: *cfg} root := &Root{Keel: *cfg}
@ -22,30 +66,41 @@ func testingConfigYaml(cfg *KeelChartConfig) (vals chartutil.Values, err error)
return chartutil.ReadValues(bts) return chartutil.ReadValues(bts)
} }
func TestParseImage(t *testing.T) {
imp := NewHelmImplementer("192.168.99.100:30083")
releases, err := imp.ListReleases()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
fmt.Println(releases.Count)
for _, release := range releases.Releases {
ref, err := parseImage(release.Chart, release.Config)
if err != nil {
t.Errorf("failed to parse image, error: %s", err)
}
fmt.Println(ref.Remote())
}
}
func TestGetChartPolicy(t *testing.T) { func TestGetChartPolicy(t *testing.T) {
imp := NewHelmImplementer("192.168.99.100:30083")
releases, err := imp.ListReleases() chartVals := `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world
tag: 1.1.0
keel:
policy: all
trigger: poll
images:
- repository: image.repository
tag: image.tag
`
fakeImpl := &fakeImplementer{
listReleasesResponse: &rls.ListReleasesResponse{
Releases: []*hapi_release5.Release{
&hapi_release5.Release{
Name: "release-1",
Chart: &chart.Chart{
Values: &chart.Config{Raw: chartVals},
},
Config: &chart.Config{Raw: ""},
},
},
},
}
releases, err := fakeImpl.ListReleases()
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
@ -59,10 +114,6 @@ func TestGetChartPolicy(t *testing.T) {
t.Fatalf("failed to get values: %s", err) t.Fatalf("failed to get values: %s", err)
} }
// policy, err := getChartPolicy(vals)
// if err != nil {
// t.Errorf("failed to parse image, error: %s", err)
// }
cfg, err := getKeelConfig(vals) cfg, err := getKeelConfig(vals)
if err != nil { if err != nil {
t.Errorf("failed to get image paths: %s", err) t.Errorf("failed to get image paths: %s", err)
@ -79,9 +130,8 @@ func TestGetChartPolicy(t *testing.T) {
t.Errorf("policy not found") t.Errorf("policy not found")
} }
} }
func TestGetTriggerFromConfig(t *testing.T) { func TestGetTriggerFromConfig(t *testing.T) {
vals, err := testingConfigYaml(&KeelChartConfig{Trigger: "poll"}) vals, err := testingConfigYaml(&KeelChartConfig{Trigger: types.TriggerTypePoll})
if err != nil { if err != nil {
t.Fatalf("Failed to load testdata: %s", err) t.Fatalf("Failed to load testdata: %s", err)
} }
@ -91,7 +141,7 @@ func TestGetTriggerFromConfig(t *testing.T) {
t.Errorf("failed to get image paths: %s", err) t.Errorf("failed to get image paths: %s", err)
} }
if cfg.Trigger != "poll" { if cfg.Trigger != types.TriggerTypePoll {
t.Errorf("invalid trigger: %s", cfg.Trigger) t.Errorf("invalid trigger: %s", cfg.Trigger)
} }
} }
@ -112,27 +162,117 @@ func TestGetPolicyFromConfig(t *testing.T) {
} }
} }
// func TestUpdateRelease(t *testing.T) { func TestGetImagesFromConfig(t *testing.T) {
// imp := NewHelmImplementer("192.168.99.100:30083") vals, err := testingConfigYaml(&KeelChartConfig{Policy: types.PolicyTypeAll, Images: []ImageDetails{
ImageDetails{
RepositoryPath: "repopath",
TagPath: "tagpath",
},
}})
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
// releases, err := imp.ListReleases() cfg, err := getKeelConfig(vals)
// if err != nil { if err != nil {
// t.Fatalf("unexpected error: %s", err) t.Errorf("failed to get image paths: %s", err)
// } }
// for _, release := range releases.Releases { if cfg.Images[0].RepositoryPath != "repopath" {
t.Errorf("invalid repo path: %s", cfg.Images[0].RepositoryPath)
}
// ref, err := parseImage(release.Chart, release.Config) if cfg.Images[0].TagPath != "tagpath" {
// if err != nil { t.Errorf("invalid tag path: %s", cfg.Images[0].TagPath)
// t.Errorf("failed to parse image, error: %s", err) }
// } }
// fmt.Println(ref.Remote()) func TestUpdateRelease(t *testing.T) {
// imp := NewHelmImplementer("192.168.99.100:30083")
// err = updateHelmRelease(imp, release.Name, release.Chart, "image.tag=0.0.11") chartVals := `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: karolisr/webhook-demo
tag: 0.0.10
// if err != nil { keel:
// t.Errorf("failed to update release, error: %s", err) policy: all
// } trigger: poll
// } images:
// } - repository: image.repository
tag: image.tag
`
myChart := &chart.Chart{
Values: &chart.Config{Raw: chartVals},
}
fakeImpl := &fakeImplementer{
listReleasesResponse: &rls.ListReleasesResponse{
Releases: []*hapi_release5.Release{
&hapi_release5.Release{
Name: "release-1",
Chart: myChart,
Config: &chart.Config{Raw: ""},
},
},
},
}
provider := NewProvider(fakeImpl, &fakeSender{})
err := provider.processEvent(&types.Event{
Repository: types.Repository{
Name: "karolisr/webhook-demo",
Tag: "0.0.11",
},
})
if err != nil {
t.Errorf("failed to process event, error: %s", err)
}
// checking updated release
if fakeImpl.updatedChart != myChart {
t.Errorf("wrong chart updated")
}
if fakeImpl.updatedRlsName != "release-1" {
t.Errorf("unexpected release updated: %s", fakeImpl.updatedRlsName)
}
}
var pollingValues = `
name: al Rashid
where:
city: Basrah
title: caliph
image:
repository: gcr.io/v2-namespace/hello-world
tag: 1.1.0
keel:
policy: all
trigger: poll
pollSchedule: "@every 12m"
images:
- repository: image.repository
tag: image.tag
`
func TestGetPollingSchedule(t *testing.T) {
vals, _ := chartutil.ReadValues([]byte(pollingValues))
cfg, err := getKeelConfig(vals)
if err != nil {
t.Errorf("failed to get config: %s", err)
}
if cfg.PollSchedule != "@every 12m" {
t.Errorf("unexpected polling schedule: %s", cfg.PollSchedule)
}
}