-
Notifications
You must be signed in to change notification settings - Fork 1
Home
This is a (guided) walkthrough of a typical dev workflow with allaboutapps/go-starter.
We are going to reimplement the Beer PUNK API v2 🍺🍻 (and protect its routes with our default OAuth2 flow).
The source-code of this project is available at majodev/go-beer-punk-proxy. However, we expect you to start from scratch with a brand new cloned/forked go-starter project and write (or copy) the referenced files in the upcoming sections yourself.
If you have questions about the general setup, ensure you visit our allaboutapps/go-starter FAQ. Otherwise, feel free to raise an issue at majodev/go-beer-punk-proxy/issues.
It's recommended to clone this wiki (yes, separately, it does not come with the main repo!) by issuing git clone https://github.com/majodev/go-beer-punk-proxy.wiki.git
. Through that, you also get a copy of the _walkthrough/**
directory which holds all files used in the next sections.
Additionally the completed service is available at beer-workshop-dev.allaboutapps.at for you to play around with.
Running on Windows? WSL2 setup done? No, then head to FAQ: WSL2 (Windows only).
🍻🍻🍻 -> Create a new GitHub repo from the go-starter template project <- 🍻🍻🍻.
# Windows WSL 2 specific:
# ensure you are at ~ (not in /mnt/c) in your WSL2 environment
# ensure git is installed and your config is also set in the WSL2 environment
# apt-get install git
# ---
# Also see https://github.com/allaboutapps/go-starter#quickstart
git clone <your-project-repo> go-beer-punk-proxy
cd go-beer-punk-proxy
# (optional, if you want to easily merge changes later from the upstream go-starter project)
make git-merge-go-starter
./docker-helper.sh --up
# You are now attached to the container in an interactive shell (exit via CMD+D).
# It will be easier if we all use the **same** modulename, but you may customize it of course (you will need to slightly adapt some go import paths when copying over our files)
make set-module-name
# github.com/majodev/go-beer-punk-proxy
# Fully initialize the project, build it, test it.
make all
code . # Start VSCode
You may now attach to the devContainer through VSCode. See this guide: FAQ: How does our VSCode setup work?
Showcase:
-
./docker-helper.sh --up
,make all
,make
,make test
,make help
- Inspect
Dockerfile
anddocker-compose.yml
- Check VSCode test and debug support
- FAQ: Why do we need to use Docker containers / VSCode devContainers for local development?
- FAQ: I don't understand the projects' directory layout!
- FAQ: Your Makefile is massive, I don't understand.
-
Completion criteria:
-
make all
works in your Docker container (which you are attached to). - VSCode is attached to your Docker container.
- Code Completion (go tools are installed) and executing a test through VSCode works
-
See our defined dev workflow at FAQ: How does our dev workflow work? - 1. API specification (Swagger-first) and the whole FAQ: Swagger section.
First, we are going to spec this new api through OpenAPI v2 / Swagger files.
Showcase:
- Type (or copy/paste if you are lazy) from the following urls (or from this wiki's
/_walkthrough/01_api_spec/**
) to the respective spot in your project:-
/docs/beers.json
: a dump of what punkapi_v2 sends -
/api/definitions/beers.yml
: OpenAPI v2 / swagger data spec, mostly generated frombeers.json
via quicktype.io -
/api/paths/beers.yml
: OpenAPI v2 / Swagger path spec
-
-
make swagger
,make watch-swagger
(interactive changes),make
(will yield that these routes are not implemented yets) - Try to add errors into your definitions or path files and see how
make swagger
behaves - Inspect the auto-generated
/internal/types
-
Completion criteria: Head to your local swagger UI at localhost:8081 and check if all 3 routes are available there:
- GET /api/v1/beers
- GET /api/v1/beers/{id}
- GET /api/v1/beers/random
See our defined dev workflow at FAQ: How does our dev workflow work? - 2. ERM specification and migrations (SQL-first)
We will need a place for the beers in our database, thus we are going to add a beers
table through a migration.
sql-migrate new create-beers
# Created migration migrations/20201105145136-create-beers.sql
Showcase:
- Type (or copy/paste if you are lazy) from the following urls (or from this wiki's
/_walkthrough/02_erm_spec/**
) to the respective spot in your project:-
/migrations/20201105145136-create-beers.sql
: a new database migration, adds thebeers
table
-
-
make sql
,make watch-sql
(interactive changes) - Inspect the auto-generated
/internal/models
- Toy around with VSCode pgFormatter (try to format your
migration.sql
file badly) and try to make errors to see howmake sql
behaves (checkout our sql related make targets (make help | grep sql
)) - FAQ: What's in your /migrations?
-
Completion criteria:
- Ensure
make
andmake test
execute successfully. - You may try
psql spec
andSELECT * FROM beers;
to see if the model got actually applied (there will be 0 rows).
- Ensure
See our defined dev workflow at FAQ: How does our dev workflow work? - 3. Types and models
We will now (in a TDD driven manner) try to validate that the generated SQLBoiler models and go-swagger types work as expected.
Showcase:
- Type (or copy/paste if you are lazy) from the following urls (or from this wiki's
/_walkthrough/03_types_and_models/**
) to the respective spot in your project:-
/internal/data/beers.go
: quicktype.io generated (un-)marshaling of/docs/beers.json
. we use that to ... -
/internal/data/upsertable_beers.go
: ... try to load our beers into our database/models
... -
/internal/data/marshal_beers.go
: ... and we will try to marshal database stored beers into the generated/types
. -
/internal/data/upsertable_beers_test.go
automated testing for above logic -
/internal/data/marshal_beers_test.go
automated testing for above logic
-
-
make
,make test
- Inspect code and ensure all tests execute as expected.
- VSCode test debugging
- FAQ: Why should I use this?
-
Completion criteria:
- Ensure
make
andmake test
work (with the new test files) - Execute a single one of the above tests via VSCode (go test runs automatically in
-v
verbose mode there) and copy the integresql database name that got used.- e.g. the test logs:
upsertable_beers_test.go:18: WithTestDatabase: "integresql_test_7f74ebb2bc5c148cc943fb12e6fc4685_643"
psql integresql_test_7f74ebb2bc5c148cc943fb12e6fc4685_643
-
SELECT id, name FROM beers ORDER BY id;
should show you all the beers your upserted in your test.
- e.g. the test logs:
- Ensure
See our defined dev workflow at FAQ: How does our dev workflow work? - 4. Implement handlers / TDD
We will implement and test the actual beer handlers we have specced and use the models/types we have generated and already tested.
For this we will:
- add a new route group to our server and apply the (O)Auth middleware for it
- make beers available in all integration tests that utilize test DBs by configuring our IntegreSQL template database creation mechanism.
- implement the handlers
- test the handlers
You need to make the following changes to some *.go
files:
// File: internal/api/server.go
// https://github.com/majodev/go-beer-punk-proxy/blob/dev/internal/api/server.go
type Router struct {
// [...]
APIV1Beers *echo.Group
// [...]
}
// [...]
// File: internal/api/router/router.go
// https://github.com/majodev/go-beer-punk-proxy/blob/dev/internal/api/router/router.go
// [...]
// Your other endpoints, typically secured by bearer auth, available at /api/v1/**
APIV1Beers: s.Echo.Group("/api/v1/beers", middleware.Auth(s)),
// [...]
// File: internal/test/test_database.go
// https://github.com/majodev/go-beer-punk-proxy/blob/dev/internal/test/test_database.go
// [...]
// we will compute a db template hash over the following dirs/files
// [...]
beersJSONFile = filepath.Join(pUtil.GetProjectRootDir(), "/docs/beers.json")
// [...]
func initTestDatabaseHash(t *testing.T) {
// [...]
h, err := util.GetTemplateHash(migDir, fixFile, selfFile, beersJSONFile)
// [...]
}
// [...]
func insertFixtures(ctx context.Context, t *testing.T, db *sql.DB) error {
// [...]
beers, err := data.GetUpsertableBeerModels()
if err != nil {
return err
}
for _, beer := range beers {
if err := beer.Insert(ctx, tx, boil.Infer()); err != nil {
if err := tx.Rollback(); err != nil {
return err
}
return err
}
}
// Typically we do not use serial ids (auto-incrementing), resetting the sequence after bulk-importing is neccessary
// https://stackoverflow.com/questions/4448340/postgresql-duplicate-key-violates-unique-constraint/21639138
_, err = tx.ExecContext(ctx, `SELECT setval('beers_id_seq', (SELECT MAX(id) FROM beers)+1);`)
if err != nil {
return err
}
// [...]
}
// [...]
Showcase:
- New OAuth2 protected route group and autoload beers to test-dbs (see above)
- Type (or copy/paste if you are lazy) from the following urls (or from this wiki's
/_walkthrough/04_implement_handlers_tdd/**
) to the respective spot in your project:-
/internal/api/handlers/beers/get_beers.go
GET /api/v1/beers implementation -
/internal/api/handlers/beers/get_beer.go
GET /api/v1/beers/{id} implementation -
/internal/api/handlers/beers/get_random_beer.go
GET /api/v1/beers/random implementation -
/internal/api/handlers/beers/get_beers_test.go
GET /api/v1/beers integration testing -
/internal/api/handlers/beers/get_beer_test.go
GET /api/v1/beers/{id} integration testing -
/internal/api/handlers/beers/get_random_beer_test.go
GET /api/v1/beers/random integration testing
-
-
make
,make go-generate
,make test
- FAQ: Most tests use
WithTestServer
, whats that? - FAQ: What are
.golden
files? What is snapshot testing? - FAQ: How does IntegreSQL assign test-databases? How does it know about changes to my migrations / fixtures?
-
Completion criteria:
- Ensure
make
andmake test
work (with the new test files) - Debug the new tests to see how our integration testing (blackbox testing) of our endpoints actually works.
- Ensure not implemented errors (previously via
make info
) are now away - Investigate the created snapshots (
.golden
files) -
Toogle code coverage
in VSCode via the command line palette withininternal/api/handlers/beers
package to see if we still need to increase the test coverage there.
- Ensure
We now have tested our new endpoints, let's run our server!
The only thing that's still missing currently is a way to seed (upsert) the beers into our actual development
database from the compiled app
binary. This binary is going to be the entrypoint of the final Dockerfile
(stage app
)).
You need to do the following change to a single *.go
file:
// File: cmd/db_seed.go
// https://github.com/majodev/go-beer-punk-proxy/blob/dev/cmd/db_seed.go
func applyFixtures() error {
// [...]
beers, err := data.GetUpsertableBeerModels()
if err != nil {
return err
}
for _, beer := range beers {
if err := beer.Upsert(ctx, tx, true, nil, boil.Infer(), boil.Infer()); err != nil {
if err := tx.Rollback(); err != nil {
return err
}
return err
}
}
// Typically we do not use serial ids (auto-incrementing), resetting the sequence after bulk-importing is neccessary
// https://stackoverflow.com/questions/4448340/postgresql-duplicate-key-violates-unique-constraint/21639138
_, err = tx.ExecContext(ctx, `SELECT setval('beers_id_seq', (SELECT MAX(id) FROM beers)+1);`)
if err != nil {
return err
}
// [...]
}
Do the following to start the server via the compiled app
binary and automatically migrate up the development
database and seed all fixtures.
make # compile up
app --help
app server --migrate --seed # or "app db migrate && app db seed && app server"
Showcase:
- Migrate and seed beers into our
development
database through ourapp
binary (see above) - FAQ: How can I authenticate my user though Swagger-UI?
- FAQ: Why are you using separate spec, integresql_* and development PostgreSQL databases while developing locally?
- FAQ: Should I use sql-migrate up or app db migrate or app server --migrate?
- FAQ: I need to deploy this on top of docker-compose!
-
Completion criteria:
- Ensure
app server --migrate --seed
works. Try outapp --help
for other options. - You will need to authenticate a user through the Swagger UI, use the following endpoints:
-
POST /api/v1/auth/register
, Payload:{"password": "pass", "username": "user@example.com"}
-
POST /api/v1/auth/login
, Payload:{"password": "pass", "username": "user@example.com"}
- Then copy/paste the
Bearer
token into your Swagger UI Authorize Dialog (see FAQ) - Note: the default go-starter project does not enforce any password requirements yet, that's up to you.
-
- Try to successfully get beers 🍻, a single beer 🍺, a random beer 🍺 through the local swagger UI at localhost:8081 (requires your app server to be running).
- Locally debug a call triggered from Swagger-UI by running the service via VSCode
- Try out
POST /api/v1/auth/forgot-password
via your locally hosted mailhog on http://localhost:8025/ by locally starting theapp
server viaSERVER_MAILER_TRANSPORTER=SMTP app server
(no emails are actually sent). - Take a look into the
development
database by issuingpsql
andSELECT id, name FROM beers ORDER BY id;
. - Try to get the whole service running in a production like docker-compose environment locally. See FAQ: I need to deploy this on top of docker-compose!.
- Ensure
Let's allow to POST
to /api/v1/admin/beers
to add new beers into our database. We only want to allow that for an administration user (management key auth) and will thus bypass our typical OAuth flow...
// File: internal/api/server.go
type Router struct {
// [...]
APIV1Admin *echo.Group
// [...]
}
// File: internal/api/router/router.go
APIV1Admin: s.Echo.Group("/api/v1/admin", echoMiddleware.KeyAuthWithConfig(echoMiddleware.KeyAuthConfig{
KeyLookup: "query:mgmt-secret",
Validator: func(key string, c echo.Context) (bool, error) {
return key == s.Config.Management.Secret, nil
},
})),
// File: internal/test/helper_request.go
// Optional for testing basePayload overwrites only
// Deep copies GenericPayload (map[string]interface{})
// Supports nested GenericPayload and []GenericPayload
// Based on ideas from https://stackoverflow.com/questions/23057785/how-to-copy-a-map
func (g GenericPayload) Copy() GenericPayload {
cp := make(GenericPayload)
for k, v := range g {
if vm, ok := v.(GenericPayload); ok {
cp[k] = vm.Copy()
} else if vm, ok := v.([]GenericPayload); ok {
slice := make([]GenericPayload, 0, len(vm))
for _, vv := range vm {
slice = append(slice, vv.Copy())
}
cp[k] = slice
} else {
// Note that we expect this to be a copyable primitive type
cp[k] = v
}
}
return cp
}
Showcase:
- Implement the
POST /api/v1/admin/beers
endpoint, so we can actually add something to the database from a REST endpoint. - Type (or copy/paste if you are lazy) from the following urls (or from this wiki's
/_walkthrough/06_posting_beers_as_admin/**
) to the respective spot in your project. These are the most important ones:-
/api/definitions/admin.yml
Data spec for posting beers -
/api/paths/admin.yml
Path spec for posting beers -
/internal/api/handlers/admin/post_beer.go
Post beers implementation -
/internal/api/handlers/admin/post_beer_test.go
Post beers testing
-
- Try to play with testing this endpoint (basePayload / copy payload), inspect the database isolation mechanics while running these tests.
- Try to use the SwaggerUI to post new beers
- Reinspect the created snapshots
-
Completion criteria:
- Ensure
make
andmake test
work (with the new test files) - Investigate the created snapshots (
.golden
files) -
Toogle code coverage
in VSCode via the command line palette withininternal/api/handlers/admin
package to see if we still need to increase the test coverage there. - Also inspect and try out other endpoints that use the management authentication:
-
/internal/api/handlers/common/get_healthy.go
GET /-/healthy -
/internal/api/handlers/common/get_ready.go
GET /-/ready
-
- Ensure
Here is a very naive endpoint that sums up a received count e.g.:
1: 1=1
2: 1+2=3
3: 1+2+3=6
// /internal/api/handlers/common/get_sum.go
package common
import (
"fmt"
"net/http"
"github.com/labstack/echo/v4"
"github.com/majodev/go-beer-punk-proxy/internal/api"
)
func GetSumRoute(s *api.Server) *echo.Route {
return s.Router.Management.GET("/sum/:count", getSumHandler(s))
}
func getSumHandler(s *api.Server) echo.HandlerFunc {
return func(c echo.Context) error {
var cnt int64 = 1
if err := echo.PathParamsBinder(c).Int64("count", &cnt).BindError(); err != nil {
return c.String(400, "Please provide a integer.")
}
if cnt == 0 {
return c.String(400, "Count cannot be 0")
}
var sum int64 = 1
fmt.Printf("Received: %v: %v", cnt, &cnt)
var i int64 = 1
for ; i != cnt; i++ {
sum += (i + 1)
fmt.Println(sum)
}
return c.String(http.StatusOK, fmt.Sprintf("%v", sum))
}
}
Showcase:
- Try to add testcases (0 and for positive numbers) for this piece of code without fixing anything yet
- Inspect how this piece of code does perform while running with
app server
(e.g.http://localhost:8080/-/sum/21?mgmt-secret=mgmtpass
).- What happens to others users, if we have a malicious user who is requesting the sum of
-1
?
- What happens to others users, if we have a malicious user who is requesting the sum of
- Whats preemptive scheduling (see Cooperative vs. Preemptive: a quest to maximize concurrency power and Go: Asynchronous Preemption.
- In which ways does that differ to async programming in other languages (which mostly use cooperative scheduling of their async work)?
-
Completion criteria:
- After investigating how this works, try to fix it (int64 boundaries?).
- Also visit FAQ: I need to (remotely) pprof my running service!
This is typically a guided tour into our CI and Kubernetes architecture at all about apps, so I'm afraid, we can only provide some docs here.
See:
- FAQ: How can I docker build and docker run the final image?
- FAQ: I need to deploy this on top of Kubernetes!
- FAQ: What does your deployment flow look like?
- FAQ: What about those Dockerfile development -> builder -> app build stages?
Showcase (mostly all about apps internal):
- CI: Drone vs. GitHub Actions
- health endpoint (probes) vs. uptimerobot vs. k8s liveness-, readiness-, startup-checks (
/-/healthy
,/-/ready
) - netdata
- pghero
- GoAccess
- trivi
- gosec (run by our linters suite https://github.com/golangci/golangci-lint)
- dependabot
- FAQ: I need to (remotely) pprof my running service!
- distroless (secure minimal Docker base images)
- Intro Kubernetes at all about apps
- Logs (
stern
on k8s) - general maintenace perspective via https://codeberg.org/hjacobs/kube-ops-view
- several fancy graphical dashboards are available (e.g. octant, k9s), however mostly
kubectl
based flows directly in the terminal are preferred by our engineers. - Infrastructure as code walkthrough
- Logs (