Skip to content

Instantly share code, notes, and snippets.

@BenLubar
Last active January 8, 2020 20:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BenLubar/a10d4b0890d9d5894ec4c9bb0d35258a to your computer and use it in GitHub Desktop.
Save BenLubar/a10d4b0890d9d5894ec4c9bb0d35258a to your computer and use it in GitHub Desktop.
Basically this is a reverse proxy that automatically logs people (and more importantly, search engines) into my BuildMaster installation automatically.
FROM golang:latest
WORKDIR /go/src/gist.github.com/BenLubar/a10d4b0890d9d5894ec4c9bb0d35258a.git
COPY . .
RUN CGO_ENABLED=0 go build -a -o buildmaster-proxy .
FROM scratch
COPY --from=0 /go/src/gist.github.com/BenLubar/a10d4b0890d9d5894ec4c9bb0d35258a.git/buildmaster-proxy /
EXPOSE 8081
CMD ["/buildmaster-proxy"]
module gist.github.com/BenLubar/a10d4b0890d9d5894ec4c9bb0d35258a.git
go 1.13
require (
github.com/davecgh/go-spew v1.1.1
github.com/pkg/errors v0.8.1
)
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
package main
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/pkg/errors"
)
func main() {
proxyBuildMaster := &httputil.ReverseProxy{
Director: BuildMaster.Director,
FlushInterval: 100 * time.Millisecond,
}
var mux http.ServeMux
mux.HandleFunc("/hooks/github", GitHubHook)
mux.Handle("/", proxyBuildMaster)
panic(http.ListenAndServe(":8081", &mux))
}
var BuildMaster = &Proxy{
Host: "192.168.1.100:8622",
LogInAPI: "BuildMaster.Web.WebApplication/Inedo.BuildMaster.Web.WebApplication.Pages.LogInPage/LogIn",
}
type Proxy struct {
Host string
LogInAPI string
}
func (p *Proxy) Director(req *http.Request) {
req.URL.Scheme = "http"
req.URL.Host = p.Host
req.Header.Set("User-Agent", req.Header.Get("User-Agent")+" (forwarded-for/"+req.Header.Get("X-Forwarded-For")+") (cloudflare-geo/"+req.Header.Get("CF-IPCountry")+")")
//log.Println("[proxy]", req.Method, req.URL.Path)
}
var hookSecret = []byte(os.Getenv("GITHUB_HOOK_SECRET"))
// Only one hook gets handled at a time.
var hookLock sync.Mutex
func GitHubHook(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.UserAgent(), "GitHub-Hookshot/") {
http.Error(w, "This is the GitHub hook endpoint.", http.StatusForbidden)
return
}
log.Println("[webhook]", r.Method, r.URL.Path)
signature, err := hex.DecodeString(strings.TrimPrefix(r.Header.Get("X-Hub-Signature"), "sha1="))
if err != nil {
http.Error(w, "signature: "+err.Error(), http.StatusInternalServerError)
return
}
payloadBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "body: "+err.Error(), http.StatusInternalServerError)
return
}
hash := hmac.New(sha1.New, hookSecret)
_, _ = hash.Write(payloadBytes)
if !hmac.Equal(signature, hash.Sum(nil)) {
http.Error(w, "Invalid signature.", http.StatusBadRequest)
return
}
hookLock.Lock()
defer hookLock.Unlock()
switch r.Header.Get("X-GitHub-Event") {
case "push":
var payload struct {
Ref string `json:"ref"`
After string `json:"after"`
Repository struct {
FullName string `json:"full_name"`
} `json:"repository"`
}
if err = json.Unmarshal(payloadBytes, &payload); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
switch payload.Repository.FullName {
case "DFHack/dfhack":
if payload.Ref != "refs/heads/develop" {
break
}
CoreCommit(payload.After)
case "BenLubar/df-ai":
if payload.Ref != "refs/heads/master" {
break
}
PluginCommit("df-ai", payload.After)
case "BenLubar/weblegends":
if payload.Ref != "refs/heads/master" {
break
}
PluginCommit("weblegends", payload.After)
case "BenLubar/bingo":
if payload.Ref != "refs/heads/master" {
break
}
PluginCommit("bingo", payload.After)
}
case "create":
var payload struct {
RefType string `json:"ref_type"`
Ref string `json:"ref"`
Repository struct {
FullName string `json:"full_name"`
} `json:"repository"`
}
if err = json.Unmarshal(payloadBytes, &payload); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if payload.RefType == "tag" {
switch payload.Repository.FullName {
case "DFHack/dfhack":
CoreTag(payload.Ref)
case "BenLubar/df-ai":
PluginTag("BenLubar", "df-ai", payload.Ref)
case "BenLubar/weblegends":
PluginTag("BenLubar", "weblegends", payload.Ref)
case "BenLubar/bingo":
PluginTag("BenLubar", "bingo", payload.Ref)
}
}
case "pull_request":
var payload struct {
Action string `json:"action"`
Number int `json:"number"`
PullRequest PullRequest `json:"pull_request"`
}
if err = json.Unmarshal(payloadBytes, &payload); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var fn func(string, string, string, string)
switch payload.Action {
case "opened":
fn = payload.PullRequest.OnOpen
case "closed":
fn = payload.PullRequest.OnClose
case "reopened":
fn = payload.PullRequest.OnReopen
case "synchronize":
fn = payload.PullRequest.OnSynchronize
default:
w.WriteHeader(http.StatusNoContent)
return
}
var applicationName string
var pipelineName string
commitVariable := "$DFHackCommit"
expectedBranch := "master"
switch payload.PullRequest.Base.Repo.FullName {
case "DFHack/dfhack":
applicationName = "DFHack Pull Requests"
expectedBranch = "develop"
pipelineName = "dfhack-raft::DFHack-Core-Pull-Request"
case "DFHack/df-structures":
applicationName = "df-structures Pull Requests"
commitVariable = "$OverrideStructuresCommit"
pipelineName = "dfhack-raft::DFHack-Core-Pull-Request"
case "BenLubar/df-ai":
applicationName = "df-ai Pull Requests"
commitVariable = "$PluginCommit"
pipelineName = "dfhack-raft::DFHack-Plugin-Pull-Request"
case "BenLubar/weblegends":
applicationName = "weblegends Pull Requests"
commitVariable = "$PluginCommit"
pipelineName = "dfhack-raft::DFHack-Plugin-Pull-Request"
case "BenLubar/bingo":
applicationName = "bingo Pull Requests"
commitVariable = "$PluginCommit"
pipelineName = "dfhack-raft::DFHack-Plugin-Pull-Request"
default:
w.WriteHeader(http.StatusNoContent)
return
}
if payload.PullRequest.Base.Ref != expectedBranch && payload.Action != "closed" {
w.WriteHeader(http.StatusNoContent)
return
}
fn(pipelineName, applicationName, commitVariable, strconv.Itoa(payload.Number))
}
w.WriteHeader(http.StatusNoContent)
}
func EnsureRelease(applicationName, releaseNumber string, payload url.Values) (interface{}, error) {
releases, err := APIRequest("/api/releases", url.Values{
"applicationName": {applicationName},
"releaseNumber": {releaseNumber},
})
if err != nil {
return nil, err
}
if r, ok := releases.([]interface{}); ok && len(r) != 0 {
return r[0], nil
}
payload.Set("applicationName", applicationName)
payload.Set("releaseNumber", releaseNumber)
return APIRequest("/api/releases/create", payload)
}
func APIRequest(path string, payload url.Values) (interface{}, error) {
payload.Set("key", os.Getenv("BUILDMASTER_API_KEY"))
r, err := http.Post("http://"+BuildMaster.Host+path, "application/x-www-form-urlencoded", strings.NewReader(payload.Encode()))
if err != nil {
return nil, errors.Wrap(err, "performing POST request")
}
defer r.Body.Close()
if r.StatusCode >= 400 {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, errors.Wrapf(err, "reading HTTP %d response body", r.StatusCode)
}
return nil, errors.Errorf("HTTP %d error: %s", r.StatusCode, string(body))
}
var v interface{}
err = json.NewDecoder(r.Body).Decode(&v)
return v, errors.Wrap(err, "decoding JSON body")
}
func CoreCommit(sha string) {
response, err := APIRequest("/api/releases/packages/create", url.Values{
"applicationName": {"DFHack"},
"pipelineName": {"dfhack-raft::DFHack"},
"$DFHackCommit": {sha},
})
if err != nil {
panic(err)
}
_, err = APIRequest("/api/releases/packages/deploy", url.Values{
"packageId": {fmt.Sprint(response.(map[string]interface{})["id"])},
})
if err != nil {
panic(err)
}
}
func GetLatestCommitOnBranch(branch, repo string) (string, error) {
r, err := http.Get("https://api.github.com/repos/" + repo + "/branches/" + branch)
if err != nil {
return "", err
}
defer r.Body.Close()
var data struct {
Commit struct {
Sha string `json:"sha"`
} `json:"commit"`
}
err = json.NewDecoder(r.Body).Decode(&data)
return data.Commit.Sha, err
}
func PluginCommit(plugin, sha string) {
coreSha, err := GetLatestCommitOnBranch("develop", "DFHack/dfhack")
if err != nil {
panic(err)
}
response, err := APIRequest("/api/releases/packages/create", url.Values{
"applicationName": {plugin},
"pipelineName": {"dfhack-raft::DFHack-Plugin"},
"$DFHackCommit": {coreSha},
"$PluginCommit": {sha},
})
if err != nil {
panic(err)
}
_, err = APIRequest("/api/releases/packages/deploy", url.Values{
"packageId": {fmt.Sprint(response.(map[string]interface{})["id"])},
})
if err != nil {
panic(err)
}
coreTag, coreSha, err := GetTag("", "DFHack/dfhack")
if err != nil {
panic(err)
}
response, err = APIRequest("/api/releases/packages/create", url.Values{
"applicationName": {plugin},
"releaseNumber": {"dfhack-" + coreTag},
"$DFHackCommit": {coreSha},
"$PluginCommit": {sha},
})
if err != nil {
if err.Error() == "Release dfhack-"+coreTag+" for application "+plugin+" is not an active release or does not exist." {
return
}
panic(err)
}
_, err = APIRequest("/api/releases/packages/deploy", url.Values{
"packageId": {fmt.Sprint(response.(map[string]interface{})["id"])},
})
if err != nil {
panic(err)
}
}
func DeployReleases(application string) {
releases, err := APIRequest("/api/releases", url.Values{
"applicationName": {application},
"status": {"active"},
})
if err != nil {
panic(err)
}
for _, release := range releases.([]interface{}) {
data := release.(map[string]interface{})
num := data["number"].(string)
if num == "0.0.0" {
// Don't deploy legacy develop release
continue
}
if strings.HasPrefix(num, "dfhack-") {
// Don't deploy prereleases
continue
}
_, err = APIRequest("/api/releases/packages/deploy", url.Values{
"packageId": {fmt.Sprint(data["latestPackageId"])},
"toStage": {"Deployed"},
"force": {"true"},
})
if err != nil {
panic(err)
}
}
}
func GetTag(expectedName, repo string) (name, sha string, err error) {
r, err := http.Get("https://api.github.com/repos/" + repo + "/tags")
if err != nil {
return "", "", err
}
defer r.Body.Close()
var tags []struct {
Name string `json:"name"`
Commit struct {
Sha string `json:"sha"`
} `json:"commit"`
}
err = json.NewDecoder(r.Body).Decode(&tags)
if err != nil {
return "", "", err
}
for _, tag := range tags {
if tag.Name == expectedName {
return tag.Name, tag.Commit.Sha, nil
}
}
return tags[0].Name, tags[0].Commit.Sha, nil
}
func CoreTag(tag string) {
DeployReleases("DFHack")
_, coreSha, err := GetTag(tag, "DFHack/dfhack")
if err != nil {
panic(err)
}
release, err := EnsureRelease("DFHack", tag, url.Values{
"pipelineName": {"dfhack-raft::DFHack"},
"$DFHackBranch": {"develop"},
"$DFHackTag": {tag},
"$DFHackTagged": {"true"},
})
if err != nil {
panic(err)
}
response, err := APIRequest("/api/releases/packages/create", url.Values{
"releaseId": {fmt.Sprint(release.(map[string]interface{})["id"])},
"$DFHackCommit": {coreSha},
})
if err != nil {
panic(err)
}
_, err = APIRequest("/api/releases/packages/deploy", url.Values{
"packageId": {fmt.Sprint(response.(map[string]interface{})["id"])},
})
if err != nil {
panic(err)
}
}
func PluginTag(owner, plugin, tag string) {
DeployReleases(plugin)
coreTag, coreSha, err := GetTag("", "DFHack/dfhack")
if err != nil {
panic(err)
}
_, pluginSha, err := GetTag(tag, owner+"/"+plugin)
if err != nil {
panic(err)
}
release, err := EnsureRelease(plugin, tag, url.Values{
"pipelineName": {"dfhack-raft::DFHack-Plugin"},
"$DFHackBranch": {"develop"},
"$DFHackTag": {coreTag},
"$PluginTag": {tag},
"$PluginTagged": {"true"},
})
if err != nil {
panic(err)
}
response, err := APIRequest("/api/releases/packages/create", url.Values{
"releaseId": {fmt.Sprint(release.(map[string]interface{})["id"])},
"$DFHackCommit": {coreSha},
"$PluginCommit": {pluginSha},
})
if err != nil {
panic(err)
}
_, err = APIRequest("/api/releases/packages/deploy", url.Values{
"packageId": {fmt.Sprint(response.(map[string]interface{})["id"])},
})
if err != nil {
panic(err)
}
}
type PullRequest struct {
Body string `json:"body"`
Head struct {
Sha string `json:"sha"`
Repo struct {
Name string `json:"name"`
Owner struct {
Login string `json:"login"`
} `json:"owner"`
} `json:"repo"`
} `json:"head"`
Base struct {
Ref string `json:"ref"`
Repo struct {
FullName string `json:"full_name"`
} `json:"repo"`
} `json:"base"`
Merged bool `json:"merged"`
}
func (pr *PullRequest) OnOpen(pipelineName, applicationName, commitVariable, number string) {
if strings.Contains(pr.Body, "buildmaster:skip-pr") {
return
}
release, err := EnsureRelease(applicationName, number, url.Values{
"pipelineName": {pipelineName},
"$ForkOwner": {pr.Head.Repo.Owner.Login},
"$ForkRepo": {pr.Head.Repo.Name},
})
if err != nil {
panic(err)
}
response, err := APIRequest("/api/releases/packages/create", url.Values{
"releaseId": {fmt.Sprint(release.(map[string]interface{})["id"])},
commitVariable: {pr.Head.Sha},
})
if err != nil {
panic(err)
}
_, err = APIRequest("/api/releases/packages/deploy", url.Values{
"packageId": {fmt.Sprint(response.(map[string]interface{})["id"])},
})
if err != nil {
log.Println(err)
}
}
func (pr *PullRequest) OnClose(pipelineName, applicationName, commitVariable, number string) {
if pr.Merged {
releases, err := APIRequest("/api/releases", url.Values{
"applicationName": {applicationName},
"releaseNumber": {number},
})
if err != nil {
panic(err)
}
for _, release := range releases.([]interface{}) {
spew.Dump(release)
_, err = APIRequest("/api/releases/packages/deploy", url.Values{
"buildId": {fmt.Sprint(release.(map[string]interface{})["latestPackageId"])},
"toStage": {"Merged"},
"force": {"true"},
})
if err != nil {
panic(err)
}
}
} else {
_, err := APIRequest("/api/releases/cancel", url.Values{
"applicationName": {applicationName},
"releaseNumber": {number},
})
if err != nil {
log.Println(err)
}
}
}
func (pr *PullRequest) OnReopen(pipelineName, applicationName, commitVariable, number string) {
_, err := APIRequest("/api/releases/restore", url.Values{
"applicationName": {applicationName},
"releaseNumber": {number},
})
if err != nil {
log.Println(err)
return
}
response, err := APIRequest("/api/releases/packages/create", url.Values{
"applicationName": {applicationName},
"releaseNumber": {number},
commitVariable: {pr.Head.Sha},
})
if err != nil {
panic(err)
}
_, err = APIRequest("/api/releases/packages/deploy", url.Values{
"packageId": {fmt.Sprint(response.(map[string]interface{})["id"])},
})
if err != nil {
panic(err)
}
}
func (pr *PullRequest) OnSynchronize(pipelineName, applicationName, commitVariable, number string) {
response, err := APIRequest("/api/releases/packages/create", url.Values{
"applicationName": {applicationName},
"releaseNumber": {number},
commitVariable: {pr.Head.Sha},
})
if err != nil {
log.Println(err)
return
}
_, err = APIRequest("/api/releases/packages/deploy", url.Values{
"packageId": {fmt.Sprint(response.(map[string]interface{})["id"])},
})
if err != nil {
panic(err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment