mirror of
https://github.com/containrrr/watchtower.git
synced 2025-12-14 06:06:38 +01:00
refactor: move actions into internal
This commit is contained in:
parent
62f603bb25
commit
a425bf1024
17 changed files with 26 additions and 29 deletions
|
|
@ -1,177 +0,0 @@
|
|||
package actions_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
"github.com/containrrr/watchtower/pkg/container/mocks"
|
||||
"github.com/docker/docker/api/types"
|
||||
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
cli "github.com/docker/docker/client"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestActions(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Actions Suite")
|
||||
}
|
||||
|
||||
var _ = Describe("the actions package", func() {
|
||||
var dockerClient cli.CommonAPIClient
|
||||
var client mockClient
|
||||
BeforeSuite(func() {
|
||||
server := mocks.NewMockAPIServer()
|
||||
dockerClient, _ = cli.NewClientWithOpts(
|
||||
cli.WithHost(server.URL),
|
||||
cli.WithHTTPClient(server.Client()))
|
||||
})
|
||||
BeforeEach(func() {
|
||||
client = mockClient{
|
||||
api: dockerClient,
|
||||
pullImages: false,
|
||||
removeVolumes: false,
|
||||
TestData: &TestData{},
|
||||
}
|
||||
})
|
||||
|
||||
Describe("the check prerequisites method", func() {
|
||||
When("given an empty array", func() {
|
||||
It("should not do anything", func() {
|
||||
client.TestData.Containers = []container.Container{}
|
||||
err := CheckForMultipleWatchtowerInstances(client, false)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
When("given an array of one", func() {
|
||||
It("should not do anything", func() {
|
||||
client.TestData.Containers = []container.Container{
|
||||
createMockContainer(
|
||||
"test-container",
|
||||
"test-container",
|
||||
"watchtower",
|
||||
time.Now()),
|
||||
}
|
||||
err := CheckForMultipleWatchtowerInstances(client, false)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
When("given multiple containers", func() {
|
||||
BeforeEach(func() {
|
||||
client = mockClient{
|
||||
api: dockerClient,
|
||||
pullImages: false,
|
||||
removeVolumes: false,
|
||||
TestData: &TestData{
|
||||
NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []container.Container{
|
||||
createMockContainer(
|
||||
"test-container-01",
|
||||
"test-container-01",
|
||||
"watchtower",
|
||||
time.Now().AddDate(0, 0, -1)),
|
||||
createMockContainer(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
"watchtower",
|
||||
time.Now()),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
It("should stop all but the latest one", func() {
|
||||
err := CheckForMultipleWatchtowerInstances(client, false)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
When("deciding whether to cleanup images", func() {
|
||||
BeforeEach(func() {
|
||||
client = mockClient{
|
||||
api: dockerClient,
|
||||
pullImages: false,
|
||||
removeVolumes: false,
|
||||
TestData: &TestData{
|
||||
Containers: []container.Container{
|
||||
createMockContainer(
|
||||
"test-container-01",
|
||||
"test-container-01",
|
||||
"watchtower",
|
||||
time.Now().AddDate(0, 0, -1)),
|
||||
createMockContainer(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
"watchtower",
|
||||
time.Now()),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
It("should try to delete the image if the cleanup flag is true", func() {
|
||||
err := CheckForMultipleWatchtowerInstances(client, true)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImage).To(BeTrue())
|
||||
})
|
||||
It("should not try to delete the image if the cleanup flag is false", func() {
|
||||
err := CheckForMultipleWatchtowerInstances(client, false)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImage).To(BeFalse())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func createMockContainer(id string, name string, image string, created time.Time) container.Container {
|
||||
content := types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
ID: id,
|
||||
Image: image,
|
||||
Name: name,
|
||||
Created: created.String(),
|
||||
},
|
||||
}
|
||||
return *container.NewContainer(&content, nil)
|
||||
}
|
||||
|
||||
type mockClient struct {
|
||||
TestData *TestData
|
||||
api cli.CommonAPIClient
|
||||
pullImages bool
|
||||
removeVolumes bool
|
||||
}
|
||||
|
||||
type TestData struct {
|
||||
TriedToRemoveImage bool
|
||||
NameOfContainerToKeep string
|
||||
Containers []container.Container
|
||||
}
|
||||
|
||||
func (client mockClient) ListContainers(f t.Filter) ([]container.Container, error) {
|
||||
return client.TestData.Containers, nil
|
||||
}
|
||||
|
||||
func (client mockClient) StopContainer(c container.Container, d time.Duration) error {
|
||||
if c.Name() == client.TestData.NameOfContainerToKeep {
|
||||
return errors.New("tried to stop the instance we want to keep")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (client mockClient) StartContainer(c container.Container) error {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
func (client mockClient) RenameContainer(c container.Container, s string) error {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
func (client mockClient) RemoveImage(c container.Container) error {
|
||||
client.TestData.TriedToRemoveImage = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client mockClient) IsContainerStale(c container.Container) (bool, error) {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
package actions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/opencontainers/runc/Godeps/_workspace/src/github.com/Sirupsen/logrus"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
)
|
||||
|
||||
// CheckForMultipleWatchtowerInstances will ensure that there are not multiple instances of the
|
||||
// watchtower running simultaneously. If multiple watchtower containers are detected, this function
|
||||
// will stop and remove all but the most recently started container.
|
||||
func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool) error {
|
||||
awaitDockerClient()
|
||||
containers, err := client.ListContainers(container.WatchtowerContainersFilter)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if len(containers) <= 1 {
|
||||
log.Debug("There are no additional watchtower containers")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("Found multiple running watchtower instances. Cleaning up.")
|
||||
return cleanupExcessWatchtowers(containers, client, cleanup)
|
||||
}
|
||||
|
||||
func cleanupExcessWatchtowers(containers []container.Container, client container.Client, cleanup bool) error {
|
||||
var cleanupErrors int
|
||||
var stopErrors int
|
||||
|
||||
sort.Sort(container.ByCreated(containers))
|
||||
allContainersExceptLast := containers[0 : len(containers)-1]
|
||||
|
||||
for _, c := range allContainersExceptLast {
|
||||
if err := client.StopContainer(c, 60); err != nil {
|
||||
// logging the original here as we're just returning a count
|
||||
logrus.Error(err)
|
||||
stopErrors++
|
||||
continue
|
||||
}
|
||||
|
||||
if cleanup == true {
|
||||
if err := client.RemoveImage(c); err != nil {
|
||||
// logging the original here as we're just returning a count
|
||||
logrus.Error(err)
|
||||
cleanupErrors++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createErrorIfAnyHaveOccurred(stopErrors, cleanupErrors)
|
||||
}
|
||||
|
||||
func createErrorIfAnyHaveOccurred(c int, i int) error {
|
||||
if c == 0 && i == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var output strings.Builder
|
||||
|
||||
if c > 0 {
|
||||
output.WriteString(fmt.Sprintf("%d errors while stopping containers", c))
|
||||
}
|
||||
if i > 0 {
|
||||
output.WriteString(fmt.Sprintf("%d errors while cleaning up images", c))
|
||||
}
|
||||
return errors.New(output.String())
|
||||
}
|
||||
|
||||
func awaitDockerClient() {
|
||||
log.Debug("Sleeping for a seconds to ensure the docker api client has been properly initialized.")
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
package actions
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
)
|
||||
|
||||
// UpdateParams contains all different options available to alter the behavior of the Update func
|
||||
type UpdateParams struct {
|
||||
Filter t.Filter
|
||||
Cleanup bool
|
||||
NoRestart bool
|
||||
Timeout time.Duration
|
||||
MonitorOnly bool
|
||||
}
|
||||
|
||||
// Update looks at the running Docker containers to see if any of the images
|
||||
// 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, params UpdateParams) error {
|
||||
log.Debug("Checking containers for updated images")
|
||||
|
||||
containers, err := client.ListContainers(params.Filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, container := range containers {
|
||||
stale, err := client.IsContainerStale(container)
|
||||
if err != nil {
|
||||
log.Infof("Unable to update container %s. Proceeding to next.", containers[i].Name())
|
||||
log.Debug(err)
|
||||
stale = false
|
||||
}
|
||||
containers[i].Stale = stale
|
||||
}
|
||||
|
||||
containers, err = container.SortByDependencies(containers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkDependencies(containers)
|
||||
|
||||
if params.MonitorOnly {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stale containers in reverse order
|
||||
for i := len(containers) - 1; i >= 0; i-- {
|
||||
container := containers[i]
|
||||
|
||||
if container.IsWatchtower() {
|
||||
log.Debugf("This is the watchtower container %s", containers[i].Name())
|
||||
continue
|
||||
}
|
||||
|
||||
if container.Stale {
|
||||
if err := client.StopContainer(container, params.Timeout); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restart stale containers in sorted order
|
||||
for _, container := range containers {
|
||||
if container.Stale {
|
||||
// Since we can't shutdown a watchtower container immediately, we need to
|
||||
// start the new one while the old one is still running. This prevents us
|
||||
// from re-using the same container name so we first rename the current
|
||||
// instance so that the new one can adopt the old name.
|
||||
if container.IsWatchtower() {
|
||||
if err := client.RenameContainer(container, randName()); err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !params.NoRestart {
|
||||
if err := client.StartContainer(container); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
if params.Cleanup {
|
||||
client.RemoveImage(container)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkDependencies(containers []container.Container) {
|
||||
|
||||
for i, parent := range containers {
|
||||
if parent.Stale {
|
||||
continue
|
||||
}
|
||||
|
||||
LinkLoop:
|
||||
for _, linkName := range parent.Links() {
|
||||
for _, child := range containers {
|
||||
if child.Name() == linkName && child.Stale {
|
||||
containers[i].Stale = true
|
||||
break LinkLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generates a random, 32-character, Docker-compatible container name.
|
||||
func randName() string {
|
||||
b := make([]rune, 32)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
||||
|
|
@ -23,8 +23,7 @@ var _ = Describe("the container", func() {
|
|||
server := mocks.NewMockAPIServer()
|
||||
docker, _ = cli.NewClientWithOpts(
|
||||
cli.WithHost(server.URL),
|
||||
cli.WithHTTPClient(server.Client(),
|
||||
))
|
||||
cli.WithHTTPClient(server.Client()))
|
||||
client = dockerClient{
|
||||
api: docker,
|
||||
pullImages: false,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package container
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/containrrr/watchtower/pkg/container/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWatchtowerContainersFilter(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
func TestEncodedEnvAuth_ShouldReturnAnErrorIfRepoEnvsAreUnset(t *testing.T) {
|
||||
os.Unsetenv("REPO_USER")
|
||||
os.Unsetenv("REPO_PASS")
|
||||
|
|
|
|||
138
pkg/notifications/email.go
Normal file
138
pkg/notifications/email.go
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
emailType = "email"
|
||||
)
|
||||
|
||||
// Implements Notifier, logrus.Hook
|
||||
// The default logrus email integration would have several issues:
|
||||
// - It would send one email per log output
|
||||
// - It would only send errors
|
||||
// We work around that by holding on to log entries until the update cycle is done.
|
||||
type emailTypeNotifier struct {
|
||||
From, To string
|
||||
Server, User, Password string
|
||||
Port int
|
||||
tlsSkipVerify bool
|
||||
entries []*log.Entry
|
||||
logLevels []log.Level
|
||||
}
|
||||
|
||||
func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
|
||||
flags := c.PersistentFlags()
|
||||
|
||||
from, _ := flags.GetString("notification-email-from")
|
||||
to, _ := flags.GetString("notification-email-to")
|
||||
server, _ := flags.GetString("notification-email-server")
|
||||
user, _ := flags.GetString("notification-email-server-user")
|
||||
password, _ := flags.GetString("notification-email-server-password")
|
||||
port, _ := flags.GetInt("notification-email-server-port")
|
||||
tlsSkipVerify, _ := flags.GetBool("notification-email-server-tls-skip-verify")
|
||||
|
||||
n := &emailTypeNotifier{
|
||||
From: from,
|
||||
To: to,
|
||||
Server: server,
|
||||
User: user,
|
||||
Password: password,
|
||||
Port: port,
|
||||
tlsSkipVerify: tlsSkipVerify,
|
||||
logLevels: acceptedLogLevels,
|
||||
}
|
||||
|
||||
log.AddHook(n)
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (e *emailTypeNotifier) buildMessage(entries []*log.Entry) []byte {
|
||||
emailSubject := "Watchtower updates"
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
emailSubject += " on " + hostname
|
||||
}
|
||||
body := ""
|
||||
for _, entry := range entries {
|
||||
body += entry.Time.Format("2006-01-02 15:04:05") + " (" + entry.Level.String() + "): " + entry.Message + "\r\n"
|
||||
// We don't use fields in watchtower, so don't bother sending them.
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
|
||||
header := make(map[string]string)
|
||||
header["From"] = e.From
|
||||
header["To"] = e.To
|
||||
header["Subject"] = emailSubject
|
||||
header["Date"] = t.Format(time.RFC1123Z)
|
||||
header["MIME-Version"] = "1.0"
|
||||
header["Content-Type"] = "text/plain; charset=\"utf-8\""
|
||||
header["Content-Transfer-Encoding"] = "base64"
|
||||
|
||||
message := ""
|
||||
for k, v := range header {
|
||||
message += fmt.Sprintf("%s: %s\r\n", k, v)
|
||||
}
|
||||
|
||||
encodedBody := base64.StdEncoding.EncodeToString([]byte(body))
|
||||
//RFC 2045 base64 encoding demands line no longer than 76 characters.
|
||||
for _, line := range SplitSubN(encodedBody, 76) {
|
||||
message += "\r\n" + line
|
||||
}
|
||||
|
||||
return []byte(message)
|
||||
}
|
||||
|
||||
func (e *emailTypeNotifier) sendEntries(entries []*log.Entry) {
|
||||
// Do the sending in a separate goroutine so we don't block the main process.
|
||||
msg := e.buildMessage(entries)
|
||||
go func() {
|
||||
var auth smtp.Auth
|
||||
if e.User != "" {
|
||||
auth = smtp.PlainAuth("", e.User, e.Password, e.Server)
|
||||
}
|
||||
err := SendMail(e.Server+":"+strconv.Itoa(e.Port), e.tlsSkipVerify, auth, e.From, []string{e.To}, msg)
|
||||
if err != nil {
|
||||
// Use fmt so it doesn't trigger another email.
|
||||
fmt.Println("Failed to send notification email: ", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (e *emailTypeNotifier) StartNotification() {
|
||||
if e.entries == nil {
|
||||
e.entries = make([]*log.Entry, 0, 10)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *emailTypeNotifier) SendNotification() {
|
||||
if e.entries != nil && len(e.entries) != 0 {
|
||||
e.sendEntries(e.entries)
|
||||
}
|
||||
e.entries = nil
|
||||
}
|
||||
|
||||
func (e *emailTypeNotifier) Levels() []log.Level {
|
||||
return e.logLevels
|
||||
}
|
||||
|
||||
func (e *emailTypeNotifier) Fire(entry *log.Entry) error {
|
||||
if e.entries != nil {
|
||||
e.entries = append(e.entries, entry)
|
||||
} else {
|
||||
// Log output generated outside a cycle is sent immediately.
|
||||
e.sendEntries([]*log.Entry{entry})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
138
pkg/notifications/msteams.go
Normal file
138
pkg/notifications/msteams.go
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"net/http"
|
||||
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
const (
|
||||
msTeamsType = "msteams"
|
||||
)
|
||||
|
||||
type msTeamsTypeNotifier struct {
|
||||
webHookURL string
|
||||
levels []log.Level
|
||||
data bool
|
||||
}
|
||||
|
||||
func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
|
||||
|
||||
flags := cmd.PersistentFlags()
|
||||
|
||||
webHookURL, _ := flags.GetString("notification-msteams-hook")
|
||||
if len(webHookURL) <= 0 {
|
||||
log.Fatal("Required argument --notification-msteams-hook(cli) or WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL(env) is empty.")
|
||||
}
|
||||
|
||||
withData, _ := flags.GetBool("notification-msteams-data")
|
||||
n := &msTeamsTypeNotifier{
|
||||
levels: acceptedLogLevels,
|
||||
webHookURL: webHookURL,
|
||||
data: withData,
|
||||
}
|
||||
|
||||
log.AddHook(n)
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *msTeamsTypeNotifier) StartNotification() {}
|
||||
|
||||
func (n *msTeamsTypeNotifier) SendNotification() {}
|
||||
|
||||
func (n *msTeamsTypeNotifier) Levels() []log.Level {
|
||||
return n.levels
|
||||
}
|
||||
|
||||
func (n *msTeamsTypeNotifier) Fire(entry *log.Entry) error {
|
||||
|
||||
message := "(" + entry.Level.String() + "): " + entry.Message
|
||||
|
||||
go func() {
|
||||
webHookBody := messageCard{
|
||||
CardType: "MessageCard",
|
||||
Context: "http://schema.org/extensions",
|
||||
Markdown: true,
|
||||
Text: message,
|
||||
}
|
||||
|
||||
if n.data && entry.Data != nil && len(entry.Data) > 0 {
|
||||
section := messageCardSection{
|
||||
Facts: make([]messageCardSectionFact, len(entry.Data)),
|
||||
Text: "",
|
||||
}
|
||||
|
||||
index := 0
|
||||
for k, v := range entry.Data {
|
||||
section.Facts[index] = messageCardSectionFact{
|
||||
Name: k,
|
||||
Value: fmt.Sprint(v),
|
||||
}
|
||||
index++
|
||||
}
|
||||
|
||||
webHookBody.Sections = []messageCardSection{section}
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(webHookBody)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to build JSON body for MSTeams notificattion: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := http.Post(n.webHookURL, "application/json", bytes.NewBuffer([]byte(jsonBody)))
|
||||
if err != nil {
|
||||
fmt.Println("Failed to send MSTeams notificattion: ", err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
fmt.Println("Failed to send MSTeams notificattion. HTTP RESPONSE STATUS: ", resp.StatusCode)
|
||||
if resp.Body != nil {
|
||||
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
bodyString := string(bodyBytes)
|
||||
fmt.Println(bodyString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type messageCard struct {
|
||||
CardType string `json:"@type"`
|
||||
Context string `json:"@context"`
|
||||
CorrelationID string `json:"correlationId,omitempty"`
|
||||
ThemeColor string `json:"themeColor,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Markdown bool `json:"markdown,bool"`
|
||||
Sections []messageCardSection `json:"sections,omitempty"`
|
||||
}
|
||||
|
||||
type messageCardSection struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ActivityTitle string `json:"activityTitle,omitempty"`
|
||||
ActivitySubtitle string `json:"activitySubtitle,omitempty"`
|
||||
ActivityImage string `json:"activityImage,omitempty"`
|
||||
ActivityText string `json:"activityText,omitempty"`
|
||||
HeroImage string `json:"heroImage,omitempty"`
|
||||
Facts []messageCardSectionFact `json:"facts,omitempty"`
|
||||
}
|
||||
|
||||
type messageCardSectionFact struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
62
pkg/notifications/notifier.go
Normal file
62
pkg/notifications/notifier.go
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
ty "github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/johntdyer/slackrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Notifier can send log output as notification to admins, with optional batching.
|
||||
type Notifier struct {
|
||||
types []ty.Notifier
|
||||
}
|
||||
|
||||
// NewNotifier creates and returns a new Notifier, using global configuration.
|
||||
func NewNotifier(c *cobra.Command) *Notifier {
|
||||
n := &Notifier{}
|
||||
|
||||
f := c.PersistentFlags()
|
||||
|
||||
level, _ := f.GetString("notifications-level")
|
||||
logLevel, err := log.ParseLevel(level)
|
||||
if err != nil {
|
||||
log.Fatalf("Notifications invalid log level: %s", err.Error())
|
||||
}
|
||||
|
||||
acceptedLogLevels := slackrus.LevelThreshold(logLevel)
|
||||
|
||||
// Parse types and create notifiers.
|
||||
types, _ := f.GetStringSlice("notifications")
|
||||
|
||||
for _, t := range types {
|
||||
var tn ty.Notifier
|
||||
switch t {
|
||||
case emailType:
|
||||
tn = newEmailNotifier(c, acceptedLogLevels)
|
||||
case slackType:
|
||||
tn = newSlackNotifier(c, acceptedLogLevels)
|
||||
case msTeamsType:
|
||||
tn = newMsTeamsNotifier(c, acceptedLogLevels)
|
||||
default:
|
||||
log.Fatalf("Unknown notification type %q", t)
|
||||
}
|
||||
n.types = append(n.types, tn)
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// StartNotification starts a log batch. Notifications will be accumulated after this point and only sent when SendNotification() is called.
|
||||
func (n *Notifier) StartNotification() {
|
||||
for _, t := range n.types {
|
||||
t.StartNotification()
|
||||
}
|
||||
}
|
||||
|
||||
// SendNotification sends any notifications accumulated since StartNotification() was called.
|
||||
func (n *Notifier) SendNotification() {
|
||||
for _, t := range n.types {
|
||||
t.SendNotification()
|
||||
}
|
||||
}
|
||||
44
pkg/notifications/slack.go
Normal file
44
pkg/notifications/slack.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/johntdyer/slackrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
slackType = "slack"
|
||||
)
|
||||
|
||||
type slackTypeNotifier struct {
|
||||
slackrus.SlackrusHook
|
||||
}
|
||||
|
||||
func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
|
||||
flags := c.PersistentFlags()
|
||||
|
||||
hookURL, _ := flags.GetString("notification-slack-hook-url")
|
||||
userName, _ := flags.GetString("notification-slack-identifier")
|
||||
channel, _ := flags.GetString("notification-slack-channel")
|
||||
emoji, _ := flags.GetString("notification-slack-icon-emoji")
|
||||
iconURL, _ := flags.GetString("notification-slack-icon-url")
|
||||
|
||||
n := &slackTypeNotifier{
|
||||
SlackrusHook: slackrus.SlackrusHook{
|
||||
HookURL: hookURL,
|
||||
Username: userName,
|
||||
Channel: channel,
|
||||
IconEmoji: emoji,
|
||||
IconURL: iconURL,
|
||||
AcceptedLevels: acceptedLogLevels,
|
||||
},
|
||||
}
|
||||
|
||||
log.AddHook(n)
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *slackTypeNotifier) StartNotification() {}
|
||||
|
||||
func (s *slackTypeNotifier) SendNotification() {}
|
||||
77
pkg/notifications/smtp.go
Normal file
77
pkg/notifications/smtp.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// Package notifications ...
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license.
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
// SendMail connects to the server at addr, switches to TLS if
|
||||
// possible, authenticates with the optional mechanism a if possible,
|
||||
// and then sends an email from address from, to addresses to, with
|
||||
// message msg.
|
||||
// The addr must include a port, as in "mail.example.com:smtp".
|
||||
//
|
||||
// The addresses in the to parameter are the SMTP RCPT addresses.
|
||||
//
|
||||
// The msg parameter should be an RFC 822-style email with headers
|
||||
// first, a blank line, and then the message body. The lines of msg
|
||||
// should be CRLF terminated. The msg headers should usually include
|
||||
// fields such as "From", "To", "Subject", and "Cc". Sending "Bcc"
|
||||
// messages is accomplished by including an email address in the to
|
||||
// parameter but not including it in the msg headers.
|
||||
//
|
||||
// The SendMail function and the net/smtp package are low-level
|
||||
// mechanisms and provide no support for DKIM signing, MIME
|
||||
// attachments (see the mime/multipart package), or other mail
|
||||
// functionality. Higher-level packages exist outside of the standard
|
||||
// library.
|
||||
func SendMail(addr string, insecureSkipVerify bool, a smtp.Auth, from string, to []string, msg []byte) error {
|
||||
c, err := smtp.Dial(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
if err = c.Hello("localHost"); err != nil {
|
||||
return err
|
||||
}
|
||||
if ok, _ := c.Extension("STARTTLS"); ok {
|
||||
serverName, _, _ := net.SplitHostPort(addr)
|
||||
config := &tls.Config{ServerName: serverName, InsecureSkipVerify: insecureSkipVerify}
|
||||
if err = c.StartTLS(config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if a != nil {
|
||||
if ok, _ := c.Extension("AUTH"); ok {
|
||||
if err = c.Auth(a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = c.Mail(from); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Quit()
|
||||
}
|
||||
24
pkg/notifications/util.go
Normal file
24
pkg/notifications/util.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package notifications
|
||||
|
||||
import "bytes"
|
||||
|
||||
// SplitSubN splits a string into a list of string with each having
|
||||
// a maximum number of characters n
|
||||
func SplitSubN(s string, n int) []string {
|
||||
sub := ""
|
||||
subs := []string{}
|
||||
|
||||
runes := bytes.Runes([]byte(s))
|
||||
l := len(runes)
|
||||
for i, r := range runes {
|
||||
sub = sub + string(r)
|
||||
if (i+1)%n == 0 {
|
||||
subs = append(subs, sub)
|
||||
sub = ""
|
||||
} else if (i + 1) == l {
|
||||
subs = append(subs, sub)
|
||||
}
|
||||
}
|
||||
|
||||
return subs
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package types
|
||||
|
||||
// Notifier is the interface that all notification services have in common
|
||||
type Notifier interface {
|
||||
StartNotification()
|
||||
SendNotification()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue