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

Update the webex notification provider and markdown #352

Merged
merged 2 commits into from
Mar 29, 2022
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
106 changes: 101 additions & 5 deletions docs/spec/v1beta1/provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -188,7 +188,7 @@ metadata:
spec:
type: generic
address: https://api.github.com/repos/owner/repo/dispatches
secretRef:
secretRef:
name: generic-secret
---
apiVersion: v1
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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`:

Expand Down Expand Up @@ -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"
```

2 changes: 1 addition & 1 deletion internal/notifier/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
67 changes: 51 additions & 16 deletions internal/notifier/webex.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
6 changes: 2 additions & 4 deletions internal/notifier/webex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,18 @@ 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())
require.NoError(t, err)
}

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()
Expand Down
2 changes: 1 addition & 1 deletion tests/fuzz/webex_fuzzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down