forked from argoproj/notifications-engine
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support for Google Chat (argoproj#49)
feat: support for Google Chat (argoproj#49) Signed-off-by: Francílio Araújo <[email protected]> * fix: enables to use stringData from local secret
- Loading branch information
1 parent
18573c0
commit 43c9003
Showing
7 changed files
with
479 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
# Google Chat | ||
|
||
## Parameters | ||
|
||
The Google Chat notification service send message notifications to a google chat webhook. This service uses the following settings: | ||
|
||
* `webhooks` - a map of the form `webhookName: webhookUrl` | ||
|
||
## Configuration | ||
|
||
1. Open `Google chat` and go to the space to which you want to send messages | ||
2. From the menu at the top of the page, select **Configure Webhooks** | ||
3. Under **Incoming Webhooks**, click **Add Webhook** | ||
4. Give a name to the webhook, optionally add an image and click **Save** | ||
5. Copy the URL next to your webhook | ||
6. Store the URL in `argocd-notification-secret` and declare it in `argocd-notifications-cm` | ||
|
||
```yaml | ||
apiVersion: v1 | ||
kind: ConfigMap | ||
metadata: | ||
name: <config-map-name> | ||
data: | ||
service.googlechat: | | ||
webhooks: | ||
spaceName: $space-webhook-url | ||
``` | ||
```yaml | ||
apiVersion: v1 | ||
kind: Secret | ||
metadata: | ||
name: <secret-name> | ||
stringData: | ||
space-webhook-url: https://chat.googleapis.com/v1/spaces/<space_id>/messages?key=<key>&token=<token> | ||
``` | ||
6. Create a subscription for your space | ||
```yaml | ||
apiVersion: argoproj.io/v1alpha1 | ||
kind: Application | ||
metadata: | ||
annotations: | ||
notifications.argoproj.io/subscribe.on-sync-succeeded.googlechat: spaceName | ||
``` | ||
## Templates | ||
You can send [simple text](https://developers.google.com/chat/reference/message-formats/basic) or [card messages](https://developers.google.com/chat/reference/message-formats/cards) to a Google Chat space. A simple text message template can be defined as follows: | ||
```yaml | ||
template.app-sync-succeeded: | | ||
message: The app {{ .app.metadata.name }} has succesfully synced! | ||
``` | ||
A card message can be defined as follows: | ||
```yaml | ||
template.app-sync-succeeded: | | ||
googlechat: | ||
cards: | | ||
- header: | ||
title: ArgoCD Bot Notification | ||
sections: | ||
- widgets: | ||
- textParagraph: | ||
text: The app {{ .app.metadata.name }} has succesfully synced! | ||
- widgets: | ||
- keyValue: | ||
topLabel: Repository | ||
content: {{ call .repo.RepoURLToHTTPS .app.spec.source.repoURL }} | ||
- keyValue: | ||
topLabel: Revision | ||
content: {{ .app.spec.source.targetRevision }} | ||
- keyValue: | ||
topLabel: Author | ||
content: {{ (call .repo.GetCommitMetadata .app.status.sync.revision).Author }} | ||
``` | ||
The card message can be written in JSON too. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package cmd | ||
|
||
import ( | ||
"io/ioutil" | ||
"log" | ||
"os" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
"github.com/argoproj/notifications-engine/pkg/api" | ||
) | ||
|
||
var secretYaml = `apiVersion: v1 | ||
kind: Secret | ||
metadata: | ||
annotations: | ||
kubectl.kubernetes.io/last-applied-configuration: | | ||
{"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{},"name":"argocd-notifications-secret","namespace":"argocd"},"type":"Opaque"} | ||
creationTimestamp: "2021-11-09T12:43:49Z" | ||
name: argocd-notifications-secret | ||
namespace: argocd | ||
resourceVersion: "38019672" | ||
selfLink: /api/v1/namespaces/argocd/secrets/argocd-notifications-secret | ||
uid: d41860ec-1a35-46e2-b194-093529554df5 | ||
type: Opaque` | ||
|
||
func Test_getSecretFromFile(t *testing.T) { | ||
file, err := ioutil.TempFile(os.TempDir(), "") | ||
if err != nil { | ||
panic(err) | ||
} | ||
defer func() { | ||
_ = os.Remove(file.Name()) | ||
}() | ||
|
||
_, _ = file.WriteString(secretYaml) | ||
_ = file.Sync() | ||
|
||
if _, err := file.Seek(0, 0); err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
ctx := commandContext{ | ||
secretPath: file.Name(), | ||
Settings: api.Settings{ | ||
SecretName: "argocd-notifications-secret", | ||
}, | ||
} | ||
|
||
secret, err := ctx.getSecret() | ||
assert.NoError(t, err) | ||
assert.NotEmpty(t, secret) | ||
assert.Equal(t, secret.Name, "argocd-notifications-secret") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
package services | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
texttemplate "text/template" | ||
|
||
"github.com/ghodss/yaml" | ||
log "github.com/sirupsen/logrus" | ||
|
||
httputil "github.com/argoproj/notifications-engine/pkg/util/http" | ||
) | ||
|
||
type GoogleChatNotification struct { | ||
Cards string `json:"cards"` | ||
} | ||
|
||
type googleChatMessage struct { | ||
Text string `json:"text"` | ||
Cards []cardMessage `json:"cards"` | ||
} | ||
|
||
type cardMessage struct { | ||
Header cardHeader `json:"header,omitempty"` | ||
Sections []cardSection `json:"sections"` | ||
} | ||
|
||
type cardHeader struct { | ||
Title string `json:"title,omitempty"` | ||
Subtitle string `json:"subtitle,omitempty"` | ||
ImageUrl string `json:"imageUrl,omitempty"` | ||
ImageStyle string `json:"imageStyle,omitempty"` | ||
} | ||
|
||
type cardSection struct { | ||
Header string `json:"header"` | ||
Widgets []cardWidget `json:"widgets"` | ||
} | ||
|
||
type cardWidget struct { | ||
TextParagraph map[string]interface{} `json:"textParagraph,omitempty"` | ||
Keyvalue map[string]interface{} `json:"keyValue,omitempty"` | ||
Image map[string]interface{} `json:"image,omitempty"` | ||
Buttons []map[string]interface{} `json:"buttons,omitempty"` | ||
} | ||
|
||
func (n *GoogleChatNotification) GetTemplater(name string, f texttemplate.FuncMap) (Templater, error) { | ||
cards, err := texttemplate.New(name).Funcs(f).Parse(n.Cards) | ||
if err != nil { | ||
return nil, fmt.Errorf("error in '%s' googlechat.cards : %w", name, err) | ||
} | ||
return func(notification *Notification, vars map[string]interface{}) error { | ||
if notification.GoogleChat == nil { | ||
notification.GoogleChat = &GoogleChatNotification{} | ||
} | ||
var cardsBuff bytes.Buffer | ||
if err := cards.Execute(&cardsBuff, vars); err != nil { | ||
return err | ||
} | ||
if val := cardsBuff.String(); val != "" { | ||
notification.GoogleChat.Cards = val | ||
} | ||
return nil | ||
}, nil | ||
} | ||
|
||
type GoogleChatOptions struct { | ||
WebhookUrls map[string]string `json:"webhooks"` | ||
} | ||
|
||
type googleChatService struct { | ||
opts GoogleChatOptions | ||
} | ||
|
||
func NewGoogleChatService(opts GoogleChatOptions) NotificationService { | ||
return &googleChatService{opts: opts} | ||
} | ||
|
||
type webhookReturn struct { | ||
Error *webhookError `json:"error"` | ||
} | ||
|
||
type webhookError struct { | ||
Code int `json:"code"` | ||
Message string `json:"message"` | ||
Status string `json:"status"` | ||
} | ||
|
||
func (s googleChatService) getClient(recipient string) (*googlechatClient, error) { | ||
webhookUrl, ok := s.opts.WebhookUrls[recipient] | ||
if !ok { | ||
return nil, fmt.Errorf("no Google chat webhook configured for recipient %s", recipient) | ||
} | ||
transport := httputil.NewTransport(webhookUrl, false) | ||
client := &http.Client{ | ||
Transport: httputil.NewLoggingRoundTripper(transport, log.WithField("service", "googlechat")), | ||
} | ||
return &googlechatClient{httpClient: client, url: webhookUrl}, nil | ||
} | ||
|
||
type googlechatClient struct { | ||
httpClient *http.Client | ||
url string | ||
} | ||
|
||
func (c *googlechatClient) sendMessage(message *googleChatMessage) (*webhookReturn, error) { | ||
jsonMessage, err := json.Marshal(message) | ||
if err != nil { | ||
return nil, err | ||
} | ||
response, err := c.httpClient.Post(c.url, "application/json", bytes.NewReader(jsonMessage)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
defer func() { | ||
_ = response.Body.Close() | ||
}() | ||
|
||
bodyBytes, err := ioutil.ReadAll(response.Body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
body := webhookReturn{} | ||
err = json.Unmarshal(bodyBytes, &body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &body, nil | ||
} | ||
|
||
func (s googleChatService) Send(notification Notification, dest Destination) error { | ||
client, err := s.getClient(dest.Recipient) | ||
if err != nil { | ||
return fmt.Errorf("error creating client to webhook: %w", err) | ||
} | ||
message, err := googleChatNotificationToMessage(notification) | ||
if err != nil { | ||
return fmt.Errorf("cannot create message: %w", err) | ||
} | ||
|
||
body, err := client.sendMessage(message) | ||
if err != nil { | ||
return fmt.Errorf("cannot send message: %w", err) | ||
} | ||
if body.Error != nil { | ||
return fmt.Errorf("error with message: code=%d status=%s message=%s", body.Error.Code, body.Error.Status, body.Error.Message) | ||
} | ||
return nil | ||
} | ||
|
||
func googleChatNotificationToMessage(n Notification) (*googleChatMessage, error) { | ||
message := &googleChatMessage{} | ||
if n.GoogleChat != nil && n.GoogleChat.Cards != "" { | ||
err := yaml.Unmarshal([]byte(n.GoogleChat.Cards), &message.Cards) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} else { | ||
message.Text = n.Message | ||
} | ||
return message, nil | ||
} |
Oops, something went wrong.