diff --git a/.travis.yml b/.travis.yml index 4d8715574418768902a14c482ccc22200e1f2c7c..2176f307c4b7862535cfa74513683de328e35bbb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,9 @@ --- language: go go: - - '1.10' - '1.11' - '1.12' + - '1.13' script: - make lint diff --git a/Dockerfile b/Dockerfile index d09d92cf3b05895467aae969c99f29761d085218..b75fd4a5f859a6b083a74c8a678b704b513221e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.11 +FROM golang:1.13 WORKDIR /go/src/github.com/mccutchen/go-httpbin diff --git a/Makefile b/Makefile index 0cdcfcf23c29316ea41b2066983fe5a811731e64..b2759bd15a935d420a5e88025aba2c681c48c1ba 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,9 @@ # go-httpbin:latest image use `make imagepush VERSION=latest)` VERSION ?= $(shell git rev-parse --short HEAD) -# Override this to deploy to a different App Engine project +# Override these values to deploy to a different App Engine project GCLOUD_PROJECT ?= httpbingo +GCLOUD_ACCOUNT ?= mccutchen@gmail.com # Built binaries will be placed here DIST_PATH ?= dist @@ -72,10 +73,10 @@ lint: $(GOLINT) # deploy & run locally # ============================================================================= deploy: build - gcloud app deploy --quiet --project=$(GCLOUD_PROJECT) --version=$(VERSION) --promote + gcloud --account=$(GCLOUD_ACCOUNT) app deploy --quiet --project=$(GCLOUD_PROJECT) --version=$(VERSION) --promote stagedeploy: build - gcloud app deploy --quiet --project=$(GCLOUD_PROJECT) --version=$(VERSION) --no-promote + gcloud --account=$(GCLOUD_ACCOUNT) app deploy --quiet --project=$(GCLOUD_PROJECT) --version=$(VERSION) --no-promote run: build $(DIST_PATH)/go-httpbin diff --git a/cmd/maincmd/main.go b/cmd/maincmd/main.go index 7393bfe516afd9840c47723444cde0d53535eae8..ebe53a010030684380e3c93da66254c791923dac 100644 --- a/cmd/maincmd/main.go +++ b/cmd/maincmd/main.go @@ -1,14 +1,16 @@ package maincmd import ( - "crypto/tls" + "context" "flag" "fmt" "log" "net" "net/http" "os" + "os/signal" "strconv" + "syscall" "time" "github.com/mccutchen/go-httpbin/httpbin" @@ -43,7 +45,7 @@ func Main() { if maxBodySize == httpbin.DefaultMaxBodySize && os.Getenv("MAX_BODY_SIZE") != "" { maxBodySize, err = strconv.ParseInt(os.Getenv("MAX_BODY_SIZE"), 10, 64) if err != nil { - fmt.Printf("invalid value %#v for env var MAX_BODY_SIZE: %s\n", os.Getenv("MAX_BODY_SIZE"), err) + fmt.Fprintf(os.Stderr, "Error: invalid value %#v for env var MAX_BODY_SIZE: %s\n\n", os.Getenv("MAX_BODY_SIZE"), err) flag.Usage() os.Exit(1) } @@ -51,7 +53,7 @@ func Main() { if maxDuration == httpbin.DefaultMaxDuration && os.Getenv("MAX_DURATION") != "" { maxDuration, err = time.ParseDuration(os.Getenv("MAX_DURATION")) if err != nil { - fmt.Printf("invalid value %#v for env var MAX_DURATION: %s\n", os.Getenv("MAX_DURATION"), err) + fmt.Fprintf(os.Stderr, "Error: invalid value %#v for env var MAX_DURATION: %s\n\n", os.Getenv("MAX_DURATION"), err) flag.Usage() os.Exit(1) } @@ -62,7 +64,7 @@ func Main() { if port == defaultPort && os.Getenv("PORT") != "" { port, err = strconv.Atoi(os.Getenv("PORT")) if err != nil { - fmt.Printf("invalid value %#v for env var PORT: %s\n", os.Getenv("PORT"), err) + fmt.Fprintf(os.Stderr, "Error: invalid value %#v for env var PORT: %s\n\n", os.Getenv("PORT"), err) flag.Usage() os.Exit(1) } @@ -75,8 +77,29 @@ func Main() { httpsKeyFile = os.Getenv("HTTPS_KEY_FILE") } + var serveTLS bool + if httpsCertFile != "" || httpsKeyFile != "" { + serveTLS = true + if httpsCertFile == "" || httpsKeyFile == "" { + fmt.Fprintf(os.Stderr, "Error: https cert and key must both be provided\n\n") + flag.Usage() + os.Exit(1) + } + } + logger := log.New(os.Stderr, "", 0) + // A hacky log helper function to ensure that shutdown messages are + // formatted the same as other messages. See StdLogObserver in + // httpbin/middleware.go for the format we're matching here. + serverLog := func(msg string, args ...interface{}) { + const ( + logFmt = "time=%q msg=%q" + dateFmt = "2006-01-02T15:04:05.9999" + ) + logger.Printf(logFmt, time.Now().Format(dateFmt), fmt.Sprintf(msg, args...)) + } + h := httpbin.New( httpbin.WithMaxBodySize(maxBodySize), httpbin.WithMaxDuration(maxDuration), @@ -90,22 +113,41 @@ func Main() { Handler: h.Handler(), } - var listenErr error - if httpsCertFile != "" && httpsKeyFile != "" { - cert, err := tls.LoadX509KeyPair(httpsCertFile, httpsKeyFile) - if err != nil { - logger.Fatal("Failed to generate https key pair: ", err) - } - server.TLSConfig = &tls.Config{ - Certificates: []tls.Certificate{cert}, + // shutdownCh triggers graceful shutdown on SIGINT or SIGTERM + shutdownCh := make(chan os.Signal, 1) + signal.Notify(shutdownCh, syscall.SIGINT, syscall.SIGTERM) + + // exitCh will be closed when it is safe to exit, after graceful shutdown + exitCh := make(chan struct{}) + + go func() { + sig := <-shutdownCh + serverLog("shutdown started by signal: %s", sig) + + shutdownTimeout := maxDuration + 1*time.Second + ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) + defer cancel() + + server.SetKeepAlivesEnabled(false) + if err := server.Shutdown(ctx); err != nil { + serverLog("shutdown error: %s", err) } - logger.Printf("go-httpbin listening on https://%s", listenAddr) - listenErr = server.ListenAndServeTLS("", "") + + close(exitCh) + }() + + var listenErr error + if serveTLS { + serverLog("go-httpbin listening on https://%s", listenAddr) + listenErr = server.ListenAndServeTLS(httpsCertFile, httpsKeyFile) } else { - logger.Printf("go-httpbin listening on http://%s", listenAddr) + serverLog("go-httpbin listening on http://%s", listenAddr) listenErr = server.ListenAndServe() } - if listenErr != nil { - logger.Fatalf("Failed to listen: %s", listenErr) + if listenErr != nil && listenErr != http.ErrServerClosed { + logger.Fatalf("failed to listen: %s", listenErr) } + + <-exitCh + serverLog("shutdown finished") }