Last active
September 29, 2019 17:18
-
-
Save foxcpp/0c68dfe1bcbdc2e3d85b1ac1192f5640 to your computer and use it in GitHub Desktop.
Shameless HTTP wrapper for go get command
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Requires a note that it builds only for the server-side chosen OS.
Edit: tbh generally requires explanation.