notifications, testing
parent
72d7fc6092
commit
76bc42c8af
|
@ -2,13 +2,16 @@ package helm
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rusenask/keel/types"
|
||||
"github.com/rusenask/keel/util/image"
|
||||
"github.com/rusenask/keel/util/version"
|
||||
|
||||
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"
|
||||
"github.com/ghodss/yaml"
|
||||
|
@ -17,15 +20,15 @@ import (
|
|||
"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
|
||||
const ProviderName = "helm"
|
||||
|
||||
// keel paths
|
||||
const (
|
||||
policyPath = "keel.policy"
|
||||
imagesPath = "keel.images"
|
||||
)
|
||||
|
||||
// UpdatePlan - release update plan
|
||||
type UpdatePlan struct {
|
||||
Namespace string
|
||||
|
@ -43,6 +46,7 @@ type UpdatePlan struct {
|
|||
// policy: all
|
||||
// # trigger type, defaults to events such as pubsub, webhooks
|
||||
// trigger: poll
|
||||
// pollSchedule: "@every 2m"
|
||||
// # images to track and update
|
||||
// images:
|
||||
// - repository: image.repository
|
||||
|
@ -55,9 +59,10 @@ type Root struct {
|
|||
|
||||
// KeelChartConfig - keel related configuration taken from values.yaml
|
||||
type KeelChartConfig struct {
|
||||
Policy types.PolicyType `json:"policy"`
|
||||
Trigger string `json:"trigger"`
|
||||
Images []ImageDetails `json:"images"`
|
||||
Policy types.PolicyType `json:"policy"`
|
||||
Trigger types.TriggerType `json:"trigger"`
|
||||
PollSchedule string `json:"pollSchedule"`
|
||||
Images []ImageDetails `json:"images"`
|
||||
}
|
||||
|
||||
// ImageDetails - image details
|
||||
|
@ -70,18 +75,23 @@ type ImageDetails struct {
|
|||
type Provider struct {
|
||||
implementer Implementer
|
||||
|
||||
sender notification.Sender
|
||||
|
||||
events chan *types.Event
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func NewProvider(implementer Implementer) *Provider {
|
||||
// NewProvider - create new Helm provider
|
||||
func NewProvider(implementer Implementer, sender notification.Sender) *Provider {
|
||||
return &Provider{
|
||||
implementer: implementer,
|
||||
sender: sender,
|
||||
events: make(chan *types.Event, 100),
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// GetName - get provider name
|
||||
func (p *Provider) GetName() string {
|
||||
return ProviderName
|
||||
}
|
||||
|
@ -102,8 +112,9 @@ func (p *Provider) Stop() {
|
|||
close(p.stop)
|
||||
}
|
||||
|
||||
func (p *Provider) Releases() ([]*types.HelmRelease, error) {
|
||||
releases := []*types.HelmRelease{}
|
||||
// TrackedImages - returns tracked images from all releases that have keel configuration
|
||||
func (p *Provider) TrackedImages() ([]*types.TrackedImage, error) {
|
||||
var trackedImages []*types.TrackedImage
|
||||
|
||||
releaseList, err := p.implementer.ListReleases()
|
||||
if err != nil {
|
||||
|
@ -111,10 +122,53 @@ func (p *Provider) Releases() ([]*types.HelmRelease, error) {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -162,6 +216,22 @@ func (p *Provider) createUpdatePlans(event *types.Event) ([]*UpdatePlan, error)
|
|||
|
||||
newVersion, err := version.GetVersion(event.Repository.Tag)
|
||||
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{
|
||||
"error": err,
|
||||
}).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 {
|
||||
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)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
|
@ -194,8 +273,25 @@ func (p *Provider) applyPlans(plans []*UpdatePlan) error {
|
|||
"name": plan.Name,
|
||||
"namespace": plan.Namespace,
|
||||
}).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
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -4,13 +4,57 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
|
||||
"github.com/rusenask/keel/extension/notification"
|
||||
"github.com/rusenask/keel/types"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
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
|
||||
func testingConfigYaml(cfg *KeelChartConfig) (vals chartutil.Values, err error) {
|
||||
root := &Root{Keel: *cfg}
|
||||
|
@ -22,30 +66,41 @@ func testingConfigYaml(cfg *KeelChartConfig) (vals chartutil.Values, err error)
|
|||
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) {
|
||||
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 {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
@ -59,10 +114,6 @@ func TestGetChartPolicy(t *testing.T) {
|
|||
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)
|
||||
if err != nil {
|
||||
t.Errorf("failed to get image paths: %s", err)
|
||||
|
@ -79,9 +130,8 @@ func TestGetChartPolicy(t *testing.T) {
|
|||
t.Errorf("policy not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTriggerFromConfig(t *testing.T) {
|
||||
vals, err := testingConfigYaml(&KeelChartConfig{Trigger: "poll"})
|
||||
vals, err := testingConfigYaml(&KeelChartConfig{Trigger: types.TriggerTypePoll})
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
if cfg.Trigger != "poll" {
|
||||
if cfg.Trigger != types.TriggerTypePoll {
|
||||
t.Errorf("invalid trigger: %s", cfg.Trigger)
|
||||
}
|
||||
}
|
||||
|
@ -112,27 +162,117 @@ func TestGetPolicyFromConfig(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// func TestUpdateRelease(t *testing.T) {
|
||||
// imp := NewHelmImplementer("192.168.99.100:30083")
|
||||
func TestGetImagesFromConfig(t *testing.T) {
|
||||
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()
|
||||
// if err != nil {
|
||||
// t.Fatalf("unexpected error: %s", err)
|
||||
// }
|
||||
cfg, err := getKeelConfig(vals)
|
||||
if err != nil {
|
||||
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 err != nil {
|
||||
// t.Errorf("failed to parse image, error: %s", err)
|
||||
// }
|
||||
if cfg.Images[0].TagPath != "tagpath" {
|
||||
t.Errorf("invalid tag path: %s", cfg.Images[0].TagPath)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// t.Errorf("failed to update release, error: %s", err)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
keel:
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue