diff --git a/.gitignore b/.gitignore index 32a9dd4d..dfd9352e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ deployment.yml .test_env.sh hack/deployment-norbac.yaml hack/deployment-rbac.yaml +hack/deployment-norbac-helm.yaml diff --git a/constants/constants.go b/constants/constants.go index bfb5b546..e26e5197 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -34,3 +34,6 @@ const ( // EnvNotificationLevel - minimum level for notifications, defaults to info const EnvNotificationLevel = "NOTIFICATION_LEVEL" + +// KeelLogoURL - is a logo URL for bot icon +const KeelLogoURL = "https://keel.sh/images/logo.png" diff --git a/extension/notification/hipchat/hipchat.go b/extension/notification/hipchat/hipchat.go index af117d2d..095e7fc3 100644 --- a/extension/notification/hipchat/hipchat.go +++ b/extension/notification/hipchat/hipchat.go @@ -65,7 +65,12 @@ func (s *sender) Send(event types.EventNotification) error { From: s.botName, } - for _, channel := range s.channels { + channels := s.channels + if len(event.Channels) > 0 { + channels = event.Channels + } + + for _, channel := range channels { _, err := s.hipchatClient.Room.Notification(channel, notification) if err != nil { log.WithFields(log.Fields{ diff --git a/extension/notification/mattermost/mattermost.go b/extension/notification/mattermost/mattermost.go index ad56bb31..84fac057 100644 --- a/extension/notification/mattermost/mattermost.go +++ b/extension/notification/mattermost/mattermost.go @@ -83,7 +83,6 @@ func (s *sender) Configure(config *notification.Config) (bool, error) { } type notificationEnvelope struct { - Chanel string `json:"channel"` Username string `json:"username"` IconURL string `json:"icon_url"` Text string `json:"text"` @@ -92,7 +91,7 @@ type notificationEnvelope struct { func (s *sender) Send(event types.EventNotification) error { // Marshal notification. jsonNotification, err := json.Marshal(notificationEnvelope{ - IconURL: "https://keel.sh/images/logo.png", + IconURL: constants.KeelLogoURL, Username: s.name, Text: fmt.Sprintf("#### %s \n %s", event.Type.String(), event.Message), }) diff --git a/extension/notification/slack/slack.go b/extension/notification/slack/slack.go index aa7cbd90..724a2cc1 100644 --- a/extension/notification/slack/slack.go +++ b/extension/notification/slack/slack.go @@ -62,6 +62,7 @@ func (s *sender) Configure(config *notification.Config) (bool, error) { func (s *sender) Send(event types.EventNotification) error { params := slack.NewPostMessageParameters() params.Username = s.botName + params.IconURL = constants.KeelLogoURL params.Attachments = []slack.Attachment{ slack.Attachment{ @@ -79,7 +80,12 @@ func (s *sender) Send(event types.EventNotification) error { }, } - for _, channel := range s.channels { + chans := s.channels + if len(event.Channels) > 0 { + chans = event.Channels + } + + for _, channel := range chans { _, _, err := s.slackClient.PostMessage(channel, "", params) if err != nil { log.WithFields(log.Fields{ diff --git a/provider/helm/helm.go b/provider/helm/helm.go index fbfc0830..c4acf8b0 100644 --- a/provider/helm/helm.go +++ b/provider/helm/helm.go @@ -70,12 +70,13 @@ type Root struct { // KeelChartConfig - keel related configuration taken from values.yaml type KeelChartConfig struct { - Policy types.PolicyType `json:"policy"` - Trigger types.TriggerType `json:"trigger"` - PollSchedule string `json:"pollSchedule"` - Approvals int `json:"approvals"` // Minimum required approvals - ApprovalDeadline int `json:"approvalDeadline"` // Deadline in hours - Images []ImageDetails `json:"images"` + Policy types.PolicyType `json:"policy"` + Trigger types.TriggerType `json:"trigger"` + PollSchedule string `json:"pollSchedule"` + Approvals int `json:"approvals"` // Minimum required approvals + ApprovalDeadline int `json:"approvalDeadline"` // Deadline in hours + Images []ImageDetails `json:"images"` + NotificationChannels []string `json:"notificationChannels"` // optional notification channels } // ImageDetails - image details @@ -285,6 +286,7 @@ func (p *Provider) applyPlans(plans []*UpdatePlan) error { CreatedAt: time.Now(), Type: types.NotificationPreReleaseUpdate, Level: types.LevelDebug, + Channels: plan.Config.NotificationChannels, }) err := updateHelmRelease(p.implementer, plan.Name, plan.Chart, plan.Values) @@ -301,6 +303,7 @@ func (p *Provider) applyPlans(plans []*UpdatePlan) error { CreatedAt: time.Now(), Type: types.NotificationReleaseUpdate, Level: types.LevelError, + Channels: plan.Config.NotificationChannels, }) continue } @@ -311,6 +314,7 @@ func (p *Provider) applyPlans(plans []*UpdatePlan) error { CreatedAt: time.Now(), Type: types.NotificationReleaseUpdate, Level: types.LevelSuccess, + Channels: plan.Config.NotificationChannels, }) } diff --git a/provider/helm/helm_test.go b/provider/helm/helm_test.go index c16603d7..41f3d652 100644 --- a/provider/helm/helm_test.go +++ b/provider/helm/helm_test.go @@ -400,6 +400,27 @@ keel: ` valuesBasic, _ := chartutil.ReadValues([]byte(valuesBasicStr)) + var valuesChannelsStr = ` +name: al Rashid +where: + city: Basrah + title: caliph +image: + repository: gcr.io/v2-namespace/hello-world + tag: 1.1.0 + +keel: + policy: all + notificationChannels: + - chan1 + - chan2 + images: + - repository: image.repository + tag: image.tag + +` + valuesChannels, _ := chartutil.ReadValues([]byte(valuesChannelsStr)) + var valuesPollStr = ` name: al Rashid where: @@ -440,6 +461,18 @@ keel: }, }, }, + { + name: "custom notification channels", + args: args{vals: valuesChannels}, + want: &KeelChartConfig{ + Policy: types.PolicyTypeAll, + Trigger: types.TriggerTypeDefault, + NotificationChannels: []string{"chan1", "chan2"}, + Images: []ImageDetails{ + ImageDetails{RepositoryPath: "image.repository", TagPath: "image.tag"}, + }, + }, + }, { name: "correct polling config", args: args{vals: valuesPoll}, diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index 248b62f9..ab3077ac 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -204,6 +204,8 @@ func (p *Provider) updateDeployments(plans []*UpdatePlan) (updated []*v1beta1.De deployment := plan.Deployment + notificationChannels := types.ParseEventNotificationChannels(deployment.Annotations) + reset, delta, err := checkForReset(deployment, p.implementer) if err != nil { log.WithFields(log.Fields{ @@ -244,13 +246,13 @@ func (p *Provider) updateDeployments(plans []*UpdatePlan) (updated []*v1beta1.De } deployment = refresh - p.sender.Send(types.EventNotification{ Name: "preparing to update deployment after reset", Message: fmt.Sprintf("Preparing to update deployment %s/%s %s->%s (%s)", deployment.Namespace, deployment.Name, plan.CurrentVersion, plan.NewVersion, strings.Join(getImages(&refresh), ", ")), CreatedAt: time.Now(), Type: types.NotificationPreDeploymentUpdate, Level: types.LevelDebug, + Channels: notificationChannels, }) err = p.implementer.Update(&refresh) @@ -267,6 +269,7 @@ func (p *Provider) updateDeployments(plans []*UpdatePlan) (updated []*v1beta1.De CreatedAt: time.Now(), Type: types.NotificationDeploymentUpdate, Level: types.LevelError, + Channels: notificationChannels, }) continue } @@ -277,6 +280,7 @@ func (p *Provider) updateDeployments(plans []*UpdatePlan) (updated []*v1beta1.De CreatedAt: time.Now(), Type: types.NotificationDeploymentUpdate, Level: types.LevelSuccess, + Channels: notificationChannels, }) updated = append(updated, &refresh) @@ -291,6 +295,7 @@ func (p *Provider) updateDeployments(plans []*UpdatePlan) (updated []*v1beta1.De CreatedAt: time.Now(), Type: types.NotificationPreDeploymentUpdate, Level: types.LevelDebug, + Channels: notificationChannels, }) err = p.implementer.Update(&deployment) @@ -307,6 +312,7 @@ func (p *Provider) updateDeployments(plans []*UpdatePlan) (updated []*v1beta1.De CreatedAt: time.Now(), Type: types.NotificationDeploymentUpdate, Level: types.LevelError, + Channels: notificationChannels, }) continue @@ -318,6 +324,7 @@ func (p *Provider) updateDeployments(plans []*UpdatePlan) (updated []*v1beta1.De CreatedAt: time.Now(), Type: types.NotificationDeploymentUpdate, Level: types.LevelSuccess, + Channels: notificationChannels, }) log.WithFields(log.Fields{ diff --git a/readme.md b/readme.md index 41d8a397..6c2eb575 100644 --- a/readme.md +++ b/readme.md @@ -41,7 +41,7 @@ Keel provides several key features: * __Notifications__ - out of the box Keel has Slack and standard webhook notifications, more info [here](https://keel.sh/v1/guide/documentation.html#Notifications)
### Support diff --git a/secrets/secrets.go b/secrets/secrets.go index d0dc995a..9888d21a 100644 --- a/secrets/secrets.go +++ b/secrets/secrets.go @@ -101,7 +101,7 @@ func (g *DefaultGetter) lookupSecrets(image *types.TrackedImage) ([]string, erro "image": image.Image.Repository(), "pod_selector": selector, "pods_checked": len(podList.Items), - }).Warn("secrets.defaultGetter.lookupSecrets: no secrets for image found") + }).Debug("secrets.defaultGetter.lookupSecrets: no secrets for image found") } return secrets, nil diff --git a/types/types.go b/types/types.go index a3b39437..715e71cd 100644 --- a/types/types.go +++ b/types/types.go @@ -33,6 +33,10 @@ const KeelPollDefaultSchedule = "@every 1m" // KeelDigestAnnotation - digest annotation const KeelDigestAnnotation = "keel.sh/digest" +// KeelNotificationChanAnnotation - optional notification to override +// default notification channel(-s) per deployment/chart +const KeelNotificationChanAnnotation = "keel.sh/notify" + // KeelMinimumApprovalsLabel - min approvals const KeelMinimumApprovalsLabel = "keel.sh/approvals" @@ -174,6 +178,27 @@ type EventNotification struct { CreatedAt time.Time `json:"createdAt"` Type Notification `json:"type"` Level Level `json:"level"` + // Channels is an optional variable to override + // default channel(-s) when performing an update + Channels []string `json:"-"` +} + +// ParseEventNotificationChannels - parses deployment annotations or chart config +// to get channel overrides +func ParseEventNotificationChannels(annotations map[string]string) []string { + channels := []string{} + if annotations == nil { + return channels + } + chanStr, ok := annotations[KeelNotificationChanAnnotation] + if ok { + chans := strings.Split(chanStr, ",") + for _, c := range chans { + channels = append(channels, strings.TrimSpace(c)) + } + } + + return channels } // Notification - notification types used by notifier diff --git a/types/types_test.go b/types/types_test.go index a9ce3c5a..91abec50 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -1,6 +1,7 @@ package types import ( + "reflect" "testing" "time" ) @@ -126,3 +127,42 @@ func TestNotExpired(t *testing.T) { } } + +func TestParseEventNotificationChannels(t *testing.T) { + type args struct { + annotations map[string]string + } + tests := []struct { + name string + args args + want []string + }{ + { + name: "no chans", + args: args{map[string]string{"foo": "bar"}}, + want: []string{}, + }, + { + name: "one chan", + args: args{map[string]string{KeelNotificationChanAnnotation: "verychan"}}, + want: []string{"verychan"}, + }, + { + name: "two chans with space", + args: args{map[string]string{KeelNotificationChanAnnotation: "verychan, corp"}}, + want: []string{"verychan", "corp"}, + }, + { + name: "two chans no space", + args: args{map[string]string{KeelNotificationChanAnnotation: "verychan,corp"}}, + want: []string{"verychan", "corp"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ParseEventNotificationChannels(tt.args.annotations); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseEventNotificationChannels() = %v, want %v", got, tt.want) + } + }) + } +}