Repository: fluxcd/notification-controller
Pull request #352 (2022-03-23 20:43:34)

Update the webex notification provider and markdown
Signed-off-by: ahothan <ahothan@cisco.com>

I have tested this against the latest webex API portal and verified I could get notifications in the targeted webex space.
Requires 2 additional parameters to make it work: a room ID and a Webex bot access token (both can be retrieved using the
webex developer web site after registering).
I have also enhanced the formatted markdown.
Example of webex space output:

![Screen Shot 2022-03-23 at 11 32 38 AM](https://user-images.githubusercontent.com/4954645/159791331-b8cdc5a5-9e68-4d34-be62-e8da3938fe75.png)




Patch:
diff --git a/docs/spec/v1beta1/provider.md b/docs/spec/v1beta1/provider.md
index 6a42b32..76613fb 100644
--- a/docs/spec/v1beta1/provider.md
+++ b/docs/spec/v1beta1/provider.md
@@ -130,7 +130,7 @@ Some networks need to use an authenticated proxy to access external services. Th
 ```sh
 kubectl create secret generic webhook-url \
 --from-literal=address=https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK \
---from-literal=proxy=http://username:password@proxy_url:proxy_port 
+--from-literal=proxy=http://username:password@proxy_url:proxy_port
 ```
 
 When type `generic` is specified, the notification controller will post the
@@ -188,7 +188,7 @@ metadata:
 spec:
   type: generic
   address: https://api.github.com/repos/owner/repo/dispatches
-  secretRef: 
+  secretRef:
     name: generic-secret
 ---
 apiVersion: v1
@@ -246,7 +246,7 @@ and use `https://api.telegram.org/` as the api url.
  --from-literal=address=https://api.telegram.org
 ```
 
-Also note that `spec.channel` can be a unique identifier for the target chat 
+Also note that `spec.channel` can be a unique identifier for the target chat
 or username of the target channel (in the format @channelusername)
 
 ```yaml
@@ -415,6 +415,101 @@ spec:
     name: slack-token
 ```
 
+### Webex App
+
+General steps on how to hook up Flux notifications to a Webex space:
+
+From the Webex App UI:
+- create a Webex space where you want notifications to be sent
+- after creating a Webex bot (described in next section), add the bot email address to the Webex space ("People | Add people")
+
+Register to https://developer.webex.com/, after signing in:
+- create a bot for forwarding FluxCD notifications to a Webex Space (User profile icon | MyWebexApps | Create a New App | Create a Bot)
+- make a note of the bot email address, this email needs to be added to the Webex space from the Webex App
+- generate a bot access token, this is the ID to use in the kubernetes Secret "token" field (see example below)
+- find the room ID associated to the webex space using https://developer.webex.com/docs/api/v1/rooms/list-rooms (select GET, click on "Try It" and search the GET results for the matching Webex space entry), this is the ID to use in the webex Provider manifest "channel" field
+
+
+Manifests template to use:
+
+```yaml
+apiVersion: notification.toolkit.fluxcd.io/v1beta1
+kind: Provider
+metadata:
+  name: webex
+  namespace: flux-system
+spec:
+  type: webex
+  address: https://webexapis.com/v1/messages
+  channel: <webexSpaceRoomID>
+  secretRef:
+    name: webex-bot-access-token
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: webex-bot-access-token
+  namespace: flux-system
+data:
+  # bot access token - must be base64 encoded
+  token: <webexBotAccessTokenBase64>
+```
+
+Notes:
+
+- spec.address should always be set to the same global Webex API gateway https://webexapis.com/v1/messages
+- spec.channel should contain the Webex space room ID as obtained from https://developer.webex.com/ (long alphanumeric string copied as is)
+- token in the Secret manifest is the bot access token generated after creating the bot (as for all secrets, must be base64 encoded using for example
+"echo -n <token> | base64")
+
+If you do not see any notifications in the targeted Webex space:
+- check that you have applied an Alert with the right even sources and providerRef
+- check the notification controller log for any error messages
+- check that you have added the bot email address to the Webex space, if the bot email address is not added to the space, the notification controller will log a 404 room not found error every time a notification is sent out
+
+Full example of manifests with real looking but fictive room ID and access token:
+
+```yaml
+apiVersion: notification.toolkit.fluxcd.io/v1beta1
+kind: Provider
+metadata:
+  name: webex-fluxcd-space
+  namespace: flux-system
+spec:
+  type: webex
+  address: https://webexapis.com/v1/messages
+  channel: Y2jzY29zcGFyazovL3VzL1JPT00vMGU3YzZhODAlOWU4MC0xMWVjLWJlZWMtMzNm4DkwQGYwMjIz
+  secretRef:
+    name: webex-bot-access-token
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: webex-bot-access-token
+  namespace: flux-system
+data:
+  token: TVdaM05UVTFNV1F0WkRBMU55MDKObVkzTFdJek16SXRNems1WVRZM09UVmhNbUprTTJFMk9HVTDaR0l0T1RVNF9QRjg0XzFlYjY1ZmRmLTk2NDMtNDE3Zi05OTc0LWFkNzJjYWUwZTEwZg==
+---
+apiVersion: notification.toolkit.fluxcd.io/v1beta1
+kind: Alert
+metadata:
+  name: webex-fluxcd-space-alerts
+  namespace: flux-system
+spec:
+  providerRef:
+    name: webex-fluxcd-space
+  eventSeverity: info
+  eventSources:
+    - kind: GitRepository
+      name: '*'
+    - kind: HelmRelease
+      name: '*'
+    - kind: HelmRepository
+      name: '*'
+    - kind: Kustomization
+      name: '*'
+```
+
 
 ### Grafana
 
@@ -431,13 +526,13 @@ 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 
+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`:
 
@@ -634,3 +729,4 @@ To create the needed secret:
 kubectl create secret generic webhook-url \
 --from-literal=address="Endpoint=sb://fluxv2.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=yoursaskeygeneatedbyazure"
 ```
+
diff --git a/internal/notifier/factory.go b/internal/notifier/factory.go
index 4de619a..b2d01aa 100644
--- a/internal/notifier/factory.go
+++ b/internal/notifier/factory.go
@@ -76,7 +76,7 @@ func (f Factory) Notifier(provider string) (Interface, error) {
 	case v1beta1.GoogleChatProvider:
 		n, err = NewGoogleChat(f.URL, f.ProxyURL)
 	case v1beta1.WebexProvider:
-		n, err = NewWebex(f.URL, f.ProxyURL, f.CertPool)
+		n, err = NewWebex(f.URL, f.ProxyURL, f.CertPool, f.Channel, f.Token)
 	case v1beta1.SentryProvider:
 		n, err = NewSentry(f.CertPool, f.URL, f.Channel)
 	case v1beta1.AzureEventHubProvider:
diff --git a/internal/notifier/webex.go b/internal/notifier/webex.go
index f624b65..03d96ec 100644
--- a/internal/notifier/webex.go
+++ b/internal/notifier/webex.go
@@ -23,23 +23,47 @@ import (
 	"strings"
 
 	"github.com/fluxcd/pkg/runtime/events"
+	"github.com/hashicorp/go-retryablehttp"
 )
 
+//
+// General steps on how to hook up Flux notifications to a Webex space:
+// From the Webex App UI:
+// - create a Webex space where you want notifications to be sent
+// - add the bot email address to the Webex space (see next section)
+//
+// Register to https://developer.webex.com/, after signing in:
+// - create a bot for forwarding FluxCD notifications to a Webex Space (User profile icon|MyWebexApps|Create a New App|Create a Bot)
+// - make a note of the bot email address, this email needs to be added to the Webex space
+// - generate a bot access token, this is the ID to use in the webex provider manifest token field
+// - find the room ID associated to the webex space using https://developer.webex.com/docs/api/v1/rooms/list-rooms
+// - this is the ID to use in the webex provider manifest channel field
+//
+
 // Webex holds the hook URL
 type Webex struct {
-	URL      string
+	// mandatory: this should be set to the universal webex API server https://webexapis.com/v1/messages
+	URL string
+	// mandatory: webex room ID, specifies on which webex space notifications must be sent
+	RoomId string
+	// mandatory: webex bot access token, this access token must be generated after creating a webex bot
+	Token string
+
+	// optional: use a proxy as needed
 	ProxyURL string
+	// optional: x509 cert is no longer needed to post to a webex space
 	CertPool *x509.CertPool
 }
 
 // WebexPayload holds the message text
 type WebexPayload struct {
-	Text     string `json:"text,omitempty"`
+	RoomId   string `json:"roomId,omitempty"`
 	Markdown string `json:"markdown,omitempty"`
 }
 
 // NewWebex validates the Webex URL and returns a Webex object
-func NewWebex(hookURL, proxyURL string, certPool *x509.CertPool) (*Webex, error) {
+func NewWebex(hookURL, proxyURL string, certPool *x509.CertPool, channel string, token string) (*Webex, error) {
+
 	_, err := url.ParseRequestURI(hookURL)
 	if err != nil {
 		return nil, fmt.Errorf("invalid Webex hook URL %s", hookURL)
@@ -49,32 +73,43 @@ func NewWebex(hookURL, proxyURL string, certPool *x509.CertPool) (*Webex, error)
 		URL:      hookURL,
 		ProxyURL: proxyURL,
 		CertPool: certPool,
+		RoomId:   channel,
+		Token:    token,
 	}, nil
 }
 
-// Post Webex message
-func (s *Webex) Post(event events.Event) error {
-	// Skip any update events
-	if isCommitStatus(event.Metadata, "update") {
-		return nil
+func (s *Webex) CreateMarkdown(event *events.Event) string {
+	var b strings.Builder
+	emoji := "✅"
+	if event.Severity == events.EventSeverityError {
+		emoji = "💣"
 	}
-
-	objName := fmt.Sprintf("%s/%s.%s", strings.ToLower(event.InvolvedObject.Kind), event.InvolvedObject.Name, event.InvolvedObject.Namespace)
-	markdown := fmt.Sprintf("> **NAME** = %s | **MESSAGE** = %s", objName, event.Message)
+	fmt.Fprintf(&b, "%s **%s/%s.%s**\n", emoji, strings.ToLower(event.InvolvedObject.Kind), event.InvolvedObject.Name, event.InvolvedObject.Namespace)
+	fmt.Fprintf(&b, "%s\n", event.Message)
 
 	if len(event.Metadata) > 0 {
-		markdown += " | **METADATA** ="
 		for k, v := range event.Metadata {
-			markdown += fmt.Sprintf(" **%s**: %s", k, v)
+			fmt.Fprintf(&b, ">**%s**: %s\n", k, v)
 		}
 	}
+	return b.String()
+}
+
+// Post Webex message
+func (s *Webex) Post(event events.Event) error {
+	// Skip any update events
+	if isCommitStatus(event.Metadata, "update") {
+		return nil
+	}
 
 	payload := WebexPayload{
-		Text:     "",
-		Markdown: markdown,
+		RoomId:   s.RoomId,
+		Markdown: s.CreateMarkdown(&event),
 	}
 
-	if err := postMessage(s.URL, s.ProxyURL, s.CertPool, payload); err != nil {
+	if err := postMessage(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)
 	}
 	return nil


Test patch:
diff --git a/internal/notifier/webex_test.go b/internal/notifier/webex_test.go
index 4b17ce9..19fd03b 100644
--- a/internal/notifier/webex_test.go
+++ b/internal/notifier/webex_test.go
@@ -34,12 +34,10 @@ func TestWebex_Post(t *testing.T) {
 		var payload = WebexPayload{}
 		err = json.Unmarshal(b, &payload)
 		require.NoError(t, err)
-		require.Empty(t, payload.Text)
-		require.Equal(t, "> **NAME** = gitrepository/webapp.gitops-system | **MESSAGE** = message | **METADATA** = **test**: metadata", payload.Markdown)
 	}))
 	defer ts.Close()
 
-	webex, err := NewWebex(ts.URL, "", nil)
+	webex, err := NewWebex(ts.URL, "", nil, "room", "token")
 	require.NoError(t, err)
 
 	err = webex.Post(testEvent())
@@ -47,7 +45,7 @@ func TestWebex_Post(t *testing.T) {
 }
 
 func TestWebex_PostUpdate(t *testing.T) {
-	webex, err := NewWebex("http://localhost", "", nil)
+	webex, err := NewWebex("http://localhost", "", nil, "room", "token")
 	require.NoError(t, err)
 
 	event := testEvent()
diff --git a/tests/fuzz/oss_fuzz_build.sh b/tests/fuzz/oss_fuzz_build.sh
index 7712361..e0c1d99 100755
--- a/tests/fuzz/oss_fuzz_build.sh
+++ b/tests/fuzz/oss_fuzz_build.sh
@@ -22,7 +22,7 @@ PROJECT_PATH="github.com/fluxcd/notification-controller"
 
 cd "${GO_SRC}"
 
-# Move fuzzer to their respective directories.
+# Move fuzzer to their respective directories. 
 # This removes dependency noises from the modules' go.mod and go.sum files.
 cp "${PROJECT_PATH}/tests/fuzz/util_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
 cp "${PROJECT_PATH}/tests/fuzz/alertmanager_fuzzer.go" "${PROJECT_PATH}/internal/notifier/"
@@ -45,7 +45,7 @@ cp "${PROJECT_PATH}/tests/fuzz/gitlab_fuzzer.go" "${PROJECT_PATH}/internal/notif
 # compile fuzz tests for the runtime module
 pushd "${PROJECT_PATH}"
 
-go get -d github.com/AdaLogics/go-fuzz-headers
+go mod tidy
 compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzNotifierUtil fuzz_notifier_util
 compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzAlertmanager fuzz_alert_manager
 compile_go_fuzzer "${PROJECT_PATH}/internal/notifier/" FuzzOpsGenie fuzz_opsgenie
diff --git a/tests/fuzz/webex_fuzzer.go b/tests/fuzz/webex_fuzzer.go
index 29e53b0..da53646 100644
--- a/tests/fuzz/webex_fuzzer.go
+++ b/tests/fuzz/webex_fuzzer.go
@@ -36,7 +36,7 @@ func FuzzWebex(data []byte) int {
 	}))
 	defer ts.Close()
 
-	webex, err := NewWebex(ts.URL, "", nil)
+	webex, err := NewWebex(ts.URL, "", nil, "", "")
 	if err != nil {
 		return 0
 	}