From 4a4a4c1644c96a5a3195be9ea2f569b3fd5429ea Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 26 May 2020 22:37:20 +0200 Subject: [PATCH] Add script notification (#75) * Add script notification (#53) * Fix SysProcAttr * Fix build constraint Co-authored-by: CrazyMax --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- doc/configuration.md | 20 +++-- doc/faq.md | 4 +- doc/notifications.md | 23 ++++++ doc/providers/docker.md | 4 +- doc/providers/file.md | 12 +-- doc/providers/swarm.md | 6 +- internal/config/config_test.go | 14 +++- internal/config/dummy.yml | 0 internal/config/notif.go | 20 +++++ internal/config/{ => test}/config.invalid.yml | 0 internal/config/{ => test}/config.test.yml | 8 +- internal/config/test/dummy.yml | 1 + internal/config/test/myscript.sh | 1 + internal/model/notif.go | 8 ++ internal/notif/client.go | 4 + internal/notif/script/client.go | 73 +++++++++++++++++++ internal/notif/script/cmd.go | 10 +++ internal/notif/script/cmd_windows.go | 10 +++ 19 files changed, 193 insertions(+), 27 deletions(-) delete mode 100644 internal/config/dummy.yml rename internal/config/{ => test}/config.invalid.yml (100%) rename internal/config/{ => test}/config.test.yml (93%) create mode 100644 internal/config/test/dummy.yml create mode 100644 internal/config/test/myscript.sh create mode 100644 internal/notif/script/client.go create mode 100644 internal/notif/script/cmd.go create mode 100644 internal/notif/script/cmd_windows.go diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 36f29baa..a0408db5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -25,7 +25,7 @@ about: Create a report to help us improve * Platform (windows/linux) : * System info (type `uname -a`) : -```yml +```yaml # paste your YAML configuration file here and remove sensitive data ``` diff --git a/doc/configuration.md b/doc/configuration.md index fc0dafc9..52b7ea1d 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -10,7 +10,7 @@ ## Overview -```yml +```yaml db: path: diun.db @@ -20,19 +20,19 @@ watch: first_check_notif: false notif: - amqp: + amqp: host: localhost port: 5672 username: guest password: guest exchange: queue: queue - gotify: + gotify: endpoint: http://gotify.foo.com token: Token123456 priority: 1 timeout: 10 - mail: + mail: host: localhost port: 25 ssl: false @@ -41,15 +41,20 @@ notif: password: from: to: - rocketchat: + rocketchat: endpoint: http://rocket.foo.com:3000 channel: "#general" user_id: abcdEFGH012345678 token: Token123456 timeout: 10 - slack: + script: + cmd: "myprogram" + args: + - "--anarg" + - "another" + slack: webhook_url: https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij - teams: + teams: webhook_url: https://outlook.office.com/webhook/ABCD12EFG/HIJK34LMN/01234567890abcdefghij telegram: token: aabbccdd:11223344 @@ -101,6 +106,7 @@ providers: * [gotify](notifications.md#gotify) * [mail](notifications.md#mail) * [rocketchat](notifications.md#rocketchat) +* [script](notifications.md#script) * [slack](notifications.md#slack) * [teams](notifications.md#teams) * [telegram](notifications.md#telegram) diff --git a/doc/faq.md b/doc/faq.md index 789e50ec..1fa0d557 100644 --- a/doc/faq.md +++ b/doc/faq.md @@ -8,7 +8,7 @@ If you encounter this kind of error, you are probably using the [file provider]( In the example below, Diun is running (`diun_x.x.x_windows_i386.zip`) on Windows 10 and tries to analyze the `crazymax/cloudflared` image with the detected platform (`windows/386)`: -```yml +```yaml - name: crazymax/cloudflared:2020.2.1 watch_repo: true ``` @@ -22,7 +22,7 @@ Fri, 27 Mar 2020 01:20:03 UTC ERR Cannot list tags from registry error="Error ch You have to force the platform for this image if you are not on a supported platform. For example: -```yml +```yaml - name: crazymax/cloudflared:2020.2.1 watch_repo: true platform: diff --git a/doc/notifications.md b/doc/notifications.md index fee58323..e7a82c32 100644 --- a/doc/notifications.md +++ b/doc/notifications.md @@ -4,6 +4,7 @@ * [Gotify](#gotify) * [Mail](#mail) * [Rocket.Chat](#rocketchat) +* [Script](#script) * [Slack](#slack) * [Teams](#teams) * [Telegram](#telegram) @@ -83,6 +84,28 @@ To be able to send notifications to your Rocket.Chat channel: ![](../.res/notif-rocketchat.png) +## Script + +You can send script notifications with the following settings: + +* `script` + * `cmd`: Command or script to execute. **required** + * `args`: List of args to pass to `cmd`. + * `dir`: Specifies the working directory of the command. + +Following environment variables are passed to the process and will look like this: + +``` +DIUN_VERSION=3.0.0 +DIUN_ENTRY_STATUS=new +DIUN_ENTRY_PROVIDER=file +DIUN_ENTRY_IMAGE=docker.io/crazymax/diun:latest +DIUN_ENTRY_MIMETYPE=application/vnd.docker.distribution.manifest.list.v2+json +DIUN_ENTRY_DIGEST=sha256:216e3ae7de4ca8b553eb11ef7abda00651e79e537e85c46108284e5e91673e01 +DIUN_ENTRY_CREATED=2020-03-26 12:23:56 +0000 UTC +DIUN_ENTRY_PLATFORM=linux/adm64 +``` + ## Slack You can send notifications to your Slack channel using an [incoming webhook URL](https://api.slack.com/messaging/webhooks): diff --git a/doc/providers/docker.md b/doc/providers/docker.md index 7610f5ee..ac1ff1a6 100644 --- a/doc/providers/docker.md +++ b/doc/providers/docker.md @@ -15,7 +15,7 @@ In this section we quickly go over a basic docker-compose file using your local First of all, let's create a Diun configuration we named `diun.yml`: -```yml +```yaml watch: workers: 20 schedule: "*/30 * * * *" @@ -29,7 +29,7 @@ Here we use a single Docker provider with a minimum configuration to analyze lab Now let's create a simple docker-compose file with Diun and some simple services: -```yml +```yaml version: "3.5" services: diff --git a/doc/providers/file.md b/doc/providers/file.md index f453da4f..0fb5018c 100644 --- a/doc/providers/file.md +++ b/doc/providers/file.md @@ -16,7 +16,7 @@ The file provider lets you define Docker images to analyze through a YAML file o Register the file provider: -```yml +```yaml db: path: diun.db @@ -39,7 +39,7 @@ providers: filename: /path/to/config.yml ``` -```yml +```yaml ### /path/to/config.yml # Watch latest tag of crazymax/nextcloud image on docker.io (DockerHub) with registry ID 'someregistryoptions'. @@ -82,7 +82,7 @@ providers: Let's take a look with a simple example: -```yml +```yaml db: path: diun.db @@ -100,7 +100,7 @@ providers: filename: /path/to/config.yml ``` -```yml +```yaml # /path/to/config.yml - name: crazymax/cloudflared watch_repo: true @@ -137,7 +137,7 @@ Defines the path to the [configuration file](#yaml-configuration-file). > :warning: `filename` and `directory` are mutually exclusive. -```yml +```yaml providers: file: filename: /path/to/config/conf.yml @@ -149,7 +149,7 @@ Defines the path to the directory that contains the [configuration files](#yaml- > :warning: `filename` and `directory` are mutually exclusive. -```yml +```yaml providers: file: directory: /path/to/config diff --git a/doc/providers/swarm.md b/doc/providers/swarm.md index ccc841f7..1eca526c 100644 --- a/doc/providers/swarm.md +++ b/doc/providers/swarm.md @@ -15,7 +15,7 @@ In this section we quickly go over a basic stack using your local swarm cluster. First of all, let's create a Diun configuration we named `diun.yml`: -```yml +```yaml watch: workers: 20 schedule: "*/30 * * * *" @@ -28,7 +28,7 @@ Here we use our local Swarm provider with a minimum configuration to analyze lab Now let's create a simple stack for Diun: -```yml +```yaml version: "3.5" services: @@ -50,7 +50,7 @@ services: And another one with a simple service: -```yml +```yaml version: "3.5" services: diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 66dd2c05..8554b1f3 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -24,18 +24,18 @@ func TestLoad(t *testing.T) { { name: "Fail on wrong file format", cli: model.Cli{ - Cfgfile: "config.invalid.yml", + Cfgfile: "./test/config.invalid.yml", }, wantErr: true, }, { name: "Success", cli: model.Cli{ - Cfgfile: "config.test.yml", + Cfgfile: "./test/config.test.yml", }, wantData: &config.Config{ Cli: model.Cli{ - Cfgfile: "config.test.yml", + Cfgfile: "./test/config.test.yml", }, App: model.App{ ID: "diun", @@ -82,6 +82,12 @@ func TestLoad(t *testing.T) { Token: "Token123456", Timeout: 10, }, + Script: &model.NotifScript{ + Cmd: "go", + Args: []string{ + "version", + }, + }, Slack: &model.NotifSlack{ WebhookURL: "https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij", }, @@ -126,7 +132,7 @@ func TestLoad(t *testing.T) { WatchByDefault: utl.NewTrue(), }, File: &model.PrdFile{ - Filename: "./dummy.yml", + Filename: "./test/dummy.yml", }, }, }, diff --git a/internal/config/dummy.yml b/internal/config/dummy.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/internal/config/notif.go b/internal/config/notif.go index e52c115b..61c1f84b 100644 --- a/internal/config/notif.go +++ b/internal/config/notif.go @@ -2,6 +2,7 @@ package config import ( "net/mail" + "os/exec" "github.com/crazy-max/diun/internal/model" "github.com/crazy-max/diun/pkg/utl" @@ -26,6 +27,9 @@ func (cfg *Config) validateNotif() error { if err := cfg.validateNotifRocketChat(); err != nil { return err } + if err := cfg.validateNotifScript(); err != nil { + return err + } if err := cfg.validateNotifSlack(); err != nil { return err } @@ -106,6 +110,22 @@ func (cfg *Config) validateNotifRocketChat() error { return nil } +func (cfg *Config) validateNotifScript() error { + if cfg.Notif.Script == nil { + return nil + } + + if cfg.Notif.Script.Cmd == "" { + return errors.New("command required for script provider") + } + + if _, err := exec.LookPath(cfg.Notif.Script.Cmd); err != nil { + return errors.Wrap(err, "command not found for script provider") + } + + return nil +} + func (cfg *Config) validateNotifSlack() error { if cfg.Notif.Slack == nil { return nil diff --git a/internal/config/config.invalid.yml b/internal/config/test/config.invalid.yml similarity index 100% rename from internal/config/config.invalid.yml rename to internal/config/test/config.invalid.yml diff --git a/internal/config/config.test.yml b/internal/config/test/config.test.yml similarity index 93% rename from internal/config/config.test.yml rename to internal/config/test/config.test.yml index 3314d2c9..7b39320f 100644 --- a/internal/config/config.test.yml +++ b/internal/config/test/config.test.yml @@ -35,9 +35,13 @@ notif: user_id: abcdEFGH012345678 token: Token123456 timeout: 10 + script: + cmd: "go" + args: + - "version" slack: webhook_url: https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij - teams: + teams: webhook_url: https://outlook.office.com/webhook/ABCD12EFG/HIJK34LMN/01234567890abcdefghij telegram: token: abcdef123456 @@ -69,4 +73,4 @@ providers: swarm: watch_by_default: true file: - filename: ./dummy.yml + filename: ./test/dummy.yml diff --git a/internal/config/test/dummy.yml b/internal/config/test/dummy.yml new file mode 100644 index 00000000..740503eb --- /dev/null +++ b/internal/config/test/dummy.yml @@ -0,0 +1 @@ +# noop diff --git a/internal/config/test/myscript.sh b/internal/config/test/myscript.sh new file mode 100644 index 00000000..740503eb --- /dev/null +++ b/internal/config/test/myscript.sh @@ -0,0 +1 @@ +# noop diff --git a/internal/model/notif.go b/internal/model/notif.go index b95b6072..5de20c39 100644 --- a/internal/model/notif.go +++ b/internal/model/notif.go @@ -18,6 +18,7 @@ type Notif struct { Gotify *NotifGotify `yaml:"gotify,omitempty"` Mail *NotifMail `yaml:"mail,omitempty"` RocketChat *NotifRocketChat `yaml:"rocketchat,omitempty"` + Script *NotifScript `yaml:"script,omitempty"` Slack *NotifSlack `yaml:"slack,omitempty"` Teams *NotifTeams `yaml:"teams,omitempty"` Telegram *NotifTelegram `yaml:"telegram,omitempty"` @@ -67,6 +68,13 @@ type NotifRocketChat struct { Timeout int `yaml:"timeout,omitempty"` } +// NotifScript holds script notification configuration details +type NotifScript struct { + Cmd string `yaml:"cmd,omitempty"` + Args []string `yaml:"args,omitempty"` + Dir string `yaml:"dir,omitempty"` +} + // NotifSlack holds slack notification configuration details type NotifSlack struct { WebhookURL string `yaml:"webhook_url,omitempty"` diff --git a/internal/notif/client.go b/internal/notif/client.go index c0eb8f6a..98ed23e5 100644 --- a/internal/notif/client.go +++ b/internal/notif/client.go @@ -7,6 +7,7 @@ import ( "github.com/crazy-max/diun/internal/notif/mail" "github.com/crazy-max/diun/internal/notif/notifier" "github.com/crazy-max/diun/internal/notif/rocketchat" + "github.com/crazy-max/diun/internal/notif/script" "github.com/crazy-max/diun/internal/notif/slack" "github.com/crazy-max/diun/internal/notif/teams" "github.com/crazy-max/diun/internal/notif/telegram" @@ -47,6 +48,9 @@ func New(config *model.Notif, app model.App, userAgent string) (*Client, error) if config.RocketChat != nil { c.notifiers = append(c.notifiers, rocketchat.New(config.RocketChat, app, userAgent)) } + if config.Script != nil { + c.notifiers = append(c.notifiers, script.New(config.Script, app)) + } if config.Slack != nil { c.notifiers = append(c.notifiers, slack.New(config.Slack, app)) } diff --git a/internal/notif/script/client.go b/internal/notif/script/client.go new file mode 100644 index 00000000..bb65ec16 --- /dev/null +++ b/internal/notif/script/client.go @@ -0,0 +1,73 @@ +package script + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/crazy-max/diun/internal/model" + "github.com/crazy-max/diun/internal/notif/notifier" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) + +// Client represents an active script notification object +type Client struct { + *notifier.Notifier + cfg *model.NotifScript + app model.App + userAgent string +} + +// New creates a new script notification instance +func New(config *model.NotifScript, app model.App) notifier.Notifier { + return notifier.Notifier{ + Handler: &Client{ + cfg: config, + app: app, + }, + } +} + +// Name returns notifier's name +func (c *Client) Name() string { + return "script" +} + +// Send creates and sends a script notification with an entry +func (c *Client) Send(entry model.NotifEntry) error { + cmd := exec.Command(c.cfg.Cmd, c.cfg.Args...) + setSysProcAttr(cmd) + + // Capture output + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + // Set working dir + if c.cfg.Dir != "" { + cmd.Dir = c.cfg.Dir + } + + // Set env vars + cmd.Env = append(os.Environ(), []string{ + fmt.Sprintf("DIUN_VERSION=%s", c.app.Version), + fmt.Sprintf("DIUN_ENTRY_STATUS=%s", string(entry.Status)), + fmt.Sprintf("DIUN_ENTRY_PROVIDER=%s", entry.Provider), + fmt.Sprintf("DIUN_ENTRY_IMAGE=%s", entry.Image.String()), + fmt.Sprintf("DIUN_ENTRY_MIMETYPE=%s", entry.Manifest.MIMEType), + fmt.Sprintf("DIUN_ENTRY_DIGEST=%s", entry.Manifest.Digest), + fmt.Sprintf("DIUN_ENTRY_CREATED=%s", entry.Manifest.Created), + fmt.Sprintf("DIUN_ENTRY_PLATFORM=%s", entry.Manifest.Platform), + }...) + + // Run + if err := cmd.Run(); err != nil { + return errors.Wrap(err, strings.TrimSpace(stderr.String())) + } + + log.Debug().Msgf(strings.TrimSpace(stdout.String())) + return nil +} diff --git a/internal/notif/script/cmd.go b/internal/notif/script/cmd.go new file mode 100644 index 00000000..825a3230 --- /dev/null +++ b/internal/notif/script/cmd.go @@ -0,0 +1,10 @@ +// +build !windows + +package script + +import ( + "os/exec" +) + +func setSysProcAttr(cmd *exec.Cmd) { +} diff --git a/internal/notif/script/cmd_windows.go b/internal/notif/script/cmd_windows.go new file mode 100644 index 00000000..e273d99f --- /dev/null +++ b/internal/notif/script/cmd_windows.go @@ -0,0 +1,10 @@ +package script + +import ( + "os/exec" + "syscall" +) + +func setSysProcAttr(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} +}