2020-09-28 07:26:16 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"compress/flate"
|
2020-09-29 03:15:30 +00:00
|
|
|
"encoding/base64"
|
2020-09-28 07:26:16 +00:00
|
|
|
"flag"
|
|
|
|
"fmt"
|
2020-09-29 03:15:30 +00:00
|
|
|
"log"
|
2020-09-28 07:26:16 +00:00
|
|
|
"net/http"
|
|
|
|
"os"
|
2020-09-29 03:15:30 +00:00
|
|
|
"os/exec"
|
2020-09-28 07:55:23 +00:00
|
|
|
"time"
|
2020-09-28 07:26:16 +00:00
|
|
|
|
|
|
|
"git.ryanburnette.com/ryanburnette/git-deploy/assets"
|
2020-09-28 23:39:05 +00:00
|
|
|
"git.ryanburnette.com/ryanburnette/git-deploy/internal/options"
|
|
|
|
"git.ryanburnette.com/ryanburnette/git-deploy/internal/webhooks"
|
2020-09-28 09:56:51 +00:00
|
|
|
|
2020-09-28 07:26:16 +00:00
|
|
|
"github.com/go-chi/chi"
|
|
|
|
"github.com/go-chi/chi/middleware"
|
2020-09-28 09:56:51 +00:00
|
|
|
_ "github.com/joho/godotenv/autoload"
|
2020-09-28 07:26:16 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
name = "gitdeploy"
|
|
|
|
version = "0.0.0"
|
|
|
|
date = "0001-01-01T00:00:00Z"
|
|
|
|
commit = "0000000"
|
|
|
|
)
|
|
|
|
|
|
|
|
func usage() {
|
|
|
|
ver()
|
|
|
|
fmt.Println("")
|
|
|
|
fmt.Println("Use 'help <command>'")
|
|
|
|
fmt.Println(" help")
|
|
|
|
fmt.Println(" init")
|
|
|
|
fmt.Println(" run")
|
|
|
|
}
|
|
|
|
|
|
|
|
func ver() {
|
|
|
|
fmt.Printf("%s v%s %s (%s)\n", name, version, commit[:7], date)
|
|
|
|
}
|
|
|
|
|
2020-09-29 03:15:30 +00:00
|
|
|
type job struct {
|
|
|
|
ID string // {HTTPSURL}#{BRANCH}
|
|
|
|
Cmd *exec.Cmd
|
|
|
|
Ref webhooks.Ref
|
|
|
|
}
|
|
|
|
|
|
|
|
var jobs = make(map[string]*job)
|
|
|
|
|
2020-09-28 23:39:05 +00:00
|
|
|
var runOpts *options.ServerConfig
|
2020-09-28 07:26:16 +00:00
|
|
|
var runFlags *flag.FlagSet
|
|
|
|
var initFlags *flag.FlagSet
|
|
|
|
|
|
|
|
func init() {
|
2020-09-28 23:39:05 +00:00
|
|
|
runOpts = options.Server
|
|
|
|
runFlags = options.ServerFlags
|
|
|
|
initFlags = options.InitFlags
|
|
|
|
runFlags.StringVar(&runOpts.Addr, "listen", ":3000", "the address and port on which to listen")
|
|
|
|
runFlags.BoolVar(&runOpts.TrustProxy, "trust-proxy", false, "trust X-Forwarded-For header")
|
|
|
|
runFlags.BoolVar(&runOpts.Compress, "compress", true, "enable compression for text,html,js,css,etc")
|
2020-09-29 03:15:30 +00:00
|
|
|
runFlags.StringVar(
|
|
|
|
&runOpts.ServePath, "serve-path", "",
|
|
|
|
"path to serve, falls back to built-in web app")
|
|
|
|
runFlags.StringVar(
|
|
|
|
&runOpts.Exec, "exec", "",
|
|
|
|
"path to bash script to run with git info as arguments")
|
2020-09-28 07:26:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
args := os.Args[:]
|
|
|
|
if 1 == len(args) {
|
|
|
|
// "run" should be the default
|
|
|
|
args = append(args, "run")
|
|
|
|
}
|
|
|
|
|
|
|
|
if "help" == args[1] {
|
|
|
|
// top-level help
|
|
|
|
if 2 == len(args) {
|
|
|
|
usage()
|
|
|
|
os.Exit(0)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// move help to subcommand argument
|
|
|
|
self := args[0]
|
|
|
|
args = append([]string{self}, args[2:]...)
|
|
|
|
args = append(args, "--help")
|
|
|
|
}
|
|
|
|
|
|
|
|
switch args[1] {
|
|
|
|
case "version":
|
|
|
|
ver()
|
|
|
|
os.Exit(0)
|
|
|
|
return
|
|
|
|
case "init":
|
2020-09-29 08:08:35 +00:00
|
|
|
_ = initFlags.Parse(args[2:])
|
2020-09-28 07:26:16 +00:00
|
|
|
case "run":
|
2020-09-29 08:08:35 +00:00
|
|
|
_ = runFlags.Parse(args[2:])
|
2020-09-29 03:15:30 +00:00
|
|
|
if "" == runOpts.Exec {
|
|
|
|
fmt.Printf("--exec <path/to/script.sh> is a required flag")
|
|
|
|
os.Exit(1)
|
|
|
|
return
|
|
|
|
}
|
2020-09-28 23:39:05 +00:00
|
|
|
webhooks.MustRegisterAll()
|
2020-09-28 07:26:16 +00:00
|
|
|
serve()
|
|
|
|
default:
|
|
|
|
usage()
|
|
|
|
os.Exit(1)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func serve() {
|
|
|
|
r := chi.NewRouter()
|
|
|
|
|
|
|
|
// A good base middleware stack
|
2020-09-28 23:39:05 +00:00
|
|
|
if runOpts.TrustProxy {
|
2020-09-28 07:26:16 +00:00
|
|
|
r.Use(middleware.RealIP)
|
|
|
|
}
|
2020-09-28 23:39:05 +00:00
|
|
|
if runOpts.Compress {
|
2020-09-28 07:26:16 +00:00
|
|
|
r.Use(middleware.Compress(flate.DefaultCompression))
|
|
|
|
}
|
|
|
|
r.Use(middleware.Logger)
|
|
|
|
r.Use(middleware.Recoverer)
|
2020-09-28 09:43:30 +00:00
|
|
|
r.Use(middleware.Recoverer)
|
2020-09-28 07:26:16 +00:00
|
|
|
|
2020-09-28 07:55:23 +00:00
|
|
|
var staticHandler http.HandlerFunc
|
2020-09-28 07:26:16 +00:00
|
|
|
pub := http.FileServer(assets.Assets)
|
|
|
|
|
2020-09-28 23:39:05 +00:00
|
|
|
if len(runOpts.ServePath) > 0 {
|
2020-09-28 07:55:23 +00:00
|
|
|
// try the user-provided directory first, then fallback to the built-in
|
2020-09-28 23:39:05 +00:00
|
|
|
devFS := http.Dir(runOpts.ServePath)
|
2020-09-28 07:55:23 +00:00
|
|
|
dev := http.FileServer(devFS)
|
|
|
|
staticHandler = func(w http.ResponseWriter, r *http.Request) {
|
2020-09-28 07:26:16 +00:00
|
|
|
if _, err := devFS.Open(r.URL.Path); nil != err {
|
|
|
|
pub.ServeHTTP(w, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
dev.ServeHTTP(w, r)
|
2020-09-28 07:55:23 +00:00
|
|
|
}
|
2020-09-28 07:26:16 +00:00
|
|
|
} else {
|
2020-09-28 07:55:23 +00:00
|
|
|
staticHandler = func(w http.ResponseWriter, r *http.Request) {
|
2020-09-28 07:26:16 +00:00
|
|
|
pub.ServeHTTP(w, r)
|
2020-09-28 07:55:23 +00:00
|
|
|
}
|
2020-09-28 07:26:16 +00:00
|
|
|
}
|
|
|
|
|
2020-09-28 23:39:05 +00:00
|
|
|
webhooks.RouteHandlers(r)
|
2020-09-28 09:43:30 +00:00
|
|
|
|
2020-09-28 07:55:23 +00:00
|
|
|
r.Get("/*", staticHandler)
|
|
|
|
|
2020-09-28 23:39:05 +00:00
|
|
|
fmt.Println("Listening for http (with reasonable timeouts) on", runOpts.Addr)
|
2020-09-28 07:55:23 +00:00
|
|
|
srv := &http.Server{
|
2020-09-28 23:39:05 +00:00
|
|
|
Addr: runOpts.Addr,
|
2020-09-28 07:55:23 +00:00
|
|
|
Handler: r,
|
|
|
|
ReadHeaderTimeout: 2 * time.Second,
|
|
|
|
ReadTimeout: 10 * time.Second,
|
|
|
|
WriteTimeout: 20 * time.Second,
|
|
|
|
MaxHeaderBytes: 1024 * 1024, // 1MiB
|
|
|
|
}
|
2020-09-28 09:43:30 +00:00
|
|
|
|
|
|
|
go func() {
|
|
|
|
for {
|
2020-09-28 23:39:05 +00:00
|
|
|
hook := webhooks.Accept()
|
2020-09-28 09:43:30 +00:00
|
|
|
// TODO os.Exec
|
2020-09-29 03:15:30 +00:00
|
|
|
fmt.Printf("%#v\n", hook)
|
|
|
|
jobID := base64.URLEncoding.EncodeToString([]byte(
|
|
|
|
fmt.Sprintf("%s#%s", hook.HTTPSURL, hook.RefName),
|
|
|
|
))
|
|
|
|
|
|
|
|
args := []string{
|
|
|
|
runOpts.Exec,
|
|
|
|
jobID,
|
|
|
|
hook.RefName,
|
|
|
|
hook.RefType,
|
|
|
|
hook.Owner,
|
|
|
|
hook.Repo,
|
|
|
|
hook.HTTPSURL,
|
|
|
|
}
|
|
|
|
cmd := exec.Command("bash", args...)
|
|
|
|
|
|
|
|
env := os.Environ()
|
|
|
|
envs := []string{
|
|
|
|
"GIT_DEPLOY_JOB_ID=" + jobID,
|
|
|
|
"GIT_REF_NAME=" + hook.RefName,
|
|
|
|
"GIT_REF_TYPE=" + hook.RefType,
|
|
|
|
"GIT_REPO_OWNER=" + hook.Owner,
|
|
|
|
"GIT_REPO_NAME=" + hook.Repo,
|
|
|
|
"GIT_CLONE_URL=" + hook.HTTPSURL,
|
|
|
|
}
|
|
|
|
cmd.Env = append(env, envs...)
|
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
cmd.Stderr = os.Stderr
|
|
|
|
|
|
|
|
if _, exists := jobs[jobID]; exists {
|
|
|
|
// TODO put job in backlog
|
|
|
|
log.Printf("git-deploy job already started for %s#%s\n", hook.HTTPSURL, hook.RefName)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := cmd.Start(); nil != err {
|
|
|
|
log.Printf("git-deploy exec error: %s\n", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
jobs[jobID] = &job{
|
|
|
|
ID: jobID,
|
|
|
|
Cmd: cmd,
|
|
|
|
Ref: hook,
|
|
|
|
}
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
log.Printf("git-deploy job for %s#%s started\n", hook.HTTPSURL, hook.RefName)
|
2020-09-29 08:08:35 +00:00
|
|
|
_ = cmd.Wait()
|
2020-09-29 03:15:30 +00:00
|
|
|
delete(jobs, jobID)
|
|
|
|
log.Printf("git-deploy job for %s#%s finished\n", hook.HTTPSURL, hook.RefName)
|
|
|
|
}()
|
2020-09-28 09:43:30 +00:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2020-09-28 07:55:23 +00:00
|
|
|
if err := srv.ListenAndServe(); nil != err {
|
2020-09-28 07:26:16 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "%s", err)
|
|
|
|
os.Exit(1)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|