├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── bootstrap_example.sh ├── chi └── handlers.go ├── cmd └── gloss │ └── main.go ├── domain.go ├── pgsql ├── db.go └── db_test.go ├── test.env └── test_stack.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Jetbrains IDE files 15 | *.idea 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | 3 | # download git so 'go get' works, then copy src to builder 4 | RUN apk update && apk add --no-cache git 5 | WORKDIR $GOPATH/src/github.com/diffuse/gloss 6 | COPY . . 7 | 8 | # get dependencies and build 9 | RUN go get -d ./... && CGO_ENABLED=0 GOOS=linux go install ./... 10 | 11 | # copy binary from builder 12 | FROM alpine 13 | COPY --from=builder /go/bin/gloss . 14 | 15 | ENTRYPOINT ["/gloss"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 diffuse 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 | # gloss 2 | gloss (Golang open simple service) provides boilerplate routing, database setup, and Docker files to get a minimal 3 | microservice up and running. It includes example code to increment and retrieve counter values from a PostgreSQL 4 | database. Ideally, one would fork, or clone + mirror push this repository, then edit the handlers + routes, database 5 | queries, and configurations for their own purposes. It uses [chi](https://github.com/go-chi/chi) for routing, and 6 | [pgx](https://github.com/jackc/pgx) for its PostgreSQL database driver, but other databases or routers can be used 7 | by implementing the `Database` interface from `domain.go`, and/or the `Handler` interface from `net/http`. 8 | 9 | ### Prerequisites 10 | - [Docker](https://www.docker.com/) 11 | - [Golang](https://golang.org/) (for a non-Docker build) 12 | 13 | ### Running 14 | #### Using Docker 15 | If you have Docker installed, getting the example stack up and running is as simple as: 16 | 17 | `./bootstrap_example.sh` 18 | 19 | This will build the image locally with a `gloss:latest` tag, then deploy a service stack with a PostgreSQL instance 20 | exposed on port `5432`, and the example gloss service exposed on port `8080` on the host machine. 21 | 22 | #### Local build 23 | If you don't want to use Docker stack, you can build and run the example service locally. 24 | 25 | You must first have a PostgreSQL service exposed on your host machine at port `5432`, with the configuration in 26 | `test.env`, for example (using Docker cli for brevity): 27 | ```bash 28 | docker run -d -p 5432:5432 \ 29 | -e POSTGRES_PASSWORD="password" \ 30 | -e POSTGRES_USER="test" \ 31 | -e POSTGRES_DB="test" \ 32 | postgres 33 | ``` 34 | 35 | then you can build 36 | and run the service locally: 37 | ```bash 38 | # get the dependencies 39 | go get -d ./... 40 | 41 | # install to ~/go/bin 42 | go install ./... 43 | 44 | # export the test environment variables 45 | while read l; do export $l; done < test.env 46 | 47 | # run the service (this binds to port 8080 and waits for requests) 48 | ~/go/bin/gloss 49 | ``` 50 | 51 | ### Interacting with the example counter service 52 | If you have deployed the example successfully, you can interact with the service at `http://localhost:8080/v1` 53 | 54 | Example interaction: 55 | ```bash 56 | # @URL: http://localhost:8080/v1/counter/{counterId} 57 | # - POST: to increment/create a counter in the database 58 | # - GET: to get the current counter value, if it exists 59 | 60 | # create a counter with ID 4 61 | curl -v http://localhost:8080/v1/counter/4 -X POST 62 | 63 | # a counter has now been created with ID 4 and value 0 64 | 65 | # increment it a few times 66 | for i in $(seq 1 10); do curl -v http://localhost:8080/v1/counter/4 -X POST; done 67 | 68 | # get the counter value 69 | curl -v http://localhost:8080/v1/counter/4 70 | # returns 10 71 | 72 | # connect to the database and run some other queries 73 | psql -h localhost -p 5432 -U test -d test 74 | ``` 75 | 76 | ### Adapting to your own implementation 77 | #### Database 78 | - Add your business logic methods to the `Database` interface (found in `domain.go`), then implement them in a 79 | custom package 80 | - See the `IncrementCounter` and `GetCounterVal` methods for an example of this, implemented in the `pgsql` 81 | package 82 | - You can also just edit the `pgsql` package and change the domain interface methods if you want to continue using 83 | PostgreSQL 84 | 85 | #### Routing 86 | - Implement the `net/http` `Handler` interface, or edit the existing implementation in the `chi` package 87 | - Replace/change the example handler bodies in `handlers.go` to perform your business logic 88 | - Update the routes in `handlers.go`:`setupRoutes` to use your handlers 89 | 90 | #### Putting it all together 91 | - Pass an instance of your `Database` implementation to your custom `Handler`, and use the instance to perform 92 | business logic in handler functions 93 | - An example of this is shown in `cmd/gloss/main.go`, where `pgsql`'s `Database` implementation is used with `chi`'s 94 | `Handler` implementation 95 | - A `pgsql.Database` instance is created, then passed as a parameter to `chi.NewRouter`, which creates a router, 96 | associates handler functions in the `chi` package with routes, then stores the `pgsql.Database` instance so it can 97 | be used in the custom handlers 98 | 99 | ### Disclaimer and considerations for deployment 100 | The deployment scripts, configurations, and any defaults included in this repository are not, under any circumstances, 101 | to be used in production. They are here to provide a basic example deployment so you can quickly get up and running in 102 | a development environment. For a legitimate deployment, you must first edit them, or create your own configurations 103 | with secure settings. 104 | 105 | As stated above, the responsibility to properly secure your code and deployment is entirely yours, but here are some 106 | things you may want to consider: 107 | - Ensure timeouts and size defaults are configured properly for the `http.Server` in `cmd/gloss/main.go` 108 | - Use SSL for communication with the database 109 | - Choose routing middleware to fit your needs 110 | - Properly manage credentials used/shared by the microservice and database 111 | 112 | ### License 113 | This project is licensed under the terms of the [MIT license](LICENSE). 114 | -------------------------------------------------------------------------------- /bootstrap_example.sh: -------------------------------------------------------------------------------- 1 | docker build -t gloss:latest . 2 | docker stack deploy -c test_stack.yml gloss_example 3 | docker service logs --follow gloss_example_web -------------------------------------------------------------------------------- /chi/handlers.go: -------------------------------------------------------------------------------- 1 | package chi 2 | 3 | import ( 4 | "github.com/diffuse/gloss" 5 | "github.com/go-chi/chi" 6 | "github.com/go-chi/chi/middleware" 7 | "net/http" 8 | "strconv" 9 | ) 10 | 11 | type Router struct { 12 | *chi.Mux 13 | db gloss.Database 14 | } 15 | 16 | // NewRouter creates a router, associates a database 17 | // with it, and mounts API routes 18 | func NewRouter(db gloss.Database) *Router { 19 | r := &Router{db: db} 20 | r.setupRoutes() 21 | 22 | return r 23 | } 24 | 25 | // setupRoutes 26 | func (rt *Router) setupRoutes() { 27 | // set routes 28 | routes := chi.NewRouter() 29 | routes.Post("/counter/{counterId}", rt.IncrementCounterById) 30 | routes.Get("/counter/{counterId}", rt.GetCounterById) 31 | 32 | // setup router 33 | rt.Mux = chi.NewRouter() 34 | 35 | // set middleware 36 | rt.Mux.Use( 37 | middleware.Logger, 38 | middleware.Recoverer) 39 | 40 | // mount routes on versioned path 41 | rt.Mux.Mount("/v1", routes) 42 | } 43 | 44 | // Increment the value of the counter with ID counterId 45 | func (rt *Router) IncrementCounterById(w http.ResponseWriter, r *http.Request) { 46 | // get the counter ID 47 | counterId, err := strconv.ParseUint(chi.URLParam(r, "counterId"), 10, 32) 48 | if err != nil { 49 | http.Error(w, "invalid counter ID", http.StatusBadRequest) 50 | return 51 | } 52 | 53 | // increment in db 54 | if err := rt.db.IncrementCounter(int(counterId)); err != nil { 55 | http.Error(w, "failed to increment counter value", http.StatusInternalServerError) 56 | } 57 | } 58 | 59 | // Get the value of the counter with ID counterId 60 | func (rt *Router) GetCounterById(w http.ResponseWriter, r *http.Request) { 61 | // get the counter ID 62 | counterId, err := strconv.ParseInt(chi.URLParam(r, "counterId"), 10, 32) 63 | if err != nil { 64 | http.Error(w, "invalid counter ID", http.StatusBadRequest) 65 | return 66 | } 67 | 68 | // get value and return to requester 69 | val, err := rt.db.GetCounterVal(int(counterId)) 70 | if err != nil { 71 | http.Error(w, "failed to get counter value", http.StatusNotFound) 72 | return 73 | } 74 | 75 | if _, err := w.Write([]byte(strconv.Itoa(val))); err != nil { 76 | panic(err) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /cmd/gloss/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/diffuse/gloss/chi" 5 | "github.com/diffuse/gloss/pgsql" 6 | "log" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | func main() { 12 | // create a thread-safe database instance for use with the router 13 | log.Println("connecting to database") 14 | db := pgsql.NewDatabase() 15 | defer db.Close() 16 | 17 | // create a router and associate the database with it 18 | log.Println("setting up routes") 19 | r := chi.NewRouter(db) 20 | 21 | // create server with some reasonable defaults 22 | s := &http.Server{ 23 | Addr: ":8080", 24 | Handler: r, 25 | ReadTimeout: 2 * time.Second, 26 | ReadHeaderTimeout: 2 * time.Second, 27 | WriteTimeout: 5 * time.Second, 28 | MaxHeaderBytes: 64e3, 29 | } 30 | 31 | // serve 32 | log.Println("serving") 33 | log.Fatal(s.ListenAndServe()) 34 | } 35 | -------------------------------------------------------------------------------- /domain.go: -------------------------------------------------------------------------------- 1 | package gloss 2 | 3 | // Database represents a wrapper around a database connection + driver 4 | // 5 | // additional methods can be added here, then implemented in custom packages 6 | type Database interface { 7 | // Init connects to the database, creates tables, etc... 8 | Init() 9 | 10 | // Close shuts down connections to the database 11 | Close() error 12 | 13 | // these are examples of business logic for the 14 | // counter service, replace them with your own 15 | IncrementCounter(counterId int) error 16 | GetCounterVal(counterId int) (int, error) 17 | } 18 | -------------------------------------------------------------------------------- /pgsql/db.go: -------------------------------------------------------------------------------- 1 | package pgsql 2 | 3 | import ( 4 | "context" 5 | "github.com/jackc/pgx" 6 | ) 7 | 8 | type Database struct { 9 | db *pgx.Conn 10 | ctx context.Context 11 | } 12 | 13 | // NewDatabase creates and initializes a new Database 14 | func NewDatabase() *Database { 15 | db := &Database{ctx: context.Background()} 16 | db.Init() 17 | 18 | return db 19 | } 20 | 21 | // Init connects to a PostgreSQL instance and creates the 22 | // tables this service relies on if they don't already exist 23 | func (d *Database) Init() { 24 | // read the PostgreSQL connection info from the environment 25 | config, err := pgx.ParseConfig("") 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | // connect to database using configuration created from environment variables 31 | if d.db, err = pgx.ConnectConfig(d.ctx, config); err != nil { 32 | panic(err) 33 | } 34 | 35 | // create tables 36 | d.createTables() 37 | } 38 | 39 | // Close closes connections to the database 40 | func (d *Database) Close() error { 41 | return d.db.Close(d.ctx) 42 | } 43 | 44 | // createTables creates the tables that this service relies on 45 | func (d *Database) createTables() { 46 | query := ` 47 | CREATE TABLE IF NOT EXISTS counter 48 | ( 49 | counter_id INTEGER PRIMARY KEY, 50 | val INTEGER NOT NULL 51 | )` 52 | 53 | if _, err := d.db.Exec(d.ctx, query); err != nil { 54 | panic(err) 55 | } 56 | } 57 | 58 | // IncrementCounter increments the value in the counter table 59 | func (d *Database) IncrementCounter(counterId int) error { 60 | query := ` 61 | INSERT INTO counter(counter_id, val) 62 | VALUES($1, 0) 63 | ON CONFLICT(counter_id) 64 | DO UPDATE 65 | SET val = counter.val + 1` 66 | 67 | _, err := d.db.Exec(d.ctx, query, counterId) 68 | return err 69 | } 70 | 71 | // GetCounterVal gets the value of a counter with ID counterId 72 | func (d *Database) GetCounterVal(counterId int) (int, error) { 73 | query := `SELECT val FROM counter WHERE counter_id = $1` 74 | 75 | var val int 76 | return val, d.db.QueryRow(d.ctx, query, counterId).Scan(&val) 77 | } 78 | -------------------------------------------------------------------------------- /pgsql/db_test.go: -------------------------------------------------------------------------------- 1 | package pgsql 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | var ( 10 | testDb *Database 11 | 12 | // testing context 13 | ctx = context.Background() 14 | ) 15 | 16 | // NOTE: A PostgreSQL instance must be running on db:5432 with 17 | // the below environment configuration for these tests to work 18 | func init() { 19 | // set environment vars 20 | os.Setenv("PGHOST", "db") 21 | os.Setenv("PGUSER", "test") 22 | os.Setenv("PGPASSWORD", "password") 23 | os.Setenv("PGDATABASE", "test") 24 | os.Setenv("PGPORT", "5432") 25 | os.Setenv("PGSSLMODE", "disable") 26 | 27 | // create and init db 28 | testDb = NewDatabase() 29 | 30 | // drop and recreate tables so tests have a known starting point 31 | if _, err := testDb.db.Exec(ctx, "DROP TABLE IF EXISTS counter"); err != nil { 32 | panic(err) 33 | } 34 | testDb.createTables() 35 | } 36 | 37 | // setupDbTest deletes all rows in the test tables, so each test 38 | // has a known starting point 39 | func setupDbTest() { 40 | // delete all rows 41 | if _, err := testDb.db.Exec(ctx, "DELETE FROM counter"); err != nil { 42 | panic(err) 43 | } 44 | } 45 | 46 | // getCounterVal gets a value from the counter, or fails the test 47 | func getCounterVal(counterId int, t *testing.T) int { 48 | query := `SELECT val FROM counter WHERE counter_id = $1` 49 | 50 | var val int 51 | if err := testDb.db.QueryRow(ctx, query, counterId).Scan(&val); err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | return val 56 | } 57 | 58 | func TestIncrementCounter(t *testing.T) { 59 | setupDbTest() 60 | counterId := 0 61 | 62 | // expect first insert to zero init counter 63 | if err := testDb.IncrementCounter(0); err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | got := getCounterVal(counterId, t) 68 | if got != 0 { 69 | t.Fatalf("expected counter val: %v, got: %v", 0, got) 70 | } 71 | 72 | // expect an increment 73 | if err := testDb.IncrementCounter(0); err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | got = getCounterVal(counterId, t) 78 | if got != 1 { 79 | t.Fatalf("expected counter val: %v, got: %v", 1, got) 80 | } 81 | } 82 | 83 | func TestGetCounterVal(t *testing.T) { 84 | setupDbTest() 85 | counterId := 8 86 | counterVal := 1234 87 | 88 | // insert a value at counterId 8 89 | query := `INSERT INTO counter(counter_id, val) VALUES($1, $2)` 90 | if _, err := testDb.db.Exec(ctx, query, counterId, counterVal); err != nil { 91 | t.Fatal(err) 92 | } 93 | 94 | // get the value 95 | val, err := testDb.GetCounterVal(counterId) 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | 100 | if val != counterVal { 101 | t.Errorf("expected counter val: %v, got: %v", counterVal, val) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test.env: -------------------------------------------------------------------------------- 1 | PGHOST=db 2 | PGUSER=test 3 | PGPASSWORD=password 4 | PGDATABASE=test 5 | PGPORT=5432 6 | PGSSLMODE=disable 7 | -------------------------------------------------------------------------------- /test_stack.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | db: 5 | image: postgres:latest 6 | environment: 7 | POSTGRES_USER: test 8 | POSTGRES_PASSWORD: password 9 | POSTGRES_DB: test 10 | ports: 11 | - "5432:5432" 12 | web: 13 | image: gloss:latest 14 | env_file: 15 | - test.env 16 | ports: 17 | - "8080:8080" --------------------------------------------------------------------------------