diff --git a/.env b/.env index fcadf39..a75865e 100644 --- a/.env +++ b/.env @@ -12,7 +12,7 @@ CORS_MAX_AGE=300 DB_HOST=db DB_PORT=5432 -DB_USER=myapp_user -DB_PASS=myapp_pass -DB_NAME=myapp_db +DB_USER=book-service-user +DB_PASS=book-service-pass +DB_NAME=book-service-db DB_DEBUG=true \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index fc8267c..a06e9eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN go mod download COPY . . RUN go build -ldflags '-w -s' -a -o ./bin/app ./cmd/app \ - && go build -ldflags '-w -s' -a -o ./bin/migrate ./cmd/migrate + && go build -tags=embed -ldflags '-w -s' -a -o ./bin/migration ./cmd/migration CMD ["/myapp/bin/app"] EXPOSE 8080 \ No newline at end of file diff --git a/Justfile b/Justfile index e5fc711..7274f99 100644 --- a/Justfile +++ b/Justfile @@ -15,10 +15,10 @@ app: go run ./cmd/app # Run DB migration CLI (defaults to up) -migrate cmd="up": +migration *cmd="up": @export $(grep -v '^#' .env | xargs) && \ DB_HOST={{ db_host }} \ - go run ./cmd/migrate {{ cmd }} + go run ./cmd/migration -dir={{ justfile_directory() }}/cmd/migration/migrations {{ cmd }} # Run docker compose build build: diff --git a/README.md b/README.md index 4218e37..3ea6616 100644 --- a/README.md +++ b/README.md @@ -107,18 +107,18 @@ To keep this simple, we use only a single database table named `books`. ```just MYAPP - help # List available commands - install # Install development tools - app # Run server app - migrate cmd="up" # Run DB migration CLI (defaults to up) - build # Run docker compose build - up cmd="" # Run docker compose up - down # Run docker compose down - lint # Run lints using gofumpt, go vet, staticcheck and govulncheck - test # Run tests - gen # Run go generate for all packages - apidoc # Generate openapi.yaml - repos # Generate gorm repositories using gorm cli + help # List available commands + install # Install development tools + app # Run server app + migration cmd="up" # Run DB migration CLI (defaults to up) + build # Run docker compose build + up cmd="" # Run docker compose up + down # Run docker compose down + lint # Run lints using gofumpt, go vet, staticcheck and govulncheck + test # Run tests + gen # Run go generate for all packages + apidoc # Generate openapi.yaml + repos # Generate gorm repositories using gorm cli ``` ## Sample Request Logs @@ -166,8 +166,9 @@ app-1 | {"level":"info","request_id":"d5mqjmhqvtmc73foh3dg","received_time":"20 ├── cmd │ ├── app │ │ └── main.go -│ └── migrate +│ └── migration │ ├── main.go +│ ├── embed.go │ └── migrations │ └── 00001_create_books_table.sql │ diff --git a/app/book/handler.go b/app/book/handler.go index e560ea2..78928c5 100644 --- a/app/book/handler.go +++ b/app/book/handler.go @@ -33,14 +33,14 @@ func New(validator *validator.Validate, db *gorm.DB) *Handler { } func (h *Handler) Register(r chi.Router) { - r.Get("/", h.List) - r.With(m.Validate[form.BookForm](h.validator)).Post("/", h.Create) - r.Get("/{id}", h.Read) - r.With(m.Validate[form.BookForm](h.validator)).Put("/{id}", h.Update) - r.Delete("/{id}", h.Delete) + r.Get("/", h.list) + r.With(m.Validate[form.BookForm](h.validator)).Post("/", h.create) + r.Get("/{id}", h.read) + r.With(m.Validate[form.BookForm](h.validator)).Put("/{id}", h.update) + r.Delete("/{id}", h.delete) } -// List godoc +// list godoc // // @summary List books // @description List books @@ -52,7 +52,7 @@ func (h *Handler) Register(r chi.Router) { // @success 200 {array} model.Book // @failure 500 {object} e.Error // @router /books [get] -func (h *Handler) List(w http.ResponseWriter, r *http.Request) { +func (h *Handler) list(w http.ResponseWriter, r *http.Request) { ctx := r.Context() logger := hlog.FromRequest(r) @@ -76,7 +76,7 @@ func (h *Handler) List(w http.ResponseWriter, r *http.Request) { } } -// Create godoc +// create godoc // // @summary Create book // @description Create book @@ -89,7 +89,7 @@ func (h *Handler) List(w http.ResponseWriter, r *http.Request) { // @failure 422 {object} e.Errors // @failure 500 {object} e.Error // @router /books [post] -func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { +func (h *Handler) create(w http.ResponseWriter, r *http.Request) { ctx := r.Context() logger := hlog.FromRequest(r) @@ -117,7 +117,7 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { logger.Info().Str("id", book.ID.String()).Msg("new book created") } -// Read godoc +// read godoc // // @summary Read book // @description Read book @@ -130,7 +130,7 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { // @failure 404 // @failure 500 {object} e.Error // @router /books/{id} [get] -func (h *Handler) Read(w http.ResponseWriter, r *http.Request) { +func (h *Handler) read(w http.ResponseWriter, r *http.Request) { ctx := r.Context() logger := hlog.FromRequest(r) @@ -159,7 +159,7 @@ func (h *Handler) Read(w http.ResponseWriter, r *http.Request) { } } -// Update godoc +// update godoc // // @summary Update book // @description Update book @@ -174,7 +174,7 @@ func (h *Handler) Read(w http.ResponseWriter, r *http.Request) { // @failure 422 {object} e.Errors // @failure 500 {object} e.Error // @router /books/{id} [put] -func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { +func (h *Handler) update(w http.ResponseWriter, r *http.Request) { ctx := r.Context() logger := hlog.FromRequest(r) @@ -212,7 +212,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { logger.Info().Str("id", id.String()).Msg("book updated") } -// Delete godoc +// delete godoc // // @summary Delete book // @description Delete book @@ -225,7 +225,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { // @failure 404 // @failure 500 {object} e.Error // @router /books/{id} [delete] -func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { +func (h *Handler) delete(w http.ResponseWriter, r *http.Request) { ctx := r.Context() logger := hlog.FromRequest(r) diff --git a/cmd/migrate/main.go b/cmd/migrate/main.go deleted file mode 100644 index 8accb3a..0000000 --- a/cmd/migrate/main.go +++ /dev/null @@ -1,87 +0,0 @@ -package main - -import ( - "context" - "embed" - "flag" - "fmt" - "log" - "os" - - _ "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" - - "myapp/config" -) - -const ( - dialect = "pgx" - fmtDBString = "host=%s user=%s password=%s dbname=%s port=%d sslmode=disable" -) - -var ( - flags = flag.NewFlagSet("migrate", flag.ExitOnError) - - //go:embed migrations/*.sql - embedMigrations embed.FS -) - -func main() { - flags.Usage = usage - flags.Parse(os.Args[1:]) - - args := flags.Args() - if len(args) == 0 || args[0] == "-h" || args[0] == "--help" { - flags.Usage() - return - } - - command := args[0] - - c := config.NewDB() - dbString := fmt.Sprintf(fmtDBString, c.Host, c.Username, c.Password, c.DBName, c.Port) - - db, err := goose.OpenDBWithDriver(dialect, dbString) - if err != nil { - log.Fatal(err.Error()) - } - - defer func() { - if err := db.Close(); err != nil { - log.Fatal(err.Error()) - } - }() - - goose.SetBaseFS(embedMigrations) - - if err := goose.RunContext(context.Background(), command, db, "migrations", args[1:]...); err != nil { - log.Fatalf("migrate %v: %v", command, err) - } -} - -func usage() { - fmt.Println(usagePrefix) - flags.PrintDefaults() - fmt.Println(usageCommands) -} - -var ( - usagePrefix = `Usage: migrate COMMAND -Examples: - migrate status -` - - usageCommands = ` -Commands: - up Migrate the DB to the most recent version available - up-by-one Migrate the DB up by 1 - up-to VERSION Migrate the DB to a specific VERSION - down Roll back the version by 1 - down-to VERSION Roll back to a specific VERSION - redo Re-run the latest migration - reset Roll back all migrations - status Dump the migration status for the current DB - version Print the current version of the database - create NAME [sql|go] Creates new migration file with the current timestamp - fix Apply sequential ordering to migrations` -) diff --git a/cmd/migration/main.go b/cmd/migration/main.go new file mode 100644 index 0000000..6e1289b --- /dev/null +++ b/cmd/migration/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "os" + + _ "github.com/jackc/pgx/v5/stdlib" + "github.com/pressly/goose/v3" + + "myapp/config" +) + +const ( + dialect = "pgx" + fmtDBString = "host=%s user=%s password=%s dbname=%s port=%d sslmode=disable" +) + +var ( + flags = flag.NewFlagSet("migration", flag.ExitOnError) + dir = flags.String("dir", "migrations", "directory with migration files") +) + +func main() { + flags.Usage = usage + flags.Parse(os.Args[1:]) + + args := flags.Args() + if len(args) == 0 || args[0] == "-h" || args[0] == "--help" { + flags.Usage() + return + } + + command := args[0] + + c := config.NewDB() + dbString := fmt.Sprintf(fmtDBString, c.Host, c.Username, c.Password, c.DBName, c.Port) + + db, err := goose.OpenDBWithDriver(dialect, dbString) + if err != nil { + log.Fatal(err.Error()) + } + + defer func() { + if err := db.Close(); err != nil { + log.Fatal(err.Error()) + } + }() + + if err := goose.RunContext(context.Background(), command, db, *dir, args[1:]...); err != nil { + log.Fatalf("migration %v: %v", command, err) + } +} diff --git a/cmd/migrate/migrations/00001_create_books_table.sql b/cmd/migration/migrations/00001_create_books_table.sql similarity index 100% rename from cmd/migrate/migrations/00001_create_books_table.sql rename to cmd/migration/migrations/00001_create_books_table.sql diff --git a/cmd/migration/usage_embed.go b/cmd/migration/usage_embed.go new file mode 100644 index 0000000..630b665 --- /dev/null +++ b/cmd/migration/usage_embed.go @@ -0,0 +1,36 @@ +//go:build embed + +package main + +import ( + "embed" + "fmt" + + "github.com/pressly/goose/v3" +) + +//go:embed migrations/*.sql +var migrations embed.FS + +func init() { + goose.SetBaseFS(migrations) +} + +func usage() { + fmt.Println(usageCommands) +} + +var usageCommands = `Usage: migration COMMAND +Examples: + migration status + +Commands: + up Migrate the DB to the most recent version available + up-by-one Migrate the DB up by 1 + up-to VERSION Migrate the DB to a specific VERSION + down Roll back the version by 1 + down-to VERSION Roll back to a specific VERSION + redo Re-run the latest migration + reset Roll back all migrations + status Dump the migration status for the current DB + version Print the current version of the database` diff --git a/cmd/migration/usage_no_embed.go b/cmd/migration/usage_no_embed.go new file mode 100644 index 0000000..d7d1f21 --- /dev/null +++ b/cmd/migration/usage_no_embed.go @@ -0,0 +1,32 @@ +//go:build !embed + +package main + +import "fmt" + +func usage() { + fmt.Println(usagePrefix) + flags.PrintDefaults() + fmt.Println(usageCommands) +} + +var ( + usagePrefix = `Usage: migration COMMAND +Examples: + migration create initial_tables sql +` + + usageCommands = ` +Commands: + create NAME [sql|go] Creates new migration file with the current timestamp + fix Apply sequential ordering to migrations + up Migrate the DB to the most recent version available + up-by-one Migrate the DB up by 1 + up-to VERSION Migrate the DB to a specific VERSION + down Roll back the version by 1 + down-to VERSION Roll back to a specific VERSION + redo Re-run the latest migration + reset Roll back all migrations + status Dump the migration status for the current DB + version Print the current version of the database` +) diff --git a/compose.yml b/compose.yml index 0cceab6..3cac26e 100644 --- a/compose.yml +++ b/compose.yml @@ -8,20 +8,20 @@ services: depends_on: db: condition: service_healthy - command: [ "sh", "-c", "/myapp/bin/migrate up && /myapp/bin/app" ] + command: [ "sh", "-c", "/myapp/bin/migration up && /myapp/bin/app" ] restart: always db: image: postgres:18-alpine environment: TZ: "Asia/Singapore" - POSTGRES_DB: myapp_db - POSTGRES_USER: myapp_user - POSTGRES_PASSWORD: myapp_pass + POSTGRES_DB: book-service-db + POSTGRES_USER: book-service-user + POSTGRES_PASSWORD: book-service-pass ports: - "5432:5432" healthcheck: - test: [ "CMD-SHELL", "pg_isready -U myapp_user -d myapp_db" ] + test: [ "CMD-SHELL", "pg_isready -U book-service-user -d book-service-db" ] interval: 3s timeout: 5s retries: 5 diff --git a/k8s/services/base/myapp/job.yaml b/k8s/services/base/myapp/job.yaml index 864bd6b..1edf517 100644 --- a/k8s/services/base/myapp/job.yaml +++ b/k8s/services/base/myapp/job.yaml @@ -2,7 +2,7 @@ apiVersion: batch/v1 kind: Job metadata: - name: myapp-migrate + name: myapp-migration annotations: argocd.argoproj.io/sync-wave: '1' argocd.argoproj.io/hook: PreSync @@ -26,9 +26,9 @@ spec: runAsNonRoot: true seccompProfile: { type: RuntimeDefault } containers: - - name: migrate + - name: migration image: learningcloudnativego/myapp-app:2.0.0 - command: [ /myapp/migrate ] + command: [ /myapp/migration ] args: [ up ] resources: requests: diff --git a/prod.Dockerfile b/prod.Dockerfile index 674a3b3..7f6e309 100644 --- a/prod.Dockerfile +++ b/prod.Dockerfile @@ -16,7 +16,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \ go build -ldflags '-w -s' -o ./bin/app ./cmd/app \ - && go build -ldflags '-w -s' -o ./bin/migrate ./cmd/migrate + && go build -tags=embed -ldflags '-w -s' -o ./bin/migration ./cmd/migration # Deployment environment @@ -29,6 +29,6 @@ COPY --from=build-env /usr/share/zoneinfo /usr/share/zoneinfo COPY --from=build-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=build-env /myapp/bin/app /myapp/ -COPY --from=build-env /myapp/bin/migrate /myapp/ +COPY --from=build-env /myapp/bin/migration /myapp/ CMD ["/myapp/app"] \ No newline at end of file