├── .gitignore ├── .ruby-gemset ├── .ruby-version ├── LICENSE ├── Readme.md ├── _build ├── Gemfile ├── Gemfile.lock ├── Rakefile ├── config.yml ├── consul_keys.yml ├── dockercompose │ └── helloworld │ │ └── docker-compose.yml ├── dockerfile │ └── helloworld │ │ ├── Dockerfile │ │ ├── config.ctmpl │ │ └── s6-etc │ │ ├── .s6-svscan │ │ ├── crash │ │ └── finish │ │ ├── app │ │ ├── finish │ │ └── run │ │ └── consul-template │ │ ├── finish │ │ └── run ├── features │ ├── echo.feature │ ├── health.feature │ └── support │ │ └── env.rb └── swagger_spec │ └── swagger.yml ├── circle.yml ├── global └── global.go ├── handlers ├── const.go ├── echo.go ├── echo_test.go ├── health.go ├── health_test.go ├── middleware_requestvalidation.go ├── middleware_requestvalidation_test.go └── xx_router.go ├── logging └── StatsD.go ├── main.go └── mocks └── mocks.go /.gitignore: -------------------------------------------------------------------------------- 1 | # binaries 2 | /helloworld 3 | _build/dockerfile/helloworld/helloworld 4 | 5 | # other 6 | .DS_Store 7 | _build/dockerfile/helloworld/swagger_spec 8 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | helloworld 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.3.1 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nicholas Jackson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Hello world 2 | CircleCi build status: 3 | [![Circle CI](https://circleci.com/gh/nicholasjackson/helloworld.svg?style=svg)](https://circleci.com/gh/nicholasjackson/helloworld) 4 | 5 | This simple microservice is an example of the output of go-microservice-template. 6 | -------------------------------------------------------------------------------- /_build/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "rake" 4 | gem "cucumber", "~> 1.3.10" 5 | gem "cucumber-rest-api", '= 0.3' 6 | gem 'docker-api', :require => 'docker' 7 | 8 | gem 'minke' 9 | gem 'minke-generator-go' 10 | -------------------------------------------------------------------------------- /_build/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | builder (3.2.2) 5 | colorize (0.7.7) 6 | consul_loader (1.0.0) 7 | rest-client 8 | cucumber (1.3.20) 9 | builder (>= 2.1.2) 10 | diff-lcs (>= 1.1.3) 11 | gherkin (~> 2.12) 12 | multi_json (>= 1.7.5, < 2.0) 13 | multi_test (>= 0.1.2) 14 | cucumber-rest-api (0.3) 15 | cucumber (>= 1.2.1) 16 | jsonpath (>= 0.1.2) 17 | nokogiri (>= 1.6.0) 18 | rspec (>= 2.12.0) 19 | diff-lcs (1.2.5) 20 | docker-api (1.28.0) 21 | excon (>= 0.38.0) 22 | json 23 | domain_name (0.5.20160310) 24 | unf (>= 0.0.5, < 1.0.0) 25 | excon (0.49.0) 26 | gherkin (2.12.2) 27 | multi_json (~> 1.3) 28 | http-cookie (1.0.2) 29 | domain_name (~> 0.5) 30 | json (1.8.3) 31 | jsonpath (0.5.8) 32 | multi_json 33 | mime-types (2.99.2) 34 | mini_portile2 (2.1.0) 35 | minke (1.5.2) 36 | colorize 37 | consul_loader (~> 1.0) 38 | cucumber 39 | docker-api 40 | multi_json 41 | rake (~> 10.0) 42 | rest-client (~> 1.8) 43 | sshkey 44 | minke-generator-go (0.5.0) 45 | multi_json (1.12.1) 46 | multi_test (0.1.2) 47 | netrc (0.11.0) 48 | nokogiri (1.6.8) 49 | mini_portile2 (~> 2.1.0) 50 | pkg-config (~> 1.1.7) 51 | pkg-config (1.1.7) 52 | rake (10.5.0) 53 | rest-client (1.8.0) 54 | http-cookie (>= 1.0.2, < 2.0) 55 | mime-types (>= 1.16, < 3.0) 56 | netrc (~> 0.7) 57 | rspec (3.4.0) 58 | rspec-core (~> 3.4.0) 59 | rspec-expectations (~> 3.4.0) 60 | rspec-mocks (~> 3.4.0) 61 | rspec-core (3.4.4) 62 | rspec-support (~> 3.4.0) 63 | rspec-expectations (3.4.0) 64 | diff-lcs (>= 1.2.0, < 2.0) 65 | rspec-support (~> 3.4.0) 66 | rspec-mocks (3.4.1) 67 | diff-lcs (>= 1.2.0, < 2.0) 68 | rspec-support (~> 3.4.0) 69 | rspec-support (3.4.1) 70 | sshkey (1.8.0) 71 | unf (0.1.4) 72 | unf_ext 73 | unf_ext (0.0.7.2) 74 | 75 | PLATFORMS 76 | ruby 77 | 78 | DEPENDENCIES 79 | cucumber (~> 1.3.10) 80 | cucumber-rest-api (= 0.3) 81 | docker-api 82 | minke 83 | minke-generator-go 84 | rake 85 | 86 | BUNDLED WITH 87 | 1.12.3 88 | -------------------------------------------------------------------------------- /_build/Rakefile: -------------------------------------------------------------------------------- 1 | require 'minke' 2 | 3 | spec = Gem::Specification.find_by_name 'minke' 4 | Rake.add_rakelib "#{spec.gem_dir}/lib/minke/rake" 5 | -------------------------------------------------------------------------------- /_build/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | application_name: 'helloworld' 3 | namespace: 'github.com/nicholasjackson' 4 | generator_name: minke-generator-go 5 | docker_registry: 6 | url: 7 | secure: 8 | fingerprint: bd:63:42:21:b3:09:bf:d4:d5:29:3f:79:c5:b6:c6:b0 9 | value: > 10 | m0YUF/p5m0tJ1unXba+5BtzTXZunQsSFmDVLSk+6DD7zlt2Lyp33nOvU4gjk 11 | pYyEw/Yjc8gmbaUlMybACKKdcQcAIkdmAfZeZDmc61rXWox/FXskHeLAFGdP 12 | 10Gyy7d5Ph9Jkpfuy0DBIGM4XiLl/k6G5yPM83upC7Ede5sg2feYcP4Z+Bqe 13 | mhsM7dwWG3iZ012+LeYpa25ArGRJvvEV9HGDsClib7nqLHmwn2itiG8FTKFS 14 | atLGiioHhPN1/EvfEjPw+0T5184eYJq8RK955JZeOLFsRWk+2FfUHBSQB0Ir 15 | bCUOylvJOXMNAe3or5crS8uvPFRMmZuyshcsSvM4+vglq4jn+hzVKTPEK3T4 16 | oRHDUre5MJuCag+N0xgeSwmi3jqk/ClOu/y1aZP9Vhu7w9syniLLR+o8dRyE 17 | 6wuQelaKkVYTDwnV4wiEa7P+jBKRFT9iwdNNdA7WKKp5sK2gIrTpl8yY1mhy 18 | OxKjbLCEXa3m+uClpbtGny5o0T8gjVdUy1kRfKxOW8BdgJ1+1LX+75LP7Wts 19 | qlh2VDqPUAarHkr8w4ejtFcGOvMCAMsYrE6YTVQiI8n1REbEQWtkd+qQqwn8 20 | oViBRmOJyYQ81VkgR8lRai7hvmatq5HTGqmTRT2JHqa5NtXY12byFsP74Jib 21 | IStl1041+vWBUU8AtTbzVhs= 22 | user: 23 | secure: 24 | fingerprint: bd:63:42:21:b3:09:bf:d4:d5:29:3f:79:c5:b6:c6:b0 25 | value: > 26 | Glll5xQ2C+Tn1Dd6OWenHw25+CaUPCHNPtx9ojwf0aaVCzuK6SbVqCSS80vg 27 | ezrBciOGMeiH1Glmi6sQgGNSYZBbwCmQKXyrEL3L+d66/qU+bWlGIkc17DGj 28 | gHsIa63/Oh3HKX60yh9xPDfsMuUKCYjJL4ahqzDbWax5SUXzHGicLe6gQS6B 29 | 0WA5tzkzfXh1IT9M6LqXALGTxxSD3iaq2ILl37ZrTfgojabCP7Xj2CW3U3EI 30 | Xjo1ds0TxrgCFWQOZ82ohiu0np/2tJfORoGocY2p00l18ksLFIN6p+y36K4Q 31 | 9LYJ2OsYjiDUE/Zx3lEN4Ju5xy54fid1hIGKeMtjjEWzM2531JEgTjFsMh/r 32 | v/QqcGfxgGTA4Mhi2kjh1UUta0Za//bIkYzroGipwPQU+ePviNOYMm5iFWyW 33 | b8lxwVZ50p/b+0V4UidP2Iy7rY9BGoaOKvuosqAPi0CxjozhwCwFmQLtsREf 34 | wgjWVHZYw+JWU3Yd20Q91H2WTIilKcQ0eL4G/+kztOIs+T43Sszgqu4Uzu7o 35 | NnfWzKR3af7+EGulvlVV+ceVKIawaiyXSOo/WfheVnwO4XR1fskz/tzafla8 36 | C3YAIlrB58+CAWh+vdmOIsar9jNVq2Zrv7dRgYT9XO3QY+eADNuMv612zDbR 37 | kU3rDnf1n2JARCqOFQCD270= 38 | password: 39 | secure: 40 | fingerprint: bd:63:42:21:b3:09:bf:d4:d5:29:3f:79:c5:b6:c6:b0 41 | value: > 42 | Pp7uEBxSGoYrVZgHZkIxyZcKqdKq+L4IsnuN30flulBJK0YiBOGdR0+Qd82j 43 | oaZ5BrA8WeC+cmCoE6+TUT5LbkjpxmhzjUZ4jXSdmP6rmLMOlHfpR9JZe+sd 44 | 6xVDY01qkPm5bGtK9QxP72tavaRHexRtgprJPWkKPgXW52M1XJIAgS/7J7mo 45 | W5khv6v+2AG8nLWj+F2a4Tpv63IiEgbmTTNE7Kx2vT16v9hXt6TdV1Tex9vZ 46 | 2KbEtj42wF8PQjSPbDIHzlbae21l6r50hNG6ef/nC0kasXVFA/pmENZCbFAa 47 | zig96QUkldiBXA0SdRHxvFEe82S3lAlHATwTNZaTn48ctkJoJ4Qe97g4jGGi 48 | kAokABO18wHJDRdN2AJ7kqetInzlu0qzd06zYtjp+WjQj2Z9qLxPrvYDmMwS 49 | dgFYd3BRWPGVC8DcRYJheD787XWAlpI6UJ7uHm2my81hlh0A2hTkbO/hNpKZ 50 | ENNV1gL/2Zq8pz76C+JAYrKPXBY4Ja4ptpDvB4mDsKXToWbvc+F4xxI4O32O 51 | bqejFjWvadjBJi3w0CFVrZU/OBbZIFh9JpNkgwiFreBQFkg1RkjXLE+nLsro 52 | YSBG4bnmqbOetQbbRNIZ62+LLwYTa3GI/NpYNiiWSSCbMizhpiECkGSLNYgM 53 | vX6/6Dy7Ft5//IAuYx+MXuI= 54 | email: 55 | secure: 56 | fingerprint: bd:63:42:21:b3:09:bf:d4:d5:29:3f:79:c5:b6:c6:b0 57 | value: > 58 | ri9mM9jgTvwO2kZ8Il6958R3QyLf3n6jUFBcsmpJLtAQssHIlnU1sibfaVwY 59 | rvtRG7UZYV+1vCUgbjURmuTJ+vhJf6eWdWe/Ybs+Q7LT1xKcCD2zNiPOsBX1 60 | GLchZCRtfwV37VIs19kjPLauumaigYgIpv4/2e2ESVCgxa6uC0ifkfPubdAe 61 | 2JvCDKqum4NwfRdCHtQfs16LqYVJLjOhrCkFUtvifJpG+IpnTzMjJXorUT2q 62 | pG1JYydLp7fdCDP19AmxaVtcALFedly5v2YK9vA2Y93v1++4E3L9qf5X/DcZ 63 | bsoH4FA/xxvq+wUgg4h8mNanaK0kLwihfI6/JJTuaGLxOeiCLWeyGlPA/GpA 64 | BS5HptCSGKssSkRUeKyO1AZXXjkJNkhIFrrj6OMvktQeER+GuuOLfqVkov/Z 65 | bU22W6vVecEW6gW1nRERWkEjMqtLBu1EQo8IO6x1tE0eErkzU3MjzORuCNs+ 66 | Hs62PScG4hAI/4pvEhZBZNg9suf2dB9KeCoCohamaNSfVYLr69mH03L9VvQl 67 | GOaaYHNW/NB9WlYUgXxEeIl/7RaBh/HOFY/WWgzEN3z21h+/4UX5xeRDm6Rs 68 | mtcW/ZIw1HdFQzNJqkl7ASZvs347PZqbP0qgSTkvmPMdUSJtLxQ8HAfVQfO3 69 | qHjdiZrrI3KsceKlvATSc08= 70 | namespace: 'nicholasjackson' 71 | docker: 72 | build_image: 'golang:latest' 73 | application_docker_file: './dockerfile/helloworld/' 74 | application_compose_file: './dockercompose/helloworld/docker-compose.yml' 75 | fetch: 76 | enabled: true 77 | test: 78 | enabled: true 79 | build: 80 | post: 81 | copy: 82 | - 83 | from: '../helloworld' 84 | to: './dockerfile/helloworld' 85 | - 86 | from: './swagger_spec/swagger.yml' 87 | to: './dockerfile/helloworld/swagger_spec' 88 | run: 89 | pre: 90 | consul_loader: 91 | config_file: './consul_keys.yml' 92 | url: 93 | address: consul 94 | port: 8500 95 | type: private 96 | cucumber: 97 | pre: 98 | consul_loader: 99 | config_file: './consul_keys.yml' 100 | url: 101 | address: consul 102 | port: 8500 103 | type: private 104 | health_check: 105 | address: helloworld 106 | port: 8001 107 | path: /v1/health 108 | type: private 109 | -------------------------------------------------------------------------------- /_build/consul_keys.yml: -------------------------------------------------------------------------------- 1 | app: 2 | stats_d_server: 'statsd:8125' 3 | -------------------------------------------------------------------------------- /_build/dockercompose/helloworld/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | helloworld: 4 | image: helloworld 5 | ports: 6 | - "::8001" 7 | environment: 8 | - "CONSUL=consul:8500" 9 | links: 10 | - consul:consul 11 | - statsd:statsd 12 | consul: 13 | image: progrium/consul 14 | ports: 15 | - "::8500" 16 | hostname: node1 17 | command: "-server -bootstrap -ui-dir /ui" 18 | statsd: 19 | image: hopsoft/graphite-statsd 20 | ports: 21 | - "::80" 22 | expose: 23 | - "8125/udp" 24 | environment: 25 | - "SERVICE_8125_NAME=statsd-8125" 26 | registrator: 27 | image: 'gliderlabs/registrator:latest' 28 | links: 29 | - consul:consul 30 | command: '-internal -tags=dev consul://consul:8500' 31 | volumes: 32 | - '/var/run/docker.sock:/tmp/docker.sock' 33 | syslog: 34 | image: 'factorish/syslog' 35 | command: '-t udp' 36 | environment: 37 | - "SERVICE_514_NAME=syslog-514" 38 | -------------------------------------------------------------------------------- /_build/dockerfile/helloworld/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nicholasjackson/microservice-basebox 2 | 3 | EXPOSE 8001 4 | 5 | # Create directory for server files 6 | RUN mkdir /helloworld 7 | 8 | # Add s6 config 9 | ADD s6-etc /etc/s6 10 | RUN chmod -R 755 /etc/s6; \ 11 | chmod -R 755 /etc/s6 12 | 13 | # Add consul template 14 | ADD config.ctmpl /helloworld/config.ctmpl 15 | 16 | # Add server files 17 | ADD swagger_spec /swagger 18 | ADD helloworld /helloworld/helloworld 19 | 20 | RUN chmod 755 /helloworld/helloworld 21 | 22 | ENTRYPOINT ["/usr/bin/s6-svscan","/etc/s6"] 23 | CMD [] 24 | -------------------------------------------------------------------------------- /_build/dockerfile/helloworld/config.ctmpl: -------------------------------------------------------------------------------- 1 | { 2 | "stats_d_server": "{{range service "statsd-8125"}}{{.Address}}{{end}}:{{range service "statsd-8125"}}{{.Port}}{{end}}", 3 | "syslog_server": "{{range $index, $element := service "syslog-514"}}{{if eq $index 0}}{{.Address}}:{{.Port}}{{end}}{{end}}" 4 | } 5 | -------------------------------------------------------------------------------- /_build/dockerfile/helloworld/s6-etc/.s6-svscan/crash: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | -------------------------------------------------------------------------------- /_build/dockerfile/helloworld/s6-etc/.s6-svscan/finish: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | -------------------------------------------------------------------------------- /_build/dockerfile/helloworld/s6-etc/app/finish: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | -------------------------------------------------------------------------------- /_build/dockerfile/helloworld/s6-etc/app/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec /helloworld/helloworld /helloworld/config.json /helloworld 4 | -------------------------------------------------------------------------------- /_build/dockerfile/helloworld/s6-etc/consul-template/finish: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | -------------------------------------------------------------------------------- /_build/dockerfile/helloworld/s6-etc/consul-template/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec /usr/bin/consul-template -consul=$CONSUL -template "/helloworld/config.ctmpl:/helloworld/config.json:killall helloworld" 4 | -------------------------------------------------------------------------------- /_build/features/echo.feature: -------------------------------------------------------------------------------- 1 | @echo 2 | Feature: Echo 3 | In order to ensure quality 4 | As a user 5 | I want to be able to test functionality of my API 6 | 7 | Scenario: Echo returns same data as posted 8 | Given I send a POST request to "/v1/echo" with the following: 9 | | echo | Hello World | 10 | Then the response status should be "200" 11 | And the JSON response should have "$..echo" with the text "Hello World" 12 | 13 | Scenario: Echo returns bad request with no post data 14 | Given I send a POST request to "/v1/echo" 15 | Then the response status should be "400" 16 | 17 | Scenario: Echo returns 404 on GET request 18 | Given I send a GET request to "/v1/echo" 19 | Then the response status should be "404" 20 | -------------------------------------------------------------------------------- /_build/features/health.feature: -------------------------------------------------------------------------------- 1 | @healthcheck 2 | Feature: Health check 3 | In order to ensure quality 4 | As a user 5 | I want to be able to test functionality of my API 6 | 7 | Scenario: Health check returns ok 8 | Given I send a GET request to "/v1/health" 9 | Then the response status should be "200" 10 | And the JSON response should have "$..status_message" with the text "OK" 11 | -------------------------------------------------------------------------------- /_build/features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'minke' 2 | require 'cucumber/rest_api' 3 | 4 | discovery = Minke::Docker::ServiceDiscovery.new 'config.yml' 5 | $SERVER_PATH = "http://#{discovery.public_address_for 'helloworld', '8001', :cucumber}" 6 | -------------------------------------------------------------------------------- /_build/swagger_spec/swagger.yml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | title: helloworld API 4 | description: helloworld API Description 5 | version: 1.0.0 6 | host: api.test.com 7 | schemes: 8 | - http 9 | basePath: /v1 10 | produces: 11 | - application/json 12 | paths: 13 | /health: 14 | get: 15 | summary: Health Check 16 | description: | 17 | The Health Check endpoint is used to determine the current status for the health of the api. 18 | This endpoint will be used by other systems such as Consul and other service discovery systems. 19 | tags: 20 | - Health 21 | responses: 22 | '200': 23 | description: Status message from server describing current health 24 | schema: 25 | type: array 26 | items: 27 | $ref: '#/definitions/HealthResponse' 28 | definitions: 29 | HealthResponse: 30 | type: object 31 | properties: 32 | status_message: 33 | type: string 34 | description: 'Plain text readable response corresponding to current health status' 35 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | pre: 3 | - curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | bash -s -- 1.10.0 4 | - sudo rm /usr/local/bin/docker-compose 5 | - sudo curl -L https://github.com/docker/compose/releases/download/1.7.1/docker-compose-`uname -s`-`uname -m` > docker-compose 6 | - sudo mv docker-compose /usr/local/bin/ 7 | - chmod +x /usr/local/bin/docker-compose 8 | ruby: 9 | version: 2.3.1 10 | services: 11 | - docker 12 | environment: 13 | GOPATH: /home/ubuntu/go 14 | SSL_KEY_PATH: /home/ubuntu/.ssh 15 | 16 | dependencies: 17 | override: 18 | - bundle config build.nokogiri --use-system-libraries 19 | - cd _build && bundle 20 | - cd _build && bundle update 21 | - mkdir -p /home/ubuntu/go/src/github.com/nicholasjackson 22 | - cp -R /home/ubuntu/helloworld /home/ubuntu/go/src/github.com/nicholasjackson/ 23 | 24 | test: 25 | override: 26 | - cd /home/ubuntu/go/src/github.com/nicholasjackson/helloworld/_build && rake app:build_image && rake app:cucumber && rake app:push 27 | -------------------------------------------------------------------------------- /global/global.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | type ConfigStruct struct { 10 | StatsDServerIP string `json:"stats_d_server"` 11 | SysLogIP string `json:"syslog_server"` 12 | RootFolder string 13 | } 14 | 15 | var Config ConfigStruct 16 | 17 | func LoadConfig(config string, rootfolder string) error { 18 | fmt.Println("Loading Config: ", config) 19 | 20 | file, err := os.Open(config) 21 | if err != nil { 22 | return fmt.Errorf("Unable to open config") 23 | } 24 | 25 | decoder := json.NewDecoder(file) 26 | Config = ConfigStruct{} 27 | err = decoder.Decode(&Config) 28 | Config.RootFolder = rootfolder 29 | 30 | fmt.Println(Config) 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /handlers/const.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | const GET = ".get" 4 | const POST = ".post" 5 | const CALLED = ".called" 6 | const SUCCESS = ".success" 7 | const BAD_REQUEST = ".bad_request" 8 | const INVALID_REQUEST = ".invalid_request" 9 | const VALID_REQUEST = ".valid_request" 10 | 11 | const HEALTH_HANDLER = "helloworld.health_handler" 12 | const ECHO_HANDLER = "helloworld.echo_handler" 13 | -------------------------------------------------------------------------------- /handlers/echo.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/gorilla/context" 10 | "github.com/nicholasjackson/helloworld/logging" 11 | ) 12 | 13 | type EchoDependenciesContainer struct { 14 | StatsD logging.StatsD `inject:"statsd"` 15 | Log *log.Logger `inject:""` 16 | } 17 | 18 | var EchoDependencies *EchoDependenciesContainer = &EchoDependenciesContainer{} 19 | 20 | const EHTAGNAME = "EchoHandler: " 21 | 22 | // use the validation middleware to automatically validate input 23 | // github.com/asaskevich/govalidator 24 | type Echo struct { 25 | Echo string `json:"echo" valid:"stringlength(1|255),required"` 26 | } 27 | 28 | func EchoHandler(rw http.ResponseWriter, r *http.Request) { 29 | EchoDependencies.StatsD.Increment(ECHO_HANDLER + POST + CALLED) 30 | EchoDependencies.Log.Printf("%v Called GET\n", EHTAGNAME) 31 | 32 | // request is set into the context from the middleware 33 | request := context.Get(r, "request").(*Echo) 34 | fmt.Println("r: ", request) 35 | 36 | encoder := json.NewEncoder(rw) 37 | encoder.Encode(request) 38 | 39 | EchoDependencies.StatsD.Increment(ECHO_HANDLER + POST + SUCCESS) 40 | } 41 | -------------------------------------------------------------------------------- /handlers/echo_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "testing" 11 | 12 | "github.com/facebookgo/inject" 13 | "github.com/gorilla/context" 14 | "github.com/nicholasjackson/helloworld/mocks" 15 | "github.com/stretchr/testify/assert" 16 | "github.com/stretchr/testify/mock" 17 | ) 18 | 19 | var echoStatsDMock *mocks.MockStatsD 20 | 21 | func echoTestSetup(t *testing.T) { 22 | // create an injection graph containing the mocked elements we wish to replace 23 | 24 | var g inject.Graph 25 | 26 | echoStatsDMock = &mocks.MockStatsD{} 27 | EchoDependencies = &EchoDependenciesContainer{} 28 | 29 | err := g.Provide( 30 | &inject.Object{Value: EchoDependencies}, 31 | &inject.Object{Value: echoStatsDMock, Name: "statsd"}, 32 | &inject.Object{Value: log.New(os.Stdout, "tester", log.Lshortfile)}, 33 | ) 34 | 35 | if err != nil { 36 | fmt.Println(err) 37 | } 38 | 39 | if err := g.Populate(); err != nil { 40 | fmt.Println(err) 41 | } 42 | 43 | echoStatsDMock.Mock.On("Increment", mock.Anything).Return() 44 | } 45 | 46 | func TestEchoHandlerSetStats(t *testing.T) { 47 | echoTestSetup(t) 48 | 49 | var responseRecorder httptest.ResponseRecorder 50 | var request http.Request 51 | 52 | echo := Echo{Echo: "Hello World"} 53 | context.Set(&request, "request", &echo) 54 | 55 | EchoHandler(&responseRecorder, &request) 56 | 57 | echoStatsDMock.Mock.AssertCalled(t, "Increment", ECHO_HANDLER+POST+CALLED) 58 | echoStatsDMock.Mock.AssertCalled(t, "Increment", ECHO_HANDLER+POST+SUCCESS) 59 | } 60 | 61 | func TestEchoHandlerCorrectlyEchosResponse(t *testing.T) { 62 | echoTestSetup(t) 63 | 64 | var responseRecorder *httptest.ResponseRecorder 65 | var request http.Request 66 | 67 | responseRecorder = httptest.NewRecorder() 68 | 69 | echo := Echo{Echo: "Hello World"} 70 | context.Set(&request, "request", &echo) 71 | 72 | EchoHandler(responseRecorder, &request) 73 | 74 | body := responseRecorder.Body.Bytes() 75 | response := Echo{} 76 | json.Unmarshal(body, &response) 77 | 78 | assert.Equal(t, 200, responseRecorder.Code) 79 | assert.Equal(t, response.Echo, "Hello World") 80 | } 81 | -------------------------------------------------------------------------------- /handlers/health.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/nicholasjackson/helloworld/logging" 9 | ) 10 | 11 | // This is not particularlly a real world example it mearly shows how a builder or a factory could be injected 12 | // into the HealthHandler 13 | type HealthResponseBuilder struct { 14 | statusMessage string 15 | } 16 | 17 | func (b *HealthResponseBuilder) SetStatusMessage(message string) *HealthResponseBuilder { 18 | b.statusMessage = message 19 | return b 20 | } 21 | 22 | func (b *HealthResponseBuilder) Build() HealthResponse { 23 | var hr HealthResponse 24 | hr.StatusMessage = b.statusMessage 25 | return hr 26 | } 27 | 28 | type HealthDependenciesContainer struct { 29 | // if not specified will create singleton 30 | SingletonBuilder *HealthResponseBuilder `inject:""` 31 | 32 | // statsD interface must use a name type as injection cannot infer ducktypes 33 | Stats logging.StatsD `inject:"statsd"` 34 | 35 | // reference to the log writer 36 | Log *log.Logger `inject:""` 37 | 38 | // if not specified in the graph will automatically create private instance 39 | PrivateBuilder *HealthResponseBuilder `inject:"private"` 40 | } 41 | 42 | type HealthResponse struct { 43 | StatusMessage string `json:"status_message"` 44 | } 45 | 46 | var HealthDependencies *HealthDependenciesContainer = &HealthDependenciesContainer{} 47 | 48 | const HHTAGNAME = "HealthHandler: " 49 | 50 | func HealthHandler(rw http.ResponseWriter, r *http.Request) { 51 | // all HealthHandlerDependencies are automatically created by injection process 52 | HealthDependencies.Stats.Increment(HEALTH_HANDLER + GET + CALLED) 53 | HealthDependencies.Log.Printf("%v Called GET\n", HHTAGNAME) 54 | 55 | response := HealthDependencies.SingletonBuilder.SetStatusMessage("OK").Build() 56 | 57 | encoder := json.NewEncoder(rw) 58 | encoder.Encode(&response) 59 | 60 | HealthDependencies.Stats.Increment(HEALTH_HANDLER + GET + SUCCESS) 61 | } 62 | -------------------------------------------------------------------------------- /handlers/health_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "testing" 10 | 11 | "github.com/facebookgo/inject" 12 | "github.com/nicholasjackson/helloworld/mocks" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/mock" 15 | ) 16 | 17 | var healthStatsDMock *mocks.MockStatsD 18 | 19 | func healthTestSetup(t *testing.T) { 20 | // create an injection graph containing the mocked elements we wish to replace 21 | 22 | var g inject.Graph 23 | 24 | healthStatsDMock = &mocks.MockStatsD{} 25 | HealthDependencies = &HealthDependenciesContainer{} 26 | 27 | err := g.Provide( 28 | &inject.Object{Value: HealthDependencies}, 29 | &inject.Object{Value: healthStatsDMock, Name: "statsd"}, 30 | &inject.Object{Value: log.New(os.Stdout, "tester", log.Lshortfile)}, 31 | ) 32 | 33 | if err != nil { 34 | fmt.Println(err) 35 | } 36 | 37 | if err := g.Populate(); err != nil { 38 | fmt.Println(err) 39 | } 40 | 41 | healthStatsDMock.Mock.On("Increment", mock.Anything).Return() 42 | } 43 | 44 | // Simple test to show how we can use the ResponseRecorder to test our HTTP handlers 45 | func TestHealthHandler(t *testing.T) { 46 | healthTestSetup(t) 47 | 48 | var responseRecorder httptest.ResponseRecorder 49 | var request http.Request 50 | 51 | HealthHandler(&responseRecorder, &request) 52 | 53 | assert.Equal(t, 200, responseRecorder.Code) 54 | } 55 | 56 | func TestHealthHandlerSetStats(t *testing.T) { 57 | healthTestSetup(t) 58 | 59 | var responseRecorder httptest.ResponseRecorder 60 | var request http.Request 61 | 62 | HealthHandler(&responseRecorder, &request) 63 | 64 | healthStatsDMock.Mock.AssertCalled(t, "Increment", HEALTH_HANDLER+GET+CALLED) 65 | healthStatsDMock.Mock.AssertCalled(t, "Increment", HEALTH_HANDLER+GET+SUCCESS) 66 | } 67 | -------------------------------------------------------------------------------- /handlers/middleware_requestvalidation.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "reflect" 9 | 10 | "github.com/asaskevich/govalidator" 11 | "github.com/gorilla/context" 12 | "github.com/nicholasjackson/helloworld/logging" 13 | ) 14 | 15 | func requestValidationHandler(mainHandlerRef string, t reflect.Type, statsD logging.StatsD, next http.Handler) http.Handler { 16 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 | 18 | request := reflect.New(t).Interface() 19 | 20 | defer r.Body.Close() 21 | data, _ := ioutil.ReadAll(r.Body) 22 | 23 | err := json.Unmarshal(data, &request) 24 | if err != nil { 25 | http.Error(w, "Invalid Request", http.StatusBadRequest) 26 | statsD.Increment(mainHandlerRef + BAD_REQUEST) 27 | return 28 | } 29 | 30 | _, err = govalidator.ValidateStruct(request) 31 | if err != nil { 32 | fmt.Println("Validation Error:", err) 33 | http.Error(w, "Invalid Request", http.StatusBadRequest) 34 | statsD.Increment(mainHandlerRef + INVALID_REQUEST) 35 | return 36 | } 37 | 38 | context.Set(r, "request", request) 39 | statsD.Increment(mainHandlerRef + VALID_REQUEST) 40 | next.ServeHTTP(w, r) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /handlers/middleware_requestvalidation_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/gorilla/context" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/mock" 14 | "github.com/nicholasjackson/helloworld/mocks" 15 | ) 16 | 17 | type mockType struct { 18 | FirstName string `json:"first_name" valid:"alphanum,stringlength(1|255),required"` 19 | } 20 | 21 | var mockHandler *mocks.MockHandler 22 | var mockRequestStatsD *mocks.MockStatsD 23 | 24 | func setupRequestValidationTests(t *testing.T) { 25 | mockHandler = &mocks.MockHandler{} 26 | mockRequestStatsD = &mocks.MockStatsD{} 27 | 28 | mockRequestStatsD.Mock.On("Increment", mock.Anything) 29 | mockHandler.Mock.On("ServeHTTP", mock.Anything, mock.Anything) 30 | } 31 | 32 | func TestCallsNextOnSuccessfulValidation(t *testing.T) { 33 | setupRequestValidationTests(t) 34 | var responseRecorder httptest.ResponseRecorder 35 | var request http.Request 36 | request.Body = ioutil.NopCloser(bytes.NewBufferString(`{"first_name": "Nic"}`)) 37 | 38 | handlerFunc := requestValidationHandler(HEALTH_HANDLER, reflect.TypeOf(mockType{}), mockRequestStatsD, mockHandler) 39 | 40 | handlerFunc.ServeHTTP(&responseRecorder, &request) 41 | 42 | mockHandler.Mock.AssertCalled(t, "ServeHTTP", mock.Anything, mock.Anything) 43 | mockRequestStatsD.Mock.AssertCalled(t, "Increment", HEALTH_HANDLER+VALID_REQUEST) 44 | } 45 | 46 | func TestSetsContextSuccessfully(t *testing.T) { 47 | setupRequestValidationTests(t) 48 | var responseRecorder httptest.ResponseRecorder 49 | var request http.Request 50 | request.Body = ioutil.NopCloser(bytes.NewBufferString(`{"first_name": "Nic"}`)) 51 | 52 | handlerFunc := requestValidationHandler(HEALTH_HANDLER, reflect.TypeOf(mockType{}), mockRequestStatsD, mockHandler) 53 | 54 | handlerFunc.ServeHTTP(&responseRecorder, &request) 55 | requestObj := context.Get(&request, "request").(*mockType) 56 | assert.Equal(t, "Nic", requestObj.FirstName) 57 | } 58 | 59 | func TestReturnsBadRequestWhenNoObject(t *testing.T) { 60 | setupRequestValidationTests(t) 61 | var responseRecorder httptest.ResponseRecorder 62 | var request http.Request 63 | request.Body = ioutil.NopCloser(bytes.NewBufferString(``)) 64 | 65 | handlerFunc := requestValidationHandler(HEALTH_HANDLER, reflect.TypeOf(mockType{}), mockRequestStatsD, mockHandler) 66 | 67 | handlerFunc.ServeHTTP(&responseRecorder, &request) 68 | 69 | assert.Equal(t, http.StatusBadRequest, responseRecorder.Code) 70 | mockRequestStatsD.Mock.AssertCalled(t, "Increment", HEALTH_HANDLER+BAD_REQUEST) 71 | } 72 | 73 | func TestReturnsBadRequestWhenRequestInvalid(t *testing.T) { 74 | setupRequestValidationTests(t) 75 | var responseRecorder httptest.ResponseRecorder 76 | var request http.Request 77 | request.Body = ioutil.NopCloser(bytes.NewBufferString(`{"first_name": ""}`)) 78 | 79 | handlerFunc := requestValidationHandler(HEALTH_HANDLER, reflect.TypeOf(mockType{}), mockRequestStatsD, mockHandler) 80 | 81 | handlerFunc.ServeHTTP(&responseRecorder, &request) 82 | 83 | assert.Equal(t, http.StatusBadRequest, responseRecorder.Code) 84 | mockRequestStatsD.Mock.AssertCalled(t, "Increment", HEALTH_HANDLER+INVALID_REQUEST) 85 | } 86 | -------------------------------------------------------------------------------- /handlers/xx_router.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | 7 | "github.com/gorilla/pat" 8 | "github.com/nicholasjackson/helloworld/logging" 9 | ) 10 | 11 | type RouterDependenciesContainer struct { 12 | StatsD logging.StatsD `inject:"statsd"` 13 | } 14 | 15 | var RouterDependencies *RouterDependenciesContainer = &RouterDependenciesContainer{} 16 | 17 | func GetRouter() *pat.Router { 18 | r := pat.New() 19 | 20 | r.Get("/v1/health", HealthHandler) 21 | 22 | r.Add("POST", "/v1/echo", requestValidationHandler( 23 | ECHO_HANDLER+POST, 24 | reflect.TypeOf(Echo{}), 25 | RouterDependencies.StatsD, 26 | http.HandlerFunc(EchoHandler), 27 | )) 28 | 29 | //Add routing for static routes 30 | s := http.StripPrefix("/swagger/", http.FileServer(http.Dir("/swagger"))) 31 | r.PathPrefix("/swagger/").Handler(s) 32 | 33 | return r 34 | } 35 | -------------------------------------------------------------------------------- /logging/StatsD.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | // StatsD interface is to allow the class .. to be injected as a DuckType 4 | type StatsD interface { 5 | Increment(string) 6 | } 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "log/syslog" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/nicholasjackson/helloworld/global" 11 | "github.com/nicholasjackson/helloworld/handlers" 12 | 13 | "github.com/alexcesaro/statsd" 14 | "github.com/facebookgo/inject" 15 | ) 16 | 17 | func main() { 18 | config := os.Args[1] 19 | rootfolder := os.Args[2] 20 | 21 | global.LoadConfig(config, rootfolder) 22 | 23 | setupInjection() 24 | setupHandlers() 25 | } 26 | 27 | func setupHandlers() { 28 | http.Handle("/", handlers.GetRouter()) 29 | 30 | fmt.Println("Listening for connections on port", 8001) 31 | http.ListenAndServe(fmt.Sprintf(":%v", 8001), nil) 32 | } 33 | 34 | func setupInjection() { 35 | var g inject.Graph 36 | 37 | var err error 38 | 39 | statsdClient, err := statsd.New(statsd.Address(global.Config.StatsDServerIP)) // reference to a statsd client 40 | if err != nil { 41 | panic(fmt.Sprintln("Unable to create StatsD Client: ", err)) 42 | } 43 | 44 | syslogWriter, err := syslog.Dial("udp", global.Config.SysLogIP, syslog.LOG_SYSLOG, "sorcery") 45 | if err != nil { 46 | panic(fmt.Sprintln("Unable to connect to syslog: ", err)) 47 | } 48 | 49 | logWriter := log.New(syslogWriter, "hellos: ", log.Lshortfile) 50 | 51 | err = g.Provide( 52 | &inject.Object{Value: handlers.RouterDependencies}, 53 | &inject.Object{Value: handlers.HealthDependencies}, 54 | &inject.Object{Value: handlers.EchoDependencies}, 55 | &inject.Object{Value: statsdClient, Name: "statsd"}, 56 | &inject.Object{Value: logWriter}, 57 | ) 58 | 59 | if err != nil { 60 | fmt.Println(err) 61 | } 62 | 63 | // Here the Populate call is creating instances of NameAPI & 64 | // PlanetAPI, and setting the HTTPTransport on both to the 65 | // http.DefaultTransport provided above: 66 | if err := g.Populate(); err != nil { 67 | fmt.Println(err) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /mocks/mocks.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/stretchr/testify/mock" 7 | ) 8 | 9 | type MockStatsD struct { 10 | mock.Mock 11 | } 12 | 13 | func (m *MockStatsD) Increment(label string) { 14 | _ = m.Mock.Called(label) 15 | } 16 | 17 | type MockHandler struct { 18 | mock.Mock 19 | } 20 | 21 | func (m *MockHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 22 | _ = m.Mock.Called(w, r) 23 | } 24 | --------------------------------------------------------------------------------