Merge pull request #107 from rusenask/feature/notification_levels

Feature/notification levels
pull/106/head
Karolis Rusenas 2017-10-05 12:48:35 +01:00 committed by GitHub
commit b08b8b9af3
8 changed files with 352 additions and 33 deletions

View File

@ -15,3 +15,6 @@ const (
EnvSlackBotName = "SLACK_BOT_NAME"
EnvSlackChannels = "SLACK_CHANNELS"
)
// EnvNotificationLevel - minimum level for notifications, defaults to info
const EnvNotificationLevel = "NOTIFICATION_LEVEL"

View File

@ -32,6 +32,7 @@ var (
// notifiers.
type Config struct {
Attempts int
Level types.Level
Params map[string]interface{} `yaml:",inline"`
}
@ -76,6 +77,7 @@ func RegisterSender(name string, s Sender) {
type DefaultNotificationSender struct {
config *Config
stopper *stopper.Stopper
level types.Level
}
// New - create new sender
@ -118,6 +120,10 @@ func (m *DefaultNotificationSender) Senders() map[string]Sender {
// Send - send notifications through all configured senders
func (m *DefaultNotificationSender) Send(event types.EventNotification) error {
if event.Level < m.config.Level {
return nil
}
sendersM.RLock()
defer sendersM.RUnlock()

View File

@ -0,0 +1,129 @@
package notification
import (
"context"
"fmt"
"testing"
"github.com/rusenask/keel/types"
)
type fakeSender struct {
sent *types.EventNotification
shouldConfigure bool
shouldError error
}
func (s *fakeSender) Configure(*Config) (bool, error) {
return s.shouldConfigure, nil
}
func (s *fakeSender) Send(event types.EventNotification) error {
s.sent = &event
fmt.Println("sending event")
return s.shouldError
}
func TestSend(t *testing.T) {
sndr := New(context.Background())
sndr.Configure(&Config{
Level: types.LevelDebug,
Attempts: 1,
})
fs := &fakeSender{
shouldConfigure: true,
shouldError: nil,
}
RegisterSender("fakeSender", fs)
defer sndr.UnregisterSender("fakeSender")
err := sndr.Send(types.EventNotification{
Level: types.LevelInfo,
Type: types.NotificationPreDeploymentUpdate,
Message: "foo",
})
if err != nil {
t.Errorf("unexpected error: %s", err)
}
if fs.sent.Message != "foo" {
t.Errorf("unexpected notification message: %s", fs.sent.Message)
}
if fs.sent.Level != types.LevelInfo {
t.Errorf("unexpected level: %s", fs.sent.Level)
}
}
// test when configured level is higher than the event
func TestSendLevelNotificationA(t *testing.T) {
sndr := New(context.Background())
sndr.Configure(&Config{
Level: types.LevelInfo,
Attempts: 1,
})
fs := &fakeSender{
shouldConfigure: true,
shouldError: nil,
}
RegisterSender("fakeSender", fs)
defer sndr.UnregisterSender("fakeSender")
err := sndr.Send(types.EventNotification{
Level: types.LevelDebug,
Type: types.NotificationPreDeploymentUpdate,
Message: "foo",
})
if err != nil {
t.Errorf("unexpected error: %s", err)
}
if fs.sent != nil {
t.Errorf("didn't expect to find sent even for this level")
}
}
// event level is higher than the configured
func TestSendLevelNotificationB(t *testing.T) {
sndr := New(context.Background())
sndr.Configure(&Config{
Level: types.LevelInfo,
Attempts: 1,
})
fs := &fakeSender{
shouldConfigure: true,
shouldError: nil,
}
RegisterSender("fakeSender", fs)
defer sndr.UnregisterSender("fakeSender")
err := sndr.Send(types.EventNotification{
Level: types.LevelSuccess,
Type: types.NotificationPreDeploymentUpdate,
Message: "foo",
})
if err != nil {
t.Errorf("unexpected error: %s", err)
}
if fs.sent.Message != "foo" {
t.Errorf("unexpected notification message: %s", fs.sent.Message)
}
if fs.sent.Level != types.LevelSuccess {
t.Errorf("unexpected level: %s", fs.sent.Level)
}
}

View File

@ -25,7 +25,7 @@ func TestWebhookRequest(t *testing.T) {
t.Errorf("missing deployment type")
}
if !strings.Contains(bodyStr, "LevelDebug") {
if !strings.Contains(bodyStr, "debug") {
t.Errorf("missing level")
}

13
main.go
View File

@ -74,8 +74,21 @@ func main() {
ctx, cancel := netContext.WithCancel(context.Background())
defer cancel()
notificationLevel := types.LevelInfo
if os.Getenv(constants.EnvNotificationLevel) != "" {
parsedLevel, err := types.ParseLevel(os.Getenv(constants.EnvNotificationLevel))
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Errorf("main: got error while parsing notification level, defaulting to: %s", notificationLevel)
} else {
notificationLevel = parsedLevel
}
}
notifCfg := &notification.Config{
Attempts: 10,
Level: notificationLevel,
}
sender := notification.New(ctx)

View File

@ -18,7 +18,9 @@ import (
)
// const dockerConfigJSONKey = ".dockerconfigjson"
const dockerConfigJSONKey = ".dockercfg"
const dockerConfigKey = ".dockercfg"
const dockerConfigJSONKey = ".dockerconfigjson"
// common errors
var (
@ -129,7 +131,58 @@ func (g *DefaultGetter) getCredentialsFromSecret(image *types.TrackedImage) (*ty
continue
}
if secret.Type != v1.SecretTypeDockercfg {
dockerCfg := make(DockerCfg)
switch secret.Type {
case v1.SecretTypeDockercfg:
secretDataBts, ok := secret.Data[dockerConfigKey]
if !ok {
log.WithFields(log.Fields{
"image": image.Image.Repository(),
"namespace": image.Namespace,
"secret_ref": secretRef,
"type": secret.Type,
"data": secret.Data,
}).Warn("secrets.defaultGetter: secret is missing key '.dockerconfig', ensure that key exists")
continue
}
dockerCfg, err = decodeSecret(secretDataBts)
if err != nil {
log.WithFields(log.Fields{
"image": image.Image.Repository(),
"namespace": image.Namespace,
"secret_ref": secretRef,
"secret_data": string(secretDataBts),
"error": err,
}).Error("secrets.defaultGetter: failed to decode secret")
continue
}
case v1.SecretTypeDockerConfigJson:
secretDataBts, ok := secret.Data[dockerConfigJSONKey]
if !ok {
log.WithFields(log.Fields{
"image": image.Image.Repository(),
"namespace": image.Namespace,
"secret_ref": secretRef,
"type": secret.Type,
"data": secret.Data,
}).Warn("secrets.defaultGetter: secret is missing key '.dockerconfigjson', ensure that key exists")
continue
}
dockerCfg, err = decodeJSONSecret(secretDataBts)
if err != nil {
log.WithFields(log.Fields{
"image": image.Image.Repository(),
"namespace": image.Namespace,
"secret_ref": secretRef,
"secret_data": string(secretDataBts),
"error": err,
}).Error("secrets.defaultGetter: failed to decode secret")
continue
}
default:
log.WithFields(log.Fields{
"image": image.Image.Repository(),
"namespace": image.Namespace,
@ -139,29 +192,6 @@ func (g *DefaultGetter) getCredentialsFromSecret(image *types.TrackedImage) (*ty
continue
}
secretDataBts, ok := secret.Data[dockerConfigJSONKey]
if !ok {
log.WithFields(log.Fields{
"image": image.Image.Repository(),
"namespace": image.Namespace,
"secret_ref": secretRef,
"type": secret.Type,
"data": secret.Data,
}).Warn("secrets.defaultGetter: secret is missing key '.dockerconfigjson', ensure that key exists")
continue
}
dockerCfg, err := decodeSecret(secretDataBts)
if err != nil {
log.WithFields(log.Fields{
"image": image.Image.Repository(),
"namespace": image.Namespace,
"secret_ref": secretRef,
"secret_data": string(secretDataBts),
"error": err,
}).Error("secrets.defaultGetter: failed to decode secret")
continue
}
// looking for our registry
for registry, auth := range dockerCfg {
h, err := hostname(registry)
@ -246,11 +276,15 @@ func decodeBase64Secret(authSecret string) (username, password string, err error
}
func hostname(registry string) (string, error) {
u, err := url.Parse(registry)
if err != nil {
return "", err
if strings.HasPrefix(registry, "http://") || strings.HasPrefix(registry, "https://") {
u, err := url.Parse(registry)
if err != nil {
return "", err
}
return u.Hostname(), nil
}
return u.Hostname(), nil
return registry, nil
}
func decodeSecret(data []byte) (DockerCfg, error) {
@ -262,6 +296,20 @@ func decodeSecret(data []byte) (DockerCfg, error) {
return cfg, nil
}
func decodeJSONSecret(data []byte) (DockerCfg, error) {
var cfg DockerCfgJSON
err := json.Unmarshal(data, &cfg)
if err != nil {
return nil, err
}
return cfg.Auths, nil
}
// DockerCfgJSON - secret structure when dockerconfigjson is used
type DockerCfgJSON struct {
Auths DockerCfg `json:"auths"`
}
// DockerCfg - registry_name=auth
type DockerCfg map[string]*Auth

View File

@ -12,6 +12,14 @@ import (
)
var secretDataPayload = `{"https://index.docker.io/v1/":{"username":"user-x","password":"pass-x","email":"karolis.rusenas@gmail.com","auth":"somethinghere"}}`
var secretDockerConfigJSONPayload = `{
"auths": {
"quay.io": {
"auth": "a2VlbHVzZXIra2VlbHRlc3Q6U05NR0lIVlRHUkRLSTZQMTdPTkVWUFBDQUpON1g5Sk1XUDg2ODJLWDA1RDdUQU5SWDRXMDhIUEw5QldRTDAxSg==",
"email": ""
}
}
}`
func mustEncode(data string) string {
return base64.StdEncoding.EncodeToString([]byte(data))
@ -23,7 +31,7 @@ func TestGetSecret(t *testing.T) {
impl := &testutil.FakeK8sImplementer{
AvailableSecret: &v1.Secret{
Data: map[string][]byte{
dockerConfigJSONKey: []byte(secretDataPayload),
dockerConfigKey: []byte(secretDataPayload),
},
Type: v1.SecretTypeDockercfg,
},
@ -51,6 +59,40 @@ func TestGetSecret(t *testing.T) {
}
}
func TestGetDockerConfigJSONSecret(t *testing.T) {
imgRef, _ := image.Parse("quay.io/karolisr/webhook-demo:0.0.11")
impl := &testutil.FakeK8sImplementer{
AvailableSecret: &v1.Secret{
Data: map[string][]byte{
dockerConfigJSONKey: []byte(secretDockerConfigJSONPayload),
},
Type: v1.SecretTypeDockerConfigJson,
},
}
getter := NewGetter(impl)
trackedImage := &types.TrackedImage{
Image: imgRef,
Namespace: "default",
Secrets: []string{"myregistrysecret"},
}
creds, err := getter.Get(trackedImage)
if err != nil {
t.Errorf("failed to get creds: %s", err)
}
if creds.Username != "keeluser+keeltest" {
t.Errorf("unexpected username: %s", creds.Username)
}
if creds.Password != "SNMGIHVTGRDKI6P17ONEVPPCAJN7X9JMWP8682KX05D7TANRX4W08HPL9BWQL01J" {
t.Errorf("unexpected pass: %s", creds.Password)
}
}
func TestGetSecretNotFound(t *testing.T) {
imgRef, _ := image.Parse("karolisr/webhook-demo:0.0.11")
@ -100,7 +142,7 @@ func TestLookupHelmSecret(t *testing.T) {
},
AvailableSecret: &v1.Secret{
Data: map[string][]byte{
dockerConfigJSONKey: []byte(fmt.Sprintf(secretDataPayloadEncoded, mustEncode("user-y:pass-y"))),
dockerConfigKey: []byte(fmt.Sprintf(secretDataPayloadEncoded, mustEncode("user-y:pass-y"))),
},
Type: v1.SecretTypeDockercfg,
},
@ -146,7 +188,7 @@ func TestLookupHelmEncodedSecret(t *testing.T) {
},
AvailableSecret: &v1.Secret{
Data: map[string][]byte{
dockerConfigJSONKey: []byte(secretDataPayload),
dockerConfigKey: []byte(secretDataPayload),
},
Type: v1.SecretTypeDockercfg,
},
@ -265,3 +307,40 @@ func Test_decodeBase64Secret(t *testing.T) {
})
}
}
func Test_hostname(t *testing.T) {
type args struct {
registry string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "dockerhub",
args: args{registry: "https://index.docker.io/v1/"},
want: "index.docker.io",
wantErr: false,
},
{
name: "quay",
args: args{registry: "quay.io"},
want: "quay.io",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := hostname(tt.args.registry)
if (err != nil) != tt.wantErr {
t.Errorf("hostname() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("hostname() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -9,6 +9,7 @@ package types
import (
"bytes"
"fmt"
"strings"
"time"
)
@ -224,6 +225,46 @@ const (
LevelFatal
)
// ParseLevel takes a string level and returns notification level constant.
func ParseLevel(lvl string) (Level, error) {
switch strings.ToLower(lvl) {
case "fatal":
return LevelFatal, nil
case "error":
return LevelError, nil
case "warn", "warning":
return LevelWarn, nil
case "info":
return LevelInfo, nil
case "success":
return LevelSuccess, nil
case "debug":
return LevelDebug, nil
}
var l Level
return l, fmt.Errorf("not a valid notification Level: %q", lvl)
}
func (l Level) String() string {
switch l {
case LevelDebug:
return "debug"
case LevelInfo:
return "info"
case LevelSuccess:
return "success"
case LevelWarn:
return "warn"
case LevelError:
return "error"
case LevelFatal:
return "fatal"
default:
return "unknown"
}
}
// Color - used to assign different colors for events
func (l Level) Color() string {
switch l {