Merge pull request #107 from rusenask/feature/notification_levels
Feature/notification levelspull/106/head
commit
b08b8b9af3
|
@ -15,3 +15,6 @@ const (
|
|||
EnvSlackBotName = "SLACK_BOT_NAME"
|
||||
EnvSlackChannels = "SLACK_CHANNELS"
|
||||
)
|
||||
|
||||
// EnvNotificationLevel - minimum level for notifications, defaults to info
|
||||
const EnvNotificationLevel = "NOTIFICATION_LEVEL"
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
13
main.go
|
@ -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 := ¬ification.Config{
|
||||
Attempts: 10,
|
||||
Level: notificationLevel,
|
||||
}
|
||||
sender := notification.New(ctx)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue