Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Better slack notifications #415

Merged
merged 4 commits into from Feb 8, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 6 additions & 4 deletions config.go
Expand Up @@ -22,9 +22,11 @@ type GitConfig struct {
Key string `json:"key" yaml:"key"`
}

type SlackConfig struct {
HookURL string `json:"hookURL" yaml:"hookURL"`
Username string `json:"username" yaml:"username"`
// NotifierConfig is the config used to set up a notifier.
type NotifierConfig struct {
HookURL string `json:"hookURL" yaml:"hookURL"`
Username string `json:"username" yaml:"username"`
ReleaseTemplate string `json:"releaseTemplate" yaml:"releaseTemplate"`
}

type RegistryConfig struct {
Expand All @@ -40,7 +42,7 @@ type Auth struct {

type InstanceConfig struct {
Git GitConfig `json:"git" yaml:"git"`
Slack SlackConfig `json:"slack" yaml:"slack"`
Slack NotifierConfig `json:"slack" yaml:"slack"`
Registry RegistryConfig `json:"registry" yaml:"registry"`
}

Expand Down
78 changes: 0 additions & 78 deletions history/slack.go

This file was deleted.

13 changes: 1 addition & 12 deletions instance/multi.go
@@ -1,8 +1,6 @@
package instance

import (
"net/http"

"github.com/go-kit/kit/log"
"github.com/go-kit/kit/metrics"
"github.com/pkg/errors"
Expand Down Expand Up @@ -58,15 +56,6 @@ func (m *MultitenantInstancer) Get(instanceID flux.InstanceID) (*Instance, error

// Events for this instance
eventRW := EventReadWriter{instanceID, m.History}
var eventW history.EventWriter = eventRW
if c.Settings.Slack.HookURL != "" {
eventW = history.TeeWriter(eventRW, history.NewSlackEventWriter(
http.DefaultClient,
c.Settings.Slack.HookURL,
c.Settings.Slack.Username,
`(done|failed|\(no result expected\))$`, // only catch the final message, or started msg for async releases
))
}

// Configuration for this instance
config := configurer{instanceID, m.DB}
Expand All @@ -79,7 +68,7 @@ func (m *MultitenantInstancer) Get(instanceID flux.InstanceID) (*Instance, error
instanceLogger,
m.Histogram,
eventRW,
eventW,
eventRW,
), nil
}

Expand Down
11 changes: 1 addition & 10 deletions jobs/job.go
Expand Up @@ -150,16 +150,7 @@ func (j *Job) UnmarshalJSON(data []byte) error {
}

// ReleaseJobParams are the params for a release job
type ReleaseJobParams struct {
ServiceSpecs []flux.ServiceSpec
ImageSpec flux.ImageSpec
Kind flux.ReleaseKind
Excludes []flux.ServiceID

// Backwards Compatibility, remove once no more jobs
// TODO: Remove this once there are no more jobs with ServiceSpec, only ServiceSpecs
ServiceSpec flux.ServiceSpec
}
type ReleaseJobParams flux.ReleaseSpec

This comment was marked as abuse.

This comment was marked as abuse.


// AutomatedInstanceJobParams are the params for an automated_instance job
type AutomatedInstanceJobParams struct {
Expand Down
21 changes: 21 additions & 0 deletions notifications/notifications.go
@@ -0,0 +1,21 @@
package notifications

import (
"github.com/weaveworks/flux"
"github.com/weaveworks/flux/instance"
)

// Release performs post-release notifications for an instance
func Release(cfg instance.Config, r flux.Release, releaseError error) error {
if r.Spec.Kind != flux.ReleaseKindExecute {
return nil
}

// TODO: Use a config settings format which allows multiple notifiers to be
// configured.
var err error
if cfg.Settings.Slack.HookURL != "" {
err = slackNotifyRelease(cfg.Settings.Slack, r, releaseError)
}
return err
}
71 changes: 71 additions & 0 deletions notifications/notifications_test.go
@@ -0,0 +1,71 @@
package notifications

import (
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/weaveworks/flux"
"github.com/weaveworks/flux/instance"
)

// Generate an example release
func exampleRelease(t *testing.T) flux.Release {
now := time.Now().UTC()
img1a1, _ := flux.ParseImageID("img1:a1")
img1a2, _ := flux.ParseImageID("img1:a2")
return flux.Release{
ID: flux.NewReleaseID(),
CreatedAt: now.Add(-1 * time.Minute),
StartedAt: now.Add(-30 * time.Second),
EndedAt: now.Add(-1 * time.Second),
Done: true,
Priority: 100,
Status: flux.ReleaseStatusFailed,
Log: []string{string(flux.ReleaseStatusFailed)},

Spec: flux.ReleaseSpec{
ServiceSpecs: []flux.ServiceSpec{flux.ServiceSpec("default/helloworld")},
ImageSpec: flux.ImageSpecLatest,
Kind: flux.ReleaseKindExecute,
Excludes: nil,
},
Result: flux.ReleaseResult{
flux.ServiceID("default/helloworld"): {
Status: flux.ReleaseStatusFailed,
Error: "overall-release-error",
PerContainer: []flux.ContainerResult{
{
Error: "",
ContainerUpdate: flux.ContainerUpdate{
Container: "container1",
Current: img1a1,
Target: img1a2,
},
},
},
},
},
}
}

func TestRelease_DryRun(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("Expected no http request to have been made")
}))
defer server.Close()

// It should send releases to slack
r := exampleRelease(t)
r.Spec.Kind = flux.ReleaseKindPlan
if err := Release(instance.Config{
Settings: flux.UnsafeInstanceConfig{
Slack: flux.NotifierConfig{
HookURL: server.URL,
},
},
}, r, nil); err != nil {
t.Fatal(err)
}
}
102 changes: 102 additions & 0 deletions notifications/slack.go
@@ -0,0 +1,102 @@
package notifications

import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"reflect"
"strings"
"text/template"
"time"

"github.com/pkg/errors"

"github.com/weaveworks/flux"
)

const (
defaultReleaseTemplate = `Release {{trim (print .Spec.ImageSpec) "<>"}} to {{with .Spec.ServiceSpecs}}{{range $index, $spec := .}}{{if not (eq $index 0)}}, {{if last $index $}}and {{end}}{{end}}{{trim (print .) "<>"}}{{end}}{{end}}. {{with .Error}}{{.}}. failed{{else}}done{{end}}`
)

var (
httpClient = &http.Client{Timeout: 5 * time.Second}
)

func slackNotifyRelease(config flux.NotifierConfig, release flux.Release, releaseError error) error {
if release.Spec.Kind == flux.ReleaseKindPlan {
return nil
}

template := defaultReleaseTemplate
if config.ReleaseTemplate != "" {
template = config.ReleaseTemplate
}

errorMessage := ""
if releaseError != nil {
errorMessage = releaseError.Error()
}
text, err := instantiateTemplate("release", template, struct {
flux.Release
Error string
}{
Release: release,
Error: errorMessage,
})
if err != nil {
return err
}

return notify(config, text)
}

func notify(config flux.NotifierConfig, text string) error {
buf := &bytes.Buffer{}
if err := json.NewEncoder(buf).Encode(map[string]string{
"username": config.Username,
"text": text,
}); err != nil {
return errors.Wrap(err, "encoding Slack POST request")
}

req, err := http.NewRequest("POST", config.HookURL, buf)
if err != nil {
return errors.Wrap(err, "constructing Slack HTTP request")
}
resp, err := httpClient.Do(req)
if err != nil {
return errors.Wrap(err, "executing HTTP POST to Slack")
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*1024))
return fmt.Errorf("%s from Slack (%s)", resp.Status, strings.TrimSpace(string(body)))
}

return nil
}

func instantiateTemplate(tmplName, tmplStr string, args interface{}) (string, error) {
tmpl, err := template.New(tmplName).Funcs(template.FuncMap{
"iso8601": func(t time.Time) string { return t.Format(time.RFC3339) },
"join": strings.Join,
"replace": strings.Replace,
"trim": strings.Trim,
"trimLeft": strings.TrimLeft,
"trimPrefix": strings.TrimPrefix,
"trimRight": strings.TrimRight,
"trimSuffix": strings.TrimSuffix,
"trimSpace": strings.TrimSpace,
"last": func(i int, a interface{}) bool { return i == reflect.ValueOf(a).Len()-1 },
}).Parse(tmplStr)
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, args); err != nil {
return "", err
}
return buf.String(), nil
}