Skip to content

Commit

Permalink
feat: support for Google Chat (argoproj#49)
Browse files Browse the repository at this point in the history
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
pasha-codefresh authored Nov 12, 2021
1 parent 18573c0 commit 43c9003
Show file tree
Hide file tree
Showing 7 changed files with 479 additions and 1 deletion.
81 changes: 81 additions & 0 deletions docs/services/googlechat.md
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.
1 change: 1 addition & 0 deletions docs/services/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ metadata:
* [Webhook](./webhook.md)
* [Telegram](./telegram.md)
* [Teams](./teams.md)
* [Google Chat](./googlechat.md)
* [Rocket.Chat](./rocketchat.md)
* [Pushover](./pushover.md)
* [Alertmanager](./alertmanager.md)
8 changes: 7 additions & 1 deletion pkg/cmd/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func (c *commandContext) unmarshalFromFile(filePath string, name string, gk sche
if filePath == "-" {
data, err = ioutil.ReadAll(c.stdin)
} else {
data, err = ioutil.ReadFile(c.configMapPath)
data, err = ioutil.ReadFile(filePath)
}
if err != nil {
return err
Expand Down Expand Up @@ -117,6 +117,12 @@ func (c *commandContext) getSecret() (*v1.Secret, error) {
if err := c.unmarshalFromFile(c.secretPath, c.SecretName, schema.GroupKind{Kind: "Secret"}, &secret); err != nil {
return nil, err
}
if secret.Data == nil {
secret.Data = map[string][]byte{}
}
for k, v := range secret.StringData {
secret.Data[k] = []byte(v)
}
}
secret.Name = c.SecretName
secret.Namespace = c.namespace
Expand Down
55 changes: 55 additions & 0 deletions pkg/cmd/context_test.go
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")
}
167 changes: 167 additions & 0 deletions pkg/services/googlechat.go
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
}
Loading

0 comments on commit 43c9003

Please sign in to comment.