Repository: fluxcd/notification-controller
Pull request #317 (2022-01-26 09:33:25)

Pass headers to generic provider through secretRef
Closes: #312 
Supersedes: #291 

Signed-off-by: Somtochi Onyekwere <somtochionyekwere@gmail.com>


Patch:
diff --git a/config/samples/notification_v1beta1_alert.yaml b/config/samples/notification_v1beta1_alert.yaml
index df95eb4..2858565 100644
--- a/config/samples/notification_v1beta1_alert.yaml
+++ b/config/samples/notification_v1beta1_alert.yaml
@@ -4,7 +4,7 @@ metadata:
   name: alert-sample
 spec:
   providerRef:
-    name: provider-sample
+    name: slack-provider-sample
   eventSeverity: info
   eventSources:
     - kind: GitRepository
diff --git a/config/samples/notification_v1beta1_provider.yaml b/config/samples/notification_v1beta1_provider.yaml
index 37b71d1..12390a9 100644
--- a/config/samples/notification_v1beta1_provider.yaml
+++ b/config/samples/notification_v1beta1_provider.yaml
@@ -1,7 +1,7 @@
 apiVersion: notification.toolkit.fluxcd.io/v1beta1
 kind: Provider
 metadata:
-  name: provider-sample
+  name: slack-provider-sample
 spec:
   type: slack
   channel: general
@@ -14,3 +14,21 @@ metadata:
   name: slack-url
 data:
   address: aHR0cHM6Ly9ob29rcy5zbGFjay5jb20vc2VydmljZXMv
+---
+apiVersion: notification.toolkit.fluxcd.io/v1beta1
+kind: Provider
+metadata:
+  name: generic-provider-sample
+spec:
+  type: generic
+  address: https://api.github.com/repos/fluxcd/notification-controller/dispatches
+  secretRef:
+    name: generic-secret
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: generic-secret
+stringData:
+  headers: |
+    Authorization: token
diff --git a/controllers/provider_controller.go b/controllers/provider_controller.go
index 7d9ebf3..e3aa883 100644
--- a/controllers/provider_controller.go
+++ b/controllers/provider_controller.go
@@ -31,6 +31,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
+	"sigs.k8s.io/yaml"
 
 	"github.com/fluxcd/pkg/apis/meta"
 	"github.com/fluxcd/pkg/runtime/conditions"
@@ -148,6 +149,7 @@ 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
 	token := ""
+	headers := make(map[string]string)
 	if provider.Spec.SecretRef != nil {
 		var secret corev1.Secret
 		secretName := types.NamespacedName{Namespace: provider.Namespace, Name: provider.Spec.SecretRef.Name}
@@ -163,6 +165,13 @@ func (r *ProviderReconciler) validate(ctx context.Context, provider *v1beta1.Pro
 		if t, ok := secret.Data["token"]; ok {
 			token = string(t)
 		}
+
+		if h, ok := secret.Data["headers"]; ok {
+			err := yaml.Unmarshal(h, headers)
+			if err != nil {
+				return fmt.Errorf("failed to read headers from secret, error: %w", err)
+			}
+		}
 	}
 
 	if address == "" {
@@ -190,7 +199,7 @@ func (r *ProviderReconciler) validate(ctx context.Context, provider *v1beta1.Pro
 		}
 	}
 
-	factory := notifier.NewFactory(address, provider.Spec.Proxy, provider.Spec.Username, provider.Spec.Channel, token, certPool)
+	factory := notifier.NewFactory(address, provider.Spec.Proxy, provider.Spec.Username, provider.Spec.Channel, token, headers, certPool)
 	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 00aac0a..107c928 100644
--- a/docs/spec/v1beta1/provider.md
+++ b/docs/spec/v1beta1/provider.md
@@ -163,6 +163,32 @@ The body of the request looks like this:
 
 The `involvedObject` key contains the object that triggered the event.
 
+You can add additional headers to the POST request by providing a `headers` field to the secret
+referenced by the provider. An example is given below:
+
+```yaml
+apiVersion: notification.toolkit.fluxcd.io/v1beta1
+kind: Provider
+metadata:
+  name: generic
+  namespace: default
+spec:
+  type: generic
+  address: https://api.github.com/repos/owner/repo/dispatches
+  secretRef: 
+    name: generic-secret
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: generic-secret
+  namespace: default
+stringData:
+  headers: |
+     Authorization: token
+     X-Forwarded-Proto: https
+```
+
 ### Self-signed certificates
 
 The `certSecretRef` field names a secret with TLS certificate data. This is for the purpose
diff --git a/go.mod b/go.mod
index 6f5e2b7..ff0c79b 100644
--- a/go.mod
+++ b/go.mod
@@ -123,7 +123,7 @@ require (
 	sigs.k8s.io/kustomize/api v0.10.1 // indirect
 	sigs.k8s.io/kustomize/kyaml v0.13.0 // indirect
 	sigs.k8s.io/structured-merge-diff/v4 v4.2.0 // indirect
-	sigs.k8s.io/yaml v1.3.0 // indirect
+	sigs.k8s.io/yaml v1.3.0
 )
 
 // Fix for CVE-2020-29652: https://github.com/golang/crypto/commit/8b5274cf687fd9316b4108863654cc57385531e8
diff --git a/internal/notifier/factory.go b/internal/notifier/factory.go
index e07a344..75f9dc9 100644
--- a/internal/notifier/factory.go
+++ b/internal/notifier/factory.go
@@ -29,16 +29,18 @@ type Factory struct {
 	Username string
 	Channel  string
 	Token    string
+	Headers  map[string]string
 	CertPool *x509.CertPool
 }
 
-func NewFactory(url string, proxy string, username string, channel string, token string, certPool *x509.CertPool) *Factory {
+func NewFactory(url string, proxy string, username string, channel string, token string, headers map[string]string, certPool *x509.CertPool) *Factory {
 	return &Factory{
 		URL:      url,
 		ProxyURL: proxy,
 		Channel:  channel,
 		Username: username,
 		Token:    token,
+		Headers:  headers,
 		CertPool: certPool,
 	}
 }
@@ -52,7 +54,7 @@ func (f Factory) Notifier(provider string) (Interface, error) {
 	var err error
 	switch provider {
 	case v1beta1.GenericProvider:
-		n, err = NewForwarder(f.URL, f.ProxyURL, f.CertPool)
+		n, err = NewForwarder(f.URL, f.ProxyURL, f.Headers, f.CertPool)
 	case v1beta1.SlackProvider:
 		n, err = NewSlack(f.URL, f.ProxyURL, f.Token, f.CertPool, f.Username, f.Channel)
 	case v1beta1.DiscordProvider:
diff --git a/internal/notifier/forwarder.go b/internal/notifier/forwarder.go
index 8b89076..6107a94 100644
--- a/internal/notifier/forwarder.go
+++ b/internal/notifier/forwarder.go
@@ -35,10 +35,11 @@ const NotificationHeader = "gotk-component"
 type Forwarder struct {
 	URL      string
 	ProxyURL string
+	Headers  map[string]string
 	CertPool *x509.CertPool
 }
 
-func NewForwarder(hookURL string, proxyURL string, certPool *x509.CertPool) (*Forwarder, error) {
+func NewForwarder(hookURL string, proxyURL string, headers map[string]string, certPool *x509.CertPool) (*Forwarder, error) {
 	if _, err := url.ParseRequestURI(hookURL); err != nil {
 		return nil, fmt.Errorf("invalid hook URL %s: %w", hookURL, err)
 	}
@@ -46,6 +47,7 @@ func NewForwarder(hookURL string, proxyURL string, certPool *x509.CertPool) (*Fo
 	return &Forwarder{
 		URL:      hookURL,
 		ProxyURL: proxyURL,
+		Headers:  headers,
 		CertPool: certPool,
 	}, nil
 }
@@ -53,6 +55,9 @@ func NewForwarder(hookURL string, proxyURL string, certPool *x509.CertPool) (*Fo
 func (f *Forwarder) Post(event events.Event) error {
 	err := postMessage(f.URL, f.ProxyURL, f.CertPool, event, func(req *retryablehttp.Request) {
 		req.Header.Set(NotificationHeader, event.ReportingController)
+		for key, val := range f.Headers {
+			req.Header.Set(key, val)
+		}
 	})
 
 	if err != nil {
diff --git a/internal/server/event_handlers.go b/internal/server/event_handlers.go
index 6173b51..e7da7e4 100644
--- a/internal/server/event_handlers.go
+++ b/internal/server/event_handlers.go
@@ -30,6 +30,7 @@ import (
 	"github.com/fluxcd/pkg/runtime/conditions"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/yaml"
 
 	"github.com/fluxcd/pkg/runtime/events"
 
@@ -139,6 +140,7 @@ func (s *EventServer) handleEvent() func(w http.ResponseWriter, r *http.Request)
 
 			webhook := provider.Spec.Address
 			token := ""
+			headers := make(map[string]string)
 			if provider.Spec.SecretRef != nil {
 				var secret corev1.Secret
 				secretName := types.NamespacedName{Namespace: alert.Namespace, Name: provider.Spec.SecretRef.Name}
@@ -159,6 +161,17 @@ func (s *EventServer) handleEvent() func(w http.ResponseWriter, r *http.Request)
 				if t, ok := secret.Data["token"]; ok {
 					token = string(t)
 				}
+
+				if h, ok := secret.Data["headers"]; ok {
+					err := yaml.Unmarshal(h, &headers)
+					if err != nil {
+						s.logger.Error(err, "failed to read headers from secret",
+							"reconciler kind", v1beta1.ProviderKind,
+							"name", providerName.Name,
+							"namespace", providerName.Namespace)
+						continue
+					}
+				}
 			}
 
 			var certPool *x509.CertPool
@@ -203,7 +216,7 @@ func (s *EventServer) handleEvent() func(w http.ResponseWriter, r *http.Request)
 				continue
 			}
 
-			factory := notifier.NewFactory(webhook, provider.Spec.Proxy, provider.Spec.Username, provider.Spec.Channel, token, certPool)
+			factory := notifier.NewFactory(webhook, provider.Spec.Proxy, provider.Spec.Username, provider.Spec.Channel, token, headers, certPool)
 			sender, err := factory.Notifier(provider.Spec.Type)
 			if err != nil {
 				s.logger.Error(err, "failed to initialize provider",


Test patch:
diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml
index 1a4eddf..8993d60 100644
--- a/.github/workflows/e2e.yaml
+++ b/.github/workflows/e2e.yaml
@@ -95,7 +95,8 @@ jobs:
       - name: Run smoke tests
         run: |
           kubectl -n notification-system apply -f ./config/samples
-          kubectl -n notification-system wait provider/provider-sample --for=condition=ready --timeout=1m
+          kubectl -n notification-system wait provider/slack-provider-sample --for=condition=ready --timeout=1m
+          kubectl -n notification-system wait provider/generic-provider-sample --for=condition=ready --timeout=1m
           kubectl -n notification-system wait alert/alert-sample --for=condition=ready --timeout=1m
           kubectl -n notification-system wait receiver/receiver-sample --for=condition=ready --timeout=1m
       - name: Logs
diff --git a/internal/notifier/forwarder_test.go b/internal/notifier/forwarder_test.go
index b535937..a1860bc 100644
--- a/internal/notifier/forwarder_test.go
+++ b/internal/notifier/forwarder_test.go
@@ -34,6 +34,7 @@ func TestForwarder_Post(t *testing.T) {
 		require.NoError(t, err)
 
 		require.Equal(t, "source-controller", r.Header.Get("gotk-component"))
+		require.Equal(t, "token", r.Header.Get("Authorization"))
 		var payload = events.Event{}
 		err = json.Unmarshal(b, &payload)
 		require.NoError(t, err)
@@ -42,7 +43,9 @@ func TestForwarder_Post(t *testing.T) {
 	}))
 	defer ts.Close()
 
-	forwarder, err := NewForwarder(ts.URL, "", nil)
+	headers := make(map[string]string)
+	headers["Authorization"] = "token"
+	forwarder, err := NewForwarder(ts.URL, "", headers, nil)
 	require.NoError(t, err)
 
 	err = forwarder.Post(testEvent())
diff --git a/tests/fuzz/forwarder_fuzzer.go b/tests/fuzz/forwarder_fuzzer.go
index 5e9787e..796d1eb 100644
--- a/tests/fuzz/forwarder_fuzzer.go
+++ b/tests/fuzz/forwarder_fuzzer.go
@@ -36,7 +36,8 @@ func FuzzForwarder(data []byte) int {
 	}))
 	defer ts.Close()
 
-	forwarder, err := NewForwarder(ts.URL, "", nil)
+	header := make(map[string]string)
+	forwarder, err := NewForwarder(ts.URL, "", header, nil)
 	if err != nil {
 		return 0
 	}