From fdad4feeff3772e8e97531120caf278d7e76cbd0 Mon Sep 17 00:00:00 2001 From: Alexander Matyushentsev Date: Tue, 12 Jan 2021 20:36:52 -0800 Subject: [PATCH 1/3] feat: argocd-notifications should be published with release (#148) Signed-off-by: Alexander Matyushentsev --- Dockerfile | 6 ++- Makefile | 6 +++ cmd/bot.go | 3 +- cmd/controller.go | 3 +- cmd/main.go | 23 +++++++---- cmd/tools/context.go | 41 ++++++++++++++----- cmd/tools/template.go | 8 ++-- cmd/tools/tools.go | 3 +- cmd/tools/trigger.go | 8 ++-- cmd/tools/trigger_test.go | 3 ++ docs/subscriptions.md | 2 +- docs/troubleshooting-commands.md | 32 +++++++-------- docs/troubleshooting.md | 37 +++++++++++------ go.mod | 1 + .../argocd-notifications-bot-deployment.yaml | 2 +- ...d-notifications-controller-deployment.yaml | 2 +- manifests/install-bot.yaml | 4 +- manifests/install.yaml | 2 +- 18 files changed, 119 insertions(+), 67 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8f86e4b6..6e133c88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,12 +11,14 @@ RUN go mod download # Perform the build COPY . . -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /dist/argocd-notifications ./cmd +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /app/argocd-notifications ./cmd +RUN ln -s /app/argocd-notifications /app/argocd-notifications-backend FROM scratch COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt -COPY --from=builder /dist/argocd-notifications /app/argocd-notifications +COPY --from=builder /app/argocd-notifications /app/argocd-notifications +COPY --from=builder /app/argocd-notifications-backend /app/argocd-notifications-backend # User numeric user so that kubernetes can assert that the user id isn't root (0). # We are also using the root group (the 0 in 1000:0), it doesn't have any diff --git a/Makefile b/Makefile index b2c4a897..75255b91 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,13 @@ generate: manifests catalog .PHONY: build build: +ifeq ($(RELEASE), true) + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o ./dist/argocd-notifications-linux-amd64 ./cmd + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-w -s" -o ./dist/argocd-notifications-darwin-amd64 ./cmd + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-w -s" -o ./dist/argocd-notifications-windows-amd64.exe ./cmd +else CGO_ENABLED=0 go build -ldflags="-w -s" -o ./dist/argocd-notifications ./cmd +endif .PHONY: image image: diff --git a/cmd/bot.go b/cmd/bot.go index 54f77977..21496d26 100644 --- a/cmd/bot.go +++ b/cmd/bot.go @@ -25,7 +25,8 @@ func newBotCommand() *cobra.Command { port int ) var command = cobra.Command{ - Use: "bot", + Use: "bot", + Short: "Starts Argo CD Notifications bot", RunE: func(c *cobra.Command, args []string) error { restConfig, err := clientConfig.ClientConfig() if err != nil { diff --git a/cmd/controller.go b/cmd/controller.go index 294abd0c..1e5db260 100644 --- a/cmd/controller.go +++ b/cmd/controller.go @@ -40,7 +40,8 @@ func newControllerCommand() *cobra.Command { argocdRepoServer string ) var command = cobra.Command{ - Use: "controller", + Use: "controller", + Short: "Starts Argo CD Notifications controller", RunE: func(c *cobra.Command, args []string) error { restConfig, err := clientConfig.ClientConfig() if err != nil { diff --git a/cmd/main.go b/cmd/main.go index fb773cdd..5b965079 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "path/filepath" "github.com/spf13/cobra" @@ -10,16 +11,20 @@ import ( ) func main() { - var command = &cobra.Command{ - Use: "argocd-notifications", - Short: "argocd-notifications notifies about Argo CD application changes", - Run: func(c *cobra.Command, args []string) { - c.HelpFunc()(c, args) - }, + var command *cobra.Command + if filepath.Base(os.Args[0]) == "argocd-notifications-backend" || os.Getenv("ARGOCD_NOTIFICATIONS_BACKEND") == "true" { + command = &cobra.Command{ + Use: "argocd-notifications-backend", + Run: func(c *cobra.Command, args []string) { + c.HelpFunc()(c, args) + }, + } + command.AddCommand(newControllerCommand()) + command.AddCommand(newBotCommand()) + } else { + command = tools.NewToolsCommand() } - command.AddCommand(newControllerCommand()) - command.AddCommand(newBotCommand()) - command.AddCommand(tools.NewToolsCommand()) + if err := command.Execute(); err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/tools/context.go b/cmd/tools/context.go index cdb1c5b1..80b7e50f 100644 --- a/cmd/tools/context.go +++ b/cmd/tools/context.go @@ -2,15 +2,19 @@ package tools import ( "context" + "fmt" "io" "io/ioutil" "path/filepath" "sync" + "github.com/argoproj/gitops-engine/pkg/utils/kube" "github.com/ghodss/yaml" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" @@ -30,6 +34,7 @@ type commandContext struct { configMapPath string secretPath string stdout io.Writer + stdin io.Reader stderr io.Writer getK8SClients clientsSource argocdService *lazyArgocdServiceInitializer @@ -86,6 +91,30 @@ func getK8SClients(clientConfig clientcmd.ClientConfig) (kubernetes.Interface, d return k8sClient, dynamicClient, ns, nil } +func (c *commandContext) unmarshalFromFile(filePath string, name string, gk schema.GroupKind, result interface{}) error { + var err error + var data []byte + if filePath == "-" { + data, err = ioutil.ReadAll(c.stdin) + } else { + data, err = ioutil.ReadFile(c.configMapPath) + } + if err != nil { + return err + } + objs, err := kube.SplitYAML(data) + if err != nil { + return err + } + + for _, obj := range objs { + if obj.GetName() == name && obj.GroupVersionKind().GroupKind() == gk { + return runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, result) + } + } + return fmt.Errorf("file '%s' does not have '%s/%s/%s'", filePath, gk.Group, gk.Kind, name) +} + func (c *commandContext) getConfig() (*settings.Config, error) { var configMap v1.ConfigMap if c.configMapPath == "" { @@ -99,11 +128,7 @@ func (c *commandContext) getConfig() (*settings.Config, error) { } configMap = *cm } else { - data, err := ioutil.ReadFile(c.configMapPath) - if err != nil { - return nil, err - } - if err = yaml.Unmarshal(data, &configMap); err != nil { + if err := c.unmarshalFromFile(c.configMapPath, k8s.ConfigMapName, schema.GroupKind{Kind: "ConfigMap"}, &configMap); err != nil { return nil, err } } @@ -122,11 +147,7 @@ func (c *commandContext) getConfig() (*settings.Config, error) { } secret = *s } else { - data, err := ioutil.ReadFile(c.secretPath) - if err != nil { - return nil, err - } - if err = yaml.Unmarshal(data, &secret); err != nil { + if err := c.unmarshalFromFile(c.secretPath, k8s.SecretName, schema.GroupKind{Kind: "Secret"}, &secret); err != nil { return nil, err } } diff --git a/cmd/tools/template.go b/cmd/tools/template.go index 555e22de..783561bd 100644 --- a/cmd/tools/template.go +++ b/cmd/tools/template.go @@ -35,10 +35,10 @@ func newTemplateNotifyCommand(cmdContext *commandContext) *cobra.Command { Use: "notify NAME APPLICATION", Example: ` # Trigger notification using in-cluster config map and secret -argocd-notifications tools template notify app-sync-succeeded guestbook --recipient slack:argocd-notifications +argocd-notifications template notify app-sync-succeeded guestbook --recipient slack:argocd-notifications # Render notification render generated notification in console -argocd-notifications tools template notify app-sync-succeeded guestbook +argocd-notifications template notify app-sync-succeeded guestbook `, Short: "Generates notification using the specified template and send it to specified recipients", RunE: func(c *cobra.Command, args []string) error { @@ -92,9 +92,9 @@ func newTemplateGetCommand(cmdContext *commandContext) *cobra.Command { Use: "get", Example: ` # prints all templates -argocd-notifications tools template get +argocd-notifications template get # print YAML formatted app-sync-succeeded template definition -argocd-notifications tools template get app-sync-succeeded -o=yaml +argocd-notifications template get app-sync-succeeded -o=yaml `, Short: "Prints information about configured templates", RunE: func(c *cobra.Command, args []string) error { diff --git a/cmd/tools/tools.go b/cmd/tools/tools.go index ab928833..50e7d453 100644 --- a/cmd/tools/tools.go +++ b/cmd/tools/tools.go @@ -29,11 +29,12 @@ func NewToolsCommand() *cobra.Command { cmdContext = commandContext{ stdout: os.Stdout, stderr: os.Stderr, + stdin: os.Stdin, argocdService: &lazyArgocdServiceInitializer{argocdRepoServer: &argocdRepoServer}, } ) var command = cobra.Command{ - Use: "tools", + Use: "argocd-notifications", Short: "Set of CLI commands that helps to configure the controller", Run: func(c *cobra.Command, args []string) { c.HelpFunc()(c, args) diff --git a/cmd/tools/trigger.go b/cmd/tools/trigger.go index 3a8fffff..f6a8df3a 100644 --- a/cmd/tools/trigger.go +++ b/cmd/tools/trigger.go @@ -33,10 +33,10 @@ func newTriggerRunCommand(cmdContext *commandContext) *cobra.Command { Short: "Evaluates specified trigger condition and prints the result", Example: ` # Execute trigger configured in 'argocd-notification-cm' ConfigMap -argocd-notifications tools trigger run on-sync-status-unknown ./sample-app.yaml +argocd-notifications trigger run on-sync-status-unknown ./sample-app.yaml # Execute trigger using argocd-notifications-cm.yaml instead of 'argocd-notification-cm' ConfigMap -argocd-notifications tools trigger run on-sync-status-unknown ./sample-app.yaml \ +argocd-notifications trigger run on-sync-status-unknown ./sample-app.yaml \ --config-map ./argocd-notifications-cm.yaml`, RunE: func(c *cobra.Command, args []string) error { if len(args) < 2 { @@ -90,9 +90,9 @@ func newTriggerGetCommand(cmdContext *commandContext) *cobra.Command { Use: "get", Example: ` # prints all triggers -argocd-notifications tools trigger get +argocd-notifications trigger get # print YAML formatted on-sync-failed trigger definition -argocd-notifications tools trigger get on-sync-failed -o=yaml +argocd-notifications trigger get on-sync-failed -o=yaml `, Short: "Prints information about configured triggers", RunE: func(c *cobra.Command, args []string) error { diff --git a/cmd/tools/trigger_test.go b/cmd/tools/trigger_test.go index 3e9616e8..ae7081d2 100644 --- a/cmd/tools/trigger_test.go +++ b/cmd/tools/trigger_test.go @@ -5,6 +5,7 @@ import ( "io" "io/ioutil" "os" + "strings" "testing" "github.com/ghodss/yaml" @@ -23,6 +24,7 @@ import ( func newTestContext(stdout io.Writer, stderr io.Writer, data map[string]string, apps ...runtime.Object) (*commandContext, func(), error) { cm := v1.ConfigMap{ + TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "ConfigMap"}, ObjectMeta: metav1.ObjectMeta{ Name: k8s.ConfigMapName, }, @@ -47,6 +49,7 @@ func newTestContext(stdout io.Writer, stderr io.Writer, data map[string]string, ctx := &commandContext{ stdout: stdout, stderr: stderr, + stdin: strings.NewReader(""), secretPath: ":empty", configMapPath: tmpFile.Name(), getK8SClients: func() (kubernetes.Interface, dynamic.Interface, string, error) { diff --git a/docs/subscriptions.md b/docs/subscriptions.md index a435060e..66900a98 100644 --- a/docs/subscriptions.md +++ b/docs/subscriptions.md @@ -25,7 +25,7 @@ metadata: notifications.argoproj.io/subscribe.on-sync-succeeded.slack: my-channel1;my-channel2 ``` -## Default Subscriptions (v0.6.1) +## Default Subscriptions The subscriptions might be configured globally in the `argocd-notifications-cm` ConfigMap using `subscriptions` field. The default subscriptions are applied to all applications. The trigger and applications might be configured using the diff --git a/docs/troubleshooting-commands.md b/docs/troubleshooting-commands.md index bafd2155..0cf0a046 100644 --- a/docs/troubleshooting-commands.md +++ b/docs/troubleshooting-commands.md @@ -1,4 +1,4 @@ -## tools template get +## argocd-notifications template get Prints information about configured templates @@ -7,7 +7,7 @@ Prints information about configured templates Prints information about configured templates ``` -tools template get [flags] +argocd-notifications template get [flags] ``` ### Examples @@ -15,9 +15,9 @@ tools template get [flags] ``` # prints all templates -argocd-notifications tools template get +argocd-notifications template get # print YAML formatted app-sync-succeeded template definition -argocd-notifications tools template get app-sync-succeeded -o=yaml +argocd-notifications template get app-sync-succeeded -o=yaml ``` @@ -53,7 +53,7 @@ argocd-notifications tools template get app-sync-succeeded -o=yaml --username string Username for basic authentication to the API server ``` -## tools template notify +## argocd-notifications template notify Generates notification using the specified template and send it to specified recipients @@ -62,7 +62,7 @@ Generates notification using the specified template and send it to specified rec Generates notification using the specified template and send it to specified recipients ``` -tools template notify NAME APPLICATION [flags] +argocd-notifications template notify NAME APPLICATION [flags] ``` ### Examples @@ -70,10 +70,10 @@ tools template notify NAME APPLICATION [flags] ``` # Trigger notification using in-cluster config map and secret -argocd-notifications tools template notify app-sync-succeeded guestbook --recipient slack:argocd-notifications +argocd-notifications template notify app-sync-succeeded guestbook --recipient slack:argocd-notifications # Render notification render generated notification in console -argocd-notifications tools template notify app-sync-succeeded guestbook +argocd-notifications template notify app-sync-succeeded guestbook ``` @@ -109,7 +109,7 @@ argocd-notifications tools template notify app-sync-succeeded guestbook --username string Username for basic authentication to the API server ``` -## tools trigger get +## argocd-notifications trigger get Prints information about configured triggers @@ -118,7 +118,7 @@ Prints information about configured triggers Prints information about configured triggers ``` -tools trigger get [flags] +argocd-notifications trigger get [flags] ``` ### Examples @@ -126,9 +126,9 @@ tools trigger get [flags] ``` # prints all triggers -argocd-notifications tools trigger get +argocd-notifications trigger get # print YAML formatted on-sync-failed trigger definition -argocd-notifications tools trigger get on-sync-failed -o=yaml +argocd-notifications trigger get on-sync-failed -o=yaml ``` @@ -164,7 +164,7 @@ argocd-notifications tools trigger get on-sync-failed -o=yaml --username string Username for basic authentication to the API server ``` -## tools trigger run +## argocd-notifications trigger run Evaluates specified trigger condition and prints the result @@ -173,7 +173,7 @@ Evaluates specified trigger condition and prints the result Evaluates specified trigger condition and prints the result ``` -tools trigger run NAME APPLICATION [flags] +argocd-notifications trigger run NAME APPLICATION [flags] ``` ### Examples @@ -181,10 +181,10 @@ tools trigger run NAME APPLICATION [flags] ``` # Execute trigger configured in 'argocd-notification-cm' ConfigMap -argocd-notifications tools trigger run on-sync-status-unknown ./sample-app.yaml +argocd-notifications trigger run on-sync-status-unknown ./sample-app.yaml # Execute trigger using argocd-notifications-cm.yaml instead of 'argocd-notification-cm' ConfigMap -argocd-notifications tools trigger run on-sync-status-unknown ./sample-app.yaml \ +argocd-notifications trigger run on-sync-status-unknown ./sample-app.yaml \ --config-map ./argocd-notifications-cm.yaml ``` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index c34edb42..2908ac18 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,15 +1,11 @@ ## Troubleshooting -(v0.7.0) The `argocd-notifications` binary includes a set of CLI commands that helps to configure the controller -settings and troubleshooting issues. All CLI commands are available as `argocd-notifications tools` sub-commands: - -```bash -argocd-notifications tools -``` +settings and troubleshoot issues. ## Global flags Following global flags are available for all sub-commands: + * `config-map` - path to the file containing `argocd-notifications-cm` ConfigMap. If not specified then the command loads `argocd-notification-cm` ConfigMap using the local Kubernetes config file. * `secret` - path to the file containing `argocd-notifications-secret` ConfigMap. If not @@ -20,22 +16,37 @@ Additionally, you can specify `:empty` value to use empty secret with no notific * Get list of triggers configured in the local config map: -``` -argocd-notifications tools trigger get \ +```bash +argocd-notifications trigger get \ --config-map ./argocd-notifications-cm.yaml --secret :empty ``` * Trigger notification using in-cluster config map and secret: -``` -argocd-notifications tools template notify \ +```bash +argocd-notifications template notify \ app-sync-succeeded guestbook --recipient slack:argocd-notifications ``` -## How to use it +## Kustomize + +If you are managing `argocd-notifications` config using Kustomize you can pipe whole `kustomize build` output +into stdin using `--config-map -` flag: + +```bash +kustomize build ./argocd-notifications | \ + argocd-notifications \ + template notify app-sync-succeeded guestbook --recipient grafana:argocd \ + --config-map - +``` + +## How to get it ### On your laptop +You can download `argocd-notifications` from the github [release](https://github.com/argoproj-labs/argocd-notifications/releases) +attachments. + The binary is available in `argoprojlabs/argocd-notifications` image. Use the `docker run` and volume mount to execute binary on any platform. @@ -44,7 +55,7 @@ to execute binary on any platform. ```bash docker run --rm -it -w /src -v $(pwd):/src \ argoprojlabs/argocd-notifications: \ - /app/argocd-notifications tools trigger get \ + /app/argocd-notifications trigger get \ --config-map ./argocd-notifications-cm.yaml --secret :empty ``` @@ -56,7 +67,7 @@ configuration. **Example** ```bash kubectl exec -it argocd-notifications-controller- \ - /app/argocd-notifications tools trigger get + /app/argocd-notifications trigger get ``` ## Commands diff --git a/go.mod b/go.mod index d3d900a7..600afce1 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/Masterminds/sprig v2.22.0+incompatible github.com/antonmedv/expr v1.8.9 github.com/argoproj/argo-cd v1.8.0 + github.com/argoproj/gitops-engine v0.2.1 github.com/evanphx/json-patch v4.9.0+incompatible github.com/ghodss/yaml v1.0.0 github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible diff --git a/manifests/bot/argocd-notifications-bot-deployment.yaml b/manifests/bot/argocd-notifications-bot-deployment.yaml index 1a6a2eea..893fa288 100644 --- a/manifests/bot/argocd-notifications-bot-deployment.yaml +++ b/manifests/bot/argocd-notifications-bot-deployment.yaml @@ -13,7 +13,7 @@ spec: spec: containers: - command: - - /app/argocd-notifications + - /app/argocd-notifications-backend - bot workingDir: /app image: argoprojlabs/argocd-notifications:latest diff --git a/manifests/controller/argocd-notifications-controller-deployment.yaml b/manifests/controller/argocd-notifications-controller-deployment.yaml index 083a4dd3..35524465 100644 --- a/manifests/controller/argocd-notifications-controller-deployment.yaml +++ b/manifests/controller/argocd-notifications-controller-deployment.yaml @@ -19,7 +19,7 @@ spec: name: argocd-tls-certs-cm containers: - command: - - /app/argocd-notifications + - /app/argocd-notifications-backend - controller workingDir: /app image: argoprojlabs/argocd-notifications:latest diff --git a/manifests/install-bot.yaml b/manifests/install-bot.yaml index 9edb009b..9df3a01e 100644 --- a/manifests/install-bot.yaml +++ b/manifests/install-bot.yaml @@ -140,7 +140,7 @@ spec: spec: containers: - command: - - /app/argocd-notifications + - /app/argocd-notifications-backend - bot image: argoprojlabs/argocd-notifications:latest imagePullPolicy: Always @@ -165,7 +165,7 @@ spec: spec: containers: - command: - - /app/argocd-notifications + - /app/argocd-notifications-backend - controller image: argoprojlabs/argocd-notifications:latest imagePullPolicy: Always diff --git a/manifests/install.yaml b/manifests/install.yaml index aa1505b3..a6455402 100644 --- a/manifests/install.yaml +++ b/manifests/install.yaml @@ -85,7 +85,7 @@ spec: spec: containers: - command: - - /app/argocd-notifications + - /app/argocd-notifications-backend - controller image: argoprojlabs/argocd-notifications:latest imagePullPolicy: Always From dc9984c81b1b63758ccf6061856c8a4e6c8a49aa Mon Sep 17 00:00:00 2001 From: paydaycay Date: Wed, 13 Jan 2021 17:46:43 +0100 Subject: [PATCH 2/3] Fix documentation for webhook implementation (#149) --- docs/services/webhook.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/services/webhook.md b/docs/services/webhook.md index 3d664047..783026af 100644 --- a/docs/services/webhook.md +++ b/docs/services/webhook.md @@ -13,7 +13,7 @@ kind: ConfigMap metadata: name: argocd-notifications-cm data: - service.: | + service.webhook.: | url: https:/// headers: #optional headers - name: @@ -46,7 +46,7 @@ apiVersion: argoproj.io/v1alpha1 kind: Application metadata: annotations: - notifications.argoproj.io/subscribe.sync-operation-change.: "" + notifications.argoproj.io/subscribe..: "" ``` ## Examples From 5586bb3967e9393b0e7cb11030b01b7aea10f1a1 Mon Sep 17 00:00:00 2001 From: Alexander Matyushentsev Date: Wed, 13 Jan 2021 09:44:06 -0800 Subject: [PATCH 3/3] refactor: Replace `template.body`, `template.title` fields with `template.message` and `template.email.subject` fields (#151) --- CHANGELOG.md | 1 + catalog/install.yaml | 58 ++++- catalog/templates/app-deployed.yaml | 5 +- catalog/templates/app-health-degraded.yaml | 5 +- catalog/templates/app-sync-failed.yaml | 7 +- catalog/templates/app-sync-running.yaml | 5 +- .../templates/app-sync-status-unknown.yaml | 5 +- catalog/templates/app-sync-succeeded.yaml | 5 +- cmd/main.go | 12 +- cmd/tools/template_test.go | 6 +- cmd/tools/trigger_test.go | 4 +- docs/argocd-notifications-cm.yaml | 8 +- docs/catalog.md | 233 +++++++++++++++--- docs/services/slack.md | 3 +- docs/templates.md | 19 +- docs/upgrading/0.x-1.0.md | 34 ++- hack/gen/main.go | 9 +- pkg/api_test.go | 4 +- pkg/config_test.go | 4 +- pkg/services/email.go | 52 +++- pkg/services/email_test.go | 35 +++ pkg/services/grafana.go | 2 +- pkg/services/opsgenie.go | 33 ++- pkg/services/services.go | 112 +++++++-- pkg/services/services_test.go | 29 +++ pkg/services/slack.go | 32 ++- pkg/services/slack_test.go | 28 +++ pkg/services/telegram.go | 2 +- pkg/services/webhook.go | 49 +++- pkg/services/webook_test.go | 32 +++ pkg/templates/compiled.go | 129 ---------- pkg/templates/service.go | 18 +- pkg/templates/service_test.go | 66 +---- shared/legacy/settings.go | 13 +- shared/legacy/settings_test.go | 6 +- 35 files changed, 731 insertions(+), 334 deletions(-) create mode 100644 pkg/services/email_test.go create mode 100644 pkg/services/services_test.go delete mode 100644 pkg/templates/compiled.go diff --git a/CHANGELOG.md b/CHANGELOG.md index b058fe75..ae998bc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * Built-in triggers/templates replaced with triggers/templates "catalog" (#56) * `config.yaml` and `notifiers.yaml` configs split into multiple ConfigMap keys (#76) * `trigger.enabled` field is replaced with `defaultTriggers` setting +* Replace `template.body`, `template.title` fields with `template.message` and `template.email.subject` fields ## v0.7.0 (2020-05-10) diff --git a/catalog/install.yaml b/catalog/install.yaml index 78ab9fd8..2488e8b0 100644 --- a/catalog/install.yaml +++ b/catalog/install.yaml @@ -1,7 +1,9 @@ apiVersion: v1 data: template.app-deployed: | - body: | + email: + subject: New version of an application {{.app.metadata.name}} is up and running. + message: | {{if eq .serviceType "slack"}}:white_check_mark:{{end}} Application {{.app.metadata.name}} is now running new version of deployments manifests. slack: attachments: | @@ -36,9 +38,10 @@ data: {{end}} ] }] - title: New version of an application {{.app.metadata.name}} is up and running. template.app-health-degraded: | - body: | + email: + subject: Application {{.app.metadata.name}} has degraded. + message: | {{if eq .serviceType "slack"}}:exclamation:{{end}} Application {{.app.metadata.name}} has degraded. Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}. slack: @@ -69,16 +72,44 @@ data: {{end}} ] }] - title: Application {{.app.metadata.name}} has degraded. template.app-sync-failed: | - body: | + email: + subject: Failed to sync application {{.app.metadata.name}}. + message: | {{if eq .serviceType "slack"}}:exclamation:{{end}} The sync operation of application {{.app.metadata.name}} has failed at {{.app.status.operationState.finishedAt}} with the following error: {{.app.status.operationState.message}} Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true . slack: - attachments: "[{\n \"title\": \"{{ .app.metadata.name}}\",\n \"title_link\":\"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}\",\n \"color\": \"#E96D76\",\n \"fields\": [\n {\n \"title\": \"Sync Status\",\n \"value\": \"{{.app.status.sync.status}}\",\n \"short\": true\n },\n {\n \"title\": \"Repository\",\n \"value\": \"{{.app.spec.source.repoURL}}\",\n \"short\": true\n }\n {{range $index, $c := .app.status.conditions}}\n {{if not $index}},{{end}}\n {{if $index}},{{end}}\n {\n \"title\": \"{{$c.type}}\",\n \"value\": \"{{$c.message}}\",\n \"short\": true\n }\n {{end}}\n ]\n}] " - title: Failed to sync application {{.app.metadata.name}}. + attachments: |- + [{ + "title": "{{ .app.metadata.name}}", + "title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}", + "color": "#E96D76", + "fields": [ + { + "title": "Sync Status", + "value": "{{.app.status.sync.status}}", + "short": true + }, + { + "title": "Repository", + "value": "{{.app.spec.source.repoURL}}", + "short": true + } + {{range $index, $c := .app.status.conditions}} + {{if not $index}},{{end}} + {{if $index}},{{end}} + { + "title": "{{$c.type}}", + "value": "{{$c.message}}", + "short": true + } + {{end}} + ] + }] template.app-sync-running: | - body: | + email: + subject: Start syncing application {{.app.metadata.name}}. + message: | The sync operation of application {{.app.metadata.name}} has started at {{.app.status.operationState.startedAt}}. Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true . slack: @@ -109,9 +140,10 @@ data: {{end}} ] }] - title: Start syncing application {{.app.metadata.name}}. template.app-sync-status-unknown: | - body: | + email: + subject: Application {{.app.metadata.name}} sync status is 'Unknown' + message: | {{if eq .serviceType "slack"}}:exclamation:{{end}} Application {{.app.metadata.name}} sync is 'Unknown'. Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}. {{if ne .serviceType "slack"}} @@ -147,14 +179,14 @@ data: {{end}} ] }] - title: Application {{.app.metadata.name}} sync status is 'Unknown' template.app-sync-succeeded: | - body: | + email: + subject: Application {{.app.metadata.name}} has been successfully synced. + message: | {{if eq .serviceType "slack"}}:white_check_mark:{{end}} Application {{.app.metadata.name}} has been successfully synced at {{.app.status.operationState.finishedAt}}. Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true . slack: attachments: "[{\n \"title\": \"{{ .app.metadata.name}}\",\n \"title_link\":\"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}\",\n \"color\": \"#18be52\",\n \"fields\": [\n {\n \"title\": \"Sync Status\",\n \"value\": \"{{.app.status.sync.status}}\",\n \"short\": true\n },\n {\n \"title\": \"Repository\",\n \"value\": \"{{.app.spec.source.repoURL}}\",\n \"short\": true\n }\n {{range $index, $c := .app.status.conditions}}\n {{if not $index}},{{end}}\n {{if $index}},{{end}}\n {\n \"title\": \"{{$c.type}}\",\n \"value\": \"{{$c.message}}\",\n \"short\": true\n }\n {{end}}\n ]\n}] " - title: Application {{.app.metadata.name}} has been successfully synced. trigger.on-deployed: | - description: Application is synced and healthy. Triggered once per commit. oncePer: app.status.sync.revision diff --git a/catalog/templates/app-deployed.yaml b/catalog/templates/app-deployed.yaml index 2bb500eb..acc293c6 100644 --- a/catalog/templates/app-deployed.yaml +++ b/catalog/templates/app-deployed.yaml @@ -1,6 +1,7 @@ -title: "New version of an application {{.app.metadata.name}} is up and running." -body: | +message: | {{if eq .serviceType "slack"}}:white_check_mark:{{end}} Application {{.app.metadata.name}} is now running new version of deployments manifests. +email: + subject: New version of an application {{.app.metadata.name}} is up and running. slack: attachments: | [{ diff --git a/catalog/templates/app-health-degraded.yaml b/catalog/templates/app-health-degraded.yaml index 05295d53..1256e7de 100644 --- a/catalog/templates/app-health-degraded.yaml +++ b/catalog/templates/app-health-degraded.yaml @@ -1,7 +1,8 @@ -title: "Application {{.app.metadata.name}} has degraded." -body: | +message: | {{if eq .serviceType "slack"}}:exclamation:{{end}} Application {{.app.metadata.name}} has degraded. Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}. +email: + subject: Application {{.app.metadata.name}} has degraded. slack: attachments: | [{ diff --git a/catalog/templates/app-sync-failed.yaml b/catalog/templates/app-sync-failed.yaml index 0467ad36..b7a0d1ab 100644 --- a/catalog/templates/app-sync-failed.yaml +++ b/catalog/templates/app-sync-failed.yaml @@ -1,7 +1,8 @@ -title: "Failed to sync application {{.app.metadata.name}}." -body: | +message: | {{if eq .serviceType "slack"}}:exclamation:{{end}} The sync operation of application {{.app.metadata.name}} has failed at {{.app.status.operationState.finishedAt}} with the following error: {{.app.status.operationState.message}} Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true . +email: + subject: Failed to sync application {{.app.metadata.name}}. slack: attachments: | [{ @@ -29,4 +30,4 @@ slack: } {{end}} ] - }] \ No newline at end of file + }] \ No newline at end of file diff --git a/catalog/templates/app-sync-running.yaml b/catalog/templates/app-sync-running.yaml index 22eafe8c..8f0a5307 100644 --- a/catalog/templates/app-sync-running.yaml +++ b/catalog/templates/app-sync-running.yaml @@ -1,7 +1,8 @@ -title: "Start syncing application {{.app.metadata.name}}." -body: | +message: | The sync operation of application {{.app.metadata.name}} has started at {{.app.status.operationState.startedAt}}. Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true . +email: + subject: "Start syncing application {{.app.metadata.name}}." slack: attachments: | [{ diff --git a/catalog/templates/app-sync-status-unknown.yaml b/catalog/templates/app-sync-status-unknown.yaml index 704402c8..8c85e47a 100644 --- a/catalog/templates/app-sync-status-unknown.yaml +++ b/catalog/templates/app-sync-status-unknown.yaml @@ -1,5 +1,4 @@ -title: "Application {{.app.metadata.name}} sync status is 'Unknown'" -body: | +message: | {{if eq .serviceType "slack"}}:exclamation:{{end}} Application {{.app.metadata.name}} sync is 'Unknown'. Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}. {{if ne .serviceType "slack"}} @@ -7,6 +6,8 @@ body: | * {{$c.message}} {{end}} {{end}} +email: + subject: Application {{.app.metadata.name}} sync status is 'Unknown' slack: attachments: | [{ diff --git a/catalog/templates/app-sync-succeeded.yaml b/catalog/templates/app-sync-succeeded.yaml index 7e350230..989db502 100644 --- a/catalog/templates/app-sync-succeeded.yaml +++ b/catalog/templates/app-sync-succeeded.yaml @@ -1,7 +1,8 @@ -title: "Application {{.app.metadata.name}} has been successfully synced." -body: | +message: | {{if eq .serviceType "slack"}}:white_check_mark:{{end}} Application {{.app.metadata.name}} has been successfully synced at {{.app.status.operationState.finishedAt}}. Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true . +email: + subject: Application {{.app.metadata.name}} has been successfully synced. slack: attachments: | [{ diff --git a/cmd/main.go b/cmd/main.go index 5b965079..2072e6bd 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,14 +5,18 @@ import ( "os" "path/filepath" - "github.com/spf13/cobra" - "github.com/argoproj-labs/argocd-notifications/cmd/tools" + "github.com/spf13/cobra" ) func main() { + binaryName := filepath.Base(os.Args[0]) + if val := os.Getenv("ARGOCD_NOTIFICATIONS_BINARY"); val != "" { + binaryName = val + } var command *cobra.Command - if filepath.Base(os.Args[0]) == "argocd-notifications-backend" || os.Getenv("ARGOCD_NOTIFICATIONS_BACKEND") == "true" { + switch binaryName { + case "argocd-notifications-backend": command = &cobra.Command{ Use: "argocd-notifications-backend", Run: func(c *cobra.Command, args []string) { @@ -21,7 +25,7 @@ func main() { } command.AddCommand(newControllerCommand()) command.AddCommand(newBotCommand()) - } else { + default: command = tools.NewToolsCommand() } diff --git a/cmd/tools/template_test.go b/cmd/tools/template_test.go index 96ac0fa1..3de10fe8 100644 --- a/cmd/tools/template_test.go +++ b/cmd/tools/template_test.go @@ -13,7 +13,7 @@ func TestTemplateNotifyConsole(t *testing.T) { cmData := map[string]string{ "trigger.my-trigger": `[{when: "app.metadata.name == 'guestbook'", send: [my-template]}]`, "template.my-template": ` -title: hello {{.app.metadata.name}}`, +message: hello {{.app.metadata.name}}`, } var stdout bytes.Buffer var stderr bytes.Buffer @@ -32,8 +32,8 @@ title: hello {{.app.metadata.name}}`, func TestTemplateGet(t *testing.T) { cmData := map[string]string{ - "template.my-template1": `{title: hello}`, - "template.my-template2": `{title: hello}`, + "template.my-template1": `{message: hello}`, + "template.my-template2": `{message: hello}`, } var stdout bytes.Buffer diff --git a/cmd/tools/trigger_test.go b/cmd/tools/trigger_test.go index ae7081d2..7625a5c6 100644 --- a/cmd/tools/trigger_test.go +++ b/cmd/tools/trigger_test.go @@ -68,7 +68,7 @@ func TestTriggerRun(t *testing.T) { - when: app.metadata.name == 'guestbook' send: [my-template]`, "template.my-template": ` -title: hello {{.app.metadata.name}}`, +message: hello {{.app.metadata.name}}`, } var stdout bytes.Buffer @@ -95,7 +95,7 @@ func TestTriggerGet(t *testing.T) { - when: 'false' send: [my-template]`, "template.my-template": ` -title: hello {{.app.metadata.name}}`, +message: hello {{.app.metadata.name}}`, } var stdout bytes.Buffer diff --git a/docs/argocd-notifications-cm.yaml b/docs/argocd-notifications-cm.yaml index 1ec0f37b..2fcd0407 100644 --- a/docs/argocd-notifications-cm.yaml +++ b/docs/argocd-notifications-cm.yaml @@ -18,16 +18,16 @@ data: # Templates are used to generate the notification template message template.my-custom-template: | - title: Hello {{.app.metadata.name}} - body: | + message: | Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}. # Templates might have notification service specific fields. E.g. slack message might include annotations template.my-custom-template-slack-template: | - title: Application {{.app.metadata.name}} sync status is {{.app.status.sync.status}} - body: | + message: | Application {{.app.metadata.name}} sync is {{.app.status.sync.status}}. Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}. + email: + subject: Application {{.app.metadata.name}} sync status is {{.app.status.sync.status}} slack: attachments: | [{ diff --git a/docs/catalog.md b/docs/catalog.md index 6eb1d490..46495068 100644 --- a/docs/catalog.md +++ b/docs/catalog.md @@ -11,60 +11,213 @@ ## Templates ### app-deployed -**title**: `New version of an application {{.app.metadata.name}} is up and running.` - -**body**: -``` -{{if eq .serviceType "slack"}}:white_check_mark:{{end}} Application {{.app.metadata.name}} is now running new version of deployments manifests. +**definition**: +```yaml +email: + subject: New version of an application {{.app.metadata.name}} is up and running. +message: | + {{if eq .serviceType "slack"}}:white_check_mark:{{end}} Application {{.app.metadata.name}} is now running new version of deployments manifests. +slack: + attachments: | + [{ + "title": "{{ .app.metadata.name}}", + "title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}", + "color": "#18be52", + "fields": [ + { + "title": "Sync Status", + "value": "{{.app.status.sync.status}}", + "short": true + }, + { + "title": "Repository", + "value": "{{.app.spec.source.repoURL}}", + "short": true + }, + { + "title": "Revision", + "value": "{{.app.status.sync.revision}}", + "short": true + } + {{range $index, $c := .app.status.conditions}} + {{if not $index}},{{end}} + {{if $index}},{{end}} + { + "title": "{{$c.type}}", + "value": "{{$c.message}}", + "short": true + } + {{end}} + ] + }] ``` ### app-health-degraded -**title**: `Application {{.app.metadata.name}} has degraded.` - -**body**: -``` -{{if eq .serviceType "slack"}}:exclamation:{{end}} Application {{.app.metadata.name}} has degraded. -Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}. +**definition**: +```yaml +email: + subject: Application {{.app.metadata.name}} has degraded. +message: | + {{if eq .serviceType "slack"}}:exclamation:{{end}} Application {{.app.metadata.name}} has degraded. + Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}. +slack: + attachments: |- + [{ + "title": "{{ .app.metadata.name}}", + "title_link": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}", + "color": "#f4c030", + "fields": [ + { + "title": "Sync Status", + "value": "{{.app.status.sync.status}}", + "short": true + }, + { + "title": "Repository", + "value": "{{.app.spec.source.repoURL}}", + "short": true + } + {{range $index, $c := .app.status.conditions}} + {{if not $index}},{{end}} + {{if $index}},{{end}} + { + "title": "{{$c.type}}", + "value": "{{$c.message}}", + "short": true + } + {{end}} + ] + }] ``` ### app-sync-failed -**title**: `Failed to sync application {{.app.metadata.name}}.` - -**body**: -``` -{{if eq .serviceType "slack"}}:exclamation:{{end}} The sync operation of application {{.app.metadata.name}} has failed at {{.app.status.operationState.finishedAt}} with the following error: {{.app.status.operationState.message}} -Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true . +**definition**: +```yaml +email: + subject: Failed to sync application {{.app.metadata.name}}. +message: | + {{if eq .serviceType "slack"}}:exclamation:{{end}} The sync operation of application {{.app.metadata.name}} has failed at {{.app.status.operationState.finishedAt}} with the following error: {{.app.status.operationState.message}} + Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true . +slack: + attachments: |- + [{ + "title": "{{ .app.metadata.name}}", + "title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}", + "color": "#E96D76", + "fields": [ + { + "title": "Sync Status", + "value": "{{.app.status.sync.status}}", + "short": true + }, + { + "title": "Repository", + "value": "{{.app.spec.source.repoURL}}", + "short": true + } + {{range $index, $c := .app.status.conditions}} + {{if not $index}},{{end}} + {{if $index}},{{end}} + { + "title": "{{$c.type}}", + "value": "{{$c.message}}", + "short": true + } + {{end}} + ] + }] ``` ### app-sync-running -**title**: `Start syncing application {{.app.metadata.name}}.` - -**body**: -``` -The sync operation of application {{.app.metadata.name}} has started at {{.app.status.operationState.startedAt}}. -Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true . +**definition**: +```yaml +email: + subject: Start syncing application {{.app.metadata.name}}. +message: | + The sync operation of application {{.app.metadata.name}} has started at {{.app.status.operationState.startedAt}}. + Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true . +slack: + attachments: |- + [{ + "title": "{{ .app.metadata.name}}", + "title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}", + "color": "#0DADEA", + "fields": [ + { + "title": "Sync Status", + "value": "{{.app.status.sync.status}}", + "short": true + }, + { + "title": "Repository", + "value": "{{.app.spec.source.repoURL}}", + "short": true + } + {{range $index, $c := .app.status.conditions}} + {{if not $index}},{{end}} + {{if $index}},{{end}} + { + "title": "{{$c.type}}", + "value": "{{$c.message}}", + "short": true + } + {{end}} + ] + }] ``` ### app-sync-status-unknown -**title**: `Application {{.app.metadata.name}} sync status is 'Unknown'` - -**body**: -``` -{{if eq .serviceType "slack"}}:exclamation:{{end}} Application {{.app.metadata.name}} sync is 'Unknown'. -Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}. -{{if ne .serviceType "slack"}} -{{range $c := .app.status.conditions}} - * {{$c.message}} -{{end}} -{{end}} +**definition**: +```yaml +email: + subject: Application {{.app.metadata.name}} sync status is 'Unknown' +message: | + {{if eq .serviceType "slack"}}:exclamation:{{end}} Application {{.app.metadata.name}} sync is 'Unknown'. + Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}. + {{if ne .serviceType "slack"}} + {{range $c := .app.status.conditions}} + * {{$c.message}} + {{end}} + {{end}} +slack: + attachments: |- + [{ + "title": "{{ .app.metadata.name}}", + "title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}", + "color": "#E96D76", + "fields": [ + { + "title": "Sync Status", + "value": "{{.app.status.sync.status}}", + "short": true + }, + { + "title": "Repository", + "value": "{{.app.spec.source.repoURL}}", + "short": true + } + {{range $index, $c := .app.status.conditions}} + {{if not $index}},{{end}} + {{if $index}},{{end}} + { + "title": "{{$c.type}}", + "value": "{{$c.message}}", + "short": true + } + {{end}} + ] + }] ``` ### app-sync-succeeded -**title**: `Application {{.app.metadata.name}} has been successfully synced.` - -**body**: -``` -{{if eq .serviceType "slack"}}:white_check_mark:{{end}} Application {{.app.metadata.name}} has been successfully synced at {{.app.status.operationState.finishedAt}}. -Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true . +**definition**: +```yaml +email: + subject: Application {{.app.metadata.name}} has been successfully synced. +message: | + {{if eq .serviceType "slack"}}:white_check_mark:{{end}} Application {{.app.metadata.name}} has been successfully synced at {{.app.status.operationState.finishedAt}}. + Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true . +slack: + attachments: "[{\n \"title\": \"{{ .app.metadata.name}}\",\n \"title_link\":\"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}\",\n \"color\": \"#18be52\",\n \"fields\": [\n {\n \"title\": \"Sync Status\",\n \"value\": \"{{.app.status.sync.status}}\",\n \"short\": true\n },\n {\n \"title\": \"Repository\",\n \"value\": \"{{.app.spec.source.repoURL}}\",\n \"short\": true\n }\n {{range $index, $c := .app.status.conditions}}\n {{if not $index}},{{end}}\n {{if $index}},{{end}}\n {\n \"title\": \"{{$c.type}}\",\n \"value\": \"{{$c.message}}\",\n \"short\": true\n }\n {{end}}\n ]\n}] " ``` diff --git a/docs/services/slack.md b/docs/services/slack.md index 62d3a42a..c944628f 100644 --- a/docs/services/slack.md +++ b/docs/services/slack.md @@ -46,8 +46,7 @@ The message blocks and attachments can be specified in `blocks` and `attachments ```yaml template.app-sync-status: | - title: Application {{.app.metadata.name}} sync status is {{.app.status.sync.status}} - body: | + message: | Application {{.app.metadata.name}} sync is {{.app.status.sync.status}}. Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}. slack: diff --git a/docs/templates.md b/docs/templates.md index 6d86bf9b..ebf27983 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -1,5 +1,5 @@ The notification template is used to generate the notification content and configured in `argocd-notifications-cm` ConfigMap. The template is leveraging -[html/template](https://golang.org/pkg/html/template/) golang package and allow to define notification title and body. +[html/template](https://golang.org/pkg/html/template/) golang package and allow to customize notification message. Templates are meant to be reusable and can be referenced by multiple triggers. The following template is used to notify the user about application sync status. @@ -11,8 +11,7 @@ metadata: name: argocd-notifications-cm data: template.my-custom-template-slack-template: | - title: Application {{.app.metadata.name}} sync status is {{.app.status.sync.status}} - body: | + message: | Application {{.app.metadata.name}} sync is {{.app.status.sync.status}}. Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}. ``` @@ -25,6 +24,12 @@ Each template has access to the following fields: render service specific fields. - `recipient` holds the recipient name. +## Notification Service Specific Fields + +The `message` field of the template definition allows creating a basic notification for any notification service. You can leverage notification service-specific +fields to create complex notifications. For example using service-specific you can add blocks and attachments for Slack, subject for Email or URL path, and body for Webhook. +See correspondingĀ service [documentation](./services/overview.md) for more information. + ## Functions Templates have access to the set of built-in functions: @@ -36,13 +41,7 @@ metadata: name: argocd-notifications-cm data: template.my-custom-template-slack-template: | - title: Application {{.app.metadata.name}} sync status is {{.app.status.sync.status}} - body: "Author: {{(call .repo.GetCommitMetadata .app.status.sync.revision).Author}}" + message: "Author: {{(call .repo.GetCommitMetadata .app.status.sync.revision).Author}}" ``` {!functions.md!} - -## Notification Service Specific Messages - -Templates might define notification service-specific fields, for example, attachments for Slack or URL path and body for Webhook. -See correspondingĀ service [documentation](./services/overview.md) for more information. diff --git a/docs/upgrading/0.x-1.0.md b/docs/upgrading/0.x-1.0.md index c56f8004..2bd00651 100644 --- a/docs/upgrading/0.x-1.0.md +++ b/docs/upgrading/0.x-1.0.md @@ -141,10 +141,11 @@ data: send: [my-custom-template] # Templates are used to generate the notification template message template.app-sync-status: | - title: Application {{.app.metadata.name}} sync status is {{.app.status.sync.status}} - body: | + message: | Application {{.app.metadata.name}} sync is {{.app.status.sync.status}}. Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}. + email: + subject: Application {{.app.metadata.name}} sync status is {{.app.status.sync.status}} ``` ### Trigger Definition @@ -171,6 +172,35 @@ enabled: true send: [my-custom-template] ``` +### Template Definition + +Template `title` and `body` fields have been replaced with `message` field. + + +**Before** + +```yaml +title: Application {{.app.metadata.name}} sync status is {{.app.status.sync.status}} +body: | + Application {{.app.metadata.name}} sync is {{.app.status.sync.status}}. + Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}. +``` + +**After** + +```yaml +message: | + Application {{.app.metadata.name}} sync is {{.app.status.sync.status}}. + Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}. +email: + subject: Application {{.app.metadata.name}} sync status is {{.app.status.sync.status}} +``` + +**Why was it changed?** + +The only service that uses `title` is Email/SMTP notification service. So that field was causing confusion. To remove the confusion `body` field was renamed to +`message` and `title` became Email specific field `email.subject`. + ### Recipient/Subscription Annotation The `.recipients.argocd-notifications.argoproj.io: :` annotation has been replaced diff --git a/hack/gen/main.go b/hack/gen/main.go index 8bdd4cde..af6ee8d8 100644 --- a/hack/gen/main.go +++ b/hack/gen/main.go @@ -10,11 +10,10 @@ import ( "path/filepath" "strings" - "github.com/argoproj-labs/argocd-notifications/pkg/util/misc" - "github.com/argoproj-labs/argocd-notifications/cmd/tools" "github.com/argoproj-labs/argocd-notifications/pkg/services" "github.com/argoproj-labs/argocd-notifications/pkg/triggers" + "github.com/argoproj-labs/argocd-notifications/pkg/util/misc" "github.com/ghodss/yaml" "github.com/olekukonko/tablewriter" @@ -142,7 +141,11 @@ func generateBuiltInTriggersDocs(out io.Writer, triggers map[string][]triggers.C _, _ = fmt.Fprintln(out, "## Templates") misc.IterateStringKeyMap(templates, func(name string) { t := templates[name] - _, _ = fmt.Fprintf(out, "### %s\n**title**: `%s`\n\n**body**:\n```\n%s\n```\n", name, t.Title, t.Body) + yamlData, err := yaml.Marshal(t) + if err != nil { + panic(err) + } + _, _ = fmt.Fprintf(out, "### %s\n**definition**:\n```yaml\n%s\n```\n", name, string(yamlData)) }) } diff --git a/pkg/api_test.go b/pkg/api_test.go index 308fe3c2..8226fb44 100644 --- a/pkg/api_test.go +++ b/pkg/api_test.go @@ -14,7 +14,7 @@ func getConfig(ctrl *gomock.Controller, opts ...func(service *mocks.MockNotifica return Config{ Templates: map[string]services.Notification{ "my-template": { - Body: "hello {{ .foo }} {{ .serviceType }}:{{ .recipient }}", + Message: "hello {{ .foo }} {{ .serviceType }}:{{ .recipient }}", }, }, Services: map[string]ServiceFactory{ @@ -34,7 +34,7 @@ func TestSend(t *testing.T) { api, err := NewAPI(getConfig(ctrl, func(service *mocks.MockNotificationService) { service.EXPECT().Send(services.Notification{ - Body: "hello world slack:my-channel", + Message: "hello world slack:my-channel", }, services.Destination{ Service: "slack", Recipient: "my-channel", diff --git a/pkg/config_test.go b/pkg/config_test.go index 74a98c56..a820bd6a 100644 --- a/pkg/config_test.go +++ b/pkg/config_test.go @@ -29,7 +29,7 @@ token: my-token func TestParseConfig_Templates(t *testing.T) { cfg, err := ParseConfig(&v1.ConfigMap{Data: map[string]string{ "template.my-template": ` -body: hello world +message: hello world `}}, emptySecret) if !assert.NoError(t, err) { @@ -37,7 +37,7 @@ body: hello world } assert.Equal(t, map[string]services.Notification{ - "my-template": {Body: "hello world"}, + "my-template": {Message: "hello world"}, }, cfg.Templates) } diff --git a/pkg/services/email.go b/pkg/services/email.go index 468c087e..0abf2cba 100644 --- a/pkg/services/email.go +++ b/pkg/services/email.go @@ -1,9 +1,53 @@ package services import ( + "bytes" + texttemplate "text/template" + + "github.com/argoproj-labs/argocd-notifications/pkg/util/text" "gomodules.xyz/notify/smtp" ) +type EmailNotification struct { + Subject string `json:"subject,omitempty"` + Body string `json:"body,omitempty"` +} + +func (n *EmailNotification) GetTemplater(name string, f texttemplate.FuncMap) (Templater, error) { + subject, err := texttemplate.New(name).Funcs(f).Parse(n.Subject) + if err != nil { + return nil, err + } + body, err := texttemplate.New(name).Funcs(f).Parse(n.Body) + if err != nil { + return nil, err + } + + return func(notification *Notification, vars map[string]interface{}) error { + if notification.Email == nil { + notification.Email = &EmailNotification{} + } + var emailSubjectData bytes.Buffer + if err := subject.Execute(&emailSubjectData, vars); err != nil { + return err + } + + if val := emailSubjectData.String(); val != "" { + notification.Email.Subject = val + } + + var emailBodyData bytes.Buffer + if err := body.Execute(&emailBodyData, vars); err != nil { + return err + } + if val := emailBodyData.String(); val != "" { + notification.Email.Body = val + } + + return nil + }, nil +} + type EmailOptions struct { Host string `json:"host"` Port int `json:"port"` @@ -22,6 +66,12 @@ func NewEmailService(opts EmailOptions) NotificationService { } func (s *emailService) Send(notification Notification, dest Destination) error { + subject := "" + body := notification.Message + if notification.Email != nil { + subject = notification.Email.Subject + body = text.Coalesce(notification.Email.Body, body) + } return smtp.New(smtp.Options{ From: s.opts.From, Host: s.opts.Host, @@ -29,5 +79,5 @@ func (s *emailService) Send(notification Notification, dest Destination) error { InsecureSkipVerify: s.opts.InsecureSkipVerify, Password: s.opts.Password, Username: s.opts.Username, - }).WithSubject(notification.Title).WithBody(notification.Body).To(dest.Recipient).Send() + }).WithSubject(subject).WithBody(body).To(dest.Recipient).Send() } diff --git a/pkg/services/email_test.go b/pkg/services/email_test.go new file mode 100644 index 00000000..bc507320 --- /dev/null +++ b/pkg/services/email_test.go @@ -0,0 +1,35 @@ +package services + +import ( + "testing" + "text/template" + + "github.com/stretchr/testify/assert" +) + +func TestGetTemplater_Email(t *testing.T) { + n := Notification{ + Email: &EmailNotification{ + Subject: "{{.foo}}", Body: "{{.bar}}", + }, + } + + templater, err := n.GetTemplater("", template.FuncMap{}) + if !assert.NoError(t, err) { + return + } + + var notification Notification + + err = templater(¬ification, map[string]interface{}{ + "foo": "hello", + "bar": "world", + }) + + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, "hello", notification.Email.Subject) + assert.Equal(t, "world", notification.Email.Body) +} diff --git a/pkg/services/grafana.go b/pkg/services/grafana.go index ed8c0b4e..c0846ddd 100644 --- a/pkg/services/grafana.go +++ b/pkg/services/grafana.go @@ -41,7 +41,7 @@ func (s *grafanaService) Send(notification Notification, dest Destination) error Time: time.Now().Unix() * 1000, // unix ts in ms IsRegion: false, Tags: strings.Split(dest.Recipient, "|"), - Text: notification.Title, + Text: notification.Message, } client := &http.Client{ diff --git a/pkg/services/opsgenie.go b/pkg/services/opsgenie.go index 09099b03..922fcf19 100644 --- a/pkg/services/opsgenie.go +++ b/pkg/services/opsgenie.go @@ -1,9 +1,11 @@ package services import ( + "bytes" "context" "fmt" "net/http" + texttemplate "text/template" "github.com/opsgenie/opsgenie-go-sdk-v2/alert" "github.com/opsgenie/opsgenie-go-sdk-v2/client" @@ -17,6 +19,28 @@ type OpsgenieOptions struct { ApiKeys map[string]string `json:"apiKeys"` } +type OpsgenieNotification struct { + Description string `json:"description"` +} + +func (n *OpsgenieNotification) GetTemplater(name string, f texttemplate.FuncMap) (Templater, error) { + desc, err := texttemplate.New(name).Funcs(f).Parse(n.Description) + if err != nil { + return nil, err + } + return func(notification *Notification, vars map[string]interface{}) error { + if notification.Opsgenie == nil { + notification.Opsgenie = &OpsgenieNotification{} + } + var descData bytes.Buffer + if err := desc.Execute(&descData, vars); err != nil { + return err + } + notification.Opsgenie.Description = descData.String() + return nil + }, nil +} + type opsgenieService struct { opts OpsgenieOptions } @@ -38,9 +62,14 @@ func (s *opsgenieService) Send(notification Notification, dest Destination) erro httputil.NewTransport(s.opts.ApiUrl, false), log.WithField("service", "opsgenie")), }, }) + description := "" + if notification.Opsgenie != nil { + description = notification.Opsgenie.Description + } + _, err := alertClient.Create(context.TODO(), &alert.CreateAlertRequest{ - Message: notification.Title, - Description: notification.Body, + Message: notification.Message, + Description: description, Responders: []alert.Responder{ { Type: "team", diff --git a/pkg/services/services.go b/pkg/services/services.go index 37120689..471fa40d 100644 --- a/pkg/services/services.go +++ b/pkg/services/services.go @@ -1,39 +1,21 @@ package services import ( + "bytes" "encoding/json" "fmt" "strings" + texttemplate "text/template" "github.com/ghodss/yaml" ) type Notification struct { - Title string `json:"title,omitempty"` - Body string `json:"body,omitempty"` - Slack *SlackNotification `json:"slack,omitempty"` - Webhook map[string]WebhookNotification `json:"webhook,omitempty" patchStrategy:"replace"` -} - -func (n *Notification) Preview() string { - preview := "" - switch { - case n.Title != "": - preview = n.Title - case n.Body != "": - preview = n.Body - default: - if yamlData, err := json.Marshal(n); err != nil { - preview = "failed to generate preview" - } else { - preview = string(yamlData) - } - } - preview = strings.Split(preview, "\n")[0] - if len(preview) > 100 { - preview = preview[:99] + "..." - } - return preview + Message string `json:"message,omitempty"` + Email *EmailNotification `json:"email,omitempty"` + Slack *SlackNotification `json:"slack,omitempty"` + Webhook WebhookNotifications `json:"webhook,omitempty"` + Opsgenie *OpsgenieNotification `json:"opsgenie,omitempty"` } // Destination holds notification destination details @@ -42,6 +24,25 @@ type Destination struct { Recipient string `json:"recipient"` } +func (n *Notification) GetTemplater(name string, f texttemplate.FuncMap) (Templater, error) { + var sources []TemplaterSource + if n.Slack != nil { + sources = append(sources, n.Slack) + } + if n.Email != nil { + sources = append(sources, n.Email) + } + if n.Webhook != nil { + sources = append(sources, n.Webhook) + } + + if n.Opsgenie != nil { + sources = append(sources, n.Opsgenie) + } + + return n.getTemplater(name, f, sources) +} + //go:generate mockgen -destination=./mocks/mocks.go -package=mocks github.com/argoproj-labs/argocd-notifications/pkg/services NotificationService // NotificationService defines notification service interface @@ -91,3 +92,64 @@ func NewService(serviceType string, optsData []byte) (NotificationService, error return nil, fmt.Errorf("service type '%s' is not supported", serviceType) } } + +func (n *Notification) Preview() string { + preview := "" + switch { + case n.Message != "": + preview = n.Message + default: + if yamlData, err := json.Marshal(n); err != nil { + preview = "failed to generate preview" + } else { + preview = string(yamlData) + } + } + preview = strings.Split(preview, "\n")[0] + if len(preview) > 100 { + preview = preview[:99] + "..." + } + return preview +} + +func (n *Notification) getTemplater(name string, f texttemplate.FuncMap, sources []TemplaterSource) (Templater, error) { + message, err := texttemplate.New(name).Funcs(f).Parse(n.Message) + if err != nil { + return nil, err + } + + templaters := []Templater{func(notification *Notification, vars map[string]interface{}) error { + var messageData bytes.Buffer + if err := message.Execute(&messageData, vars); err != nil { + return err + } + if val := messageData.String(); val != "" { + notification.Message = messageData.String() + } + + return nil + }} + + for _, src := range sources { + t, err := src.GetTemplater(name, f) + if err != nil { + return nil, err + } + templaters = append(templaters, t) + } + + return func(notification *Notification, vars map[string]interface{}) error { + for _, t := range templaters { + if err := t(notification, vars); err != nil { + return err + } + } + return nil + }, nil +} + +type Templater func(notification *Notification, vars map[string]interface{}) error + +type TemplaterSource interface { + GetTemplater(name string, f texttemplate.FuncMap) (Templater, error) +} diff --git a/pkg/services/services_test.go b/pkg/services/services_test.go new file mode 100644 index 00000000..a907a690 --- /dev/null +++ b/pkg/services/services_test.go @@ -0,0 +1,29 @@ +package services + +import ( + "testing" + "text/template" + + "github.com/stretchr/testify/assert" +) + +func TestGetTemplater(t *testing.T) { + n := Notification{Message: "{{.foo}}"} + + templater, err := n.GetTemplater("", template.FuncMap{}) + if !assert.NoError(t, err) { + return + } + + var notification Notification + + err = templater(¬ification, map[string]interface{}{ + "foo": "hello", + }) + + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, "hello", notification.Message) +} diff --git a/pkg/services/slack.go b/pkg/services/slack.go index 67a8c614..77294641 100644 --- a/pkg/services/slack.go +++ b/pkg/services/slack.go @@ -1,12 +1,14 @@ package services import ( + "bytes" "context" "encoding/json" "fmt" "net/http" "net/url" "regexp" + texttemplate "text/template" httputil "github.com/argoproj-labs/argocd-notifications/pkg/util/http" @@ -19,6 +21,34 @@ type SlackNotification struct { Blocks string `json:"blocks,omitempty"` } +func (n *SlackNotification) GetTemplater(name string, f texttemplate.FuncMap) (Templater, error) { + slackAttachments, err := texttemplate.New(name).Funcs(f).Parse(n.Attachments) + if err != nil { + return nil, err + } + slackBlocks, err := texttemplate.New(name).Funcs(f).Parse(n.Blocks) + if err != nil { + return nil, err + } + return func(notification *Notification, vars map[string]interface{}) error { + if notification.Slack == nil { + notification.Slack = &SlackNotification{} + } + var slackAttachmentsData bytes.Buffer + if err := slackAttachments.Execute(&slackAttachmentsData, vars); err != nil { + return err + } + + notification.Slack.Attachments = slackAttachmentsData.String() + var slackBlocksData bytes.Buffer + if err := slackBlocks.Execute(&slackBlocksData, vars); err != nil { + return err + } + notification.Slack.Blocks = slackBlocksData.String() + return nil + }, nil +} + type SlackOptions struct { Username string `json:"username"` Icon string `json:"icon"` @@ -49,7 +79,7 @@ func (s *slackService) Send(notification Notification, dest Destination) error { Transport: httputil.NewLoggingRoundTripper(transport, log.WithField("service", "slack")), } sl := slack.New(s.opts.Token, slack.OptionHTTPClient(client), slack.OptionAPIURL(apiURL)) - msgOptions := []slack.MsgOption{slack.MsgOptionText(notification.Body, false)} + msgOptions := []slack.MsgOption{slack.MsgOptionText(notification.Message, false)} if s.opts.Username != "" { msgOptions = append(msgOptions, slack.MsgOptionUsername(s.opts.Username)) } diff --git a/pkg/services/slack_test.go b/pkg/services/slack_test.go index ae520142..9a315ae0 100644 --- a/pkg/services/slack_test.go +++ b/pkg/services/slack_test.go @@ -2,6 +2,7 @@ package services import ( "testing" + "text/template" "github.com/stretchr/testify/assert" ) @@ -19,3 +20,30 @@ func TestValidIconURL(t *testing.T) { assert.Equal(t, false, isValidIconURL("ftp://favicon.ico")) assert.Equal(t, false, isValidIconURL("ftp://lorempixel.com/favicon.ico")) } + +func TestGetTemplater_Slack(t *testing.T) { + n := Notification{ + Slack: &SlackNotification{ + Attachments: "{{.foo}}", + Blocks: "{{.bar}}", + }, + } + templater, err := n.GetTemplater("", template.FuncMap{}) + + if !assert.NoError(t, err) { + return + } + + var notification Notification + err = templater(¬ification, map[string]interface{}{ + "foo": "hello", + "bar": "world", + }) + + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, "hello", notification.Slack.Attachments) + assert.Equal(t, "world", notification.Slack.Blocks) +} diff --git a/pkg/services/telegram.go b/pkg/services/telegram.go index 2b677d6a..ac2362df 100644 --- a/pkg/services/telegram.go +++ b/pkg/services/telegram.go @@ -19,6 +19,6 @@ func (s telegramService) Send(notification Notification, dest Destination) error if err != nil { return err } - _, err = bot.Send(tgbotapi.NewMessageToChannel("@"+dest.Recipient, notification.Body)) + _, err = bot.Send(tgbotapi.NewMessageToChannel("@"+dest.Recipient, notification.Message)) return err } diff --git a/pkg/services/webhook.go b/pkg/services/webhook.go index ff2357a2..e8801aa2 100644 --- a/pkg/services/webhook.go +++ b/pkg/services/webhook.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "net/http" "strings" + texttemplate "text/template" "github.com/argoproj-labs/argocd-notifications/pkg/util/text" log "github.com/sirupsen/logrus" @@ -19,6 +20,52 @@ type WebhookNotification struct { Path string `json:"path"` } +type WebhookNotifications map[string]WebhookNotification + +type compiledWebhookTemplate struct { + body *texttemplate.Template + path *texttemplate.Template + method string +} + +func (n WebhookNotifications) GetTemplater(name string, f texttemplate.FuncMap) (Templater, error) { + webhooks := map[string]compiledWebhookTemplate{} + for k, v := range n { + body, err := texttemplate.New(name + k).Funcs(f).Parse(v.Body) + if err != nil { + return nil, err + } + path, err := texttemplate.New(name + k).Funcs(f).Parse(v.Path) + if err != nil { + return nil, err + } + webhooks[k] = compiledWebhookTemplate{body: body, method: v.Method, path: path} + } + return func(notification *Notification, vars map[string]interface{}) error { + for k, v := range webhooks { + if notification.Webhook == nil { + notification.Webhook = map[string]WebhookNotification{} + } + var body bytes.Buffer + err := webhooks[k].body.Execute(&body, vars) + if err != nil { + return err + } + var path bytes.Buffer + err = webhooks[k].path.Execute(&path, vars) + if err != nil { + return err + } + notification.Webhook[k] = WebhookNotification{ + Method: v.method, + Body: body.String(), + Path: path.String(), + } + } + return nil + }, nil +} + type Header struct { Name string `json:"name"` Value string `json:"value"` @@ -44,7 +91,7 @@ type webhookService struct { } func (s webhookService) Send(notification Notification, dest Destination) error { - body := notification.Body + body := notification.Message method := http.MethodGet urlPath := "" if notification.Webhook != nil { diff --git a/pkg/services/webook_test.go b/pkg/services/webook_test.go index 861b4ce9..b682a203 100644 --- a/pkg/services/webook_test.go +++ b/pkg/services/webook_test.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httptest" "testing" + "text/template" "github.com/stretchr/testify/assert" ) @@ -66,3 +67,34 @@ func TestWebhook_SubPath(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "/subpath1/subpath2", receivedPath) } + +func TestGetTemplater_Webhook(t *testing.T) { + n := Notification{ + Webhook: WebhookNotifications{ + "github": { + Method: "POST", + Body: "{{.foo}}", + Path: "{{.bar}}", + }, + }, + } + + templater, err := n.GetTemplater("", template.FuncMap{}) + if !assert.NoError(t, err) { + return + } + + var notification Notification + err = templater(¬ification, map[string]interface{}{ + "foo": "hello", + "bar": "world", + }) + + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, notification.Webhook["github"].Method, "POST") + assert.Equal(t, notification.Webhook["github"].Body, "hello") + assert.Equal(t, notification.Webhook["github"].Path, "world") +} diff --git a/pkg/templates/compiled.go b/pkg/templates/compiled.go deleted file mode 100644 index 4bfa0eed..00000000 --- a/pkg/templates/compiled.go +++ /dev/null @@ -1,129 +0,0 @@ -package templates - -import ( - "bytes" - texttemplate "text/template" - - "github.com/Masterminds/sprig" - - "github.com/argoproj-labs/argocd-notifications/pkg/services" -) - -type compiledWebhookTemplate struct { - body *texttemplate.Template - path *texttemplate.Template - method string -} - -type compiledTemplate struct { - title *texttemplate.Template - body *texttemplate.Template - slackAttachments *texttemplate.Template - slackBlocks *texttemplate.Template - webhooks map[string]compiledWebhookTemplate -} - -func (tmpl compiledTemplate) formatNotification(vars map[string]interface{}, notification *services.Notification) error { - - var title bytes.Buffer - - err := tmpl.title.Execute(&title, vars) - if err != nil { - return err - } - if val := title.String(); val != "" { - notification.Title = val - } - - var body bytes.Buffer - err = tmpl.body.Execute(&body, vars) - if err != nil { - return err - } - if val := body.String(); val != "" { - notification.Body = val - } - - if tmpl.slackAttachments != nil || tmpl.slackBlocks != nil { - notification.Slack = &services.SlackNotification{} - } - if tmpl.slackAttachments != nil { - var slackAttachments bytes.Buffer - err = tmpl.slackAttachments.Execute(&slackAttachments, vars) - if err != nil { - return err - } - notification.Slack.Attachments = slackAttachments.String() - } - if tmpl.slackBlocks != nil { - var slackBlocks bytes.Buffer - err = tmpl.slackBlocks.Execute(&slackBlocks, vars) - if err != nil { - return err - } - notification.Slack.Blocks = slackBlocks.String() - } - for k, v := range tmpl.webhooks { - if notification.Webhook == nil { - notification.Webhook = map[string]services.WebhookNotification{} - } - var body bytes.Buffer - err = tmpl.webhooks[k].body.Execute(&body, vars) - if err != nil { - return err - } - var path bytes.Buffer - err = tmpl.webhooks[k].path.Execute(&path, vars) - if err != nil { - return err - } - notification.Webhook[k] = services.WebhookNotification{ - Method: v.method, - Body: body.String(), - Path: path.String(), - } - } - return nil -} - -func compileTemplate(name string, cfg services.Notification) (*compiledTemplate, error) { - f := sprig.TxtFuncMap() - delete(f, "env") - delete(f, "expandenv") - - title, err := texttemplate.New(name).Funcs(f).Parse(cfg.Title) - if err != nil { - return nil, err - } - body, err := texttemplate.New(name).Funcs(f).Parse(cfg.Body) - if err != nil { - return nil, err - } - t := compiledTemplate{title: title, body: body} - if cfg.Slack != nil { - slackAttachments, err := texttemplate.New(name).Funcs(f).Parse(cfg.Slack.Attachments) - if err != nil { - return nil, err - } - t.slackAttachments = slackAttachments - slackBlocks, err := texttemplate.New(name).Funcs(f).Parse(cfg.Slack.Blocks) - if err != nil { - return nil, err - } - t.slackBlocks = slackBlocks - } - - t.webhooks = map[string]compiledWebhookTemplate{} - for k, v := range cfg.Webhook { - body, err := texttemplate.New(k).Funcs(f).Parse(v.Body) - if err != nil { - return nil, err - } - path, err := texttemplate.New(k).Funcs(f).Parse(v.Path) - if err != nil { - return nil, err - } - t.webhooks[k] = compiledWebhookTemplate{body: body, method: v.Method, path: path} - } - return &t, nil -} diff --git a/pkg/templates/service.go b/pkg/templates/service.go index 8c3fd51a..0e288efe 100644 --- a/pkg/templates/service.go +++ b/pkg/templates/service.go @@ -3,6 +3,8 @@ package templates import ( "fmt" + "github.com/Masterminds/sprig" + "github.com/argoproj-labs/argocd-notifications/pkg/services" ) @@ -11,17 +13,21 @@ type Service interface { } type service struct { - compiledTemplates map[string]*compiledTemplate + templaters map[string]services.Templater } func NewService(templates map[string]services.Notification) (*service, error) { - svc := &service{compiledTemplates: map[string]*compiledTemplate{}} + f := sprig.TxtFuncMap() + delete(f, "env") + delete(f, "expandenv") + + svc := &service{templaters: map[string]services.Templater{}} for name, cfg := range templates { - compiled, err := compileTemplate(name, cfg) + templater, err := cfg.GetTemplater(name, f) if err != nil { return nil, err } - svc.compiledTemplates[name] = compiled + svc.templaters[name] = templater } return svc, nil } @@ -29,12 +35,12 @@ func NewService(templates map[string]services.Notification) (*service, error) { func (s *service) FormatNotification(vars map[string]interface{}, templates ...string) (*services.Notification, error) { var notification services.Notification for _, templateName := range templates { - template, ok := s.compiledTemplates[templateName] + templater, ok := s.templaters[templateName] if !ok { return nil, fmt.Errorf("template '%s' is not supported", templateName) } - if err := template.formatNotification(vars, ¬ification); err != nil { + if err := templater(¬ification, vars); err != nil { return nil, err } } diff --git a/pkg/templates/service_test.go b/pkg/templates/service_test.go index 8c0d2431..1c5b0f3b 100644 --- a/pkg/templates/service_test.go +++ b/pkg/templates/service_test.go @@ -8,10 +8,10 @@ import ( "github.com/argoproj-labs/argocd-notifications/pkg/services" ) -func TestFormat_BodyAndTitle(t *testing.T) { +func TestFormat_Message(t *testing.T) { svc, err := NewService(map[string]services.Notification{ "test": { - Title: "{{.foo}}", Body: "{{.bar}}", + Message: "{{.foo}}", }, }) @@ -21,71 +21,11 @@ func TestFormat_BodyAndTitle(t *testing.T) { notification, err := svc.FormatNotification(map[string]interface{}{ "foo": "hello", - "bar": "world", }, "test") if !assert.NoError(t, err) { return } - assert.Equal(t, "hello", notification.Title) - assert.Equal(t, "world", notification.Body) -} - -func TestFormat_Slack(t *testing.T) { - svc, err := NewService(map[string]services.Notification{ - "test": { - Slack: &services.SlackNotification{ - Attachments: "{{.foo}}", - Blocks: "{{.bar}}", - }, - }, - }) - - if !assert.NoError(t, err) { - return - } - - notification, err := svc.FormatNotification(map[string]interface{}{ - "foo": "hello", - "bar": "world", - }, "test") - - if !assert.NoError(t, err) { - return - } - - assert.Equal(t, "hello", notification.Slack.Attachments) - assert.Equal(t, "world", notification.Slack.Blocks) -} - -func TestFormat_Webhook(t *testing.T) { - svc, err := NewService(map[string]services.Notification{ - "test": { - Webhook: map[string]services.WebhookNotification{ - "github": { - Method: "POST", - Body: "{{.foo}}", - Path: "{{.bar}}", - }, - }, - }, - }) - - if !assert.NoError(t, err) { - return - } - - notification, err := svc.FormatNotification(map[string]interface{}{ - "foo": "hello", - "bar": "world", - }, "test") - - if !assert.NoError(t, err) { - return - } - - assert.Equal(t, notification.Webhook["github"].Method, "POST") - assert.Equal(t, notification.Webhook["github"].Body, "hello") - assert.Equal(t, notification.Webhook["github"].Path, "world") + assert.Equal(t, "hello", notification.Message) } diff --git a/shared/legacy/settings.go b/shared/legacy/settings.go index 1caef6c5..d90c6d84 100644 --- a/shared/legacy/settings.go +++ b/shared/legacy/settings.go @@ -19,7 +19,9 @@ import ( ) type legacyTemplate struct { - Name string `json:"name,omitempty"` + Name string `json:"name,omitempty"` + Title string `json:"subject,omitempty"` + Body string `json:"body,omitempty"` services.Notification } @@ -87,6 +89,15 @@ func (legacy legacyConfig) merge(cfg *settings.Config) error { return err } } + if template.Title != "" { + if template.Notification.Email == nil { + template.Notification.Email = &services.EmailNotification{} + } + template.Notification.Email.Subject = template.Title + } + if template.Body != "" { + template.Notification.Message = template.Body + } cfg.Templates[template.Name] = template.Notification } diff --git a/shared/legacy/settings_test.go b/shared/legacy/settings_test.go index b9abcf89..23b99268 100644 --- a/shared/legacy/settings_test.go +++ b/shared/legacy/settings_test.go @@ -49,7 +49,7 @@ triggers: func TestMergeLegacyConfig(t *testing.T) { cfg := settings.Config{ Config: pkg.Config{ - Templates: map[string]services.Notification{"my-template1": {Body: "foo"}}, + Templates: map[string]services.Notification{"my-template1": {Message: "foo"}}, Triggers: map[string][]triggers.Condition{ "my-trigger1": {{ When: "true", @@ -93,8 +93,8 @@ slack: assert.NoError(t, err) assert.Equal(t, map[string]services.Notification{ - "my-template1": {Body: "bar"}, - "my-template2": {Body: "foo"}, + "my-template1": {Message: "bar"}, + "my-template2": {Message: "foo"}, }, cfg.Templates) assert.Equal(t, []triggers.Condition{{