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
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) {

View file

@ -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 {

23
glide.lock generated
View file

@ -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

View file

@ -26,3 +26,5 @@ import:
- context
- package: github.com/robfig/cron
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 (
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.")
}