diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1a0318f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.gitignore +.idea +.env +.env.* +graphify-out +developer_doc +deploy +tmp +*.log +Dockerfile* +docker-compose*.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5fae202 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM golang:1.27-alpine AS build + +WORKDIR /src + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build \ + -trimpath \ + -ldflags="-s -w" \ + -o /out/robot-market-api \ + ./cmd/api + +FROM alpine:3.22 AS production + +RUN apk add --no-cache ca-certificates \ + && addgroup -S app \ + && adduser -S -G app app + +COPY --from=build /out/robot-market-api /usr/local/bin/robot-market-api + +USER app +EXPOSE 8080 + +ENTRYPOINT ["/usr/local/bin/robot-market-api"] diff --git a/Makefile b/Makefile index e69de29..25bbed7 100644 --- a/Makefile +++ b/Makefile @@ -0,0 +1,10 @@ +.PHONY: dev dev-down docker-build + +dev: + docker compose -f deploy/docker-compose.dev.yml up --build + +dev-down: + docker compose -f deploy/docker-compose.dev.yml down + +docker-build: + docker compose -f deploy/docker-compose.prod.yml build diff --git a/cmd/api/main.go b/cmd/api/main.go index e69de29..3048670 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + "git.my-rm.com/Robot-Market/shop-backend/internal/app" +) + +func main() { + if _, err := app.LoadConfig(""); err != nil { + log.Fatal(err) + } +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go index e69de29..06ab7d0 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -0,0 +1 @@ +package main diff --git a/configs/config.example.yaml b/configs/config.example.yaml new file mode 100644 index 0000000..b8da5f8 --- /dev/null +++ b/configs/config.example.yaml @@ -0,0 +1,8 @@ +environment: development + +http: + address: 0.0.0.0:8080 + +# The database is managed outside Docker Compose. +database: + url: postgres://robot_market:change-me@host.docker.internal:5432/robot_market?sslmode=disable diff --git a/create-dir-structure.sh b/create-dir-structure.sh deleted file mode 100644 index 1723a15..0000000 --- a/create-dir-structure.sh +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -echo "Creating Robot Market Phase 1 backend structure..." - -# Directories -directories=( - # Applications - "cmd/api" - "cmd/cli" - - # Application bootstrap - "internal/app" - - # CLI commands - "internal/command" - - # Domain features - "internal/user" - "internal/auth" - "internal/catalog" - - # HTTP layer - "internal/http/handler" - "internal/http/route" - "internal/http/middleware" - - # Database layer - "internal/postgres/query" - "internal/postgres/sqlc" - - # Shared packages - "pkg/apperr" - "pkg/log" - - # Integration tests - "integration/auth" - "integration/catalog" - "integration/testutil" - - # Database migrations - "migrations" - - # Scripts - "scripts" -) - -for dir in "${directories[@]}"; do - mkdir -p "$dir" -done - - -# Placeholder files -files=( - # API - "cmd/api/main.go" - - # CLI - "cmd/cli/main.go" - - # App - "internal/app/app.go" - "internal/app/config.go" - - # Commands - "internal/command/root.go" - "internal/command/admin.go" - "internal/command/seed.go" - - # User - "internal/user/domain.go" - "internal/user/repository.go" - "internal/user/errors.go" - - # Auth - "internal/auth/dto.go" - "internal/auth/service.go" - "internal/auth/errors.go" - - # Catalog - "internal/catalog/domain.go" - "internal/catalog/dto.go" - "internal/catalog/service.go" - "internal/catalog/repository.go" - "internal/catalog/errors.go" - - # HTTP - "internal/http/server.go" - "internal/http/config.go" - - # PostgreSQL - "internal/postgres/db.go" - "internal/postgres/user.go" - "internal/postgres/auth.go" - "internal/postgres/catalog.go" - - # sqlc query placeholders - "internal/postgres/query/user.sql" - "internal/postgres/query/auth.sql" - "internal/postgres/query/catalog.sql" - - # Shared packages - "pkg/apperr/error.go" - "pkg/log/log.go" - - # Integration tests - "integration/auth/service_test.go" - "integration/catalog/service_test.go" - - "integration/testutil/postgres.go" - "integration/testutil/migrations.go" - "integration/testutil/fixtures.go" - - # Root files - "sqlc.yaml" - "Makefile" - "go.mod" -) - -for file in "${files[@]}"; do - touch "$file" -done - - -echo "Phase 1 structure created successfully." diff --git a/deploy/docker-compose.dev.yml b/deploy/docker-compose.dev.yml new file mode 100644 index 0000000..0703f06 --- /dev/null +++ b/deploy/docker-compose.dev.yml @@ -0,0 +1,14 @@ +services: + backend: + build: + context: .. + dockerfile: Dockerfile + image: robot-market-backend:dev + environment: + CONFIG_FILE: /etc/robot-market/config.yaml + volumes: + - ../configs/config.example.yaml:/etc/robot-market/config.yaml:ro + ports: + - "${APP_PORT:-8080}:8080" + extra_hosts: + - "host.docker.internal:host-gateway" diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml new file mode 100644 index 0000000..d3b7292 --- /dev/null +++ b/deploy/docker-compose.prod.yml @@ -0,0 +1,15 @@ +services: + backend: + build: + context: .. + dockerfile: Dockerfile + image: ${ROBOT_MARKET_IMAGE:-robot-market-backend:latest} + environment: + CONFIG_FILE: /etc/robot-market/config.yaml + volumes: + - ${CONFIG_FILE:-../configs/config.example.yaml}:/etc/robot-market/config.yaml:ro + ports: + - "${APP_PORT:-8080}:8080" + extra_hosts: + - "host.docker.internal:host-gateway" + restart: unless-stopped diff --git a/go.mod b/go.mod index ca8e2b7..d83351f 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module git.my-rm.com/Robot-Market/shop-backend go 1.27.1 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a62c313 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/integration/auth/service_test.go b/integration/auth/service_test.go index e69de29..426ea7b 100644 --- a/integration/auth/service_test.go +++ b/integration/auth/service_test.go @@ -0,0 +1 @@ +package auth_test diff --git a/integration/catalog/service_test.go b/integration/catalog/service_test.go index e69de29..ca7e353 100644 --- a/integration/catalog/service_test.go +++ b/integration/catalog/service_test.go @@ -0,0 +1 @@ +package catalog_test diff --git a/integration/testutil/fixtures.go b/integration/testutil/fixtures.go index e69de29..110b2e6 100644 --- a/integration/testutil/fixtures.go +++ b/integration/testutil/fixtures.go @@ -0,0 +1 @@ +package testutil diff --git a/integration/testutil/migrations.go b/integration/testutil/migrations.go index e69de29..110b2e6 100644 --- a/integration/testutil/migrations.go +++ b/integration/testutil/migrations.go @@ -0,0 +1 @@ +package testutil diff --git a/integration/testutil/postgres.go b/integration/testutil/postgres.go index e69de29..110b2e6 100644 --- a/integration/testutil/postgres.go +++ b/integration/testutil/postgres.go @@ -0,0 +1 @@ +package testutil diff --git a/internal/app/app.go b/internal/app/app.go index e69de29..4879f7a 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -0,0 +1 @@ +package app diff --git a/internal/app/config.go b/internal/app/config.go index e69de29..3c98da1 100644 --- a/internal/app/config.go +++ b/internal/app/config.go @@ -0,0 +1,84 @@ +package app + +import ( + "errors" + "fmt" + "os" + "strings" + + "gopkg.in/yaml.v3" +) + +const defaultConfigPath = "configs/config.example.yaml" + +// Config contains the settings required to start the application. +type Config struct { + Environment string `yaml:"environment"` + HTTP HTTPConfig `yaml:"http"` + Database DatabaseConfig `yaml:"database"` +} + +type HTTPConfig struct { + Address string `yaml:"address"` +} + +type DatabaseConfig struct { + URL string `yaml:"url"` +} + +// LoadConfig reads configuration from YAML, applies environment overrides, +// and validates the result. An empty path uses CONFIG_FILE when set. +func LoadConfig(path string) (Config, error) { + if path == "" { + path = os.Getenv("CONFIG_FILE") + } + if path == "" { + path = defaultConfigPath + } + + contents, err := os.ReadFile(path) + if err != nil { + return Config{}, fmt.Errorf("read config %q: %w", path, err) + } + + var config Config + if err := yaml.Unmarshal(contents, &config); err != nil { + return Config{}, fmt.Errorf("parse config %q: %w", path, err) + } + + applyEnvironment(&config) + if err := config.Validate(); err != nil { + return Config{}, fmt.Errorf("validate config: %w", err) + } + + return config, nil +} + +func (config Config) Validate() error { + var missing []string + if strings.TrimSpace(config.Environment) == "" { + missing = append(missing, "environment") + } + if strings.TrimSpace(config.HTTP.Address) == "" { + missing = append(missing, "http.address") + } + if strings.TrimSpace(config.Database.URL) == "" { + missing = append(missing, "database.url") + } + if len(missing) > 0 { + return errors.New("missing required values: " + strings.Join(missing, ", ")) + } + return nil +} + +func applyEnvironment(config *Config) { + if value := os.Getenv("APP_ENVIRONMENT"); value != "" { + config.Environment = value + } + if value := os.Getenv("APP_HTTP_ADDRESS"); value != "" { + config.HTTP.Address = value + } + if value := os.Getenv("APP_DATABASE_URL"); value != "" { + config.Database.URL = value + } +} diff --git a/internal/auth/dto.go b/internal/auth/dto.go index e69de29..8832b06 100644 --- a/internal/auth/dto.go +++ b/internal/auth/dto.go @@ -0,0 +1 @@ +package auth diff --git a/internal/auth/errors.go b/internal/auth/errors.go index e69de29..8832b06 100644 --- a/internal/auth/errors.go +++ b/internal/auth/errors.go @@ -0,0 +1 @@ +package auth diff --git a/internal/auth/service.go b/internal/auth/service.go index e69de29..8832b06 100644 --- a/internal/auth/service.go +++ b/internal/auth/service.go @@ -0,0 +1 @@ +package auth diff --git a/internal/catalog/domain.go b/internal/catalog/domain.go index e69de29..e571e24 100644 --- a/internal/catalog/domain.go +++ b/internal/catalog/domain.go @@ -0,0 +1 @@ +package catalog diff --git a/internal/catalog/dto.go b/internal/catalog/dto.go index e69de29..e571e24 100644 --- a/internal/catalog/dto.go +++ b/internal/catalog/dto.go @@ -0,0 +1 @@ +package catalog diff --git a/internal/catalog/errors.go b/internal/catalog/errors.go index e69de29..e571e24 100644 --- a/internal/catalog/errors.go +++ b/internal/catalog/errors.go @@ -0,0 +1 @@ +package catalog diff --git a/internal/catalog/repository.go b/internal/catalog/repository.go index e69de29..e571e24 100644 --- a/internal/catalog/repository.go +++ b/internal/catalog/repository.go @@ -0,0 +1 @@ +package catalog diff --git a/internal/catalog/service.go b/internal/catalog/service.go index e69de29..e571e24 100644 --- a/internal/catalog/service.go +++ b/internal/catalog/service.go @@ -0,0 +1 @@ +package catalog diff --git a/internal/command/admin.go b/internal/command/admin.go index e69de29..d47dcf0 100644 --- a/internal/command/admin.go +++ b/internal/command/admin.go @@ -0,0 +1 @@ +package command diff --git a/internal/command/root.go b/internal/command/root.go index e69de29..d47dcf0 100644 --- a/internal/command/root.go +++ b/internal/command/root.go @@ -0,0 +1 @@ +package command diff --git a/internal/command/seed.go b/internal/command/seed.go index e69de29..d47dcf0 100644 --- a/internal/command/seed.go +++ b/internal/command/seed.go @@ -0,0 +1 @@ +package command diff --git a/internal/http/config.go b/internal/http/config.go index e69de29..d02cfda 100644 --- a/internal/http/config.go +++ b/internal/http/config.go @@ -0,0 +1 @@ +package http diff --git a/internal/http/server.go b/internal/http/server.go index e69de29..d02cfda 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -0,0 +1 @@ +package http diff --git a/internal/postgres/auth.go b/internal/postgres/auth.go index e69de29..bf560be 100644 --- a/internal/postgres/auth.go +++ b/internal/postgres/auth.go @@ -0,0 +1 @@ +package postgres diff --git a/internal/postgres/catalog.go b/internal/postgres/catalog.go index e69de29..bf560be 100644 --- a/internal/postgres/catalog.go +++ b/internal/postgres/catalog.go @@ -0,0 +1 @@ +package postgres diff --git a/internal/postgres/db.go b/internal/postgres/db.go index e69de29..bf560be 100644 --- a/internal/postgres/db.go +++ b/internal/postgres/db.go @@ -0,0 +1 @@ +package postgres diff --git a/internal/postgres/user.go b/internal/postgres/user.go index e69de29..bf560be 100644 --- a/internal/postgres/user.go +++ b/internal/postgres/user.go @@ -0,0 +1 @@ +package postgres diff --git a/internal/user/domain.go b/internal/user/domain.go index e69de29..a00006b 100644 --- a/internal/user/domain.go +++ b/internal/user/domain.go @@ -0,0 +1 @@ +package user diff --git a/internal/user/errors.go b/internal/user/errors.go index e69de29..a00006b 100644 --- a/internal/user/errors.go +++ b/internal/user/errors.go @@ -0,0 +1 @@ +package user diff --git a/internal/user/repository.go b/internal/user/repository.go index e69de29..a00006b 100644 --- a/internal/user/repository.go +++ b/internal/user/repository.go @@ -0,0 +1 @@ +package user diff --git a/pkg/apperr/error.go b/pkg/apperr/error.go index e69de29..683c3c4 100644 --- a/pkg/apperr/error.go +++ b/pkg/apperr/error.go @@ -0,0 +1 @@ +package apperr diff --git a/pkg/log/log.go b/pkg/log/log.go index e69de29..7330d54 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -0,0 +1 @@ +package log