commit ad18bb8ad3b52ded10fe83e4e751ae3729bfa622 Author: ston1th Date: Sat Nov 30 12:09:41 2019 +0100 first commit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a1e5422 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:alpine as builder +RUN adduser -D -g '' appuser +RUN mkdir /build +ADD . /build/ +WORKDIR /build +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -gcflags 'all=-e' -ldflags "-s -w" -o main . +FROM scratch +COPY --from=builder /etc/passwd /etc/passwd +COPY --from=builder /build/main /main +USER appuser +EXPOSE 8080 +ENTRYPOINT ["/main"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..115e96e --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (C) 2018 Marius Schellenberger +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The names of the authors and/or contributors may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ston1th BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc7e2ad --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# webhook-interseptor + +The Webhook Interseptor checks for specific conditions when a webhook is received. + +This way we can trigger cleanups when the PR is closed. + +# HTTP Headers + +## Push Event + +``` +X-Interseptor-Ref: refs/heads/master +``` + +## Pull-Request Event + +``` +X-Interseptor-Action: [create|delete] +``` + +## Issue Comment Event + +**TBD** + +``` +X-Interseptor-Comment: /test +``` + +# Build and run + +``` +export KO_DOCKER_REPO='gcr.io/my-gcloud-project-name' + +ko apply -f config/ +``` diff --git a/config/00-namespace.yaml b/config/00-namespace.yaml new file mode 100644 index 0000000..bd8ba5f --- /dev/null +++ b/config/00-namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: webhook-interseptor diff --git a/config/01-podsecuritypolicy.yaml b/config/01-podsecuritypolicy.yaml new file mode 100644 index 0000000..3667a17 --- /dev/null +++ b/config/01-podsecuritypolicy.yaml @@ -0,0 +1,26 @@ +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + name: webhook-interseptor +spec: + privileged: false + allowPrivilegeEscalation: false + volumes: + - 'secret' + hostNetwork: false + hostIPC: false + hostPID: false + runAsUser: + rule: 'RunAsAny' + seLinux: + rule: 'RunAsAny' + supplementalGroups: + rule: 'MustRunAs' + ranges: + - min: 1 + max: 65535 + fsGroup: + rule: 'MustRunAs' + ranges: + - min: 1 + max: 65535 diff --git a/config/02-sa.yaml b/config/02-sa.yaml new file mode 100644 index 0000000..aaeabf2 --- /dev/null +++ b/config/02-sa.yaml @@ -0,0 +1,28 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: webhook-interseptor + namespace: webhook-interseptor +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: webhook-interseptor + namespace: webhook-interseptor +rules: +- apiGroups: [""] + resources: ["secrets"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: webhook-interseptor + namespace: webhook-interseptor +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: webhook-interseptor +subjects: +- kind: ServiceAccount + name: webhook-interseptor diff --git a/config/deployment.yaml b/config/deployment.yaml new file mode 100644 index 0000000..6d0729d --- /dev/null +++ b/config/deployment.yaml @@ -0,0 +1,28 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: webhook-interseptor + namespace: webhook-interseptor + labels: + app: webhook-interseptor +spec: + replicas: 1 + selector: + matchLabels: + app: webhook-interseptor + template: + metadata: + labels: + app: webhook-interseptor + spec: + serviceAccountName: webhook-interseptor + containers: + - name: webhook-interseptor + image: git.giftfish.de/ston1th/webhook-interseptor + resources: + requests: + cpu: 100m + memory: 128Mi + imagePullPolicy: Always + ports: + - containerPort: 8080 diff --git a/config/service.yaml b/config/service.yaml new file mode 100644 index 0000000..bdc7e14 --- /dev/null +++ b/config/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: webhook-interseptor + namespace: webhook-interseptor + labels: + app: webhook-interseptor +spec: + type: ClusterIP + ports: + - name: http + port: 80 + targetPort: 8080 + selector: + app: webhook-interseptor diff --git a/main.go b/main.go new file mode 100644 index 0000000..4fdb40f --- /dev/null +++ b/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "log" + "net/http" + "encoding/json" + "time" + "io/ioutil" +) + +var version string + +type PR struct { + Action string `json:"action"` +} + +func prHandler(w http.ResponseWriter, r *http.Request) { + var pr PR + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + log.Printf("debug headers: %#v", r.Header) + log.Printf("debug body: %s", string(body)) + err = json.Unmarshal(body, &pr) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + switch pr.Action { + case "opened", "reopened", "synchronize": + if r.Header.Get("X-Interseptor-Action") == "create" { + w.Write(body) + return + } + case "closed": + if r.Header.Get("X-Interseptor-Action") == "delete" { + w.Write(body) + return + } + } + w.WriteHeader(http.StatusBadRequest) +} + +type Push struct { + Ref string `json:"ref"` +} + +func pushHandler(w http.ResponseWriter, r *http.Request) { + var p Push + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + log.Printf("debug headers: %#v", r.Header) + log.Printf("debug: %s", string(body)) + err = json.Unmarshal(body, &p) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if r.Header.Get("X-Interseptor-Ref") == p.Ref { + w.Write(body) + return + } + w.WriteHeader(http.StatusBadRequest) +} + +type CommentBody struct { + Comment Comment `json:"comment"` +} + +type Comment struct { + Body string `json:"body"` +} + +func commentHandler(w http.ResponseWriter, r *http.Request) { + var c CommentBody + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + log.Printf("debug headers: %#v", r.Header) + log.Printf("debug: %s", string(body)) + err = json.Unmarshal(body, &c) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if r.Header.Get("X-Interseptor-Comment") == c.Comment.Body { + w.Write(body) + return + } + w.WriteHeader(http.StatusBadRequest) +} + +func dumpHandler(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + log.Printf("debug headers: %#v", r.Header) + log.Printf("debug: %s", string(body)) + w.WriteHeader(http.StatusBadRequest) +} + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + event := r.Header.Get("X-GitHub-Event") + log.Printf("%s %s %s: %s", r.RemoteAddr, r.Method, r.URL, event) + switch event { + case "pull_request": + prHandler(w, r) + case "push": + pushHandler(w, r) + //case "issue_comment": + // commentHandler(w, r) + default: + dumpHandler(w, r) + } + }) + s := &http.Server{ + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 120 * time.Second, + Addr: ":8080", + } + log.Fatal(s.ListenAndServe()) +}