From 474610e25b08a18defea41b013718aac44b6b5e5 Mon Sep 17 00:00:00 2001 From: Nicolas Carlier Date: Sun, 21 Sep 2014 19:59:47 +0000 Subject: [PATCH] feat: Add notification system and docker hook support. --- assets/docker.json | 28 ++++++++++++++ scripts/docker/echo.sh | 4 ++ src/hooks/bitbucket.go | 2 +- src/hooks/docker.go | 16 ++++++++ src/hooks/factory.go | 4 +- src/hooks/github.go | 2 +- src/main.go | 59 ++++++++++++++++------------ src/notifications/factory.go | 22 +++++++++++ src/notifications/http.go | 48 +++++++++++++++++++++++ src/notifications/smtp.go | 74 ++++++++++++++++++++++++++++++++++++ start | 7 ++++ test | 5 +++ 12 files changed, 244 insertions(+), 27 deletions(-) create mode 100644 assets/docker.json create mode 100755 scripts/docker/echo.sh create mode 100644 src/hooks/docker.go create mode 100644 src/notifications/factory.go create mode 100644 src/notifications/http.go create mode 100644 src/notifications/smtp.go create mode 100755 start diff --git a/assets/docker.json b/assets/docker.json new file mode 100644 index 0000000..2900405 --- /dev/null +++ b/assets/docker.json @@ -0,0 +1,28 @@ +{ + "push_data":{ + "pushed_at":1385141110, + "images":[ + "imagehash1", + "imagehash2", + "imagehash3" + ], + "pusher":"username" + }, + "repository":{ + "status":"Active", + "description":"my docker repo that does cool things", + "is_trusted":false, + "full_description":"This is my full description", + "repo_url":"https://registry.hub.docker.com/u/username/reponame/", + "owner":"username", + "is_official":false, + "is_private":false, + "name":"reponame", + "namespace":"username", + "star_count":1, + "comment_count":1, + "date_created":1370174400, + "dockerfile":"my full dockerfile is listed here", + "repo_name":"username/reponame" + } +} diff --git a/scripts/docker/echo.sh b/scripts/docker/echo.sh new file mode 100755 index 0000000..c028408 --- /dev/null +++ b/scripts/docker/echo.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +echo "docker echo: $@" + diff --git a/src/hooks/bitbucket.go b/src/hooks/bitbucket.go index c9ab664..a035af6 100644 --- a/src/hooks/bitbucket.go +++ b/src/hooks/bitbucket.go @@ -14,7 +14,7 @@ type BitbucketRecord struct { User string `json:"user"` } -func (r BitbucketRecord) GetGitURL() string { +func (r BitbucketRecord) GetURL() string { return fmt.Sprintf("%s%s", r.BaseURL, r.Repository.URL) } diff --git a/src/hooks/docker.go b/src/hooks/docker.go new file mode 100644 index 0000000..58bff79 --- /dev/null +++ b/src/hooks/docker.go @@ -0,0 +1,16 @@ +package hooks + +type DockerRecord struct { + Repository struct { + Name string `json:"repo_name"` + URL string `json:"repo_url"` + } `json:"repository"` +} + +func (r DockerRecord) GetURL() string { + return r.Repository.URL +} + +func (r DockerRecord) GetName() string { + return r.Repository.Name +} diff --git a/src/hooks/factory.go b/src/hooks/factory.go index 4bec0bd..fe95b64 100644 --- a/src/hooks/factory.go +++ b/src/hooks/factory.go @@ -5,7 +5,7 @@ import ( ) type Record interface { - GetGitURL() string + GetURL() string GetName() string } @@ -15,6 +15,8 @@ func RecordFactory(hookname string) (Record, error) { return new(BitbucketRecord), nil case "github": return new(GithubRecord), nil + case "docker": + return new(DockerRecord), nil default: return nil, errors.New("Unknown hookname.") } diff --git a/src/hooks/github.go b/src/hooks/github.go index a913fba..aae855d 100644 --- a/src/hooks/github.go +++ b/src/hooks/github.go @@ -7,7 +7,7 @@ type GithubRecord struct { } `json:"repository"` } -func (r GithubRecord) GetGitURL() string { +func (r GithubRecord) GetURL() string { return r.Repository.URL } diff --git a/src/main.go b/src/main.go index 2379d56..2f52eb4 100644 --- a/src/main.go +++ b/src/main.go @@ -2,11 +2,11 @@ package main import ( "./hooks" + "./notifications" "encoding/json" "flag" "fmt" "github.com/gorilla/mux" - "io" "log" "net/http" "os/exec" @@ -16,40 +16,49 @@ var ( laddr = flag.String("l", ":8080", "HTTP service address (e.g.address, ':8080')") ) -type flushWriter struct { - f http.Flusher - w io.Writer +type HookContext struct { + Hook string + Action string + args []string } -func (fw *flushWriter) Write(p []byte) (n int, err error) { - n, err = fw.w.Write(p) - if fw.f != nil { - fw.f.Flush() +func Notify(text string, context *HookContext) { + var subject = fmt.Sprintf("Action %s executed.", context.Action) + var notifier, err = notifications.NotifierFactory() + if err != nil { + log.Println(err) + return } - return + if notifier == nil { + log.Println("Notification provider not found.") + return + } + notifier.Notify(text, subject) } -func RunScript(w http.ResponseWriter, hook string, action string, params ...string) { - fw := flushWriter{w: w} - if f, ok := w.(http.Flusher); ok { - fw.f = f - } - scriptname := fmt.Sprintf("./scripts/%s/%s.sh", hook, action) +func RunScript(w http.ResponseWriter, context *HookContext) { + scriptname := fmt.Sprintf("./scripts/%s/%s.sh", context.Hook, context.Action) log.Println("Exec script: ", scriptname) - cmd := exec.Command(scriptname, params...) - cmd.Stdout = &fw - cmd.Stderr = &fw - cmd.Run() + + out, err := exec.Command(scriptname, context.args...).Output() + if err != nil { + Notify(err.Error(), context) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + Notify(fmt.Sprintf("%s", out), context) + fmt.Fprintf(w, "Action '%s' executed!", context.Action) } func Handler(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) - hookname := params["hookname"] - action := params["action"] + context := new(HookContext) + context.Hook = params["hookname"] + context.Action = params["action"] - log.Println("Hook name: ", hookname) + log.Println("Hook name: ", context.Hook) - var record, err = hooks.RecordFactory(hookname) + var record, err = hooks.RecordFactory(context.Hook) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return @@ -62,7 +71,9 @@ func Handler(w http.ResponseWriter, r *http.Request) { return } - RunScript(w, hookname, action, record.GetGitURL(), record.GetName()) + context.args = []string{record.GetURL(), record.GetName()} + + RunScript(w, context) } func main() { diff --git a/src/notifications/factory.go b/src/notifications/factory.go new file mode 100644 index 0000000..14deffe --- /dev/null +++ b/src/notifications/factory.go @@ -0,0 +1,22 @@ +package notifications + +import ( + "errors" + "os" +) + +type Notifier interface { + Notify(text string, subject string) +} + +func NotifierFactory() (Notifier, error) { + notifier := os.Getenv("APP_NOTIFIER") + switch notifier { + case "http": + return NewHttpNotifier(), nil + case "smtp": + return NewSmtpNotifier(), nil + default: + return nil, errors.New("Unknown notification provider.") + } +} diff --git a/src/notifications/http.go b/src/notifications/http.go new file mode 100644 index 0000000..509146e --- /dev/null +++ b/src/notifications/http.go @@ -0,0 +1,48 @@ +package notifications + +import ( + "log" + "net/http" + "net/url" + "os" +) + +type HttpNotifier struct { + URL string + From string + To string +} + +func NewHttpNotifier() *HttpNotifier { + notifier := new(HttpNotifier) + notifier.URL = os.Getenv("APP_HTTP_NOTIFIER_URL") + if notifier.URL == "" { + log.Println("Unable to create HTTP notifier. APP_HTTP_NOTIFIER_URL not set.") + return nil + } + notifier.From = os.Getenv("APP_NOTIFIER_FROM") + if notifier.From == "" { + notifier.From = "webhookd " + } + notifier.To = os.Getenv("APP_NOTIFIER_TO") + if notifier.To == "" { + notifier.To = "hostmaster@nunux.org" + } + return notifier +} + +func (n HttpNotifier) Notify(text string, subject string) { + log.Println("HTTP notification: ", subject) + data := make(url.Values) + data.Set("from", n.From) + data.Set("to", n.To) + data.Set("subject", subject) + data.Set("text", text) + + // Submit form + resp, err := http.PostForm(n.URL, data) + if err != nil { + log.Println(err) + } + defer resp.Body.Close() +} diff --git a/src/notifications/smtp.go b/src/notifications/smtp.go new file mode 100644 index 0000000..fdca8ae --- /dev/null +++ b/src/notifications/smtp.go @@ -0,0 +1,74 @@ +package notifications + +import ( + "fmt" + "log" + "net/smtp" + "os" +) + +type SmtpNotifier struct { + Host string + From string + To string +} + +func NewSmtpNotifier() *SmtpNotifier { + notifier := new(SmtpNotifier) + notifier.Host = os.Getenv("APP_SMTP_NOTIFIER_HOST") + if notifier.Host == "" { + notifier.Host = "localhost:25" + } + notifier.From = os.Getenv("APP_NOTIFIER_FROM") + if notifier.From == "" { + notifier.From = "webhookd " + } + notifier.To = os.Getenv("APP_NOTIFIER_TO") + if notifier.To == "" { + notifier.To = "hostmaster@nunux.org" + } + return notifier +} + +func (n SmtpNotifier) Notify(text string, subject string) { + log.Println("SMTP notification: ", subject) + // Connect to the remote SMTP server. + c, err := smtp.Dial(n.Host) + if err != nil { + log.Println(err) + return + } + + // Set the sender and recipient first + if err := c.Mail(n.From); err != nil { + log.Println(err) + return + } + if err := c.Rcpt(n.To); err != nil { + log.Println(err) + return + } + + // Send the email body. + wc, err := c.Data() + if err != nil { + log.Println(err) + return + } + _, err = fmt.Fprintf(wc, text) + if err != nil { + log.Println(err) + return + } + err = wc.Close() + if err != nil { + log.Println(err) + return + } + + // Send the QUIT command and close the connection. + err = c.Quit() + if err != nil { + log.Fatal(err) + } +} diff --git a/start b/start new file mode 100755 index 0000000..2b2dca1 --- /dev/null +++ b/start @@ -0,0 +1,7 @@ +#!/bin/sh + +export APP_NOTIFIER="http" +export APP_HTTP_NOTIFIER_URL="http://requestb.in/v9b229v9" + +go run src/main.go + diff --git a/test b/test index 7d6d777..4bb0dc1 100755 --- a/test +++ b/test @@ -14,3 +14,8 @@ echo "Test Github hook" curl -H "Content-Type: application/json" \ --data @assets/github.json \ http://localhost:8080/github/echo + +echo "Test Docker hook" +curl -H "Content-Type: application/json" \ + --data @assets/docker.json \ + http://localhost:8080/docker/echo