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

Add basic auth support to grafana provider #334

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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