From 8497e2031d34ae50fa2cbafd09db3076db28a684 Mon Sep 17 00:00:00 2001 From: Davy Jones Date: Wed, 6 Nov 2019 19:42:03 +0000 Subject: [PATCH] Add configurable response headers A request was made to allow for custom HTTP headers to be returned from the server per route. This adds a new configuraiton option that is a map of header keys and values. Thesea are then mapped to the response as required. Also made some improvements to the http server, configuration and docs --- .travis.yml | 2 +- Dockerfile | 4 ++-- Makefile | 8 ++++---- README.md | 22 ++++++++++++++++++++++ cmd/main.go | 36 ++++++++++++++++++++++++++++++++++-- config.yaml | 14 ++++++++++++-- go.mod | 6 +++--- go.sum | 18 ++++++++++-------- route.go | 1 + router/router.go | 13 +++++++++++-- router/router_test.go | 38 +++++++++++++++++++++++++++++++++++--- 11 files changed, 135 insertions(+), 27 deletions(-) diff --git a/.travis.yml b/.travis.yml index 70faea9..b9bcfc0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ services: - docker go: - - '1.12.6' + - '1.13' env: - GO111MODULE=on diff --git a/Dockerfile b/Dockerfile index b883de4..70d4fed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ ARG APP_NAME=stubby ARG MAIN_PATH=cmd/main.go # -- Builder Image -FROM golang:1.12.6-stretch As Builder +FROM golang:1.13-stretch As Builder ARG ORG_NAME ARG REPO_NAME @@ -23,7 +23,7 @@ WORKDIR /go/src/github.com/${ORG_NAME}/${REPO_NAME} # Set up dependencies COPY ./go.mod go.mod COPY ./go.sum go.sum - RUN go mod vendor + RUN go mod download # Copy rest of the package code COPY . . diff --git a/Makefile b/Makefile index 83ef10b..43ea5a7 100644 --- a/Makefile +++ b/Makefile @@ -6,13 +6,13 @@ all: help USERNAME = davyj0nes APP_NAME = stubby -APP_PORT = 8080 -LOCAL_PORT = 8080 - VERSION = $(shell git describe --exact-match --tags 2>/dev/null) COMMIT = $(shell git rev-parse HEAD | cut -c 1-6) BUILD_TIME = $(shell date -u '+%Y-%m-%d_%I:%M:%S%p') +APP_PORT = 8080 +LOCAL_PORT = 8080 + BUILD_PREFIX = CGO_ENABLED=0 GOOS=linux BUILD_FLAGS = -a -tags netgo --installsuffix netgo LDFLAGS = -ldflags "-s -w -X ${GO_PROJECT_PATH}/cmd.Release=${VERSION} -X ${GO_PROJECT_PATH}/cmd.Commit=${COMMIT} -X ${GO_PROJECT_PATH}/cmd.BuildTime=${BUILD_TIME}" @@ -49,7 +49,7 @@ publish: .PHONY: run_image run_image: $(call blue, "# Running Docker Image Locally...") - @docker run -it --rm --name ${APP_NAME} -v ${PWD}/config.yaml:/config.yaml -p ${LOCAL_PORT}:${APP_PORT} ${USERNAME}/${APP_NAME}:${VERSION} + @docker run -it --rm --name ${APP_NAME} -v ${PWD}/config.yaml:/config.yaml -p ${LOCAL_PORT}:${APP_PORT} ${USERNAME}/${APP_NAME} ## test: run test suites .PHONY: test diff --git a/README.md b/README.md index d4add57..e003842 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ Return stubbed HTTP responses defined in a config file ### Configuration +### Basic + Add the routes and the responses that you want in the [config file](./comfig.yaml). A basic route definition would look like: @@ -37,6 +39,8 @@ routes: } ``` +### URL Query Params + If the response has URL parameters then these need to be defined as follows: ```yaml @@ -57,6 +61,24 @@ The reason for having them defined in a list rather than as a key/value pair is due to how the (Queries](https://www.gorillatoolkit.org/pkg/mux#Route.Queries) method is defined in the router package used ([gorilla mux](https://www.gorillatoolkit.org)). +### Custom Response Headers + +If you want the response to include a header then you can add it as such: + +```yaml +routes: + - path: /foo + status: 200 + headers: + X-Custom: Header + X-Request-Id: ef835eaf-a658-458b-86ae-d2d771f5e745 + respose: >- + { + "id": 987, + "message": "bar" + } +``` + ### Docker The artifact is stored as a docker image and is located on [Docker Hub](https://hub.docker.com/r/davyj0nes/stubby) diff --git a/cmd/main.go b/cmd/main.go index 117da47..eb05c57 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,10 +1,15 @@ package main import ( + "context" "flag" "fmt" "log" "net/http" + "os" + "os/signal" + "syscall" + "time" "github.com/davyj0nes/stubby/config" "github.com/davyj0nes/stubby/router" @@ -22,6 +27,33 @@ func main() { r := router.NewRouter(cfg.Routes) addr := fmt.Sprintf(":%d", cfg.Port) - log.Println("starting server on ", addr) - log.Fatal(http.ListenAndServe(addr, r)) + srv := http.Server{ + Addr: addr, + Handler: r, + ReadTimeout: 1 * time.Second, + WriteTimeout: 5 * time.Second, + } + + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) + + log.Println("starting stubby on ", addr) + go func() { + log.Fatal(srv.ListenAndServe()) + + }() + + log.Print("stubby is ready to serve...") + + killSignal := <-interrupt + switch killSignal { + case os.Interrupt: + log.Println("got SIGINT...") + log.Println("stubby is shutting down...") + case syscall.SIGTERM: + log.Println("got SIGTERM...") + log.Println("stubby is shutting down...") + } + + log.Fatal(srv.Shutdown(context.Background())) } diff --git a/config.yaml b/config.yaml index fb40cba..100f629 100644 --- a/config.yaml +++ b/config.yaml @@ -1,5 +1,5 @@ --- -port: 8888 +port: 8080 routes: - path: /foo status: 200 @@ -13,7 +13,7 @@ routes: - show_deleted - true status: 200 - respose: >- + response: >- { "id": 987, "message": "bar" @@ -24,3 +24,13 @@ routes: { "message": "unauthorized" } + - path: /withResponseHeaders + headers: + X-Request-Id: ef835eaf-a658-458b-86ae-d2d771f5e745 + CustomHeader: booyah + status: 200 + response: >- + { + "id": 123, + "message": "booyah" + } diff --git a/go.mod b/go.mod index ff4345e..b4b18d4 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/davyj0nes/stubby -go 1.12 +go 1.13 require ( - github.com/gorilla/mux v1.7.2 - github.com/spf13/viper v1.4.0 + github.com/gorilla/mux v1.7.3 + github.com/spf13/viper v1.5.0 ) diff --git a/go.sum b/go.sum index f5f3926..8dfb6b6 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I= -github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -53,8 +53,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -86,11 +86,13 @@ github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.5.0 h1:GpsTwfsQ27oS/Aha/6d1oD7tpKIqWnOA6tgOX9HHkt4= +github.com/spf13/viper v1.5.0/go.mod h1:AkYRkVJF8TkSG/xet6PzXX+l39KhhXa2pdqVSxnTcn4= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= @@ -136,6 +138,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/route.go b/route.go index f8e7684..666c2f0 100644 --- a/route.go +++ b/route.go @@ -5,5 +5,6 @@ type Route struct { Path string Response string Status int + Headers map[string]string Queries []string } diff --git a/router/router.go b/router/router.go index d83b491..a32e2b0 100644 --- a/router/router.go +++ b/router/router.go @@ -1,7 +1,6 @@ package router import ( - "fmt" "log" "net/http" @@ -17,6 +16,7 @@ func NewRouter(routes []stubby.Route) *mux.Router { h := Handler{ Response: route.Response, Status: checkStatus(route.Status), + Headers: route.Headers, } r.NewRoute(). @@ -32,16 +32,25 @@ func NewRouter(routes []stubby.Route) *mux.Router { type Handler struct { Response string Status int + Headers map[string]string } // ServeHTTP is used to adhere to the http.Handler interface func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { log.Printf("received (%s) request to %s", req.Method, req.URL.String()) + for k, v := range h.Headers { + w.Header().Add(k, v) + } + w.Header().Set("Content-Type", "application/json") w.WriteHeader(h.Status) - fmt.Fprintf(w, h.Response) + _, err := w.Write([]byte(h.Response)) + if err != nil { + log.Printf("err writing response: %s", err) + } + } func checkStatus(status int) int { diff --git a/router/router_test.go b/router/router_test.go index 06dfe99..447f9a2 100644 --- a/router/router_test.go +++ b/router/router_test.go @@ -10,9 +10,15 @@ import ( "github.com/davyj0nes/stubby/router" ) +type customHeader struct { + Key string + Value string +} + type expected struct { - body string - status int + body string + customHeader customHeader + status int } func TestNewRouter(t *testing.T) { @@ -74,6 +80,22 @@ func TestNewRouter(t *testing.T) { status: http.StatusOK, }, }, + { + name: "supplied route with headers matches the right handler", + path: "/head", + routes: []stubby.Route{ + { + Path: "/head", + Headers: map[string]string{"Custom": "custom"}, + Response: "at the head", + }, + }, + want: expected{ + body: "at the head", + status: http.StatusOK, + customHeader: customHeader{Key: "Custom", Value: "custom"}, + }, + }, } for _, tt := range tests { @@ -91,8 +113,12 @@ func TestNewRouter(t *testing.T) { t.Errorf("expected: (%d), got: (%d)", tt.want.status, res.StatusCode) } - body := getResponseBody(t, res) + headerVal := getResponseHeader(t, res, tt.want.customHeader.Key) + if headerVal != tt.want.customHeader.Value { + t.Errorf("expected: (%s), got: (%s)", tt.want.customHeader.Value, headerVal) + } + body := getResponseBody(t, res) if body != tt.want.body { t.Errorf("expected: (%s), got: (%s)", tt.want.body, body) } @@ -110,3 +136,9 @@ func getResponseBody(t *testing.T, r *http.Response) string { return string(body) } + +func getResponseHeader(t *testing.T, r *http.Response, wantHeader string) string { + t.Helper() + + return r.Header.Get(wantHeader) +}