Skip to content

Instantly share code, notes, and snippets.

@foxcpp
Last active September 29, 2019 17:18
Show Gist options
  • Save foxcpp/0c68dfe1bcbdc2e3d85b1ac1192f5640 to your computer and use it in GitHub Desktop.
Save foxcpp/0c68dfe1bcbdc2e3d85b1ac1192f5640 to your computer and use it in GitHub Desktop.
Shameless HTTP wrapper for go get command
// Command gobuilder starts a HTTP-server that will build Go binaries based on the path given.
// Works only for Go 1.11+ (requires modules support).
//
// Usage:
// GET /importpath/revision
//
// Binaries are built for server OS and architecture by default, this can be changed
// using GOOS and GOARCH env. variables.
// gobuilder overrides following env. variables:
// GO111MODULE, GOPATH, GOBIN, GOCACHE, CGO_ENABLED.
//
// Example:
// curl -O maddy 'http://127.0.0.1:34777/github.com/foxcpp/maddy/cmd/maddy/master'
package main
import (
"errors"
"flag"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
)
var (
pathWhitelist map[string]bool
readOnly bool
globalLimiter chan struct{}
)
func buildID(path string) (string, error) {
cmd := exec.Command("go", "tool", "buildid", path)
output, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return "", errors.New(string(exitErr.Stderr))
}
return "", err
}
return strings.TrimSpace(string(output)), nil
}
func handler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Only GET allowed", 400)
return
}
binDir, err := ioutil.TempDir("", "gobuilder-")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer os.RemoveAll(binDir)
pathParts := strings.Split(r.URL.Path, "/")
if len(pathParts) < 2 {
http.Error(w, "Missing revision", 404)
return
}
revision := pathParts[len(pathParts)-1]
importPath := strings.TrimPrefix(strings.Join(pathParts[:len(pathParts)-1], "/"), "/")
if pathWhitelist != nil && !pathWhitelist[importPath] {
log.Println("refusing to build", importPath, revision, "for", r.RemoteAddr)
http.Error(w, "Import path is not allowed", 403)
return
}
select {
case globalLimiter <- struct{}{}:
default:
http.Error(w, "Try Again Later", 429)
return
}
defer func() { <-globalLimiter }()
log.Println("building", importPath, revision, "for", r.RemoteAddr)
os.Setenv("GOBIN", binDir)
cmd := exec.Command("go", "get")
if readOnly {
cmd.Args = append(cmd.Args, "-mod", "readonly")
}
cmd.Args = append(cmd.Args, importPath+"@"+revision)
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
err = cmd.Run()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
log.Println(strings.TrimSpace(string(exitErr.Stderr)))
w.WriteHeader(500)
w.Write(exitErr.Stderr)
return
}
log.Println(err.Error())
http.Error(w, err.Error(), 500)
return
}
dir, err := ioutil.ReadDir(binDir)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
if len(dir) == 0 {
http.Error(w, "No binary produced", 404)
return
}
if len(dir) > 1 {
http.Error(w, "More than 1 binary produced", 404)
return
}
binaryPath := filepath.Join(binDir, dir[0].Name())
// TODO
//buildId, err := buildID(binaryPath)
//if err != nil {
// http.Error(w, err.Error(), 500)
// return
//}
//w.Header().Set("E-Tag", buildId)
binF, err := os.Open(binaryPath)
if err != nil {
http.Error(w, err.Error(), 500)
}
defer binF.Close()
w.WriteHeader(200)
if _, err := io.Copy(w, binF); err != nil {
http.Error(w, err.Error(), 500)
}
}
func main() {
pwd, err := os.Getwd()
if err != nil {
log.Fatalln(err)
}
gopath := flag.String("gopath", filepath.Join(pwd, "gobuilder-gopath"), "GOPATH to use for builds")
gocache := flag.String("gocache", filepath.Join(pwd, "gobuilder-cache"), "GOCACHE to use for builds")
listenAddr := flag.String("listen", "127.0.0.1:34777", "Endpoint to listen on")
pathWhitelistRaw := flag.String("w", "", "Comma-separated list of packages to allow, if empty - all are allowed")
concurrentBuilds := flag.Int("j", 2, "Limit of builds running in parallel")
cgoOk := flag.Bool("cgo", false, "Allow CGo use")
flag.BoolVar(&readOnly, "ro", false, "Pass -mod readonly to go build")
flag.Parse()
os.Setenv("GOPATH", *gopath)
os.Setenv("GOCACHE", *gocache)
os.Setenv("GO111MODULE", "on")
if *cgoOk {
os.Setenv("CGO_ENABLED", "1")
} else {
os.Setenv("CGO_ENABLED", "0")
}
globalLimiter = make(chan struct{}, *concurrentBuilds)
whitelistParts := strings.Split(*pathWhitelistRaw, ",")
if len(whitelistParts) != 0 && !(len(whitelistParts) == 1 && whitelistParts[0] == "") {
pathWhitelist = make(map[string]bool)
for _, path := range whitelistParts {
pathWhitelist[path] = true
}
}
http.HandleFunc("/", handler)
log.Println("listening on", *listenAddr)
if err := http.ListenAndServe(*listenAddr, nil); err != nil {
log.Fatalln(err)
}
}
@vyamkovyi
Copy link

vyamkovyi commented Sep 29, 2019

Requires a note that it builds only for the server-side chosen OS.

Edit: tbh generally requires explanation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment