diff --git a/Containerfile.migrate b/Containerfile.migrate new file mode 100644 index 0000000..60219db --- /dev/null +++ b/Containerfile.migrate @@ -0,0 +1,51 @@ +ARG GOARCH=amd64 +ARG GOOS=linux +ARG CGO_ENABLED=0 +ARG VERSION=1.0.0 +ARG BUILD_TIME=unknown + +# Stage 1: Build +FROM docker.io/golang:1.24-alpine AS build + +ARG GOOS +ARG GOARCH +ARG CGO_ENABLED +ARG VERSION +ARG BUILD_TIME + +WORKDIR /src + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=${CGO_ENABLED} \ + GOOS=${GOOS} \ + GOARCH=${GOARCH} \ + go build -trimpath \ + -ldflags="-s -w -X 'main.Version=${VERSION}' -X 'main.BuildTime=${BUILD_TIME}'" \ + -o /bin/app ./cmd/migrate + +# Stage 2: Runtime +FROM docker.io/alpine:3.22 AS runtime + +ARG VERSION +ARG BUILD_TIME + +RUN addgroup -S app && adduser -S -G app app + +WORKDIR /app + +COPY --from=build /bin/app /app/bin + +RUN chown app:app /app/bin && chmod +x /app/bin + +USER app + +LABEL org.opencontainers.image.version=${VERSION} +LABEL org.opencontainers.image.created=${BUILD_TIME} + +ENTRYPOINT ["/app/bin"] + +CMD [""] diff --git a/Makefile b/Makefile index 637f420..3e710ea 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,10 @@ # Go parameters GOCMD=go -GOBUILD=$(GOCMD) build +GOBUILD=$(GOCMD) build -trimpath -ldflags="-s -w" GOTEST=$(GOCMD) test GODOWNLOAD=$(GOCMD) mod download BINARY_NAME=orderservice +MIGRATE_BINARY_NAME=orderservice-migrate BINARY_DIR=bin # Protobuf parameters @@ -12,7 +13,7 @@ PROTO_DIR=api/proto PROTO_FILE=$(PROTO_DIR)/order.proto PROTO_OUT=. -.PHONY: install i generate gen protoc test build run lint fmt format clean help +.PHONY: install i generate gen generate-gw test build run migrate lint fmt format clean help install: $(GODOWNLOAD) @@ -38,9 +39,21 @@ build: $(GOBUILD) -o ./$(BINARY_DIR)/$(BINARY_NAME) ./cmd/server chmod +x ./$(BINARY_DIR)/$(BINARY_NAME) +build-migrate: + $(GOBUILD) -o ./$(BINARY_DIR)/$(MIGRATE_BINARY_NAME) ./cmd/migrate + chmod +x ./$(BINARY_DIR)/$(MIGRATE_BINARY_NAME) + run: build ./$(BINARY_DIR)/$(BINARY_NAME) +migrate: build-migrate + @cmd=$(word 2,$(MAKECMDGOALS)); \ + if [ -z "$$cmd" ]; then \ + echo "Usage: make migrate "; \ + exit 1; \ + fi; \ + ./$(BINARY_DIR)/$(MIGRATE_BINARY_NAME) -cmd $$cmd + lint: golangci-lint run -c .golangci.yaml ./... @@ -68,3 +81,6 @@ help: @echo " clean - Clean build artifacts" .DEFAULT_GOAL := help + +%: + @: diff --git a/cmd/migrate/main.go b/cmd/migrate/main.go new file mode 100644 index 0000000..8ea720c --- /dev/null +++ b/cmd/migrate/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "database/sql" + "embed" + "errors" + "flag" + "log" + + "orderservice/internal/config" + + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/postgres" + "github.com/golang-migrate/migrate/v4/source/iofs" +) + +//go:embed migrations/*.sql +var migrationsFS embed.FS + +func main() { + var cmd string + + flag.StringVar(&cmd, "cmd", "up", "migration command: up|down|force|version") + flag.Parse() + + cfg, err := config.Load() + if err != nil { + log.Fatalf("failed to load config: %v", err) + } + + db, err := sql.Open("postgres", cfg.BuildPostgresDSN()) + if err != nil { + log.Fatalf("open db: %v", err) + } + defer db.Close() + + drv, err := postgres.WithInstance(db, &postgres.Config{}) + if err != nil { + log.Printf("postgres driver: %v", err) + return + } + + src, err := iofs.New(migrationsFS, "migrations") + if err != nil { + log.Printf("iofs source: %v", err) + return + } + + m, err := migrate.NewWithInstance("iofs", src, "postgres", drv) + if err != nil { + log.Printf("migrate NewWithInstance: %v", err) + return + } + + switch cmd { + case "up": + if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { + log.Printf("m.Up failed: %v", err) + return + } + log.Println("migrations applied (up)") + case "down": + if err := m.Steps(-1); err != nil { + log.Printf("m.Steps(-1) failed: %v", err) + return + } + log.Println("stepped down 1 migration") + case "version": + v, dirty, verr := m.Version() + if verr != nil { + log.Printf("version: %v", verr) + return + } + log.Printf("version: %d dirty: %v\n", v, dirty) + default: + log.Printf("unknown cmd: %s", cmd) + } +} diff --git a/cmd/migrate/migrations/000001_create_orders_table.down.sql b/cmd/migrate/migrations/000001_create_orders_table.down.sql new file mode 100644 index 0000000..ff1dd24 --- /dev/null +++ b/cmd/migrate/migrations/000001_create_orders_table.down.sql @@ -0,0 +1 @@ +drop table if exists orders; diff --git a/internal/repository/postgres/schema.sql b/cmd/migrate/migrations/000001_create_orders_table.up.sql similarity index 100% rename from internal/repository/postgres/schema.sql rename to cmd/migrate/migrations/000001_create_orders_table.up.sql diff --git a/compose.yaml b/compose.yaml index b2f3ca4..081852c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -6,6 +6,10 @@ services: context: . dockerfile: Containerfile depends_on: + migrate: + restart: false + condition: service_completed_successfully + required: true postgres: restart: false condition: service_healthy @@ -35,6 +39,24 @@ services: - default restart: unless-stopped shm_size: 4mb + + migrate: + build: + context: . + dockerfile: Containerfile.migrate + depends_on: + postgres: + restart: false + condition: service_healthy + required: true + env_file: + - path: ./infrastructure/core/.env.template + required: true + - path: ./infrastructure/core/.env + networks: + - default + restart: no + shm_size: 4mb postgres: image: docker.io/postgres:17-alpine