Add script notification (#75)

* Add script notification (#53)

* Fix SysProcAttr

* Fix build constraint

Co-authored-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2020-05-26 22:37:20 +02:00
committed by GitHub
parent 9aeef6eb6b
commit 4a4a4c1644
19 changed files with 193 additions and 27 deletions

View File

@@ -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
```

View File

@@ -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)

View File

@@ -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:

View File

@@ -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):

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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",
},
},
},

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
# noop

View File

@@ -0,0 +1 @@
# noop

View File

@@ -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"`

View File

@@ -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))
}

View File

@@ -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
}

View File

@@ -0,0 +1,10 @@
// +build !windows
package script
import (
"os/exec"
)
func setSysProcAttr(cmd *exec.Cmd) {
}

View File

@@ -0,0 +1,10 @@
package script
import (
"os/exec"
"syscall"
)
func setSysProcAttr(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
}