From eb4b9ba7ffaf6617c687f01af1b3f99402b20fc5 Mon Sep 17 00:00:00 2001 From: Nicolas Carlier Date: Tue, 23 Sep 2014 18:16:50 +0000 Subject: [PATCH] feat: Big refactoring. Change packaging. Add compressed logs in attachment of the notification. --- Makefile | 22 +-- src/hook/bitbucket_hook.go | 20 +++ src/{hooks/docker.go => hook/docker_hook.go} | 2 +- src/{hooks/github.go => hook/github_hook.go} | 2 +- .../factory.go => hook/hook_factory.go} | 2 +- src/hooks/bitbucket.go | 23 --- src/main.go | 89 ------------ src/notification/http_notifier.go | 100 +++++++++++++ .../notifier_factory.go} | 4 +- .../smtp.go => notification/smtp_notifier.go} | 4 +- src/notifications/http.go | 48 ------- src/tools/compress.go | 37 +++++ src/webhookd.go | 136 ++++++++++++++++++ 13 files changed, 314 insertions(+), 175 deletions(-) create mode 100644 src/hook/bitbucket_hook.go rename src/{hooks/docker.go => hook/docker_hook.go} (95%) rename src/{hooks/github.go => hook/github_hook.go} (95%) rename src/{hooks/factory.go => hook/hook_factory.go} (96%) delete mode 100644 src/hooks/bitbucket.go delete mode 100644 src/main.go create mode 100644 src/notification/http_notifier.go rename src/{notifications/factory.go => notification/notifier_factory.go} (80%) rename src/{notifications/smtp.go => notification/smtp_notifier.go} (92%) delete mode 100644 src/notifications/http.go create mode 100644 src/tools/compress.go create mode 100644 src/webhookd.go diff --git a/Makefile b/Makefile index 9b9f354..c9e120a 100644 --- a/Makefile +++ b/Makefile @@ -4,25 +4,31 @@ TAG:=`git describe --abbrev=0 --tags` LDFLAGS:=-X main.buildVersion $(TAG) APPNAME:=webhookd +ROOTPKG:=github.com/ncarlier +PKGDIR:=$(GOPATH)/src/$(ROOTPKG) + all: build -build: +prepare: + rm -rf $(PKGDIR) + mkdir -p $(PKGDIR) + ln -s $(PWD)/src $(PKGDIR)/$(APPNAME) + +build: prepare echo "Building $(APPNAME)..." go build -ldflags "$(LDFLAGS)" -o bin/$(APPNAME) ./src clean: clean-dist - rm -rf bin + rm -f bin/$(APPNAME) clean-dist: rm -rf dist dist: clean-dist - mkdir -p dist/linux/amd64 && GOOS=linux GOARCH=amd64 go build -o dist/linux/amd64/$(APPNAME) ./src -# mkdir -p dist/linux/i386 && GOOS=linux GOARCH=386 go build -o dist/linux/i386/$(APPNAME) ./src - -release: dist # godep restore - tar -cvzf $(APPNAME)-linux-amd64-$(TAG).tar.gz -C dist/linux/amd64 $(APPNAME) -# tar -cvzf $(APPNAME)-linux-i386-i386$(TAG).tar.gz -C dist/linux/i386 $(APPNAME) + mkdir -p dist/linux/amd64 && GOOS=linux GOARCH=amd64 go build -o dist/linux/amd64/$(APPNAME) ./src + tar -cvzf dist/$(APPNAME)-linux-amd64-$(TAG).tar.gz -C dist/linux/amd64 $(APPNAME) +# mkdir -p dist/linux/i386 && GOOS=linux GOARCH=386 go build -o dist/linux/i386/$(APPNAME) ./src +# tar -cvzf dist/$(APPNAME)-linux-i386-i386$(TAG).tar.gz -C dist/linux/i386 $(APPNAME) diff --git a/src/hook/bitbucket_hook.go b/src/hook/bitbucket_hook.go new file mode 100644 index 0000000..d3c8341 --- /dev/null +++ b/src/hook/bitbucket_hook.go @@ -0,0 +1,20 @@ +package hook + +import ( + "fmt" +) + +type BitbucketRecord struct { + Repository struct { + Slug string `json:"slug"` + Owner string `json:"owner"` + } `json:"repository"` +} + +func (r BitbucketRecord) GetURL() string { + return fmt.Sprintf("git@bitbucket.org:%s/%s.git", r.Repository.Owner, r.Repository.Owner) +} + +func (r BitbucketRecord) GetName() string { + return r.Repository.Slug +} diff --git a/src/hooks/docker.go b/src/hook/docker_hook.go similarity index 95% rename from src/hooks/docker.go rename to src/hook/docker_hook.go index 58bff79..e0627a3 100644 --- a/src/hooks/docker.go +++ b/src/hook/docker_hook.go @@ -1,4 +1,4 @@ -package hooks +package hook type DockerRecord struct { Repository struct { diff --git a/src/hooks/github.go b/src/hook/github_hook.go similarity index 95% rename from src/hooks/github.go rename to src/hook/github_hook.go index aae855d..27d5943 100644 --- a/src/hooks/github.go +++ b/src/hook/github_hook.go @@ -1,4 +1,4 @@ -package hooks +package hook type GithubRecord struct { Repository struct { diff --git a/src/hooks/factory.go b/src/hook/hook_factory.go similarity index 96% rename from src/hooks/factory.go rename to src/hook/hook_factory.go index fe95b64..74f1dd7 100644 --- a/src/hooks/factory.go +++ b/src/hook/hook_factory.go @@ -1,4 +1,4 @@ -package hooks +package hook import ( "errors" diff --git a/src/hooks/bitbucket.go b/src/hooks/bitbucket.go deleted file mode 100644 index a035af6..0000000 --- a/src/hooks/bitbucket.go +++ /dev/null @@ -1,23 +0,0 @@ -package hooks - -import ( - "fmt" -) - -type BitbucketRecord struct { - Repository struct { - Slug string `json:"slug"` - Name string `json:"name"` - URL string `json:"absolute_url"` - } `json:"repository"` - BaseURL string `json:"canon_url"` - User string `json:"user"` -} - -func (r BitbucketRecord) GetURL() string { - return fmt.Sprintf("%s%s", r.BaseURL, r.Repository.URL) -} - -func (r BitbucketRecord) GetName() string { - return r.Repository.Name -} diff --git a/src/main.go b/src/main.go deleted file mode 100644 index 2f52eb4..0000000 --- a/src/main.go +++ /dev/null @@ -1,89 +0,0 @@ -package main - -import ( - "./hooks" - "./notifications" - "encoding/json" - "flag" - "fmt" - "github.com/gorilla/mux" - "log" - "net/http" - "os/exec" -) - -var ( - laddr = flag.String("l", ":8080", "HTTP service address (e.g.address, ':8080')") -) - -type HookContext struct { - Hook string - Action string - args []string -} - -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 - } - if notifier == nil { - log.Println("Notification provider not found.") - return - } - notifier.Notify(text, subject) -} - -func RunScript(w http.ResponseWriter, context *HookContext) { - scriptname := fmt.Sprintf("./scripts/%s/%s.sh", context.Hook, context.Action) - log.Println("Exec script: ", scriptname) - - 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) - context := new(HookContext) - context.Hook = params["hookname"] - context.Action = params["action"] - - log.Println("Hook name: ", context.Hook) - - var record, err = hooks.RecordFactory(context.Hook) - if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) - return - } - - decoder := json.NewDecoder(r.Body) - err = decoder.Decode(&record) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - context.args = []string{record.GetURL(), record.GetName()} - - RunScript(w, context) -} - -func main() { - flag.Parse() - - rtr := mux.NewRouter() - rtr.HandleFunc("/{hookname:[a-z]+}/{action:[a-z]+}", Handler).Methods("POST") - - http.Handle("/", rtr) - - log.Println("webhookd server listening...") - log.Fatal(http.ListenAndServe(*laddr, nil)) -} diff --git a/src/notification/http_notifier.go b/src/notification/http_notifier.go new file mode 100644 index 0000000..eb645dc --- /dev/null +++ b/src/notification/http_notifier.go @@ -0,0 +1,100 @@ +package notification + +import ( + "bytes" + "io" + "log" + "mime/multipart" + "net/http" + "net/url" + "os" + "path/filepath" +) + +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(subject string, text string, attachfile 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) + + if attachfile != "" { + file, err := os.Open(attachfile) + if err != nil { + log.Println(err) + return + } + defer file.Close() + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("attachment", filepath.Base(attachfile)) + if err != nil { + log.Println("Unable to create for file", err) + return + } + _, err = io.Copy(part, file) + + for key, val := range data { + _ = writer.WriteField(key, val[0]) + } + + err = writer.Close() + if err != nil { + log.Println("Unable to close writer", err) + return + } + req, err := http.NewRequest("POST", n.URL, body) + if err != nil { + log.Println("Unable to post request", err) + } + defer req.Body.Close() + req.Header.Set("Content-Type", writer.FormDataContentType()) + // Submit the request + client := &http.Client{} + res, err := client.Do(req) + if err != nil { + log.Println("Unable to do the request", err) + return + } + + // Check the response + if res.StatusCode != http.StatusOK { + log.Println("bad status: %s", res.Status) + return + } + log.Println("HTTP notification done with attachment: ", attachfile) + } else { + resp, err := http.PostForm(n.URL, data) + if err != nil { + log.Println(err) + } + defer resp.Body.Close() + log.Println("HTTP notification done") + } +} diff --git a/src/notifications/factory.go b/src/notification/notifier_factory.go similarity index 80% rename from src/notifications/factory.go rename to src/notification/notifier_factory.go index 14deffe..7a978eb 100644 --- a/src/notifications/factory.go +++ b/src/notification/notifier_factory.go @@ -1,4 +1,4 @@ -package notifications +package notification import ( "errors" @@ -6,7 +6,7 @@ import ( ) type Notifier interface { - Notify(text string, subject string) + Notify(subject string, text string, attachfile string) } func NotifierFactory() (Notifier, error) { diff --git a/src/notifications/smtp.go b/src/notification/smtp_notifier.go similarity index 92% rename from src/notifications/smtp.go rename to src/notification/smtp_notifier.go index fdca8ae..b673719 100644 --- a/src/notifications/smtp.go +++ b/src/notification/smtp_notifier.go @@ -1,4 +1,4 @@ -package notifications +package notification import ( "fmt" @@ -30,7 +30,7 @@ func NewSmtpNotifier() *SmtpNotifier { return notifier } -func (n SmtpNotifier) Notify(text string, subject string) { +func (n SmtpNotifier) Notify(subject string, text string, attachfile string) { log.Println("SMTP notification: ", subject) // Connect to the remote SMTP server. c, err := smtp.Dial(n.Host) diff --git a/src/notifications/http.go b/src/notifications/http.go deleted file mode 100644 index 509146e..0000000 --- a/src/notifications/http.go +++ /dev/null @@ -1,48 +0,0 @@ -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/tools/compress.go b/src/tools/compress.go new file mode 100644 index 0000000..f1e8479 --- /dev/null +++ b/src/tools/compress.go @@ -0,0 +1,37 @@ +package tools + +import ( + "bufio" + "compress/gzip" + "fmt" + "log" + "os" +) + +func CompressFile(filename string) (zipfile string, err error) { + zipfile = fmt.Sprintf("%s.gz", filename) + in, err := os.Open(filename) + if err != nil { + return + } + out, err := os.Create(zipfile) + if err != nil { + log.Println("Unable to create zip file", err) + return + } + + // buffer readers from file, writes to pipe + bufin := bufio.NewReader(in) + + // gzip wraps buffer writer and wr + gw := gzip.NewWriter(out) + defer gw.Close() + + _, err = bufin.WriteTo(gw) + if err != nil { + log.Println("Unable to write into the zip file", err) + return + } + log.Println("Zip file created: ", zipfile) + return +} diff --git a/src/webhookd.go b/src/webhookd.go new file mode 100644 index 0000000..5c6c2af --- /dev/null +++ b/src/webhookd.go @@ -0,0 +1,136 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "github.com/gorilla/mux" + "github.com/ncarlier/webhookd/hook" + "github.com/ncarlier/webhookd/notification" + "github.com/ncarlier/webhookd/tools" + "log" + "net/http" + "os" + "os/exec" + "path" +) + +var ( + laddr = flag.String("l", ":8080", "HTTP service address (e.g.address, ':8080')") + workingdir = os.Getenv("APP_WORKING_DIR") + scriptsdir = os.Getenv("APP_SCRIPTS_DIR") +) + +type HookContext struct { + Hook string + Action string + args []string +} + +func Notify(subject string, text string, outfilename string) { + var notifier, err = notification.NotifierFactory() + if err != nil { + log.Println(err) + return + } + if notifier == nil { + log.Println("Notification provider not found.") + return + } + + var zipfile string + if outfilename != "" { + zipfile, err = tools.CompressFile(outfilename) + if err != nil { + log.Println(err) + zipfile = outfilename + } + } + + notifier.Notify(subject, text, zipfile) +} + +func RunScript(w http.ResponseWriter, context *HookContext) { + scriptname := path.Join(scriptsdir, context.Hook, fmt.Sprintf("%s.sh", context.Action)) + log.Println("Exec script: ", scriptname) + + cmd := exec.Command(scriptname, context.args...) + var ErrorHandler func(err error, out string) + ErrorHandler = func(err error, out string) { + subject := fmt.Sprintf("Webhook %s/%s FAILED.", context.Hook, context.Action) + Notify(subject, err.Error(), out) + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + // open the out file for writing + outfilename := path.Join(workingdir, fmt.Sprintf("%s-%s.txt", context.Hook, context.Action)) + outfile, err := os.Create(outfilename) + if err != nil { + ErrorHandler(err, "") + return + } + + defer outfile.Close() + cmd.Stdout = outfile + + err = cmd.Start() + if err != nil { + ErrorHandler(err, "") + return + } + + err = cmd.Wait() + if err != nil { + ErrorHandler(err, outfilename) + return + } + + subject := fmt.Sprintf("Webhook %s/%s SUCCEEDED.", context.Hook, context.Action) + Notify(subject, "See attached file for logs.", outfilename) + fmt.Fprintf(w, subject) +} + +func Handler(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + context := new(HookContext) + context.Hook = params["hookname"] + context.Action = params["action"] + + log.Println("Hook name: ", context.Hook) + + var record, err = hook.RecordFactory(context.Hook) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + decoder := json.NewDecoder(r.Body) + err = decoder.Decode(&record) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + context.args = []string{record.GetURL(), record.GetName()} + + RunScript(w, context) +} + +func main() { + if workingdir == "" { + workingdir = os.TempDir() + } + if scriptsdir == "" { + scriptsdir = "scripts" + } + + flag.Parse() + + rtr := mux.NewRouter() + rtr.HandleFunc("/{hookname:[a-z]+}/{action:[a-z]+}", Handler).Methods("POST") + + http.Handle("/", rtr) + + log.Println("webhookd server listening...") + log.Fatal(http.ListenAndServe(*laddr, nil)) +}