Repository: fluxcd/notification-controller
Pull request #410 (2022-08-29 23:01:56)

Add `.spec.timeout` to the Provider API
This pull request adds a `.spec.timeout` field to the `Provider` CRD. This timeout is used when sending a request to the provider and has a default of 15 seconds.

Signed-off-by: Somtochi Onyekwere <somtochionyekwere@gmail.com>

stefanprodan: @somtochiama can you squash the commit please.

Patch:
diff --git a/api/v1beta1/provider_types.go b/api/v1beta1/provider_types.go
index 63ed966..1b5c093 100644
--- a/api/v1beta1/provider_types.go
+++ b/api/v1beta1/provider_types.go
@@ -17,6 +17,8 @@ limitations under the License.
 package v1beta1
 
 import (
+	"time"
+
 	"github.com/fluxcd/pkg/apis/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
@@ -46,6 +48,10 @@ type ProviderSpec struct {
 	// +optional
 	Address string `json:"address,omitempty"`
 
+	// Timeout for sending alerts to the provider.
+	// +optional
+	Timeout *metav1.Duration `json:"timeout,omitempty"`
+
 	// HTTP/S address of the proxy
 	// +kubebuilder:validation:Pattern="^(http|https)://"
 	// +kubebuilder:validation:Optional
@@ -146,3 +152,12 @@ type ProviderList struct {
 func init() {
 	SchemeBuilder.Register(&Provider{}, &ProviderList{})
 }
+
+func (in *Provider) GetTimeout() time.Duration {
+	duration := 15 * time.Second
+	if in.Spec.Timeout != nil {
+		duration = in.Spec.Timeout.Duration
+	}
+
+	return duration
+}
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index 3b57ae8..c20ff16 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -220,6 +220,11 @@ func (in *ProviderList) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) {
 	*out = *in
+	if in.Timeout != nil {
+		in, out := &in.Timeout, &out.Timeout
+		*out = new(v1.Duration)
+		**out = **in
+	}
 	if in.SecretRef != nil {
 		in, out := &in.SecretRef, &out.SecretRef
 		*out = new(meta.LocalObjectReference)
diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml
index 9605942..377e898 100644
--- a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml
+++ b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml
@@ -81,6 +81,9 @@ spec:
                 description: This flag tells the controller to suspend subsequent
                   events handling. Defaults to false.
                 type: boolean
+              timeout:
+                description: Timeout for sending alerts to the provider.
+                type: string
               type:
                 description: Type of provider
                 enum:
diff --git a/docs/api/notification.md b/docs/api/notification.md
index 8260850..1033671 100644
--- a/docs/api/notification.md
+++ b/docs/api/notification.md
@@ -273,6 +273,20 @@ string
 </tr>
 <tr>
 <td>
+<code>timeout</code><br>
+<em>
+<a href="https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
+Kubernetes meta/v1.Duration
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Timeout for sending alerts to the provider.</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>proxy</code><br>
 <em>
 string
@@ -778,6 +792,20 @@ string
 </tr>
 <tr>
 <td>
+<code>timeout</code><br>
+<em>
+<a href="https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
+Kubernetes meta/v1.Duration
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Timeout for sending alerts to the provider.</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>proxy</code><br>
 <em>
 string
diff --git a/docs/spec/v1beta1/provider.md b/docs/spec/v1beta1/provider.md
index 5bb649d..947df03 100644
--- a/docs/spec/v1beta1/provider.md
+++ b/docs/spec/v1beta1/provider.md
@@ -26,6 +26,10 @@ type ProviderSpec struct {
 	// +optional
 	Address string `json:"address,omitempty"`
 
+	// Timeout for sending alerts to the provider.
+	// +optional
+	Timeout *metav1.Duration `json:"timeout,omitempty"`
+
 	// HTTP/S address of the proxy
 	// +kubebuilder:validation:Pattern="^(http|https)://"
 	// +optional
@@ -109,6 +113,8 @@ spec:
   address: https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK
   # HTTP(S) proxy (optional)
   proxy: https://proxy.corp:8080
+  # timeout (optional)
+  timeout: 30s
   # secret containing the webhook address (optional)
   secretRef:
     name: webhook-url
diff --git a/internal/notifier/alertmanager.go b/internal/notifier/alertmanager.go
index 4ff2fb7..505b3f3 100644
--- a/internal/notifier/alertmanager.go
+++ b/internal/notifier/alertmanager.go
@@ -88,7 +88,7 @@ func (s *Alertmanager) Post(ctx context.Context, event events.Event) error {
 		},
 	}
 
-	err := postMessage(s.URL, s.ProxyURL, s.CertPool, payload)
+	err := postMessage(ctx, s.URL, s.ProxyURL, s.CertPool, payload)
 
 	if err != nil {
 		return fmt.Errorf("postMessage failed: %w", err)
diff --git a/internal/notifier/client.go b/internal/notifier/client.go
index b76f419..c2d3ba6 100644
--- a/internal/notifier/client.go
+++ b/internal/notifier/client.go
@@ -17,6 +17,7 @@ limitations under the License.
 package notifier
 
 import (
+	"context"
 	"crypto/tls"
 	"crypto/x509"
 	"encoding/json"
@@ -33,7 +34,7 @@ import (
 
 type requestOptFunc func(*retryablehttp.Request)
 
-func postMessage(address, proxy string, certPool *x509.CertPool, payload interface{}, reqOpts ...requestOptFunc) error {
+func postMessage(ctx context.Context, address, proxy string, certPool *x509.CertPool, payload interface{}, reqOpts ...requestOptFunc) error {
 	httpClient := retryablehttp.NewClient()
 	if certPool != nil {
 		httpClient.HTTPClient.Transport = &http.Transport{
@@ -84,6 +85,9 @@ func postMessage(address, proxy string, certPool *x509.CertPool, payload interfa
 	if err != nil {
 		return fmt.Errorf("failed to create a new request: %w", err)
 	}
+	if ctx != nil {
+		req = req.WithContext(ctx)
+	}
 	req.Header.Set("Content-Type", "application/json")
 	for _, o := range reqOpts {
 		o(req)
diff --git a/internal/notifier/discord.go b/internal/notifier/discord.go
index c4c4a54..76f6528 100644
--- a/internal/notifier/discord.go
+++ b/internal/notifier/discord.go
@@ -90,7 +90,7 @@ func (s *Discord) Post(ctx context.Context, event events.Event) error {
 
 	payload.Attachments = []SlackAttachment{a}
 
-	err := postMessage(s.URL, s.ProxyURL, nil, payload)
+	err := postMessage(ctx, s.URL, s.ProxyURL, nil, payload)
 	if err != nil {
 		return fmt.Errorf("postMessage failed: %w", err)
 	}
diff --git a/internal/notifier/forwarder.go b/internal/notifier/forwarder.go
index 16af4de..203cca0 100644
--- a/internal/notifier/forwarder.go
+++ b/internal/notifier/forwarder.go
@@ -54,7 +54,7 @@ func NewForwarder(hookURL string, proxyURL string, headers map[string]string, ce
 }
 
 func (f *Forwarder) Post(ctx context.Context, event events.Event) error {
-	err := postMessage(f.URL, f.ProxyURL, f.CertPool, event, func(req *retryablehttp.Request) {
+	err := postMessage(ctx, 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)
diff --git a/internal/notifier/google_chat.go b/internal/notifier/google_chat.go
index 40208c9..e3006de 100644
--- a/internal/notifier/google_chat.go
+++ b/internal/notifier/google_chat.go
@@ -142,7 +142,7 @@ func (s *GoogleChat) Post(ctx context.Context, event events.Event) error {
 		Cards: []GoogleChatCard{card},
 	}
 
-	err := postMessage(s.URL, s.ProxyURL, nil, payload)
+	err := postMessage(ctx, s.URL, s.ProxyURL, nil, payload)
 	if err != nil {
 		return fmt.Errorf("postMessage failed: %w", err)
 	}
diff --git a/internal/notifier/grafana.go b/internal/notifier/grafana.go
index 899c97d..aeee486 100644
--- a/internal/notifier/grafana.go
+++ b/internal/notifier/grafana.go
@@ -79,7 +79,7 @@ func (g *Grafana) Post(ctx context.Context, event events.Event) error {
 		Tags: sfields,
 	}
 
-	err := postMessage(g.URL, g.ProxyURL, g.CertPool, payload, func(request *retryablehttp.Request) {
+	err := postMessage(ctx, 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))
 		}
diff --git a/internal/notifier/lark.go b/internal/notifier/lark.go
index ba32869..30437f2 100644
--- a/internal/notifier/lark.go
+++ b/internal/notifier/lark.go
@@ -109,5 +109,5 @@ func (l *Lark) Post(ctx context.Context, event events.Event) error {
 		Card:    card,
 	}
 
-	return postMessage(l.URL, "", nil, payload)
+	return postMessage(ctx, l.URL, "", nil, payload)
 }
diff --git a/internal/notifier/matrix.go b/internal/notifier/matrix.go
index 4dc9e6a..6bf15b2 100644
--- a/internal/notifier/matrix.go
+++ b/internal/notifier/matrix.go
@@ -65,7 +65,7 @@ func (m *Matrix) Post(ctx context.Context, event events.Event) error {
 		MsgType: "m.text",
 	}
 
-	err = postMessage(fullURL, "", m.CertPool, payload, func(request *retryablehttp.Request) {
+	err = postMessage(ctx, fullURL, "", m.CertPool, payload, func(request *retryablehttp.Request) {
 		request.Method = http.MethodPut
 		request.Header.Add("Authorization", "Bearer "+m.Token)
 	})
diff --git a/internal/notifier/opsgenie.go b/internal/notifier/opsgenie.go
index 79c6219..21d1acc 100644
--- a/internal/notifier/opsgenie.go
+++ b/internal/notifier/opsgenie.go
@@ -71,7 +71,7 @@ func (s *Opsgenie) Post(ctx context.Context, event events.Event) error {
 		Details:     event.Metadata,
 	}
 
-	err := postMessage(s.URL, s.ProxyURL, s.CertPool, payload, func(req *retryablehttp.Request) {
+	err := postMessage(ctx, s.URL, s.ProxyURL, s.CertPool, payload, func(req *retryablehttp.Request) {
 		req.Header.Set("Authorization", "GenieKey "+s.ApiKey)
 	})
 
diff --git a/internal/notifier/rocket.go b/internal/notifier/rocket.go
index 0c268e7..a858542 100644
--- a/internal/notifier/rocket.go
+++ b/internal/notifier/rocket.go
@@ -92,7 +92,7 @@ func (s *Rocket) Post(ctx context.Context, event events.Event) error {
 
 	payload.Attachments = []SlackAttachment{a}
 
-	err := postMessage(s.URL, s.ProxyURL, s.CertPool, payload)
+	err := postMessage(ctx, s.URL, s.ProxyURL, s.CertPool, payload)
 	if err != nil {
 		return fmt.Errorf("postMessage failed: %w", err)
 	}
diff --git a/internal/notifier/slack.go b/internal/notifier/slack.go
index 57d3545..71ee9c1 100644
--- a/internal/notifier/slack.go
+++ b/internal/notifier/slack.go
@@ -118,7 +118,7 @@ func (s *Slack) Post(ctx context.Context, event events.Event) error {
 
 	payload.Attachments = []SlackAttachment{a}
 
-	err := postMessage(s.URL, s.ProxyURL, s.CertPool, payload, func(request *retryablehttp.Request) {
+	err := postMessage(ctx, s.URL, s.ProxyURL, s.CertPool, payload, func(request *retryablehttp.Request) {
 		if s.Token != "" {
 			request.Header.Add("Authorization", "Bearer "+s.Token)
 		}
diff --git a/internal/notifier/teams.go b/internal/notifier/teams.go
index 6bea20d..c5dd045 100644
--- a/internal/notifier/teams.go
+++ b/internal/notifier/teams.go
@@ -102,7 +102,7 @@ func (s *MSTeams) Post(ctx context.Context, event events.Event) error {
 		payload.ThemeColor = "FF0000"
 	}
 
-	err := postMessage(s.URL, s.ProxyURL, s.CertPool, payload)
+	err := postMessage(ctx, s.URL, s.ProxyURL, s.CertPool, payload)
 	if err != nil {
 		return fmt.Errorf("postMessage failed: %w", err)
 	}
diff --git a/internal/notifier/webex.go b/internal/notifier/webex.go
index d092766..21e04a5 100644
--- a/internal/notifier/webex.go
+++ b/internal/notifier/webex.go
@@ -108,7 +108,7 @@ func (s *Webex) Post(ctx context.Context, event events.Event) error {
 		Markdown: s.CreateMarkdown(&event),
 	}
 
-	if err := postMessage(s.URL, s.ProxyURL, s.CertPool, payload, func(request *retryablehttp.Request) {
+	if err := postMessage(ctx, s.URL, s.ProxyURL, s.CertPool, payload, func(request *retryablehttp.Request) {
 		request.Header.Add("Authorization", "Bearer "+s.Token)
 	}); err != nil {
 		return fmt.Errorf("postMessage failed: %w", err)
diff --git a/internal/server/event_handlers.go b/internal/server/event_handlers.go
index 23d9449..3f94c77 100644
--- a/internal/server/event_handlers.go
+++ b/internal/server/event_handlers.go
@@ -265,7 +265,7 @@ func (s *EventServer) handleEvent() func(w http.ResponseWriter, r *http.Request)
 			}
 
 			go func(n notifier.Interface, e events.Event) {
-				ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+				ctx, cancel := context.WithTimeout(context.Background(), provider.GetTimeout())
 				defer cancel()
 				if err := n.Post(ctx, e); err != nil {
 					maskedErrStr, maskErr := masktoken.MaskTokenFromString(err.Error(), token)


Test patch:
diff --git a/controllers/event_handling_test.go b/controllers/event_handling_test.go
index 410cb6b..8890b98 100644
--- a/controllers/event_handling_test.go
+++ b/controllers/event_handling_test.go
@@ -78,6 +78,11 @@ func TestEventHandler(t *testing.T) {
 		},
 	}
 	g.Expect(k8sClient.Create(context.Background(), provider)).To(Succeed())
+	g.Eventually(func() bool {
+		var obj notifyv1.Provider
+		g.Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(provider), &obj))
+		return conditions.IsReady(&obj)
+	}, 30*time.Second, time.Second).Should(BeTrue())
 
 	repo, err := readManifest("./testdata/repo.yaml", namespace)
 	g.Expect(err).ToNot(HaveOccurred())
diff --git a/internal/notifier/client_test.go b/internal/notifier/client_test.go
index e52778e..ff83513 100644
--- a/internal/notifier/client_test.go
+++ b/internal/notifier/client_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package notifier
 
 import (
+	"context"
 	"crypto/x509"
 	"encoding/json"
 	"io"
@@ -43,7 +44,7 @@ func Test_postMessage(t *testing.T) {
 		require.Equal(t, "success", payload["status"])
 	}))
 	defer ts.Close()
-	err := postMessage(ts.URL, "", nil, map[string]string{"status": "success"})
+	err := postMessage(context.Background(), ts.URL, "", nil, map[string]string{"status": "success"})
 	require.NoError(t, err)
 }
 
@@ -64,7 +65,7 @@ func Test_postSelfSignedCert(t *testing.T) {
 	require.NoError(t, err)
 	certpool := x509.NewCertPool()
 	certpool.AddCert(cert)
-	err = postMessage(ts.URL, "", certpool, map[string]string{"status": "success"})
+	err = postMessage(context.Background(), ts.URL, "", certpool, map[string]string{"status": "success"})
 	require.NoError(t, err)
 }