From 7ab68760054751450b7278e0b75ebea7afa010a0 Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 21 Aug 2026 11:09:08 +0200 Subject: [PATCH 1/3] feat: added bootstrap cli (removed db migration from service) --- Dockerfile | 15 +++++--- Makefile | 28 +++++++++----- cmd/api/main.go | 6 +-- cmd/bootstrap/main.go | 86 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 20 deletions(-) create mode 100644 cmd/bootstrap/main.go diff --git a/Dockerfile b/Dockerfile index e84b8cb..ed3185f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,10 @@ -FROM golang:1.26 AS build +FROM golang:1.26-alpine AS build + +RUN apk add --no-cache make git WORKDIR /app -COPY go.mod go.sum ./ +COPY go.mod go.sum ./ RUN go mod download COPY "./internal" "./internal" @@ -11,12 +13,15 @@ COPY "Makefile" "./Makefile" RUN make build -FROM debian:latest +FROM alpine:latest + +RUN apk --no-cache add ca-certificates WORKDIR /app COPY .env.docker .env -COPY --from=build /app/bin/dttmr-api /app/dttmr-api +COPY --from=build /app/bin/api /usr/local/bin/api +COPY --from=build /app/bin/bootstrap /usr/local/bin/bootstrap EXPOSE 8080 -CMD ["/app/dttmr-api"] +CMD ["api"] diff --git a/Makefile b/Makefile index f6787b2..c6d85fa 100644 --- a/Makefile +++ b/Makefile @@ -4,9 +4,9 @@ MAIN_DIR := ./cmd/api BIN_DIR := ./bin # Dynamically pull version and commit hash from Git -VERSION := $(shell git describe --tags --always --dirty) -COMMIT := $(shell git rev-parse --short HEAD) -BUILD_TIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") +VERSION ?= $(shell git describe --tags --always --dirty="-dev" 2>/dev/null || echo "dev") +COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "none") +BUILD_TIME ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") # Linker flags to strip symbols (-s -w) and inject build data into the binary LDFLAGS := -w -s \ @@ -16,7 +16,7 @@ LDFLAGS := -w -s \ # Targets -.PHONY: all help build run test test-cover lint fmt mod clean docker +.PHONY: all help build build-api build-bootstrap run test test-cover lint fmt mod clean docker all: help @@ -27,15 +27,23 @@ help: @echo "Targets:" @sed -n 's/^##//p' $(MAKEFILE_LIST) | column -t -s ':' | sed -e 's/^/ /' -## build: Compile the binary -build: - @echo "Building $(APP_NAME) version $(VERSION)..." - @CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o $(BIN_DIR)/$(APP_NAME) $(MAIN_DIR) +## build: Compile all binaries +build: build-api build-bootstrap + +## build-api: Compile the api binary +build-api: + @echo "Building api..." + go build -ldflags="$(LDFLAGS)" -o $(BIN_DIR)/api $(MAIN_DIR) + +## build-bootstrap: Compile the bootstrap binary +build-bootstrap: + @echo "Building bootstrap cli..." + go build -ldflags="$(LDFLAGS)" -o $(BIN_DIR)/bootstrap ./cmd/bootstrap ## run: Run the application directly -run: build +run: build-api @echo "Starting $(APP_NAME)..." - @$(BIN_DIR)/$(APP_NAME) + @$(BIN_DIR)/api ## test: Run tests with race detector test: diff --git a/cmd/api/main.go b/cmd/api/main.go index 5771715..1acd796 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -21,7 +21,7 @@ import ( var ( Version = "dev" - Commit = "none" // In the Makefile, we called this 'Commit' instead of 'Hash' + Commit = "none" BuildTime = "unknown" ) @@ -98,10 +98,6 @@ func run() error { } }() - if err := database.RunMigrations(db); err != nil { - slog.Error("failed to run migrations", slog.Any("error", err)) - } - srv := makeServer(db, cfg) go func() { slog.Info("starting http server", "addr", srv.Addr) diff --git a/cmd/bootstrap/main.go b/cmd/bootstrap/main.go new file mode 100644 index 0000000..6aa38ca --- /dev/null +++ b/cmd/bootstrap/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "context" + "database/sql" + "flag" + "log/slog" + "os" + "time" + + "github.com/joho/godotenv" + "github.com/robindittmar/dttmr-api/internal/config" + "github.com/robindittmar/dttmr-api/internal/database" + "github.com/robindittmar/dttmr-api/internal/domain" + "github.com/robindittmar/dttmr-api/internal/repository" +) + +var ( + Version = "dev" + Commit = "none" + BuildTime = "unknown" +) + +func main() { + time.Local, _ = time.LoadLocation("UTC") + + _ = godotenv.Load(".env") + setupLogging() + + email := flag.String("email", "admin@example.com", "Admin email address") + name := flag.String("name", "admin", "Admin name") + password := flag.String("password", "", "Admin password") + cfg := config.Load() + + slog.Info("starting bootstrap", + slog.String("version", Version), + slog.String("commit", Commit), + slog.String("build_time", BuildTime), + ) + defer slog.Info("bootstrap complete!") + + db, err := database.New(context.Background(), cfg.DatabaseURL) + if err != nil { + slog.Error("failed to initialize database", slog.Any("error", err)) + os.Exit(1) + } + defer func() { + err := db.Close() + if err != nil { + slog.Error("failed to close database connection", slog.Any("error", err)) + } + }() + + if err := database.RunMigrations(db); err != nil { + slog.Error("failed to run migrations", slog.Any("error", err)) + os.Exit(1) + } + + if *password != "" { + err := seedAdminUser(db, *email, *name, *password) + if err != nil { + slog.Error("failed to seed admin user", slog.Any("error", err)) + } else { + slog.Info("admin user seeded", slog.String("email", *email), slog.String("name", *name)) + } + } else { + slog.Info("skipping admin user seeding, no password provided") + } +} + +func seedAdminUser(db *sql.DB, email string, name string, password string) error { + userRepo := repository.NewUserRepo(db) + userService := domain.NewUserService(userRepo) + + _, err := userService.CreateUser(context.Background(), email, name, password) + return err +} + +func setupLogging() { + baseHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, + }) + + logger := slog.New(baseHandler) + slog.SetDefault(logger) +} From 99c29880708a3dae0f2eee8393650b8c990c7e70 Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 21 Aug 2026 11:13:15 +0200 Subject: [PATCH 2/3] fix: removed copying .docker.env to docker image --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index ed3185f..8bb6995 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,6 @@ RUN apk --no-cache add ca-certificates WORKDIR /app -COPY .env.docker .env COPY --from=build /app/bin/api /usr/local/bin/api COPY --from=build /app/bin/bootstrap /usr/local/bin/bootstrap From 8e6e8fa9516694e650cd5912f2d5cc774ff129ec Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 21 Aug 2026 11:39:28 +0200 Subject: [PATCH 3/3] feat: added compose.yaml --- .env.docker | 3 -- .env.example | 17 +++++++++++ Dockerfile | 2 ++ compose.yaml | 48 +++++++++++++++++++++++++++++++ docker/compose.yml | 33 --------------------- docker/otel-collector-config.yaml | 23 --------------- docker/otel-config.yaml | 15 ++++++++++ docker/prometheus.yml | 7 ----- 8 files changed, 82 insertions(+), 66 deletions(-) delete mode 100644 .env.docker create mode 100644 .env.example create mode 100644 compose.yaml delete mode 100644 docker/compose.yml delete mode 100644 docker/otel-collector-config.yaml create mode 100644 docker/otel-config.yaml delete mode 100644 docker/prometheus.yml diff --git a/.env.docker b/.env.docker deleted file mode 100644 index f99b213..0000000 --- a/.env.docker +++ /dev/null @@ -1,3 +0,0 @@ -DTTMR_OTLP_ENDPOINT=172.17.0.1:4317 -DTTMR_DATABASE_URL=postgres://postgres:postgres@172.17.0.1:5432/postgres?sslmode=disable&timezone=utc -DTTMR_JWT_SECRET=V*$#Jt9OlYW0gEB6PyUU$qLYbJ^NC7LZ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1f2d345 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# PostgreSQL Config +POSTGRES_USER=dttmr_user +POSTGRES_PASSWORD=super_secret_db_password +POSTGRES_DB=dttmr_db + +# App Config +DTTMR_DATABASE_URL=postgres://dttmr_user:super_secret_db_password@postgres:5432/dttmr_db?sslmode=disable&timezone=utc +DTTMR_OTLP_ENDPOINT=otel-collector:4317 +DTTMR_ENVIRONMENT=production +DTTMR_JWT_SECRET=super_secret_jwt_key +DTTMR_PORT=8080 + +# Bootstrap Admin +ADMIN_EMAIL=admin@example.com +ADMIN_USERNAME=admin +ADMIN_PASSWORD=changeme + diff --git a/Dockerfile b/Dockerfile index 8bb6995..f806404 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,8 @@ RUN go mod download COPY "./internal" "./internal" COPY "./cmd" "./cmd" COPY "Makefile" "./Makefile" +# So make can fetch tag and commit +COPY "./.git" "./.git" RUN make build diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..db4d8fb --- /dev/null +++ b/compose.yaml @@ -0,0 +1,48 @@ +services: + postgres: + image: postgres:16-alpine + container_name: dttmr-postgres + restart: unless-stopped + env_file: + - .env + volumes: + - /srv/dttmr-api/postgresql:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + + otel-collector: + image: otel/opentelemetry-collector:latest + container_name: dttmr-otel + restart: unless-stopped + command: ["--config=/etc/otelcol/config.yaml"] + volumes: + - ./docker/otel-config.yaml:/etc/otelcol/config.yaml + ports: + - "4317:4317" + + bootstrap: + build: . + container_name: dttmr-bootstrap + env_file: + - .env + command: ["bootstrap", "--email=${ADMIN_EMAIL}", "--name=${ADMIN_USERNAME}", "--password=${ADMIN_PASSWORD}"] + depends_on: + postgres: + condition: service_healthy + + api: + build: . + container_name: dttmr-api + restart: unless-stopped + env_file: + - .env + ports: + - "8080:8080" + depends_on: + otel-collector: + condition: service_started + bootstrap: + condition: service_completed_successfully diff --git a/docker/compose.yml b/docker/compose.yml deleted file mode 100644 index 3afd106..0000000 --- a/docker/compose.yml +++ /dev/null @@ -1,33 +0,0 @@ -services: - otel-collector: - image: otel/opentelemetry-collector:latest - command: ["--config=/etc/otel-collector-config.yaml"] - volumes: - - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml - ports: - - "4317:4317" - - jaeger: - image: jaegertracing/all-in-one:latest - environment: - - COLLECTOR_OTLP_ENABLED=true - ports: - - "16686:16686" - - prometheus: - image: prom/prometheus:latest - volumes: - - ./prometheus.yml:/etc/prometheus/prometheus.yml - ports: - - "9090:9090" - - postgres: - image: postgres:latest - volumes: - - ./postgresql-data:/var/lib/postgresql/data - ports: - - "15432:5432" - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgres - - POSTGRES_DB=dttmr diff --git a/docker/otel-collector-config.yaml b/docker/otel-collector-config.yaml deleted file mode 100644 index 3c9d71b..0000000 --- a/docker/otel-collector-config.yaml +++ /dev/null @@ -1,23 +0,0 @@ -receivers: - otlp: - protocols: - grpc: - endpoint: 0.0.0.0:4317 - -exporters: - otlp/jaeger: - endpoint: jaeger:4317 - tls: - insecure: true - - prometheus: - endpoint: 0.0.0.0:8889 - -service: - pipelines: - traces: - receivers: [otlp] - exporters: [otlp/jaeger] - metrics: - receivers: [otlp] - exporters: [prometheus] diff --git a/docker/otel-config.yaml b/docker/otel-config.yaml new file mode 100644 index 0000000..e0331ff --- /dev/null +++ b/docker/otel-config.yaml @@ -0,0 +1,15 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + +exporters: + debug: + verbosity: basic + +service: + pipelines: + traces: + receivers: [otlp] + exporters: [debug] diff --git a/docker/prometheus.yml b/docker/prometheus.yml deleted file mode 100644 index 44b9500..0000000 --- a/docker/prometheus.yml +++ /dev/null @@ -1,7 +0,0 @@ -global: - scrape_interval: 5s - -scrape_configs: - - job_name: 'otel-collector' - static_configs: - - targets: ['otel-collector:8889']