From bfb79c3f3c7bdf8eaceba81408b671bf3f9d4308 Mon Sep 17 00:00:00 2001 From: ston1th Date: Fri, 6 Dec 2019 23:20:20 +0100 Subject: [PATCH] added labels --- README.md | 14 ++-- build.sh | 2 +- helper.go | 27 +++++-- main.go | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++---- types.go | 8 +- 5 files changed, 243 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 9120629..727f918 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This way we can trigger cleanups when the PR is closed. ## Push Event ``` -X-Interceptor-Push-Ref: []string{"refs/heads/master"} +X-Interceptor-Push-Ref: string: "refs/heads/master" ``` Response Headers: @@ -34,16 +34,16 @@ Commit: ## Issue Comment Event ``` -X-Interceptor-Comment-Action: "/test" +X-Interceptor-Comment-Action: "/test-deploy" +X-Interceptor-Label: "test-deploy:#aabbcc" +X-Interceptor-Remove-Labels: []string{"test", "deploy"} ``` ### Actions -TBD: - -* `/label ` - add a label `` to the pull request -* `/test` - run tests pipeline (requires `test` label) -* `/deploy` - deploy pull request in preview environment (requires `deploy` label) +* `/test` - add `test` label and run test pipeline +* `/deploy` - add `deploy` label and run deploy pipeline +* `/test-deploy` - add `test-deploy` label and run test-deploy pipeline Response Headers: diff --git a/build.sh b/build.sh index 424b1e7..a986ac5 100755 --- a/build.sh +++ b/build.sh @@ -4,5 +4,5 @@ source env.txt kubectl -n webhook-interceptor delete deploy webhook-interceptor ko apply -f config/ kubectl apply -f secret.yaml -sleep 5 +sleep 10 kubectl -n webhook-interceptor logs -f $(kubectl -n webhook-interceptor get pod -ojsonpath='{.items[0].metadata.name}') diff --git a/helper.go b/helper.go index 728ea0c..d7a7395 100644 --- a/helper.go +++ b/helper.go @@ -4,10 +4,12 @@ package main import ( "encoding/json" + "io" "io/ioutil" "log" "net/http" "os" + "strconv" "strings" ) @@ -16,6 +18,10 @@ const ( giteaAPI = "api/v1" ) +func itoa(i int64) string { + return strconv.FormatInt(i, 10) +} + func getNS() (string, error) { if ns := os.Getenv("POD_NAMESPACE"); ns != "" { return ns, nil @@ -36,13 +42,25 @@ func logError(w http.ResponseWriter, msg string) { func contains(a []string, s string) bool { for _, v := range a { - if strings.Contains(s, v) { + if v == s { return true } } return false } +func getLabel(l string) (label, color string) { + a := strings.SplitN(l, ":", 2) + switch len(a) { + case 1: + label = a[0] + case 2: + label = a[0] + color = a[1] + } + return +} + func detectAPI(r *http.Request) string { if r.Header.Get(giteaEvent) != "" { return giteaAPI @@ -53,14 +71,13 @@ func detectAPI(r *http.Request) string { return "" } -func readBody(r *http.Request, i interface{}) (body []byte, err error) { - defer r.Body.Close() - body, err = ioutil.ReadAll(r.Body) +func readBody(r io.ReadCloser, i interface{}) (body []byte, err error) { + defer r.Close() + body, err = ioutil.ReadAll(r) if err != nil { return } if debug { - log.Printf("debug headers: %#v", r.Header) log.Printf("debug: %s", string(body)) } err = json.Unmarshal(body, i) diff --git a/main.go b/main.go index bfbf007..d618ede 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,9 @@ package main import ( + "bytes" "encoding/json" + "errors" "fmt" "io/ioutil" "log" @@ -11,7 +13,7 @@ import ( "net/http" "net/url" "reflect" - "strconv" + "regexp" "strings" "time" @@ -25,9 +27,11 @@ import ( var debug = false const ( - prAction = "X-Interceptor-Pr-Action" - pushAction = "X-Interceptor-Push-Action" - commentAction = "X-Interceptor-Comment-Action" + prAction = "X-Interceptor-Pr-Action" + pushAction = "X-Interceptor-Push-Action" + commentAction = "X-Interceptor-Comment-Action" + labelHeader = "X-Interceptor-Label" + removeLabelsHeader = "X-Interceptor-Remove-Labels" contentType = "Content-Type" jsonType = "application/json" @@ -89,15 +93,162 @@ func (h *Handler) getPullRequest(repo Repository, api, id string) (*PullRequest, if err != nil { return nil, err } - defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New("error getting pull request") + } pr := new(PullRequest) - err = json.NewDecoder(resp.Body).Decode(pr) + _, err = readBody(resp.Body, pr) if err != nil { return nil, err } return pr, nil } +func (h *Handler) addLabel(repo Repository, api, label, color string) (labelID int64, err error) { + labelID = -1 + u, err := url.Parse(repo.CloneURL) + if err != nil { + err = fmt.Errorf("error parsing repo URL: %s", err) + return + } + repoLabels := strings.Join([]string{api, "repos", + repo.Owner.Login, repo.Name, + "labels"}, "/") + u.Path = repoLabels + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return + } + user, pass, err := h.getSecret(u.Host) + if err != nil { + return + } + req.SetBasicAuth(user, pass) + req.Header.Set(contentType, jsonType) + + resp, err := h.cl.Do(req) + if err != nil { + err = fmt.Errorf("error getting labels of %s: %s", u, err) + return + } + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf("error getting labels of %s", u) + return + } + var labels []Label + _, err = readBody(resp.Body, &labels) + if err != nil { + return + } + for _, l := range labels { + if l.Name == label { + labelID = l.ID + break + } + } + if labelID >= 0 { + return + } + buf := new(bytes.Buffer) + l := &Label{Name: label, Color: color} + err = json.NewEncoder(buf).Encode(l) + if err != nil { + return + } + req, err = http.NewRequest("POST", u.String(), buf) + if err != nil { + return + } + req.Header.Set(contentType, jsonType) + req.SetBasicAuth(user, pass) + + resp, err = h.cl.Do(req) + if err != nil { + err = fmt.Errorf("error creating label %s for %s: %s", label, u, err) + return + } + if resp.StatusCode != http.StatusCreated { + err = fmt.Errorf("error creating label %s for %s", label, u) + } + var createdLabel Label + _, err = readBody(resp.Body, &createdLabel) + labelID = createdLabel.ID + return +} + +func (h *Handler) setLabel(repo Repository, api, id, label, color string, remove []string) (err error) { + labelID, err := h.addLabel(repo, api, label, color) + if err != nil { + return + } + u, err := url.Parse(repo.CloneURL) + if err != nil { + return + } + + u.Path = strings.Join([]string{api, "repos", + repo.Owner.Login, repo.Name, + "issues", id, "labels"}, "/") + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return + } + user, pass, err := h.getSecret(u.Host) + if err != nil { + return + } + req.SetBasicAuth(user, pass) + req.Header.Set(contentType, jsonType) + resp, err := h.cl.Do(req) + if err != nil { + return fmt.Errorf("error getting issue labels of %s: %s", u, err) + } + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("error getting issue labels of %s", u) + } + var labels []Label + _, err = readBody(resp.Body, &labels) + if err != nil { + return + } + pl := &PostLabels{[]int64{labelID}} + var rm, found bool + for _, l := range labels { + if contains(remove, l.Name) { + rm = true + } else { + if l.Name == label { + found = true + } + pl.Labels = append(pl.Labels, l.ID) + } + } + if !rm && found { + return + } + + buf := new(bytes.Buffer) + err = json.NewEncoder(buf).Encode(pl) + if err != nil { + return + } + req, err = http.NewRequest("PUT", u.String(), buf) + if err != nil { + return + } + req.SetBasicAuth(user, pass) + req.Header.Set(contentType, jsonType) + + resp, err = h.cl.Do(req) + if err != nil { + return fmt.Errorf("error setting issue label %s for %s: %s", label, u, err) + } + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf("error setting issue label %s for %s", label, u) + } + return +} + type Handler struct { sec typedcorev1.SecretInterface cl *http.Client @@ -121,14 +272,27 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *Handler) prHandler(w http.ResponseWriter, r *http.Request) { var prb PullRequestBody - body, err := readBody(r, &prb) + body, err := readBody(r.Body, &prb) if err != nil { logError(w, err.Error()) return } + if contains(r.Header[prAction], prb.Action) { + if l := r.Header.Get(labelHeader); l != "" { + label, _ := getLabel(l) + var labels []string + for _, l := range prb.PullRequest.Labels { + labels = append(labels, l.Name) + } + + if !contains(labels, label) { + w.WriteHeader(http.StatusBadRequest) + return + } + } h := w.Header() - h.Set(issueHeader, strconv.FormatInt(prb.Number, 10)) + h.Set(issueHeader, itoa(prb.Number)) h.Set(commitHeader, prb.PullRequest.Head.SHA) w.Write(body) return @@ -138,12 +302,14 @@ func (h *Handler) prHandler(w http.ResponseWriter, r *http.Request) { func (h *Handler) pushHandler(w http.ResponseWriter, r *http.Request) { var pb PushBody - body, err := readBody(r, &pb) + body, err := readBody(r.Body, &pb) if err != nil { logError(w, err.Error()) return } - if contains(r.Header[pushAction], pb.Ref) { + push := r.Header.Get(pushAction) + //if contains(r.Header[pushAction], pb.Ref) { + if push != "" && push == pb.Ref { h := w.Header() h.Set(commitHeader, pb.After) w.Write(body) @@ -154,20 +320,45 @@ func (h *Handler) pushHandler(w http.ResponseWriter, r *http.Request) { func (h *Handler) commentHandler(w http.ResponseWriter, r *http.Request) { var cb CommentBody - body, err := readBody(r, &cb) + body, err := readBody(r.Body, &cb) if err != nil { logError(w, err.Error()) return } - //if label := r.Header.Get(labelHeader); label != "" { - //} - if contains(r.Header[commentAction], cb.Comment.Body) { - issue := strconv.FormatInt(cb.Issue.Number, 10) + + //if contains(r.Header[commentAction], cb.Comment.Body) { + comment := r.Header.Get(commentAction) + if comment == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + m, err := regexp.MatchString("(?m)^"+comment+"$", cb.Comment.Body) + if err != nil { + logError(w, err.Error()) + return + } + if m { + issue := itoa(cb.Issue.Number) api := detectAPI(r) if api == "" { logError(w, "unable to detect git provider API") return } + if l := r.Header.Get(labelHeader); l != "" { + label, color := getLabel(l) + var labels []string + for _, l := range cb.Issue.Labels { + labels = append(labels, l.Name) + } + if !contains(labels, label) { + err := h.setLabel(cb.Repository, api, issue, label, color, r.Header[removeLabelsHeader]) + if err != nil { + logError(w, err.Error()) + return + } + } + } + pr, err := h.getPullRequest(cb.Repository, api, issue) if err != nil { logError(w, err.Error()) diff --git a/types.go b/types.go index b425009..e444313 100644 --- a/types.go +++ b/types.go @@ -18,7 +18,13 @@ type Head struct { } type Label struct { - Name string `json:"name"` + ID int64 `json:"id"` + Color string `json:"color"` + Name string `json:"name"` +} + +type PostLabels struct { + Labels []int64 `json:"labels"` } type PushBody struct {