/
handler.go
165 lines (149 loc) · 5.72 KB
/
handler.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// Package handler contains all the code that parses an incoming web request
// (likely from github's web hooks).
package handler
import (
"log"
"net/http"
"regexp"
"strconv"
"strings"
"github.com/google/go-github/github"
"github.com/m-lab/github-maintenance-exporter/maintenancestate"
"github.com/m-lab/github-maintenance-exporter/metrics"
)
var (
machineRegExps = map[string]*regexp.Regexp{
"mlab-sandbox": regexp.MustCompile(`\/machine\s+(mlab[1-4][.-][a-z]{3}[0-9]t)(\s+del)?`),
"mlab-staging": regexp.MustCompile(`\/machine\s+(mlab[4][.-][a-z]{3}[0-9c]{2})(\s+del)?`),
"mlab-oti": regexp.MustCompile(`\/machine\s+(mlab[1-3][.-][a-z]{3}[0-9c]{2})(\s+del)?`),
}
siteRegExps = map[string]*regexp.Regexp{
"mlab-sandbox": regexp.MustCompile(`\/site\s+([a-z]{3}[0-9]t)(\s+del)?`),
"mlab-staging": regexp.MustCompile(`\/site\s+([a-z]{3}[0-9c]{2})(\s+del)?`),
"mlab-oti": regexp.MustCompile(`\/site\s+([a-z]{3}[0-9c]{2})(\s+del)?`),
}
)
type handler struct {
state *maintenancestate.MaintenanceState
githubSecret []byte
project string
}
// parseMessage scans the body of an issue or comment looking for special flags
// that match predefined patterns indicating that machine or site should be
// added to or removed from maintenance mode. If any matches are found, it
// updates the state for the item. The return value is the number of
// modifications that were made to the machine and site maintenance state.
func (h *handler) parseMessage(msg string, issueNumber string) int {
var mods = 0
siteMatches := siteRegExps[h.project].FindAllStringSubmatch(msg, -1)
if len(siteMatches) > 0 {
for _, site := range siteMatches {
log.Printf("INFO: Flag found for site: %s", site[1])
if strings.TrimSpace(site[2]) == "del" {
mods += h.state.UpdateSite(site[1], maintenancestate.LeaveMaintenance, issueNumber, h.project)
} else {
mods += h.state.UpdateSite(site[1], maintenancestate.EnterMaintenance, issueNumber, h.project)
}
}
}
machineMatches := machineRegExps[h.project].FindAllStringSubmatch(msg, -1)
if len(machineMatches) > 0 {
for _, machine := range machineMatches {
log.Printf("INFO: Flag found for machine: %s", machine[1])
label := strings.Replace(machine[1], ".", "-", 1)
if strings.TrimSpace(machine[2]) == "del" {
h.state.UpdateMachine(label, maintenancestate.LeaveMaintenance, issueNumber, h.project)
mods++
} else {
h.state.UpdateMachine(label, maintenancestate.EnterMaintenance, issueNumber, h.project)
mods++
}
}
}
return mods
}
// ServeHTTP is the handler function for received webhooks. It validates the
// hook, parses the payload, makes sure that the hook event matches at least one
// event this exporter handles, then passes off the payload to parseMessage.
func (h *handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
var issueNumber string
var mods = 0 // Number of modifications made to current state by webhook.
var status = http.StatusOK
log.Println("INFO: Received a webhook.")
payload, err := github.ValidatePayload(req, h.githubSecret)
if err != nil {
log.Printf("ERROR: Validation of Webhook failed: %s", err)
metrics.Error.WithLabelValues("validatehook", "receiveHook").Add(1)
resp.WriteHeader(http.StatusUnauthorized)
return
}
event, err := github.ParseWebHook(github.WebHookType(req), payload)
if err != nil {
log.Printf("ERROR: Failed to parse webhook with error: %s", err)
metrics.Error.WithLabelValues("parsehook", "receiveHook").Add(1)
resp.WriteHeader(http.StatusBadRequest)
return
}
switch event := event.(type) {
case *github.IssuesEvent:
log.Println("INFO: Webhook is an Issues event.")
issueNumber = strconv.Itoa(event.Issue.GetNumber())
eventAction := event.GetAction()
switch eventAction {
case "closed", "deleted":
log.Printf("INFO: Issue #%s was %s.", issueNumber, eventAction)
mods = h.state.CloseIssue(issueNumber, h.project)
case "opened", "edited":
mods = h.parseMessage(event.Issue.GetBody(), issueNumber)
default:
log.Printf("INFO: Unsupported IssueEvent action: %s.", eventAction)
status = http.StatusNotImplemented
}
case *github.IssueCommentEvent:
log.Println("INFO: Webhook is an IssueComment event.")
issueNumber = strconv.Itoa(event.Issue.GetNumber())
issueState := event.Issue.GetState()
if issueState == "open" {
mods = h.parseMessage(event.Comment.GetBody(), issueNumber)
} else {
log.Printf("INFO: Ignoring IssueComment event on closed issue #%s.", issueNumber)
status = http.StatusExpectationFailed
}
case *github.PingEvent:
log.Println("INFO: Webhook is a Ping event.")
var cnt = 0
// Since this exporter only processes "issues" and "issue_comment" Github
// webhook events, be sure that at least these two events are enabled for the
// webhook.
for _, v := range event.Hook.Events {
if v == "issues" || v == "issue_comment" {
cnt++
}
}
if cnt != 2 {
log.Printf("ERROR: Registered webhook events do not include both 'issues' and 'issue_comment'.")
status = http.StatusExpectationFailed
}
default:
log.Println("WARNING: Received unimplemented webhook event type.")
status = http.StatusNotImplemented
}
// Only write state to file if the current state was modified.
if mods > 0 {
err = h.state.Write()
if err != nil {
log.Printf("ERROR: failed to write state file: %s", err)
metrics.Error.WithLabelValues("writefile", "receiveHook").Add(1)
status = http.StatusInternalServerError
}
}
resp.WriteHeader(status)
}
// New creates an http.Handler for receiving github webhook events to update the maintenance state.
func New(state *maintenancestate.MaintenanceState, githubSecret []byte, project string) http.Handler {
return &handler{
state: state,
githubSecret: githubSecret,
project: project,
}
}