diff --git a/controllers/provider_controller.go b/controllers/provider_controller.go index d5de804e1..3f8e467d1 100644 --- a/controllers/provider_controller.go +++ b/controllers/provider_controller.go @@ -149,6 +149,8 @@ func (r *ProviderReconciler) reconcile(ctx context.Context, obj *v1beta1.Provide func (r *ProviderReconciler) validate(ctx context.Context, provider *v1beta1.Provider) error { address := provider.Spec.Address proxy := provider.Spec.Proxy + username := provider.Spec.Username + password := "" token := "" headers := make(map[string]string) if provider.Spec.SecretRef != nil { @@ -163,6 +165,10 @@ func (r *ProviderReconciler) validate(ctx context.Context, provider *v1beta1.Pro address = string(a) } + if p, ok := secret.Data["password"]; ok { + password = string(p) + } + if p, ok := secret.Data["proxy"]; ok { proxy = string(p) } @@ -171,6 +177,10 @@ func (r *ProviderReconciler) validate(ctx context.Context, provider *v1beta1.Pro token = string(t) } + if u, ok := secret.Data["username"]; ok { + username = string(u) + } + if h, ok := secret.Data["headers"]; ok { err := yaml.Unmarshal(h, headers) if err != nil { @@ -204,7 +214,7 @@ func (r *ProviderReconciler) validate(ctx context.Context, provider *v1beta1.Pro } } - factory := notifier.NewFactory(address, proxy, provider.Spec.Username, provider.Spec.Channel, token, headers, certPool) + factory := notifier.NewFactory(address, proxy, username, provider.Spec.Channel, token, headers, certPool, password) if _, err := factory.Notifier(provider.Spec.Type); err != nil { return fmt.Errorf("failed to initialize provider, error: %w", err) } diff --git a/docs/spec/v1beta1/provider.md b/docs/spec/v1beta1/provider.md index 74d036038..9f3139983 100644 --- a/docs/spec/v1beta1/provider.md +++ b/docs/spec/v1beta1/provider.md @@ -431,6 +431,14 @@ kubectl create secret generic grafana-token \ --from-literal=address=https:///api/annotations ``` +Grafana can also use `basic authorization` to authenticate the requests, if both token and +username/password are set in the secret, then `API token` takes precedence over `basic auth`. +```shell +kubectl create secret generic grafana-token \ +--from-literal=username= \ +--from-literal=password= +``` + Then reference the secret in `spec.secretRef`: ```yaml diff --git a/internal/notifier/factory.go b/internal/notifier/factory.go index e94bd2048..4de619a46 100644 --- a/internal/notifier/factory.go +++ b/internal/notifier/factory.go @@ -31,9 +31,10 @@ type Factory struct { Token string Headers map[string]string CertPool *x509.CertPool + Password string } -func NewFactory(url string, proxy string, username string, channel string, token string, headers map[string]string, certPool *x509.CertPool) *Factory { +func NewFactory(url string, proxy string, username string, channel string, token string, headers map[string]string, certPool *x509.CertPool, password string) *Factory { return &Factory{ URL: url, ProxyURL: proxy, @@ -42,6 +43,7 @@ func NewFactory(url string, proxy string, username string, channel string, token Token: token, Headers: headers, CertPool: certPool, + Password: password, } } @@ -90,7 +92,7 @@ func (f Factory) Notifier(provider string) (Interface, error) { case v1beta1.AlertManagerProvider: n, err = NewAlertmanager(f.URL, f.ProxyURL, f.CertPool) case v1beta1.GrafanaProvider: - n, err = NewGrafana(f.URL, f.ProxyURL, f.Token, f.CertPool) + n, err = NewGrafana(f.URL, f.ProxyURL, f.Token, f.CertPool, f.Username, f.Password) default: err = fmt.Errorf("provider %s not supported", provider) } diff --git a/internal/notifier/grafana.go b/internal/notifier/grafana.go index 6c6f2c010..a6cc13ffa 100644 --- a/internal/notifier/grafana.go +++ b/internal/notifier/grafana.go @@ -26,24 +26,24 @@ import ( "github.com/hashicorp/go-retryablehttp" ) -// Discord holds the hook URL type Grafana struct { URL string Token string ProxyURL string CertPool *x509.CertPool + Username string + Password string } // GraphiteAnnotation represents a Grafana API annotation in Graphite format type GraphitePayload struct { - //What string `json:"what"` //optional When int64 `json:"when"` //optional unix timestamp (ms) Text string `json:"text"` Tags []string `json:"tags,omitempty"` } // NewGrafana validates the Grafana URL and returns a Grafana object -func NewGrafana(URL string, proxyURL string, token string, certPool *x509.CertPool) (*Grafana, error) { +func NewGrafana(URL string, proxyURL string, token string, certPool *x509.CertPool, username string, password string) (*Grafana, error) { _, err := url.ParseRequestURI(URL) if err != nil { return nil, fmt.Errorf("invalid Grafana URL %s", URL) @@ -54,33 +54,36 @@ func NewGrafana(URL string, proxyURL string, token string, certPool *x509.CertPo ProxyURL: proxyURL, Token: token, CertPool: certPool, + Username: username, + Password: password, }, nil } // Post annotation -func (s *Grafana) Post(event events.Event) error { +func (g *Grafana) Post(event events.Event) error { // Skip any update events if isCommitStatus(event.Metadata, "update") { return nil } sfields := make([]string, 0, len(event.Metadata)) - sfields = append(sfields, "flux") - sfields = append(sfields, event.ReportingController) + // add tag to filter on grafana + sfields = append(sfields, "flux", event.ReportingController) for k, v := range event.Metadata { sfields = append(sfields, fmt.Sprintf("%s: %s", k, v)) } - // add tag to filter on grafana - payload := GraphitePayload{ When: event.Timestamp.Unix(), Text: fmt.Sprintf("%s/%s.%s", strings.ToLower(event.InvolvedObject.Kind), event.InvolvedObject.Name, event.InvolvedObject.Namespace), Tags: sfields, } - err := postMessage(s.URL, s.ProxyURL, s.CertPool, payload, func(request *retryablehttp.Request) { - if s.Token != "" { - request.Header.Add("Authorization", "Bearer "+s.Token) + err := postMessage(g.URL, g.ProxyURL, g.CertPool, payload, func(request *retryablehttp.Request) { + if (g.Username != "" && g.Password != "") && g.Token == "" { + request.Header.Add("Authorization", "Basic "+basicAuth(g.Username, g.Password)) + } + if g.Token != "" { + request.Header.Add("Authorization", "Bearer "+g.Token) } }) if err != nil { diff --git a/internal/notifier/grafana_test.go b/internal/notifier/grafana_test.go index 8705c38dc..d6eb0e328 100644 --- a/internal/notifier/grafana_test.go +++ b/internal/notifier/grafana_test.go @@ -23,27 +23,30 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGrafana_Post(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - b, err := io.ReadAll(r.Body) + t.Run("Successfully post and expect 200 ok", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + var payload = GraphitePayload{} + err = json.Unmarshal(b, &payload) + require.NoError(t, err) + + require.Equal(t, "gitrepository/webapp.gitops-system", payload.Text) + require.Equal(t, "flux", payload.Tags[0]) + require.Equal(t, "source-controller", payload.Tags[1]) + require.Equal(t, "test: metadata", payload.Tags[2]) + })) + defer ts.Close() + + grafana, err := NewGrafana(ts.URL, "", "", nil, "", "") require.NoError(t, err) - var payload = GraphitePayload{} - err = json.Unmarshal(b, &payload) - require.NoError(t, err) - - require.Equal(t, "gitrepository/webapp.gitops-system", payload.Text) - require.Equal(t, "flux", payload.Tags[0]) - require.Equal(t, "source-controller", payload.Tags[1]) - require.Equal(t, "test: metadata", payload.Tags[2]) - })) - defer ts.Close() - - grafana, err := NewGrafana(ts.URL, "", "", nil) - require.NoError(t, err) - err = grafana.Post(testEvent()) - require.NoError(t, err) + err = grafana.Post(testEvent()) + assert.NoError(t, err) + }) } diff --git a/internal/notifier/util.go b/internal/notifier/util.go index 3bd635339..631467f94 100644 --- a/internal/notifier/util.go +++ b/internal/notifier/util.go @@ -18,6 +18,7 @@ package notifier import ( "crypto/sha1" + "encoding/base64" "fmt" "strings" "unicode" @@ -120,3 +121,8 @@ func sha1String(str string) string { bs := []byte(str) return fmt.Sprintf("%x", sha1.Sum(bs)) } + +func basicAuth(username, password string) string { + auth := username + ":" + password + return base64.StdEncoding.EncodeToString([]byte(auth)) +} diff --git a/internal/notifier/util_test.go b/internal/notifier/util_test.go index c0e248ec9..397d0f8a6 100644 --- a/internal/notifier/util_test.go +++ b/internal/notifier/util_test.go @@ -116,3 +116,10 @@ func TestUtil_Sha1String(t *testing.T) { s := sha1String(str) require.Equal(t, "12ea142172e98435e16336acbbed8919610922c3", s) } + +func TestUtil_BasicAuth(t *testing.T) { + username := "user" + password := "password" + s := basicAuth(username, password) + require.Equal(t, "dXNlcjpwYXNzd29yZA==", s) +} diff --git a/internal/server/event_handlers.go b/internal/server/event_handlers.go index a1abf5646..7c2d326e3 100644 --- a/internal/server/event_handlers.go +++ b/internal/server/event_handlers.go @@ -146,8 +146,10 @@ func (s *EventServer) handleEvent() func(w http.ResponseWriter, r *http.Request) } webhook := provider.Spec.Address + username := provider.Spec.Username + proxy := provider.Spec.Proxy token := "" - proxy := provider.Spec.Address + password := "" headers := make(map[string]string) if provider.Spec.SecretRef != nil { var secret corev1.Secret @@ -166,6 +168,10 @@ func (s *EventServer) handleEvent() func(w http.ResponseWriter, r *http.Request) webhook = string(address) } + if p, ok := secret.Data["password"]; ok { + password = string(p) + } + if p, ok := secret.Data["proxy"]; ok { proxy = string(p) } @@ -174,6 +180,10 @@ func (s *EventServer) handleEvent() func(w http.ResponseWriter, r *http.Request) token = string(t) } + if u, ok := secret.Data["username"]; ok { + username = string(u) + } + if h, ok := secret.Data["headers"]; ok { err := yaml.Unmarshal(h, &headers) if err != nil { @@ -228,7 +238,7 @@ func (s *EventServer) handleEvent() func(w http.ResponseWriter, r *http.Request) continue } - factory := notifier.NewFactory(webhook, proxy, provider.Spec.Username, provider.Spec.Channel, token, headers, certPool) + factory := notifier.NewFactory(webhook, proxy, username, provider.Spec.Channel, token, headers, certPool, password) sender, err := factory.Notifier(provider.Spec.Type) if err != nil { s.logger.Error(err, "failed to initialize provider",