Skip to content

Commit

Permalink
add http request. unify templates
Browse files Browse the repository at this point in the history
  • Loading branch information
reddec committed Nov 28, 2017
1 parent 958075b commit 87cef14
Show file tree
Hide file tree
Showing 8 changed files with 313 additions and 90 deletions.
2 changes: 1 addition & 1 deletion .goxc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"default",
"publish-github"
],
"PackageVersion": "0.1.2",
"PackageVersion": "0.1.3",
"TaskSettings": {
"publish-github": {
"owner": "reddec",
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ It’s tool for controlling processes like a supervisord but with some important
* Developed for used inside Docker containers
* Different strategies for processes
* Support template-based email notification
* Support HTTP notification

## Installing

Expand Down Expand Up @@ -89,3 +90,15 @@ email:
Service {{.label}} {{.action}}
```
#### HTTP
Add HTTP request as notification
```yaml
http:
services:
- myservice
url: "http://example.com/{{.label}}/{{.action}}"
templateFile: "./body.txt"
```
37 changes: 37 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

# MONEXEC

[![GitHub release](https://img.shields.io/github/release/reddec/monexec.svg)](https://github.com/reddec/monexec/releases)
Expand Down Expand Up @@ -109,6 +110,9 @@ telegram:
_time: {{.time}}_
_host: {{.hostname}}_
```
Since `0.1.4` you also can specify `templateFile` instead of `template`

# How to integrate with email

Since `0.1.3` you can receive notifications over email.
Expand Down Expand Up @@ -188,6 +192,39 @@ email:
`template` will be used as fallback for `templateFile`. If template file location is not absolute, it will be calculated
from configuration directory.

# How to integrate with HTTP

Since `0.1.4` you can send notifications over HTTP

* Supports any kind of methods (by default - `POST` but can be changed in `http.method`)
* **Body** - template-based text same as in `Telegram` or `Email` plugin
* **URL** - also template-based text (yes, with same rules as in `body` ;-) )
* **Headers** - you can also provide any headers (no, no templates here)
* **Timeout** - limit time for request. By default - `20s`

Configuration avaiable only from .yaml files:

```yaml
http:
services:
- myservice
url: "http://example.com/{{.label}}/{{.action}}"
templateFile: "./body.txt"
```

`template` will be used as fallback for `templateFile`. If template file location is not absolute, it will be calculated from configuration directory.


|Parameter | Type | Required | Default | Description |
|--------------|----------|----------|---------|-------------|
|`url` |`string` | yes | | Target URL
|`method` |`string` | no | POST | HTTP method
|`services` |`list` | yes | | List of services that will trigger plugin
|`headers` |`map` | no | {} | Map (string -> string) of additional headers per request
|`timeout` |`duration`| no | 20s | Request timeout
|`template` |`string` | no | '' | Template string
|`templateFile`|`string` | no | '' | Path to file of template (more priority then `template`, but `template` will be used as fallback)

# Usage

`monexec <command> [command-flags...] [args,...]`
Expand Down
59 changes: 19 additions & 40 deletions plugins/adp_email.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package plugins

import (
"bytes"
"github.com/reddec/container"
"time"
"log"
"os"
"net/smtp"
Expand All @@ -13,38 +11,24 @@ import (
)

type Email struct {
Smtp string `yaml:"smtp"`
From string `yaml:"from"`
Password string `yaml:"password"`
To []string `yaml:"to"`
Template string `yaml:"template"`
TemplateFile string `yaml:"templateFile"` // template file (relative to config dir) has priority. Template supports basic utils
Services []string `yaml:"services"`
Smtp string `yaml:"smtp"`
From string `yaml:"from"`
Password string `yaml:"password"`
To []string `yaml:"to"`
Services []string `yaml:"services"`
withTemplate `mapstructure:",squash" yaml:",inline"`

log *log.Logger
hostname string
servicesSet map[string]bool
workDir string
}

func (c *Email) renderAndSend(params map[string]interface{}) {
message := &bytes.Buffer{}

parser, err := parseFileOrTemplate(c.TemplateFile, c.Template, c.log)
if err != nil {
c.log.Println("failed parse template:", err)
return
}
renderErr := parser.Execute(message, params)
if renderErr != nil {
c.log.Println("failed render:", renderErr, "; params:", params)
return
}

c.log.Println(message.String())
func (c *Email) renderAndSend(message string) {
c.log.Println(message)
host, _, _ := net.SplitHostPort(c.Smtp)
auth := smtp.PlainAuth("", c.From, c.Password, host)
err = smtp.SendMail(c.Smtp, auth, c.From, c.To, message.Bytes())
err := smtp.SendMail(c.Smtp, auth, c.From, c.To, []byte(message))
if err != nil {
c.log.Println("failed send mail:", err)
} else {
Expand All @@ -54,14 +38,12 @@ func (c *Email) renderAndSend(params map[string]interface{}) {

func (c *Email) Spawned(runnable container.Runnable, id container.ID) {
if c.servicesSet[runnable.Label()] {
params := map[string]interface{}{
"action": "spawned",
"id": id,
"label": runnable.Label(),
"hostname": c.hostname,
"time": time.Now().String(),
content, renderErr := c.renderDefault("spawned", string(id), runnable.Label(), nil, c.log)
if renderErr != nil {
c.log.Println("failed render:", renderErr)
} else {
c.renderAndSend(content)
}
c.renderAndSend(params)
}
}

Expand All @@ -74,15 +56,12 @@ func (c *Email) Prepare() error {

func (c *Email) Stopped(runnable container.Runnable, id container.ID, err error) {
if c.servicesSet[runnable.Label()] {
params := map[string]interface{}{
"action": "stopped",
"id": id,
"error": err,
"label": runnable.Label(),
"hostname": c.hostname,
"time": time.Now().String(),
content, renderErr := c.renderDefault("stopped", string(id), runnable.Label(), err, c.log)
if renderErr != nil {
c.log.Println("failed render:", renderErr)
} else {
c.renderAndSend(content)
}
c.renderAndSend(params)
}
}

Expand Down
136 changes: 136 additions & 0 deletions plugins/adp_http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package plugins

import (
"log"
"github.com/reddec/container"
"os"
"github.com/pkg/errors"
"path/filepath"
"net/http"
"bytes"
"github.com/Masterminds/sprig"
"html/template"
"io"
"time"
"context"
)

type Http struct {
URL string `yaml:"url" mapstructure:"url"` // template URL string
Method string `yaml:"method"` // default POST
Headers map[string]string `yaml:"headers" mapstructure:"headers"` // additional header (non-template)
Services []string `yaml:"services"`
Timeout time.Duration `yaml:"timeout"`
withTemplate `mapstructure:",squash" yaml:",inline"`
log *log.Logger `yaml:"-"`
servicesSet map[string]bool
workDir string
}

func (c *Http) renderAndSend(message string, params map[string]interface{}) {
c.log.Println(message)

tpl, err := template.New("").Funcs(sprig.FuncMap()).Parse(string(c.URL))
if err != nil {
c.log.Println("failed parse URL as template:", err)
return
}
urlM := &bytes.Buffer{}
err = tpl.Execute(urlM, params)
if err != nil {
c.log.Println("failed execute URL as template:", err)
return
}

req, err := http.NewRequest(c.Method, urlM.String(), bytes.NewBufferString(message))
if err != nil {
c.log.Println("failed prepare request:", err)
return
}

ctx, closer := context.WithTimeout(context.Background(), c.Timeout)
defer closer()

res, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil {
c.log.Println("failed make request:", err)
return
}
io.Copy(os.Stdout, res.Body) // allow keep-alive
res.Body.Close()
}

func (c *Http) Spawned(runnable container.Runnable, id container.ID) {
if c.servicesSet[runnable.Label()] {
content, params, renderErr := c.renderDefaultParams("spawned", string(id), runnable.Label(), nil, c.log)
if renderErr != nil {
c.log.Println("failed render:", renderErr)
} else {
c.renderAndSend(content, params)
}
}
}

func (c *Http) Prepare() error {
c.servicesSet = makeSet(c.Services)
c.log = log.New(os.Stderr, "[http] ", log.LstdFlags)
if c.Method == "" {
c.Method = "POST"
}
if c.Timeout == 0 {
c.Timeout = 20 * time.Second
}
return nil
}

func (c *Http) Stopped(runnable container.Runnable, id container.ID, err error) {
if c.servicesSet[runnable.Label()] {
content, params, renderErr := c.renderDefaultParams("stopped", string(id), runnable.Label(), err, c.log)
if renderErr != nil {
c.log.Println("failed render:", renderErr)
} else {
c.renderAndSend(content, params)
}
}
}

func (a *Http) MergeFrom(other interface{}) (error) {
b := other.(*Http)
if a.URL == "" {
a.URL = b.URL
}
if a.URL != b.URL {
return errors.New("different urls")
}
if a.Method == "" {
a.Method = b.Method
}
if a.Method != b.Method {
return errors.New("different methods")
}
if a.Timeout == 0 {
a.Timeout = b.Timeout
}
if a.Timeout != b.Timeout {
return errors.New("different timeout")
}
a.withTemplate.resolvePath(a.workDir)
b.withTemplate.resolvePath(b.workDir)
if err := a.withTemplate.MergeFrom(&b.withTemplate); err != nil {
return err
}
if a.Headers == nil {
a.Headers = make(map[string]string)
}
for k, v := range b.Headers {
a.Headers[k] = v
}
a.Services = append(a.Services, b.Services...)
return nil
}

func init() {
registerPlugin("http", func(file string) PluginConfig {
return &Http{workDir: filepath.Dir(file)}
})
}
Loading

0 comments on commit 87cef14

Please sign in to comment.