-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Setup an initial Heroku HTTP Server (#33)
- Loading branch information
Showing
11 changed files
with
287 additions
and
3 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1 @@ | ||
dist/ | ||
bin/ |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
web: bin/slugcmplr server |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# bin/go-post-compile is used by the heroku/go buildpack as a post-compile hook | ||
#!/bin/bash | ||
|
||
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/[email protected] |
Binary file not shown.
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
|
||
"github.com/cga1123/slugcmplr" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
func serverCmd(verbose bool) *cobra.Command { | ||
cmd := &cobra.Command{ | ||
Use: "server", | ||
Short: "start a slugmcplr server", | ||
Args: cobra.NoArgs, | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
output := outputterFromCmd(cmd, verbose) | ||
|
||
if err := defaultEnv(); err != nil { | ||
return fmt.Errorf("failed to set env defaults: %w", err) | ||
} | ||
|
||
env, err := requireEnv( | ||
"PORT", | ||
"SLUGCMPLR_ENV", | ||
) | ||
if err != nil { | ||
return fmt.Errorf("error fetching environment: %w", err) | ||
} | ||
|
||
s := &slugcmplr.ServerCmd{ | ||
Port: env["PORT"], | ||
Environment: env["SLUGCMPLR_ENV"], | ||
} | ||
|
||
return s.Execute(cmd.Context(), output) | ||
}, | ||
} | ||
|
||
return cmd | ||
} | ||
|
||
func defaultEnv() error { | ||
defaults := map[string]string{ | ||
"SLUGCMPLR_ENV": "development", | ||
} | ||
for k, v := range defaults { | ||
if _, ok := os.LookupEnv(k); ok { | ||
continue | ||
} | ||
|
||
if err := os.Setenv(k, v); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} |
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package slugcmplr | ||
|
||
import ( | ||
"context" | ||
"io" | ||
"net/http" | ||
"os" | ||
"os/signal" | ||
"syscall" | ||
"time" | ||
|
||
"github.com/gorilla/handlers" | ||
"github.com/gorilla/mux" | ||
) | ||
|
||
const ( | ||
production = "production" | ||
development = "development" | ||
test = "test" | ||
) | ||
|
||
// ServerCmd wraps up all the information required to start a slugcmplr HTTP | ||
// server. | ||
type ServerCmd struct { | ||
// Port is the port to listen to, the server will bind to the 0.0.0.0 | ||
// interface by default. | ||
Port string | ||
|
||
// Environment contains the current deployment environment, e.g. "production" | ||
Environment string | ||
} | ||
|
||
// Execute starts a slugcmplr server, blocking untile a SIGTERM/SIGINT is | ||
// received. | ||
func (s *ServerCmd) Execute(ctx context.Context, out Outputter) error { | ||
return runServer(ctx, out, s.Port, s.Router()) | ||
} | ||
|
||
// Router builds a *mux.Router for slugcmplr. | ||
func (s *ServerCmd) Router() *mux.Router { | ||
r := mux.NewRouter() | ||
|
||
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | ||
http.Redirect(w, r, "https://imgs.xkcd.com/comics/compiling.png", http.StatusFound) | ||
}) | ||
|
||
return r | ||
} | ||
|
||
// nolint:unused | ||
func loggingHandler(out io.Writer) func(n http.Handler) http.Handler { | ||
return func(n http.Handler) http.Handler { | ||
return handlers.LoggingHandler(out, n) | ||
} | ||
} | ||
|
||
// timeoutHandler will set a timeout on the request from having read the | ||
// headers until writing the full response body. | ||
// | ||
// For most requests this should be 30s or less. Heroku will close any | ||
// connection that has not started writing responses within 30s. | ||
// | ||
// See: https://devcenter.heroku.com/articles/http-routing#timeouts | ||
// | ||
// nolint:unused | ||
func timeoutHandler(t time.Duration) func(http.Handler) http.Handler { | ||
return func(n http.Handler) http.Handler { | ||
return http.TimeoutHandler(n, t, http.StatusText(http.StatusServiceUnavailable)) | ||
} | ||
} | ||
|
||
func runServer(ctx context.Context, out Outputter, port string, r *mux.Router) error { | ||
r.Use( | ||
loggingHandler(out.OutOrStdout()), | ||
) | ||
|
||
// Default Handler 404s | ||
r.PathPrefix("/").HandlerFunc( | ||
func(w http.ResponseWriter, r *http.Request) { | ||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) | ||
}, | ||
) | ||
|
||
server := &http.Server{ | ||
Addr: "0.0.0.0:" + port, | ||
|
||
// ReadTimeout sets a timeout from connection open until fully | ||
// request-body read. Mitigating slow client attacks. | ||
ReadTimeout: time.Second * 10, | ||
Handler: r, | ||
} | ||
|
||
errorC := make(chan error, 1) | ||
shutdownC := make(chan os.Signal, 1) | ||
|
||
go func(errC chan<- error) { | ||
errC <- server.ListenAndServe() | ||
}(errorC) | ||
|
||
signal.Notify(shutdownC, syscall.SIGINT, syscall.SIGTERM) | ||
|
||
select { | ||
case err := <-errorC: | ||
if err != nil && err != http.ErrServerClosed { | ||
return err | ||
} | ||
|
||
return nil | ||
case <-shutdownC: | ||
return shutdown(server) | ||
case <-ctx.Done(): | ||
return shutdown(server) | ||
} | ||
} | ||
|
||
func shutdown(server *http.Server) error { | ||
// Heroku Dynos are given 30s to shutdown gracefully. | ||
ctx, cancel := context.WithTimeout(context.Background(), 29*time.Second) | ||
defer cancel() | ||
|
||
return server.Shutdown(ctx) | ||
} | ||
|
||
// nolint:unused | ||
func (s *ServerCmd) inProduction() bool { | ||
return s.Environment == production | ||
} | ||
|
||
// nolint:unused | ||
func (s *ServerCmd) inDevelopment() bool { | ||
return s.Environment == development | ||
} | ||
|
||
// nolint:unused | ||
func (s *ServerCmd) inTest() bool { | ||
return s.Environment == test | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package slugcmplr_test | ||
|
||
import ( | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/cga1123/slugcmplr" | ||
"github.com/gorilla/mux" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func testHandler(router *mux.Router, r *http.Request) *httptest.ResponseRecorder { | ||
recorder := httptest.NewRecorder() | ||
|
||
router.ServeHTTP(recorder, r) | ||
|
||
return recorder | ||
} | ||
|
||
func TestServer_Root(t *testing.T) { | ||
t.Parallel() | ||
|
||
s := &slugcmplr.ServerCmd{Environment: "test"} | ||
req, err := http.NewRequest("GET", "/", nil) | ||
assert.NoError(t, err, "Request should be built successfully") | ||
|
||
res := testHandler(s.Router(), req).Result() | ||
defer res.Body.Close() // nolint:errcheck | ||
|
||
assert.Equal(t, 302, res.StatusCode, "Response should be a redirect") | ||
assert.Equal(t, "https://imgs.xkcd.com/comics/compiling.png", res.Header.Get("Location")) | ||
} | ||
|
||
func TestServer_BadPath(t *testing.T) { | ||
t.Parallel() | ||
|
||
s := &slugcmplr.ServerCmd{Environment: "test"} | ||
req, err := http.NewRequest("GET", "/not-a-reasonable-path", nil) | ||
assert.NoError(t, err, "Request should be built successfully") | ||
|
||
res := testHandler(s.Router(), req).Result() | ||
defer res.Body.Close() // nolint:errcheck | ||
|
||
assert.Equal(t, 404, res.StatusCode, "Response should be a not found") | ||
} |