Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slack notification integration #158

Merged
merged 12 commits into from
Dec 9, 2019
17 changes: 15 additions & 2 deletions chaoskube/chaoskube.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"github.com/linki/chaoskube/notifier"
"regexp"
"time"

Expand Down Expand Up @@ -67,6 +68,9 @@ type Chaoskube struct {
Now func() time.Time

MaxKill int

// chaos events notifier
Notifier notifier.Notifier
}

var (
Expand All @@ -90,7 +94,7 @@ var (
// * a logger implementing logrus.FieldLogger to send log output to
// * what specific terminator to use to imbue chaos on victim pods
// * whether to enable/disable dry-run mode
func New(client kubernetes.Interface, labels, annotations, namespaces, namespaceLabels labels.Selector, includedPodNames, excludedPodNames *regexp.Regexp, excludedWeekdays []time.Weekday, excludedTimesOfDay []util.TimePeriod, excludedDaysOfYear []time.Time, timezone *time.Location, minimumAge time.Duration, logger log.FieldLogger, dryRun bool, terminator terminator.Terminator, maxKill int) *Chaoskube {
func New(client kubernetes.Interface, labels, annotations, namespaces, namespaceLabels labels.Selector, includedPodNames, excludedPodNames *regexp.Regexp, excludedWeekdays []time.Weekday, excludedTimesOfDay []util.TimePeriod, excludedDaysOfYear []time.Time, timezone *time.Location, minimumAge time.Duration, logger log.FieldLogger, dryRun bool, terminator terminator.Terminator, maxKill int, notifier notifier.Notifier) *Chaoskube {
broadcaster := record.NewBroadcaster()
broadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: client.CoreV1().Events(v1.NamespaceAll)})
recorder := broadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "chaoskube"})
Expand All @@ -114,6 +118,7 @@ func New(client kubernetes.Interface, labels, annotations, namespaces, namespace
EventRecorder: recorder,
Now: time.Now,
MaxKill: maxKill,
Notifier: notifier,
}
}

Expand Down Expand Up @@ -175,7 +180,6 @@ func (c *Chaoskube) TerminateVictims() error {
for _, victim := range victims {
err = c.DeletePod(victim)
result = multierror.Append(result, err)

}

return result.ErrorOrNil()
Expand Down Expand Up @@ -257,6 +261,15 @@ func (c *Chaoskube) DeletePod(victim v1.Pod) error {

c.EventRecorder.Event(ref, v1.EventTypeNormal, "Killing", "Pod was terminated by chaoskube to introduce chaos.")

err = c.Notifier.NotifyTermination(notifier.Termination{
Pod: victim.Name,
Namespace: victim.Namespace,
})

if err != nil {
c.Logger.Warn("unable to notify pod termination", err)
}

return nil
}

Expand Down
33 changes: 33 additions & 0 deletions chaoskube/chaoskube_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package chaoskube

import (
"context"
"github.com/linki/chaoskube/notifier"
"math/rand"
"regexp"
"testing"
Expand Down Expand Up @@ -35,6 +36,7 @@ type podInfo struct {

var (
logger, logOutput = test.NewNullLogger()
testNotifier = &notifier.Noop{}
)

func (suite *Suite) SetupTest() {
Expand All @@ -59,6 +61,7 @@ func (suite *Suite) TestNew() {
dryRun = true
terminator = terminator.NewDeletePodTerminator(client, logger, 10*time.Second)
maxKill = 1
notifier = testNotifier
)

chaoskube := New(
Expand All @@ -78,6 +81,7 @@ func (suite *Suite) TestNew() {
dryRun,
terminator,
maxKill,
notifier,
)
suite.Require().NotNil(chaoskube)

Expand Down Expand Up @@ -723,6 +727,10 @@ func (suite *Suite) assertVictim(chaoskube *Chaoskube, expected map[string]strin
suite.assertVictims(chaoskube, []map[string]string{expected})
}

func (suite *Suite) assertNotified(notifier *notifier.Noop) {
suite.Assert().Greater(notifier.Calls, 0)
}

func (suite *Suite) setupWithPods(labelSelector labels.Selector, annotations labels.Selector, namespaces labels.Selector, namespaceLabels labels.Selector, includedPodNames *regexp.Regexp, excludedPodNames *regexp.Regexp, excludedWeekdays []time.Weekday, excludedTimesOfDay []util.TimePeriod, excludedDaysOfYear []time.Time, timezone *time.Location, minimumAge time.Duration, dryRun bool, gracePeriod time.Duration) *Chaoskube {
chaoskube := suite.setup(
labelSelector,
Expand Down Expand Up @@ -797,6 +805,7 @@ func (suite *Suite) setup(labelSelector labels.Selector, annotations labels.Sele
dryRun,
terminator.NewDeletePodTerminator(client, nullLogger, gracePeriod),
maxKill,
testNotifier,
)
}

Expand Down Expand Up @@ -971,3 +980,27 @@ func (suite *Suite) TestFilterByOwnerReference() {
}
}
}

func (suite *Suite) TestNotifierCall() {
chaoskube := suite.setupWithPods(
labels.Everything(),
labels.Everything(),
labels.Everything(),
labels.Everything(),
&regexp.Regexp{},
&regexp.Regexp{},
[]time.Weekday{},
[]util.TimePeriod{},
[]time.Time{},
time.UTC,
time.Duration(0),
false,
10,
)

victim := util.NewPod("default", "foo", v1.PodRunning)
err := chaoskube.DeletePod(victim)

suite.Require().NoError(err)
suite.assertNotified(testNotifier)
}
16 changes: 16 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"github.com/linki/chaoskube/notifier"
"io/ioutil"
"math/rand"
"net/http"
Expand Down Expand Up @@ -56,6 +57,7 @@ var (
gracePeriod time.Duration
logFormat string
logCaller bool
slackWebhook string
)

func init() {
Expand Down Expand Up @@ -83,6 +85,7 @@ func init() {
kingpin.Flag("grace-period", "Grace period to terminate Pods. Negative values will use the Pod's grace period.").Default("-1s").DurationVar(&gracePeriod)
kingpin.Flag("log-format", "Specify the format of the log messages. Options are text and json. Defaults to text.").Default("text").EnumVar(&logFormat, "text", "json")
kingpin.Flag("log-caller", "Include the calling function name and location in the log messages.").BoolVar(&logCaller)
kingpin.Flag("slack-webhook", "The address of the slack webhook for notifications").StringVar(&slackWebhook)
}

func main() {
Expand Down Expand Up @@ -123,6 +126,7 @@ func main() {
"metricsAddress": metricsAddress,
"gracePeriod": gracePeriod,
"logFormat": logFormat,
"slackWebhook": slackWebhook,
}).Debug("reading config")

log.WithFields(log.Fields{
Expand Down Expand Up @@ -191,6 +195,8 @@ func main() {
"offset": offset / int(time.Hour/time.Second),
}).Info("setting timezone")

notifiers := createNotifier()

chaoskube := chaoskube.New(
client,
labelSelector,
Expand All @@ -208,6 +214,7 @@ func main() {
dryRun,
terminator.NewDeletePodTerminator(client, log.StandardLogger(), gracePeriod),
maxKill,
notifiers,
)

if metricsAddress != "" {
Expand Down Expand Up @@ -277,6 +284,15 @@ func parseSelector(str string) labels.Selector {
return selector
}

func createNotifier() notifier.Notifier {
notifiers := notifier.New()
if slackWebhook != "" {
notifiers.Add(notifier.NewSlackNotifier(slackWebhook))
}

return notifiers
}

func serveMetrics() {
http.Handle("/metrics", promhttp.Handler())
http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
Expand Down
12 changes: 12 additions & 0 deletions notifier/noop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package notifier

const NotifierNoop = "noop"

type Noop struct {
Calls int
}

func (t *Noop) NotifyTermination(termination Termination) error {
t.Calls++
return nil
}
31 changes: 31 additions & 0 deletions notifier/notifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package notifier

type Notifier interface {
NotifyTermination(term Termination) error
}

type Termination struct {
Pod string
Namespace string
}

type Notifiers struct {
notifiers []Notifier
}

func New() *Notifiers {
return &Notifiers{notifiers: make([]Notifier, 0)}
}

func (m *Notifiers) NotifyTermination(term Termination) error {
for _, n := range m.notifiers {
if err := n.NotifyTermination(term); err != nil {
return err
linki marked this conversation as resolved.
Show resolved Hide resolved
}
}
return nil
}

func (m *Notifiers) Add(notifier Notifier) {
m.notifiers = append(m.notifiers, notifier)
}
48 changes: 48 additions & 0 deletions notifier/notifier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package notifier

import (
"testing"
)

func TestMultiNotifierWithoutNotifiers(t *testing.T) {
manager := New()
err := manager.NotifyTermination(Termination{})
if err != nil {
t.Fatal(err)
}
}

func TestMultiNotifierWithNotifier(t *testing.T) {
manager := New()
n := Noop{}
manager.Add(&n)
err := manager.NotifyTermination(Termination{})
if err != nil {
t.Fatal(err)
}

if n.Calls != 1 {
t.Errorf("expected %d calls to notifier but got %d", 1, n.Calls)
}
}

func TestMultiNotifierWithMultipleNotifier(t *testing.T) {
manager := New()
n1 := Noop{}
n2 := Noop{}
manager.Add(&n1)
manager.Add(&n2)

err := manager.NotifyTermination(Termination{})
if err != nil {
t.Fatal(err)
}

if n1.Calls != 1 {
t.Errorf("expected %d calls to notifier n1 but got %d", 1, n1.Calls)
}

if n2.Calls != 1 {
t.Errorf("expected %d calls to notifier n2 but got %d", 1, n2.Calls)
}
}
Loading