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) +}