diff --git a/provider/helm/helm.go b/provider/helm/helm.go index bb52789e..fefeb65a 100644 --- a/provider/helm/helm.go +++ b/provider/helm/helm.go @@ -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 diff --git a/provider/helm/helm_test.go b/provider/helm/helm_test.go index 56b1cd46..89b715c6 100644 --- a/provider/helm/helm_test.go +++ b/provider/helm/helm_test.go @@ -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) + } +}