Skip to content

Commit

Permalink
Add basic auth support to grafana provider
Browse files Browse the repository at this point in the history
Signed-off-by: Filipe Sequeira <filipe@weave.works>
  • Loading branch information
fsequeira1 committed Feb 21, 2022
1 parent 6513bef commit c904061
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 33 deletions.
12 changes: 11 additions & 1 deletion controllers/provider_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
8 changes: 8 additions & 0 deletions docs/spec/v1beta1/provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,14 @@ kubectl create secret generic grafana-token \
--from-literal=address=https://<grafana-url>/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=<your-grafana-username> \
--from-literal=password=<your-grafana-password>
```

Then reference the secret in `spec.secretRef`:

```yaml
Expand Down
6 changes: 4 additions & 2 deletions internal/notifier/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -42,6 +43,7 @@ func NewFactory(url string, proxy string, username string, channel string, token
Token: token,
Headers: headers,
CertPool: certPool,
Password: password,
}
}

Expand Down Expand Up @@ -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)
}
Expand Down
25 changes: 14 additions & 11 deletions internal/notifier/grafana.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
37 changes: 20 additions & 17 deletions internal/notifier/grafana_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
6 changes: 6 additions & 0 deletions internal/notifier/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package notifier

import (
"crypto/sha1"
"encoding/base64"
"fmt"
"strings"
"unicode"
Expand Down Expand Up @@ -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))
}
7 changes: 7 additions & 0 deletions internal/notifier/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
14 changes: 12 additions & 2 deletions internal/server/event_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit c904061

Please sign in to comment.