Skip to content

Commit 87cef14

Browse files
committed
add http request. unify templates
1 parent 958075b commit 87cef14

8 files changed

Lines changed: 313 additions & 90 deletions

File tree

.goxc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"default",
44
"publish-github"
55
],
6-
"PackageVersion": "0.1.2",
6+
"PackageVersion": "0.1.3",
77
"TaskSettings": {
88
"publish-github": {
99
"owner": "reddec",

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ It’s tool for controlling processes like a supervisord but with some important
1515
* Developed for used inside Docker containers
1616
* Different strategies for processes
1717
* Support template-based email notification
18+
* Support HTTP notification
1819

1920
## Installing
2021

@@ -89,3 +90,15 @@ email:
8990
9091
Service {{.label}} {{.action}}
9192
```
93+
94+
#### HTTP
95+
96+
Add HTTP request as notification
97+
98+
```yaml
99+
http:
100+
services:
101+
- myservice
102+
url: "http://example.com/{{.label}}/{{.action}}"
103+
templateFile: "./body.txt"
104+
```

docs/index.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
# MONEXEC
23

34
[![GitHub release](https://img.shields.io/github/release/reddec/monexec.svg)](https://github.com/reddec/monexec/releases)
@@ -109,6 +110,9 @@ telegram:
109110
_time: {{.time}}_
110111
_host: {{.hostname}}_
111112
```
113+
114+
Since `0.1.4` you also can specify `templateFile` instead of `template`
115+
112116
# How to integrate with email
113117

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

195+
# How to integrate with HTTP
196+
197+
Since `0.1.4` you can send notifications over HTTP
198+
199+
* Supports any kind of methods (by default - `POST` but can be changed in `http.method`)
200+
* **Body** - template-based text same as in `Telegram` or `Email` plugin
201+
* **URL** - also template-based text (yes, with same rules as in `body` ;-) )
202+
* **Headers** - you can also provide any headers (no, no templates here)
203+
* **Timeout** - limit time for request. By default - `20s`
204+
205+
Configuration avaiable only from .yaml files:
206+
207+
```yaml
208+
http:
209+
services:
210+
- myservice
211+
url: "http://example.com/{{.label}}/{{.action}}"
212+
templateFile: "./body.txt"
213+
```
214+
215+
`template` will be used as fallback for `templateFile`. If template file location is not absolute, it will be calculated from configuration directory.
216+
217+
218+
|Parameter | Type | Required | Default | Description |
219+
|--------------|----------|----------|---------|-------------|
220+
|`url` |`string` | yes | | Target URL
221+
|`method` |`string` | no | POST | HTTP method
222+
|`services` |`list` | yes | | List of services that will trigger plugin
223+
|`headers` |`map` | no | {} | Map (string -> string) of additional headers per request
224+
|`timeout` |`duration`| no | 20s | Request timeout
225+
|`template` |`string` | no | '' | Template string
226+
|`templateFile`|`string` | no | '' | Path to file of template (more priority then `template`, but `template` will be used as fallback)
227+
191228
# Usage
192229

193230
`monexec <command> [command-flags...] [args,...]`

plugins/adp_email.go

Lines changed: 19 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package plugins
22

33
import (
4-
"bytes"
54
"github.com/reddec/container"
6-
"time"
75
"log"
86
"os"
97
"net/smtp"
@@ -13,38 +11,24 @@ import (
1311
)
1412

1513
type Email struct {
16-
Smtp string `yaml:"smtp"`
17-
From string `yaml:"from"`
18-
Password string `yaml:"password"`
19-
To []string `yaml:"to"`
20-
Template string `yaml:"template"`
21-
TemplateFile string `yaml:"templateFile"` // template file (relative to config dir) has priority. Template supports basic utils
22-
Services []string `yaml:"services"`
14+
Smtp string `yaml:"smtp"`
15+
From string `yaml:"from"`
16+
Password string `yaml:"password"`
17+
To []string `yaml:"to"`
18+
Services []string `yaml:"services"`
19+
withTemplate `mapstructure:",squash" yaml:",inline"`
2320

2421
log *log.Logger
2522
hostname string
2623
servicesSet map[string]bool
2724
workDir string
2825
}
2926

30-
func (c *Email) renderAndSend(params map[string]interface{}) {
31-
message := &bytes.Buffer{}
32-
33-
parser, err := parseFileOrTemplate(c.TemplateFile, c.Template, c.log)
34-
if err != nil {
35-
c.log.Println("failed parse template:", err)
36-
return
37-
}
38-
renderErr := parser.Execute(message, params)
39-
if renderErr != nil {
40-
c.log.Println("failed render:", renderErr, "; params:", params)
41-
return
42-
}
43-
44-
c.log.Println(message.String())
27+
func (c *Email) renderAndSend(message string) {
28+
c.log.Println(message)
4529
host, _, _ := net.SplitHostPort(c.Smtp)
4630
auth := smtp.PlainAuth("", c.From, c.Password, host)
47-
err = smtp.SendMail(c.Smtp, auth, c.From, c.To, message.Bytes())
31+
err := smtp.SendMail(c.Smtp, auth, c.From, c.To, []byte(message))
4832
if err != nil {
4933
c.log.Println("failed send mail:", err)
5034
} else {
@@ -54,14 +38,12 @@ func (c *Email) renderAndSend(params map[string]interface{}) {
5438

5539
func (c *Email) Spawned(runnable container.Runnable, id container.ID) {
5640
if c.servicesSet[runnable.Label()] {
57-
params := map[string]interface{}{
58-
"action": "spawned",
59-
"id": id,
60-
"label": runnable.Label(),
61-
"hostname": c.hostname,
62-
"time": time.Now().String(),
41+
content, renderErr := c.renderDefault("spawned", string(id), runnable.Label(), nil, c.log)
42+
if renderErr != nil {
43+
c.log.Println("failed render:", renderErr)
44+
} else {
45+
c.renderAndSend(content)
6346
}
64-
c.renderAndSend(params)
6547
}
6648
}
6749

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

7557
func (c *Email) Stopped(runnable container.Runnable, id container.ID, err error) {
7658
if c.servicesSet[runnable.Label()] {
77-
params := map[string]interface{}{
78-
"action": "stopped",
79-
"id": id,
80-
"error": err,
81-
"label": runnable.Label(),
82-
"hostname": c.hostname,
83-
"time": time.Now().String(),
59+
content, renderErr := c.renderDefault("stopped", string(id), runnable.Label(), err, c.log)
60+
if renderErr != nil {
61+
c.log.Println("failed render:", renderErr)
62+
} else {
63+
c.renderAndSend(content)
8464
}
85-
c.renderAndSend(params)
8665
}
8766
}
8867

plugins/adp_http.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package plugins
2+
3+
import (
4+
"log"
5+
"github.com/reddec/container"
6+
"os"
7+
"github.com/pkg/errors"
8+
"path/filepath"
9+
"net/http"
10+
"bytes"
11+
"github.com/Masterminds/sprig"
12+
"html/template"
13+
"io"
14+
"time"
15+
"context"
16+
)
17+
18+
type Http struct {
19+
URL string `yaml:"url" mapstructure:"url"` // template URL string
20+
Method string `yaml:"method"` // default POST
21+
Headers map[string]string `yaml:"headers" mapstructure:"headers"` // additional header (non-template)
22+
Services []string `yaml:"services"`
23+
Timeout time.Duration `yaml:"timeout"`
24+
withTemplate `mapstructure:",squash" yaml:",inline"`
25+
log *log.Logger `yaml:"-"`
26+
servicesSet map[string]bool
27+
workDir string
28+
}
29+
30+
func (c *Http) renderAndSend(message string, params map[string]interface{}) {
31+
c.log.Println(message)
32+
33+
tpl, err := template.New("").Funcs(sprig.FuncMap()).Parse(string(c.URL))
34+
if err != nil {
35+
c.log.Println("failed parse URL as template:", err)
36+
return
37+
}
38+
urlM := &bytes.Buffer{}
39+
err = tpl.Execute(urlM, params)
40+
if err != nil {
41+
c.log.Println("failed execute URL as template:", err)
42+
return
43+
}
44+
45+
req, err := http.NewRequest(c.Method, urlM.String(), bytes.NewBufferString(message))
46+
if err != nil {
47+
c.log.Println("failed prepare request:", err)
48+
return
49+
}
50+
51+
ctx, closer := context.WithTimeout(context.Background(), c.Timeout)
52+
defer closer()
53+
54+
res, err := http.DefaultClient.Do(req.WithContext(ctx))
55+
if err != nil {
56+
c.log.Println("failed make request:", err)
57+
return
58+
}
59+
io.Copy(os.Stdout, res.Body) // allow keep-alive
60+
res.Body.Close()
61+
}
62+
63+
func (c *Http) Spawned(runnable container.Runnable, id container.ID) {
64+
if c.servicesSet[runnable.Label()] {
65+
content, params, renderErr := c.renderDefaultParams("spawned", string(id), runnable.Label(), nil, c.log)
66+
if renderErr != nil {
67+
c.log.Println("failed render:", renderErr)
68+
} else {
69+
c.renderAndSend(content, params)
70+
}
71+
}
72+
}
73+
74+
func (c *Http) Prepare() error {
75+
c.servicesSet = makeSet(c.Services)
76+
c.log = log.New(os.Stderr, "[http] ", log.LstdFlags)
77+
if c.Method == "" {
78+
c.Method = "POST"
79+
}
80+
if c.Timeout == 0 {
81+
c.Timeout = 20 * time.Second
82+
}
83+
return nil
84+
}
85+
86+
func (c *Http) Stopped(runnable container.Runnable, id container.ID, err error) {
87+
if c.servicesSet[runnable.Label()] {
88+
content, params, renderErr := c.renderDefaultParams("stopped", string(id), runnable.Label(), err, c.log)
89+
if renderErr != nil {
90+
c.log.Println("failed render:", renderErr)
91+
} else {
92+
c.renderAndSend(content, params)
93+
}
94+
}
95+
}
96+
97+
func (a *Http) MergeFrom(other interface{}) (error) {
98+
b := other.(*Http)
99+
if a.URL == "" {
100+
a.URL = b.URL
101+
}
102+
if a.URL != b.URL {
103+
return errors.New("different urls")
104+
}
105+
if a.Method == "" {
106+
a.Method = b.Method
107+
}
108+
if a.Method != b.Method {
109+
return errors.New("different methods")
110+
}
111+
if a.Timeout == 0 {
112+
a.Timeout = b.Timeout
113+
}
114+
if a.Timeout != b.Timeout {
115+
return errors.New("different timeout")
116+
}
117+
a.withTemplate.resolvePath(a.workDir)
118+
b.withTemplate.resolvePath(b.workDir)
119+
if err := a.withTemplate.MergeFrom(&b.withTemplate); err != nil {
120+
return err
121+
}
122+
if a.Headers == nil {
123+
a.Headers = make(map[string]string)
124+
}
125+
for k, v := range b.Headers {
126+
a.Headers[k] = v
127+
}
128+
a.Services = append(a.Services, b.Services...)
129+
return nil
130+
}
131+
132+
func init() {
133+
registerPlugin("http", func(file string) PluginConfig {
134+
return &Http{workDir: filepath.Dir(file)}
135+
})
136+
}

0 commit comments

Comments
 (0)