commit ffabc5541e67092280895e48a921fa1ba7d0c94c Author: Adnan Hajdarevic Date: Mon Jan 12 22:14:25 2015 +0100 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/hooks.json b/hooks.json new file mode 100644 index 0000000..9f147a6 --- /dev/null +++ b/hooks.json @@ -0,0 +1,40 @@ +[ + { + "id": "draos", + "command": "dir", + "secret": "", + "trigger-rule": { + "and": [ + { + "match": { + "parameter": "commits.100.id", + "value": "20c16a7055dae65c69b9ca3f2e8cd1ac8fe9bbf2" + } + }, + { + "match": { + "parameter": "ref", + "value": "refs/heads/master" + } + }, + { + "match": { + "parameter": "repository.id", + "value": "17341098" + } + } + ] + } + }, + { + "id": "something", + "command": "", + "cwd": ".", + "trigger-rule": { + "match": { + "parameter": "second", + "value": "2" + } + } + } +] diff --git a/hooks/hooks.go b/hooks/hooks.go new file mode 100644 index 0000000..0c7d70d --- /dev/null +++ b/hooks/hooks.go @@ -0,0 +1,146 @@ +package hooks + +import ( + "encoding/json" + "io/ioutil" + + "github.com/adnanh/webhook/rules" +) + +// Hook is a structure that contains command to be executed +// and the current working directory name where that command should be executed +type Hook struct { + ID string `json:"id"` + Command string `json:"command"` + Cwd string `json:"cwd"` + Secret string `json:"secret"` + Rule rules.Rule `json:"trigger-rule"` +} + +// Hooks represents structure that contains list of Hook objects +// and the name of file which is correspondingly mapped to it +type Hooks struct { + fileName string + list []Hook +} + +// UnmarshalJSON implementation for a single hook +func (h *Hook) UnmarshalJSON(j []byte) error { + m := make(map[string]interface{}) + err := json.Unmarshal(j, &m) + + if err != nil { + return err + } + + if v, ok := m["id"]; ok { + h.ID = v.(string) + } + + if v, ok := m["command"]; ok { + h.Command = v.(string) + } + + if v, ok := m["cwd"]; ok { + h.Cwd = v.(string) + } + + if v, ok := m["trigger-rule"]; ok { + rule := v.(map[string]interface{}) + + if ruleValue, ok := rule["match"]; ok { + ruleString, _ := json.Marshal(ruleValue) + rulePtr := new(rules.MatchRule) + + err = json.Unmarshal(ruleString, rulePtr) + if err != nil { + return err + } + + h.Rule = *rulePtr + + } else if ruleValue, ok := rule["not"]; ok { + ruleString, _ := json.Marshal(ruleValue) + rulePtr := new(rules.NotRule) + + err = json.Unmarshal(ruleString, rulePtr) + if err != nil { + return err + } + + h.Rule = *rulePtr + + } else if ruleValue, ok := rule["and"]; ok { + ruleString, _ := json.Marshal(ruleValue) + rulePtr := new(rules.AndRule) + + err = json.Unmarshal(ruleString, rulePtr) + if err != nil { + return err + } + + h.Rule = *rulePtr + + } else if ruleValue, ok := rule["or"]; ok { + ruleString, _ := json.Marshal(ruleValue) + rulePtr := new(rules.OrRule) + + err = json.Unmarshal(ruleString, rulePtr) + if err != nil { + return err + } + + h.Rule = *rulePtr + } + + } + + return nil +} + +// New creates an instance of Hooks, tries to unmarshal contents of hookFile +// and returns a pointer to the newly created instance +func New(hookFile string) (*Hooks, error) { + h := &Hooks{fileName: hookFile} + + // parse hook file for hooks + file, e := ioutil.ReadFile(hookFile) + + if e != nil { + return h, e + } + + e = json.Unmarshal(file, &(h.list)) + + h.SetDefaults() + + return h, e +} + +// Match looks for the hook with the given id in the list of hooks +// and returns the pointer to the hook if it exists, or nil if it doesn't exist +func (h *Hooks) Match(id string, params interface{}) *Hook { + for i := range h.list { + if h.list[i].ID == id { + if h.list[i].Rule.Evaluate(params) { + return &h.list[i] + } + } + } + + return nil +} + +// Count returns number of hooks in the list +func (h *Hooks) Count() int { + return len(h.list) +} + +// SetDefaults sets default values that were ommited for hooks in JSON file +func (h *Hooks) SetDefaults() { + for i := range h.list { + if h.list[i].Cwd == "" { + h.list[i].Cwd = "." + } + } +} diff --git a/rules/rules.go b/rules/rules.go new file mode 100644 index 0000000..2952cb0 --- /dev/null +++ b/rules/rules.go @@ -0,0 +1,299 @@ +package rules + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + "strings" +) + +// Rule interface +type Rule interface { + Evaluate(params interface{}) bool +} + +// AndRule type is a structure that contains list of rules (SubRules) that will be evaluated, +// and the AndRule's Evaluate method will evaluate to true if and only if all +// of the SubRules evaluate to true +type AndRule struct { + SubRules []Rule `json:"and"` +} + +// OrRule type is a structure that contains list of rules (SubRules) that will be evaluated, +// and the OrRule's Evaluate method will evaluate to true if any of the SubRules +// evaluate to true +type OrRule struct { + SubRules []Rule `json:"or"` +} + +// NotRule type is a structure that contains a single rule (SubRule) that will be evaluated, +// and the OrRule's Evaluate method will evaluate to true if any and only if +// the SubRule evaluates to false +type NotRule struct { + SubRule Rule `json:"not"` +} + +// MatchRule type is a structure that contains MatchParameter structure +type MatchRule struct { + MatchParameter MatchParameter `json:"match"` +} + +// MatchParameter type is a structure that contains Parameter and Value which are used in +// Match +type MatchParameter struct { + Parameter string `json:"parameter"` + Value string `json:"value"` +} + +// Evaluate AndRule will return true if and only if all of SubRules evaluate to true +func (r AndRule) Evaluate(params interface{}) bool { + res := true + + for _, v := range r.SubRules { + res = res && v.Evaluate(params) + if res == false { + return res + } + } + + return res +} + +// Evaluate OrRule will return true if any of SubRules evaluate to true +func (r OrRule) Evaluate(params interface{}) bool { + res := false + + for _, v := range r.SubRules { + res = res || v.Evaluate(params) + if res == true { + return res + } + } + + return res +} + +// Evaluate NotRule will return true if and only if SubRule evaluates to false +func (r NotRule) Evaluate(params interface{}) bool { + return !r.SubRule.Evaluate(params) +} + +func extract(s string, params interface{}) (string, bool) { + var p []string + + if paramsValue := reflect.ValueOf(params); paramsValue.Kind() == reflect.Slice { + if paramsValueSliceLength := paramsValue.Len(); paramsValueSliceLength > 0 { + + if p = strings.SplitN(s, ".", 3); len(p) > 3 { + index, err := strconv.ParseInt(p[1], 10, 64) + + if err != nil { + return "", false + } else if paramsValueSliceLength <= int(index) { + return "", false + } + + return extract(p[2], params.([]map[string]interface{})[index]) + } + } + + return "", false + } + + if p = strings.SplitN(s, ".", 2); len(p) > 1 { + if pValue, ok := params.(map[string]interface{})[p[0]]; ok { + return extract(p[1], pValue) + } + } else { + if pValue, ok := params.(map[string]interface{})[p[0]]; ok { + return fmt.Sprintf("%v", pValue), true + } + } + + return "", false +} + +// Evaluate MatchRule will return true if and only if the MatchParameter.Parameter +// named property value in supplied params matches the MatchParameter.Value +func (r MatchRule) Evaluate(params interface{}) bool { + if v, ok := extract(r.MatchParameter.Parameter, params); ok { + return v == r.MatchParameter.Value + } + + return false +} + +// UnmarshalJSON implementation for the MatchRule type +func (r *MatchRule) UnmarshalJSON(j []byte) error { + err := json.Unmarshal(j, &r.MatchParameter) + return err +} + +// UnmarshalJSON implementation for the NotRule type +func (r *NotRule) UnmarshalJSON(j []byte) error { + m := make(map[string]interface{}) + err := json.Unmarshal(j, &m) + + if ruleValue, ok := m["match"]; ok { + ruleString, _ := json.Marshal(ruleValue) + rulePtr := new(MatchRule) + + err = json.Unmarshal(ruleString, &rulePtr.MatchParameter) + if err != nil { + return err + } + + r.SubRule = *rulePtr + + } else if ruleValue, ok := m["not"]; ok { + ruleString, _ := json.Marshal(ruleValue) + rulePtr := new(NotRule) + + err = json.Unmarshal(ruleString, rulePtr) + if err != nil { + return err + } + + r.SubRule = *rulePtr + + } else if ruleValue, ok := m["and"]; ok { + ruleString, _ := json.Marshal(ruleValue) + rulePtr := new(AndRule) + + err = json.Unmarshal(ruleString, rulePtr) + if err != nil { + return err + } + + r.SubRule = *rulePtr + + } else if ruleValue, ok := m["or"]; ok { + ruleString, _ := json.Marshal(ruleValue) + rulePtr := new(OrRule) + + err = json.Unmarshal(ruleString, rulePtr) + if err != nil { + return err + } + + r.SubRule = *rulePtr + + } + + return err +} + +// UnmarshalJSON implementation for the AndRule type +func (r *AndRule) UnmarshalJSON(j []byte) error { + rules := new([]interface{}) + err := json.Unmarshal(j, &rules) + + for _, rulesValue := range *rules { + m := rulesValue.(map[string]interface{}) + + if ruleValue, ok := m["match"]; ok { + ruleString, _ := json.Marshal(ruleValue) + rulePtr := new(MatchRule) + + err = json.Unmarshal(ruleString, &rulePtr.MatchParameter) + if err != nil { + return err + } + + r.SubRules = append(r.SubRules, *rulePtr) + + } else if ruleValue, ok := m["not"]; ok { + ruleString, _ := json.Marshal(ruleValue) + rulePtr := new(NotRule) + + err = json.Unmarshal(ruleString, rulePtr) + if err != nil { + return err + } + + r.SubRules = append(r.SubRules, *rulePtr) + + } else if ruleValue, ok := m["and"]; ok { + ruleString, _ := json.Marshal(ruleValue) + rulePtr := new(AndRule) + + err = json.Unmarshal(ruleString, rulePtr) + if err != nil { + return err + } + + r.SubRules = append(r.SubRules, *rulePtr) + + } else if ruleValue, ok := m["or"]; ok { + ruleString, _ := json.Marshal(ruleValue) + rulePtr := new(OrRule) + + err = json.Unmarshal(ruleString, rulePtr) + if err != nil { + return err + } + + r.SubRules = append(r.SubRules, *rulePtr) + } + } + + return err +} + +// UnmarshalJSON implementation for the OrRule type +func (r *OrRule) UnmarshalJSON(j []byte) error { + rules := new([]interface{}) + err := json.Unmarshal(j, &rules) + + for _, rulesValue := range *rules { + m := rulesValue.(map[string]interface{}) + + if ruleValue, ok := m["match"]; ok { + ruleString, _ := json.Marshal(ruleValue) + rulePtr := new(MatchRule) + + err = json.Unmarshal(ruleString, &rulePtr.MatchParameter) + if err != nil { + return err + } + + r.SubRules = append(r.SubRules, *rulePtr) + + } else if ruleValue, ok := m["not"]; ok { + ruleString, _ := json.Marshal(ruleValue) + rulePtr := new(NotRule) + + err = json.Unmarshal(ruleString, rulePtr) + if err != nil { + return err + } + + r.SubRules = append(r.SubRules, *rulePtr) + + } else if ruleValue, ok := m["and"]; ok { + ruleString, _ := json.Marshal(ruleValue) + rulePtr := new(AndRule) + + err = json.Unmarshal(ruleString, rulePtr) + if err != nil { + return err + } + + r.SubRules = append(r.SubRules, *rulePtr) + + } else if ruleValue, ok := m["or"]; ok { + ruleString, _ := json.Marshal(ruleValue) + rulePtr := new(OrRule) + + err = json.Unmarshal(ruleString, rulePtr) + if err != nil { + return err + } + + r.SubRules = append(r.SubRules, *rulePtr) + } + } + + return err +} diff --git a/webhook.go b/webhook.go new file mode 100644 index 0000000..dd075e8 --- /dev/null +++ b/webhook.go @@ -0,0 +1,72 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os/exec" + "time" + + "github.com/adnanh/webhook/hooks" + + "github.com/go-martini/martini" +) + +const ( + version string = "1.0.0" + ip string = "" + port int = 9000 +) + +var ( + webhooks *hooks.Hooks + appStart time.Time +) + +func main() { + appStart = time.Now() + var e error + + webhooks, e = hooks.New("hooks.json") + + if e != nil { + fmt.Printf("Error while loading hooks from hooks.json:\n\t>>> %s\n", e) + } + + web := martini.Classic() + + web.Get("/", rootHandler) + web.Get("/hook/:id", hookHandler) + web.Post("/hook/:id", hookHandler) + + fmt.Printf("Starting go-webhook with %d hook(s)\n\n", webhooks.Count()) + + web.RunOnAddr(fmt.Sprintf("%s:%d", ip, port)) +} + +func rootHandler() string { + return fmt.Sprintf("go-webhook %s running for %s serving %d hook(s)\n%+v", version, time.Since(appStart).String(), webhooks.Count(), webhooks) +} + +func hookHandler(req *http.Request, params martini.Params) string { + + decoder := json.NewDecoder(req.Body) + decoder.UseNumber() + + p := make(map[string]interface{}) + + err := decoder.Decode(&p) + + if err != nil { + return "Error occurred while parsing the payload :-(" + } + + go func(id string, params interface{}) { + if hook := webhooks.Match(id, params); hook != nil { + cmd := exec.Command(hook.Command, "", "", hook.Cwd) + out, _ := cmd.Output() + fmt.Printf("Command output for %v >>> %s\n", hook, out) + } + }(params["id"], p) + return "Got it, thanks. :-)" +}