diff --git a/actions/notify_slack.go b/actions/notify_slack.go new file mode 100644 index 0000000..2f4b219 --- /dev/null +++ b/actions/notify_slack.go @@ -0,0 +1,92 @@ +package actions + +import ( + "fmt" + "net/http" + "strings" + + "github.com/mozillazg/request" +) + +// SlackNotifier is used to make the https requests to a specified slack +// webhook URL +type SlackNotifier struct { + slackURL string + identity string +} + +const ( + slackMessageStartup = "Watchtower startup" + slackMessageError = "Some errors while checking and redeployment (Please check logs):" + slackMessageSuccess = "Successfully redeployed images:" +) + +// NewSlackNotifier instantiates a new SlackNotifier with an URL and an identifier which will +// be prepended to each message it sends +func NewSlackNotifier(slackURL, identity string) *SlackNotifier { + identity = strings.Trim(identity, " ") + + if len(identity) != 0 { + identity = fmt.Sprintf("[%s]: ", identity) + } + + return &SlackNotifier{ + slackURL: slackURL, + identity: identity, + } +} + +func (s SlackNotifier) sendNotification(json map[string]interface{}) { + c := new(http.Client) + req := request.NewRequest(c) + req.Json = json + _, err := req.Post(s.slackURL) + + if err != nil { + fmt.Println(err) + } +} + +// NotifyStartup sends a startup message to slack +func (s SlackNotifier) NotifyStartup() { + s.sendNotification(map[string]interface{}{ + "text": fmt.Sprintf("%s%s", s.identity, slackMessageStartup), + }) +} + +func buildAttachment(items []string, title, color string) map[string]interface{} { + + var fields []map[string]string + + for _, item := range items { + fields = append(fields, map[string]string{"value": item, "short": "false"}) + } + + return map[string]interface{}{ + "fallback": title + strings.Join(items, ", "), + "color": color, + "title": title, + "fields": fields, + } +} + +// NotifyContainerUpdate sends a Message after updating containers which yielded either success or errors or both +func (s SlackNotifier) NotifyContainerUpdate(successfulContainers, errorMessages []string) { + + var attachments []map[string]interface{} + + if len(successfulContainers) != 0 { + attachments = append(attachments, buildAttachment(successfulContainers, slackMessageSuccess, "good")) + } + + if len(errorMessages) != 0 { + attachments = append(attachments, buildAttachment(errorMessages, slackMessageError, "danger")) + } + + // add a pretext to the first attachment + attachments[0]["pretext"] = s.identity + + s.sendNotification(map[string]interface{}{ + "attachments": attachments, + }) +} diff --git a/actions/update.go b/actions/update.go index c537228..f33b299 100644 --- a/actions/update.go +++ b/actions/update.go @@ -1,6 +1,7 @@ package actions import ( + "fmt" "math/rand" "time" @@ -34,12 +35,18 @@ func containerFilter(names []string) container.Filter { // used to start those containers have been updated. If a change is detected in // any of the images, the associated containers are stopped and restarted with // the new image. -func Update(client container.Client, names []string, cleanup bool, noRestart bool) error { +func Update(client container.Client, names []string, cleanup bool, noRestart bool) ([]string, []string, error) { log.Info("Checking containers for updated images") + // helper vars for notification + var ( + updatedContainers []string + errorMessages []string + ) + containers, err := client.ListContainers(containerFilter(names)) if err != nil { - return err + return updatedContainers, errorMessages, err } for i, container := range containers { @@ -48,13 +55,14 @@ func Update(client container.Client, names []string, cleanup bool, noRestart boo log.Infof("Unable to update container %s. Proceeding to next.", containers[i].Name()) log.Debug(err) stale = false + errorMessages = append(errorMessages, fmt.Sprintf("Unable to update container %s: %v", container.Details(), err)) } containers[i].Stale = stale } containers, err = container.SortByDependencies(containers) if err != nil { - return err + return updatedContainers, errorMessages, err } checkDependencies(containers) @@ -70,6 +78,7 @@ func Update(client container.Client, names []string, cleanup bool, noRestart boo if container.Stale { if err := client.StopContainer(container, waitTime); err != nil { log.Error(err) + errorMessages = append(errorMessages, fmt.Sprintf("Unable to stop container %s: %v", container.Details(), err)) } } } @@ -84,6 +93,7 @@ func Update(client container.Client, names []string, cleanup bool, noRestart boo if container.IsWatchtower() { if err := client.RenameContainer(container, randName()); err != nil { log.Error(err) + errorMessages = append(errorMessages, fmt.Sprintf("Unable to rename container %s: %v", container.Details(), err)) continue } } @@ -91,16 +101,19 @@ func Update(client container.Client, names []string, cleanup bool, noRestart boo if !noRestart { if err := client.StartContainer(container); err != nil { log.Error(err) + errorMessages = append(errorMessages, fmt.Sprintf("Unable to restart container %s: %v", container.Details(), err)) } } if cleanup { client.RemoveImage(container) } + + updatedContainers = append(updatedContainers, container.Details()) } } - return nil + return updatedContainers, errorMessages, nil } func checkDependencies(containers []container.Container) { diff --git a/container/container.go b/container/container.go index c49da33..2d4ce89 100644 --- a/container/container.go +++ b/container/container.go @@ -64,6 +64,12 @@ func (c Container) ImageName() string { return imageName } +// Details returns the name of the Docker image as defined in ImageName +// together with the name of the container +func (c Container) Details() string { + return fmt.Sprintf("%s (%s)", c.ImageName(), c.Name()) +} + // Links returns a list containing the names of all the containers to which // this container is linked. func (c Container) Links() []string { diff --git a/glide.lock b/glide.lock index 7ae50d9..e828f92 100644 --- a/glide.lock +++ b/glide.lock @@ -1,10 +1,12 @@ -hash: 9ddd729b207d71ce16ae9a0282ac87a046b9161ac4f15b108e86a03367172516 -updated: 2017-01-24T20:45:43.1914053+01:00 +hash: 79bf30c3a98ecbe4c7668765f0033f5e487836d2011b21bcd219d321ace35f13 +updated: 2017-02-07T15:03:18.178257775+01:00 imports: - name: github.com/Azure/go-ansiterm version: 388960b655244e76e24c75f48631564eaefade62 subpackages: - winterm +- name: github.com/bitly/go-simplejson + version: aabad6e819789e569bd6aabf444c935aa9ba1e44 - name: github.com/davecgh/go-spew version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 subpackages: @@ -101,7 +103,7 @@ imports: - name: github.com/docker/libtrust version: 9cbd2a1374f46905c68a4eb3694a130610adc62a - name: github.com/golang/protobuf - version: 1f49d83d9aa00e6ce4fc8258c71cc7786aec968a + version: f7137ae6b19afbfd61a94b746fda3b3fe0491874 subpackages: - proto - name: github.com/gorilla/context @@ -114,16 +116,17 @@ imports: version: f4e566c536cf69158e808ec28ef4182a37fdc981 - name: github.com/Microsoft/go-winio version: 24a3e3d3fc7451805e09d11e11e95d9a0a4f205e +- name: github.com/mozillazg/request + version: 052232e32456da4751d789b61e89aba4a07a7e25 - name: github.com/opencontainers/runc - version: 2f7393a47307a16f8cee44a37b262e8b81021e3e - repo: https://github.com/docker/runc.git + version: b263a43430ac6996a4302b891688544225197294 subpackages: - libcontainer/configs - libcontainer/devices - libcontainer/system - libcontainer/user - name: github.com/opencontainers/runtime-spec - version: 1c7c27d043c2a5e513a44084d2b10d77d1402b8c + version: 794ca7ac88234607f9d2c76da8a6e9bbbade8cb9 subpackages: - specs-go - name: github.com/pkg/errors @@ -135,10 +138,9 @@ imports: - name: github.com/robfig/cron version: 9585fd555638e77bba25f25db5c44b41f264aeb7 - name: github.com/Sirupsen/logrus - version: d26492970760ca5d33129d2d799e34be5c4782eb + version: c078b1e43f58d563c74cebe63c85789e76ddb627 - name: github.com/spf13/cobra - version: a3c09249f1a24a9d951f2738fb9b9256b8b42fa5 - repo: https://github.com/dnephin/cobra.git + version: 35136c09d8da66b901337c6e86fd8e88a1a255bd - name: github.com/spf13/pflag version: dabebe21bf790f782ea4c7bbd2efc430de182afd - name: github.com/stretchr/objx @@ -157,7 +159,7 @@ imports: - tar/asm - tar/storage - name: golang.org/x/net - version: 2beffdc2e92c8a3027590f898fe88f69af48a3f8 + version: "" repo: https://github.com/tonistiigi/net.git subpackages: - context @@ -166,6 +168,7 @@ imports: - http2/hpack - internal/timeseries - proxy + - publicsuffix - trace - name: golang.org/x/sys version: 8f0908ab3b2457e2e15403d3697c9ef5cb4b57a9 diff --git a/glide.yaml b/glide.yaml index 8478cfa..6b959a4 100644 --- a/glide.yaml +++ b/glide.yaml @@ -26,3 +26,5 @@ import: - context - package: github.com/robfig/cron version: 9585fd555638e77bba25f25db5c44b41f264aeb7 +- package: github.com/mozillazg/request + version: v0.8.0 diff --git a/main.go b/main.go index e97c4d8..b52be6e 100644 --- a/main.go +++ b/main.go @@ -23,10 +23,11 @@ const DockerAPIMinVersion string = "1.24" var version = "master" var ( - client container.Client - scheduleSpec string - cleanup bool - noRestart bool + client container.Client + scheduleSpec string + cleanup bool + noRestart bool + slackNotifier *actions.SlackNotifier ) func init() { @@ -82,6 +83,15 @@ func main() { Name: "debug", Usage: "enable debug mode with verbose logging", }, + cli.StringFlag{ + Name: "slack-hook-url", + Usage: "the url for sending slack webhooks to", + }, + cli.StringFlag{ + Name: "slack-message-identification", + Usage: "specify a string which will be used to identify the messages comming from this watchtower instance. Default if omitted is \"watchtower\"", + Value: "watchtower", + }, } if err := app.Run(os.Args); err != nil { @@ -108,6 +118,9 @@ func before(c *cli.Context) error { cleanup = c.GlobalBool("cleanup") noRestart = c.GlobalBool("no-restart") + slackURL := c.GlobalString("slack-hook-url") + slackPrefix := c.GlobalString("slack-message-identification") + // configure environment vars for client err := envConfig(c) if err != nil { @@ -115,6 +128,12 @@ func before(c *cli.Context) error { } client = container.NewClient(!c.GlobalBool("no-pull")) + + if len(slackURL) != 0 { + slackNotifier = actions.NewSlackNotifier(slackURL, slackPrefix) + slackNotifier.NotifyStartup() + } + return nil } @@ -135,9 +154,14 @@ func start(c *cli.Context) error { select { case v := <-tryLockSem: defer func() { tryLockSem <- v }() - if err := actions.Update(client, names, cleanup, noRestart); err != nil { + updatedContainers, errorMessages, err := actions.Update(client, names, cleanup, noRestart) + if err != nil { fmt.Println(err) } + if slackNotifier != nil && (len(updatedContainers) != 0 || len(errorMessages) != 0) { + slackNotifier.NotifyContainerUpdate(updatedContainers, errorMessages) + + } default: log.Debug("Skipped another update already running.") }