This commit is contained in:
Gerald Pape 2017-04-12 16:33:48 +00:00 committed by GitHub
commit 798e1b2479
6 changed files with 159 additions and 19 deletions

92
actions/notify_slack.go Normal file
View file

@ -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,
})
}

View file

@ -1,6 +1,7 @@
package actions package actions
import ( import (
"fmt"
"math/rand" "math/rand"
"time" "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 // 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 // any of the images, the associated containers are stopped and restarted with
// the new image. // 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") log.Info("Checking containers for updated images")
// helper vars for notification
var (
updatedContainers []string
errorMessages []string
)
containers, err := client.ListContainers(containerFilter(names)) containers, err := client.ListContainers(containerFilter(names))
if err != nil { if err != nil {
return err return updatedContainers, errorMessages, err
} }
for i, container := range containers { 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.Infof("Unable to update container %s. Proceeding to next.", containers[i].Name())
log.Debug(err) log.Debug(err)
stale = false stale = false
errorMessages = append(errorMessages, fmt.Sprintf("Unable to update container %s: %v", container.Details(), err))
} }
containers[i].Stale = stale containers[i].Stale = stale
} }
containers, err = container.SortByDependencies(containers) containers, err = container.SortByDependencies(containers)
if err != nil { if err != nil {
return err return updatedContainers, errorMessages, err
} }
checkDependencies(containers) checkDependencies(containers)
@ -70,6 +78,7 @@ func Update(client container.Client, names []string, cleanup bool, noRestart boo
if container.Stale { if container.Stale {
if err := client.StopContainer(container, waitTime); err != nil { if err := client.StopContainer(container, waitTime); err != nil {
log.Error(err) 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 container.IsWatchtower() {
if err := client.RenameContainer(container, randName()); err != nil { if err := client.RenameContainer(container, randName()); err != nil {
log.Error(err) log.Error(err)
errorMessages = append(errorMessages, fmt.Sprintf("Unable to rename container %s: %v", container.Details(), err))
continue continue
} }
} }
@ -91,16 +101,19 @@ func Update(client container.Client, names []string, cleanup bool, noRestart boo
if !noRestart { if !noRestart {
if err := client.StartContainer(container); err != nil { if err := client.StartContainer(container); err != nil {
log.Error(err) log.Error(err)
errorMessages = append(errorMessages, fmt.Sprintf("Unable to restart container %s: %v", container.Details(), err))
} }
} }
if cleanup { if cleanup {
client.RemoveImage(container) client.RemoveImage(container)
} }
updatedContainers = append(updatedContainers, container.Details())
} }
} }
return nil return updatedContainers, errorMessages, nil
} }
func checkDependencies(containers []container.Container) { func checkDependencies(containers []container.Container) {

View file

@ -64,6 +64,12 @@ func (c Container) ImageName() string {
return imageName 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 // Links returns a list containing the names of all the containers to which
// this container is linked. // this container is linked.
func (c Container) Links() []string { func (c Container) Links() []string {

23
glide.lock generated
View file

@ -1,10 +1,12 @@
hash: 9ddd729b207d71ce16ae9a0282ac87a046b9161ac4f15b108e86a03367172516 hash: 79bf30c3a98ecbe4c7668765f0033f5e487836d2011b21bcd219d321ace35f13
updated: 2017-01-24T20:45:43.1914053+01:00 updated: 2017-02-07T15:03:18.178257775+01:00
imports: imports:
- name: github.com/Azure/go-ansiterm - name: github.com/Azure/go-ansiterm
version: 388960b655244e76e24c75f48631564eaefade62 version: 388960b655244e76e24c75f48631564eaefade62
subpackages: subpackages:
- winterm - winterm
- name: github.com/bitly/go-simplejson
version: aabad6e819789e569bd6aabf444c935aa9ba1e44
- name: github.com/davecgh/go-spew - name: github.com/davecgh/go-spew
version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9
subpackages: subpackages:
@ -101,7 +103,7 @@ imports:
- name: github.com/docker/libtrust - name: github.com/docker/libtrust
version: 9cbd2a1374f46905c68a4eb3694a130610adc62a version: 9cbd2a1374f46905c68a4eb3694a130610adc62a
- name: github.com/golang/protobuf - name: github.com/golang/protobuf
version: 1f49d83d9aa00e6ce4fc8258c71cc7786aec968a version: f7137ae6b19afbfd61a94b746fda3b3fe0491874
subpackages: subpackages:
- proto - proto
- name: github.com/gorilla/context - name: github.com/gorilla/context
@ -114,16 +116,17 @@ imports:
version: f4e566c536cf69158e808ec28ef4182a37fdc981 version: f4e566c536cf69158e808ec28ef4182a37fdc981
- name: github.com/Microsoft/go-winio - name: github.com/Microsoft/go-winio
version: 24a3e3d3fc7451805e09d11e11e95d9a0a4f205e version: 24a3e3d3fc7451805e09d11e11e95d9a0a4f205e
- name: github.com/mozillazg/request
version: 052232e32456da4751d789b61e89aba4a07a7e25
- name: github.com/opencontainers/runc - name: github.com/opencontainers/runc
version: 2f7393a47307a16f8cee44a37b262e8b81021e3e version: b263a43430ac6996a4302b891688544225197294
repo: https://github.com/docker/runc.git
subpackages: subpackages:
- libcontainer/configs - libcontainer/configs
- libcontainer/devices - libcontainer/devices
- libcontainer/system - libcontainer/system
- libcontainer/user - libcontainer/user
- name: github.com/opencontainers/runtime-spec - name: github.com/opencontainers/runtime-spec
version: 1c7c27d043c2a5e513a44084d2b10d77d1402b8c version: 794ca7ac88234607f9d2c76da8a6e9bbbade8cb9
subpackages: subpackages:
- specs-go - specs-go
- name: github.com/pkg/errors - name: github.com/pkg/errors
@ -135,10 +138,9 @@ imports:
- name: github.com/robfig/cron - name: github.com/robfig/cron
version: 9585fd555638e77bba25f25db5c44b41f264aeb7 version: 9585fd555638e77bba25f25db5c44b41f264aeb7
- name: github.com/Sirupsen/logrus - name: github.com/Sirupsen/logrus
version: d26492970760ca5d33129d2d799e34be5c4782eb version: c078b1e43f58d563c74cebe63c85789e76ddb627
- name: github.com/spf13/cobra - name: github.com/spf13/cobra
version: a3c09249f1a24a9d951f2738fb9b9256b8b42fa5 version: 35136c09d8da66b901337c6e86fd8e88a1a255bd
repo: https://github.com/dnephin/cobra.git
- name: github.com/spf13/pflag - name: github.com/spf13/pflag
version: dabebe21bf790f782ea4c7bbd2efc430de182afd version: dabebe21bf790f782ea4c7bbd2efc430de182afd
- name: github.com/stretchr/objx - name: github.com/stretchr/objx
@ -157,7 +159,7 @@ imports:
- tar/asm - tar/asm
- tar/storage - tar/storage
- name: golang.org/x/net - name: golang.org/x/net
version: 2beffdc2e92c8a3027590f898fe88f69af48a3f8 version: ""
repo: https://github.com/tonistiigi/net.git repo: https://github.com/tonistiigi/net.git
subpackages: subpackages:
- context - context
@ -166,6 +168,7 @@ imports:
- http2/hpack - http2/hpack
- internal/timeseries - internal/timeseries
- proxy - proxy
- publicsuffix
- trace - trace
- name: golang.org/x/sys - name: golang.org/x/sys
version: 8f0908ab3b2457e2e15403d3697c9ef5cb4b57a9 version: 8f0908ab3b2457e2e15403d3697c9ef5cb4b57a9

View file

@ -26,3 +26,5 @@ import:
- context - context
- package: github.com/robfig/cron - package: github.com/robfig/cron
version: 9585fd555638e77bba25f25db5c44b41f264aeb7 version: 9585fd555638e77bba25f25db5c44b41f264aeb7
- package: github.com/mozillazg/request
version: v0.8.0

34
main.go
View file

@ -23,10 +23,11 @@ const DockerAPIMinVersion string = "1.24"
var version = "master" var version = "master"
var ( var (
client container.Client client container.Client
scheduleSpec string scheduleSpec string
cleanup bool cleanup bool
noRestart bool noRestart bool
slackNotifier *actions.SlackNotifier
) )
func init() { func init() {
@ -82,6 +83,15 @@ func main() {
Name: "debug", Name: "debug",
Usage: "enable debug mode with verbose logging", 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 { if err := app.Run(os.Args); err != nil {
@ -108,6 +118,9 @@ func before(c *cli.Context) error {
cleanup = c.GlobalBool("cleanup") cleanup = c.GlobalBool("cleanup")
noRestart = c.GlobalBool("no-restart") noRestart = c.GlobalBool("no-restart")
slackURL := c.GlobalString("slack-hook-url")
slackPrefix := c.GlobalString("slack-message-identification")
// configure environment vars for client // configure environment vars for client
err := envConfig(c) err := envConfig(c)
if err != nil { if err != nil {
@ -115,6 +128,12 @@ func before(c *cli.Context) error {
} }
client = container.NewClient(!c.GlobalBool("no-pull")) client = container.NewClient(!c.GlobalBool("no-pull"))
if len(slackURL) != 0 {
slackNotifier = actions.NewSlackNotifier(slackURL, slackPrefix)
slackNotifier.NotifyStartup()
}
return nil return nil
} }
@ -135,9 +154,14 @@ func start(c *cli.Context) error {
select { select {
case v := <-tryLockSem: case v := <-tryLockSem:
defer func() { tryLockSem <- v }() 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) fmt.Println(err)
} }
if slackNotifier != nil && (len(updatedContainers) != 0 || len(errorMessages) != 0) {
slackNotifier.NotifyContainerUpdate(updatedContainers, errorMessages)
}
default: default:
log.Debug("Skipped another update already running.") log.Debug("Skipped another update already running.")
} }