feat: Big refactoring.

Change packaging.
Add compressed logs in attachment of the notification.
This commit is contained in:
Nicolas Carlier 2014-09-23 18:16:50 +00:00
parent d913e92d5a
commit eb4b9ba7ff
13 changed files with 314 additions and 175 deletions

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package hooks
package hook
type DockerRecord struct {
Repository struct {

View File

@ -1,4 +1,4 @@
package hooks
package hook
type GithubRecord struct {
Repository struct {

View File

@ -1,4 +1,4 @@
package hooks
package hook
import (
"errors"

View File

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

View File

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

View File

@ -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 <noreply@nunux.org>"
}
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")
}
}

View File

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

View File

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

View File

@ -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 <noreply@nunux.org>"
}
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()
}

37
src/tools/compress.go Normal file
View File

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

136
src/webhookd.go Normal file
View File

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