// Copyright (C) 2019 Marius Schellenberger package main import ( "encoding/json" "fmt" "io/ioutil" "log" "net" "net/http" "net/url" "reflect" "strconv" "strings" "time" //corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/rest" ) var debug = false const ( prAction = "X-Interceptor-Pr-Action" pushAction = "X-Interceptor-Push-Action" commentAction = "X-Interceptor-Comment-Action" contentType = "Content-Type" jsonType = "application/json" githubEvent = "X-GitHub-Event" giteaEvent = "X-Gitea-Event" issueHeader = "Issue" commitHeader = "Commit" secretName = "git-auth" ) func init() { log.SetFlags(log.Flags() | log.Lshortfile) } func (h *Handler) getSecret(host string) (username, password string, err error) { sec, err := h.sec.Get(secretName, metav1.GetOptions{}) if err != nil { return } s, ok := sec.Data[host] if !ok { err = fmt.Errorf("no key found for host %s in secret %s/%s", host, sec.Namespace, sec.Name) return } a := strings.SplitN(string(s), ":", 2) if len(a) < 2 { err = fmt.Errorf("missing credentials for host %s in secret %s/%s", host, sec.Namespace, sec.Name) return } username = a[0] password = a[1] return } func (h *Handler) getPullRequest(repo Repository, api, id string) (*PullRequest, error) { u, err := url.Parse(repo.CloneURL) if err != nil { return nil, err } u.Path = strings.Join([]string{api, "repos", repo.Owner.Login, repo.Name, "pulls", id}, "/") req, err := http.NewRequest("GET", u.String(), nil) if err != nil { return nil, err } req.Header.Set(contentType, jsonType) user, pass, err := h.getSecret(u.Host) if err != nil { return nil, err } req.SetBasicAuth(user, pass) resp, err := h.cl.Do(req) if err != nil { return nil, err } defer resp.Body.Close() pr := new(PullRequest) err = json.NewDecoder(resp.Body).Decode(pr) if err != nil { return nil, err } return pr, nil } type Handler struct { sec typedcorev1.SecretInterface cl *http.Client } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { event := r.Header.Get(githubEvent) switch event { case "pull_request": h.prHandler(w, r) case "push": h.pushHandler(w, r) case "issue_comment": h.commentHandler(w, r) default: h.dumpHandler(w, r) } status := int(reflect.Indirect(reflect.ValueOf(w)).FieldByName("status").Int()) log.Printf("%s %s %s event:%s status:%d", r.RemoteAddr, r.Method, r.URL, event, status) } func (h *Handler) prHandler(w http.ResponseWriter, r *http.Request) { var prb PullRequestBody body, err := readBody(r, &prb) if err != nil { logError(w, err.Error()) return } if contains(r.Header[prAction], prb.Action) { h := w.Header() h.Set(issueHeader, strconv.FormatInt(prb.Number, 10)) h.Set(commitHeader, prb.PullRequest.Head.SHA) w.Write(body) return } w.WriteHeader(http.StatusBadRequest) } func (h *Handler) pushHandler(w http.ResponseWriter, r *http.Request) { var pb PushBody body, err := readBody(r, &pb) if err != nil { logError(w, err.Error()) return } if contains(r.Header[pushAction], pb.Ref) { h := w.Header() h.Set(commitHeader, pb.After) w.Write(body) return } w.WriteHeader(http.StatusBadRequest) } func (h *Handler) commentHandler(w http.ResponseWriter, r *http.Request) { var cb CommentBody body, err := readBody(r, &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) api := detectAPI(r) if api == "" { logError(w, "unable to detect git provider API") return } pr, err := h.getPullRequest(cb.Repository, api, issue) if err != nil { logError(w, err.Error()) return } h := w.Header() h.Set(issueHeader, issue) h.Set(commitHeader, pr.Head.SHA) w.Write(body) return } w.WriteHeader(http.StatusBadRequest) } func (h *Handler) dumpHandler(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if debug { body, err := ioutil.ReadAll(r.Body) if err != nil { logError(w, err.Error()) return } log.Printf("debug headers: %#v", r.Header) log.Printf("debug: %s", string(body)) } w.WriteHeader(http.StatusBadRequest) } func main() { cfg, err := rest.InClusterConfig() if err != nil { log.Fatalf("error configuring kube client: %s\n", err) } cl, err := kubernetes.NewForConfig(cfg) if err != nil { log.Fatalf("error creating new kube client: %s\n", err) } ns, err := getNS() if err != nil { log.Fatalf("error getting current namespace %s\n", err) } h := &Handler{ sec: cl.CoreV1().Secrets(ns), cl: &http.Client{Transport: &http.Transport{ DialContext: (&net.Dialer{ Timeout: 30 * time.Second, }).DialContext, }}, } http.Handle("/", h) s := &http.Server{ ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 120 * time.Second, Addr: ":8080", } log.Println("started webhook listener") log.Fatal(s.ListenAndServe()) }