Skip to content

Commit 7d63a14

Browse files
authored
Merge pull request #110 from zero-one-group/feat/template-go-modular
Feat/template go modular
2 parents 9539e27 + 684c333 commit 7d63a14

File tree

111 files changed

+10487
-19
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+10487
-19
lines changed

.moon/workspace.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,5 @@ generator:
6868
- 'https://oss.zero-one-group.com/monorepo/templates/strapi.zip'
6969
- 'https://oss.zero-one-group.com/monorepo/templates/swarm.zip'
7070
- 'https://oss.zero-one-group.com/monorepo/templates/terragrunt.zip'
71-
- 'glob://static/templates/*'
71+
# Uncomment to enable local templates or debug custom templates.
72+
# - './templates'

apps/go-modular/Dockerfile

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,44 @@ FROM --platform=${PLATFORM} gcr.io/distroless/cc-debian12 AS runner
5858

5959
# ----- Read application environment variables --------------------------------
6060

61-
ARG DATABASE_URL SMTP_HOST SMTP_PORT SMTP_USERNAME SMTP_PASSWORD SMTP_EMAIL_FROM
61+
ARG APP_BASE_URL
62+
ARG CORS_CREDENTIALS
63+
ARG CORS_MAX_AGE
64+
ARG CORS_ORIGINS
65+
ARG ENABLE_API_DOCS
66+
ARG JWT_ALGORITHM
67+
ARG JWT_SECRET_KEY
68+
ARG RATE_LIMIT_BURST_SIZE
69+
ARG RATE_LIMIT_ENABLED
70+
ARG RATE_LIMIT_REQUESTS
71+
ARG DATABASE_URL
72+
ARG PG_MAX_POOL_SIZE
73+
ARG PG_MAX_RETRIES
74+
ARG SMTP_HOST
75+
ARG SMTP_PASSWORD
76+
ARG SMTP_PORT
77+
ARG SMTP_SECURE
78+
ARG SMTP_SENDER_EMAIL
79+
ARG SMTP_SENDER_NAME
80+
ARG SMTP_USERNAME
81+
ARG PUBLIC_ASSETS_URL
82+
ARG S3_ACCESS_KEY
83+
ARG S3_BUCKET_NAME
84+
ARG S3_ENDPOINT
85+
ARG S3_FORCE_PATH_STYLE
86+
ARG S3_REGION
87+
ARG S3_SECRET_KEY
88+
ARG S3_USE_SSL
89+
ARG LOG_FORMAT
90+
ARG LOG_LEVEL
91+
ARG LOG_NO_COLOR
92+
ARG OTEL_ENABLE_TELEMETRY
93+
ARG OTEL_EXPORTER_OTLP_ENDPOINT
94+
ARG OTEL_EXPORTER_OTLP_HEADERS
95+
ARG OTEL_EXPORTER_OTLP_PROTOCOL
96+
ARG OTEL_INSECURE_MODE
97+
ARG OTEL_SERVICE_NAME
98+
ARG OTEL_TRACING_SAMPLE_RATE
6299

63100
# ----- Read application environment variables --------------------------------
64101

@@ -77,13 +114,17 @@ COPY --from=glibc /bin/ls /bin/ls
77114
COPY --from=glibc /bin/sh /bin/sh
78115

79116
# Define the host and port to listen on.
80-
ARG SERVER_ENV=production HOST=0.0.0.0 PORT=8000
81-
ENV SERVER_ENV=$SERVER_ENV TINI_SUBREAPER=true
82-
ENV HOST=$HOST PORT=$PORT
117+
ARG APP_MODE=production
118+
ARG SERVER_HOST=0.0.0.0
119+
ARG SERVER_PORT=8000
120+
ENV APP_MODE=$APP_MODE
121+
ENV SERVER_HOST=$SERVER_HOST
122+
ENV SERVER_PORT=$SERVER_PORT
123+
ENV TINI_SUBREAPER=true
83124

84125
WORKDIR /srv
85126
USER nonroot:nonroot
86-
EXPOSE $PORT
127+
EXPOSE $SERVER_PORT/tcp
87128

88129
ENTRYPOINT ["/usr/bin/tini", "--"]
89-
CMD ["/srv/go-modular"]
130+
CMD ["/srv/go-modular", "serve"]

apps/go-modular/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,48 @@
11
# Go Application
22

3+
This template already pre-configured basic auth, take a look at `apps/go-modular/database/seeders/user_factory.go`.
4+
5+
## Common Tasks
6+
```sh
7+
moon go-modular:dev # Run development
8+
moon go-modular:run # Execute `go run`
9+
moon go-modular:build # Build the application
10+
moon go-modular:start # Start application from build
11+
moon go-modular:test # Run testing
12+
moon go-modular:coverage # Run test coverage
13+
moon go-modular:format # Run code formatting
14+
moon go-modular:tidy # Install dependencies
15+
moon go-modular:docker-build # Build docker image
16+
moon go-modular:docker-run # Run docker image
17+
moon go-modular:docker-shell # Execute docker shell
18+
moon go-modular:dump # Dump the database
19+
moon go-modular:install-mockery # Install mockery
20+
moon go-modular:generate-swagger # Generate Swagger OpenAPI docs
21+
moon go-modular:generate-mock # Generate Mock
22+
```
23+
24+
## Migration Tasks
25+
```sh
26+
# Initiate or reset migrations and seed
27+
moon go-modular:run -- migrate:reset --up --seed --force
28+
29+
# Common migration commands
30+
moon go-modular:run -- migrate:up
31+
moon go-modular:run -- migrate:status
32+
moon go-modular:run -- migrate:version
33+
moon go-modular:run -- migrate:create [MIGRATION_NAME]
34+
moon go-modular:run -- migrate:down
35+
moon go-modular:run -- migrate:reset
36+
moon go-modular:run -- migrate:seed
37+
```
38+
39+
## Generate Sample Configuration
40+
This command will generate `.env.example` from `apps/go-modular/internal/config/default.go`:
41+
42+
```sh
43+
moon go-modular:run -- generate:config
44+
```
45+
346
## Scaffold New Module
447
```sh
548
mkdir -p apps/go-modular/modules/dummy
@@ -14,3 +57,5 @@ echo 'package repository' > apps/go-modular/modules/dummy/repository/repository.
1457
echo 'package services' > apps/go-modular/modules/dummy/services/services.go
1558
echo 'package dummy' > apps/go-modular/modules/dummy/module.go
1659
```
60+
61+
> Replace `dummy` with the module name

apps/go-modular/database/migrations/00002_create_users_table.sql

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@
88
CREATE TABLE IF NOT EXISTS public.users (
99
id UUID NOT NULL PRIMARY KEY DEFAULT uuidv7(),
1010
display_name TEXT NOT NULL CHECK (char_length(display_name) > 0),
11-
email TEXT NOT NULL CHECK (
12-
char_length(email) > 3 AND
13-
email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'
14-
),
11+
email TEXT NOT NULL UNIQUE,
1512
username TEXT UNIQUE,
1613
avatar_url TEXT,
1714
metadata JSONB DEFAULT NULL,
@@ -22,6 +19,7 @@ CREATE TABLE IF NOT EXISTS public.users (
2219
banned_at TIMESTAMPTZ DEFAULT NULL,
2320
ban_expires TIMESTAMPTZ DEFAULT NULL,
2421
ban_reason TEXT DEFAULT NULL,
22+
CONSTRAINT chk_email_format CHECK (char_length(email) > 3 AND email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
2523
-- Username only allows alphanumeric characters and underscores, must be between 3 and 32 characters long
2624
CONSTRAINT chk_username_format CHECK (username IS NULL OR username ~ '^[a-zA-Z0-9_]{3,32}$')
2725
);

apps/go-modular/database/migrator.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,9 @@ func NewMigrator(dbURL string) *Migrator {
106106
}
107107

108108
// Configure goose migrator
109-
goose.SetBaseFS(MigrationFS)
109+
_ = goose.SetDialect("postgres")
110110
goose.SetTableName("app_migrations")
111-
goose.SetDialect("postgres")
111+
goose.SetBaseFS(MigrationFS)
112112
goose.SetSequential(true)
113113

114114
slog.Info("Database migrator initialized")

apps/go-modular/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ toolchain go1.25.1
77
require (
88
github.com/alexliesenfeld/health v0.8.1
99
github.com/bdpiprava/scalar-go v0.12.1
10+
github.com/exaring/otelpgx v0.9.3
1011
github.com/go-playground/validator/v10 v10.27.0
1112
github.com/gofrs/uuid/v5 v5.3.2
1213
github.com/jackc/pgx/v5 v5.7.6

apps/go-modular/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
5757
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
5858
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
5959
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
60+
github.com/exaring/otelpgx v0.9.3 h1:4yO02tXC7ZJZ+hcqcUkfxblYNCIFGVhpUWI0iw1TzPU=
61+
github.com/exaring/otelpgx v0.9.3/go.mod h1:R5/M5LWsPPBZc1SrRE5e0DiU48bI78C1/GPTWs6I66U=
6062
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
6163
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
6264
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=

apps/go-modular/internal/adapter/postgres.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package adapter
33
import (
44
"context"
55
"fmt"
6+
"strings"
67
"time"
78

9+
"github.com/exaring/otelpgx"
810
"github.com/jackc/pgx/v5"
911
"github.com/jackc/pgx/v5/pgconn"
1012
"github.com/jackc/pgx/v5/pgxpool"
@@ -25,6 +27,7 @@ type PostgresConfig struct {
2527
MaxConnIdleTime time.Duration
2628
SearchPath string
2729
Timezone string
30+
EnableOTel bool
2831
}
2932

3033
// NewPostgres creates a new database connection pool.
@@ -69,6 +72,17 @@ func NewPostgres(cfg PostgresConfig) (*PostgresDB, error) {
6972
poolConfig.ConnConfig.RuntimeParams["TimeZone"] = cfg.Timezone
7073
poolConfig.ConnConfig.RuntimeParams["timezone"] = cfg.Timezone
7174

75+
// Initialize OpenTelemetry tracing for pgx if enabled
76+
// Uses otelpgx package to create a tracer that trims SQL in span names
77+
// and uses a custom function to generate span names from statements
78+
// See pgxSpanNameFunc below for details
79+
if cfg.EnableOTel {
80+
poolConfig.ConnConfig.Tracer = otelpgx.NewTracer(
81+
otelpgx.WithTrimSQLInSpanName(),
82+
otelpgx.WithSpanNameFunc(pgxSpanNameFunc),
83+
)
84+
}
85+
7286
pool, err := pgxpool.NewWithConfig(context.Background(), poolConfig)
7387
if err != nil {
7488
return nil, fmt.Errorf("failed to create pgxpool: %w", err)
@@ -170,8 +184,8 @@ func (db *PostgresDB) GetPoolConnection(ctx context.Context) (*pgxpool.Conn, err
170184
return db.Pool.Acquire(ctx)
171185
}
172186

173-
// TestConnection tests database connectivity with timeout
174-
func TestConnection(cfg PostgresConfig) error {
187+
// CheckPostgresConnection tests database connectivity with timeout
188+
func CheckPostgresConnection(cfg PostgresConfig) error {
175189
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
176190
defer cancel()
177191

@@ -193,7 +207,7 @@ func TestConnection(cfg PostgresConfig) error {
193207
}
194208

195209
// NewSingleConnection creates and returns a single pgx.Conn using the provided PostgresConfig.
196-
// This is used by TestConnection and other helpers that need a standalone connection.
210+
// This is used by CheckPostgresConnection and other helpers that need a standalone connection.
197211
func NewSingleConnection(cfg PostgresConfig) (*pgx.Conn, error) {
198212
// apply sensible defaults used elsewhere
199213
if cfg.SearchPath == "" {
@@ -221,9 +235,31 @@ func NewSingleConnection(cfg PostgresConfig) (*pgx.Conn, error) {
221235
connCfg.ConnectTimeout = 10 * time.Second
222236
}
223237

238+
// Initialize OpenTelemetry tracing for pgx if enabled
239+
// Uses otelpgx package to create a tracer that trims SQL in span names
240+
// and uses a custom function to generate span names from statements
241+
// See pgxSpanNameFunc below for details
242+
if cfg.EnableOTel {
243+
connCfg.Tracer = otelpgx.NewTracer(
244+
otelpgx.WithTrimSQLInSpanName(),
245+
otelpgx.WithSpanNameFunc(pgxSpanNameFunc),
246+
)
247+
}
248+
224249
conn, err := pgx.ConnectConfig(context.Background(), connCfg)
225250
if err != nil {
226251
return nil, fmt.Errorf("failed to create single connection: %w", err)
227252
}
228253
return conn, nil
229254
}
255+
256+
// pgxSpanNameFunc trims "-- name: " prefix and returns the statement name for tracing
257+
func pgxSpanNameFunc(stmt string) string {
258+
// If stmt is of the sqlc form "-- name: Example :one\n...",
259+
// extract "Example". Otherwise, leave as-is.
260+
stmt = strings.TrimPrefix(stmt, "-- name: ")
261+
if i := strings.IndexByte(stmt, ' '); i != -1 {
262+
return stmt[:i]
263+
}
264+
return stmt
265+
}

apps/go-modular/internal/adapter/postgres_test.go

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77

88
"github.com/stretchr/testify/assert"
99
"github.com/stretchr/testify/require"
10-
1110
"go-modular/pkg/testutils"
1211
)
1312

@@ -25,6 +24,7 @@ func TestPostgres_WithTestEnv(t *testing.T) {
2524
URL: pgURL,
2625
MaxConnections: 5,
2726
MinConnections: 1,
27+
EnableOTel: false,
2828
}
2929

3030
db, err := NewPostgres(cfg)
@@ -50,14 +50,49 @@ func TestPostgres_WithTestEnv(t *testing.T) {
5050
assert.Equal(t, 1, cnt)
5151
})
5252

53+
t.Run("NewPostgres_pool_exec_query_with_otel", func(t *testing.T) {
54+
te := testutils.NewTestEnv(t)
55+
pgPool, pgURL, err := te.SetupPostgres()
56+
require.NoError(t, err)
57+
require.NotNil(t, pgPool)
58+
require.NotEmpty(t, pgURL)
59+
60+
cfg := PostgresConfig{
61+
URL: pgURL,
62+
MaxConnections: 5,
63+
MinConnections: 1,
64+
EnableOTel: true,
65+
}
66+
67+
db, err := NewPostgres(cfg)
68+
require.NoError(t, err)
69+
require.NotNil(t, db)
70+
defer db.Close()
71+
72+
require.NoError(t, db.Ping(ctx))
73+
74+
_, err = db.Pool.Exec(ctx, `CREATE TABLE IF NOT EXISTS items_otel (id serial PRIMARY KEY, name text)`)
75+
require.NoError(t, err)
76+
77+
_, err = db.Pool.Exec(ctx, `INSERT INTO items_otel (name) VALUES ($1)`, "bar")
78+
require.NoError(t, err)
79+
80+
row := db.Pool.QueryRow(ctx, `SELECT count(*) FROM items_otel`)
81+
var cnt int
82+
err = row.Scan(&cnt)
83+
require.NoError(t, err)
84+
assert.Equal(t, 1, cnt)
85+
})
86+
5387
t.Run("NewPostgresWithSingleConn_pool_and_conn", func(t *testing.T) {
5488
te := testutils.NewTestEnv(t)
5589
_, pgURL, err := te.SetupPostgres()
5690
require.NoError(t, err)
5791
require.NotEmpty(t, pgURL)
5892

5993
cfg := PostgresConfig{
60-
URL: pgURL,
94+
URL: pgURL,
95+
EnableOTel: false,
6196
}
6297

6398
db, err := NewPostgresWithSingleConn(cfg)
@@ -78,6 +113,33 @@ func TestPostgres_WithTestEnv(t *testing.T) {
78113
require.NoError(t, err)
79114
})
80115

116+
t.Run("NewPostgresWithSingleConn_pool_and_conn_with_otel", func(t *testing.T) {
117+
te := testutils.NewTestEnv(t)
118+
_, pgURL, err := te.SetupPostgres()
119+
require.NoError(t, err)
120+
require.NotEmpty(t, pgURL)
121+
122+
cfg := PostgresConfig{
123+
URL: pgURL,
124+
EnableOTel: true,
125+
}
126+
127+
db, err := NewPostgresWithSingleConn(cfg)
128+
require.NoError(t, err)
129+
require.NotNil(t, db)
130+
defer func() {
131+
if db.Conn != nil {
132+
_ = db.Conn.Close(ctx)
133+
}
134+
db.Close()
135+
}()
136+
137+
require.NoError(t, db.Ping(ctx))
138+
139+
_, err = db.Pool.Exec(ctx, `CREATE TABLE IF NOT EXISTS t_single_otel (id serial PRIMARY KEY)`)
140+
require.NoError(t, err)
141+
})
142+
81143
// keep test run-time reasonable
82144
t.Run("TestConnection_helper", func(t *testing.T) {
83145
te := testutils.NewTestEnv(t)

docker/_stacks_/postgres.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,15 @@ services:
3333
pgweb:
3434
image: sosedoff/pgweb:latest
3535
restart: always
36-
ports: ['54321:8081']
36+
ports: ['54321:54321']
3737
environment:
3838
PGWEB_DATABASE_URL: "postgres://postgres:securedb@pgsql:5432/postgres?sslmode=disable"
39+
command: ['/usr/bin/pgweb', '--bind=0.0.0.0', '--listen=54321']
40+
healthcheck:
41+
test: ['CMD', 'nc', '-vz', '127.0.0.1', '54321']
42+
interval: 3s
43+
timeout: 5s
44+
retries: 3
3945
extra_hosts: ['host.docker.internal:host-gateway']
4046
networks: ['myorg_network']
4147
depends_on:

0 commit comments

Comments
 (0)