52857d1d55
- internal/config: rewrite Config with PANGOLIN_ prefix fields (HTTPAddr, AdminAddr, GRPCAddr, MySQL DSN/fields, RedisAddr, AutoMigrate, JWTSecret placeholder, WebhookSecret); Load() replaces FromEnv(); missing required MySQL vars return named-field error. - internal/store/mysql.go: single Open(cfg) entry point; uses mysql.ParseDSN to structurally override ParseTime=true, Loc=UTC, Collation=utf8mb4_unicode_ci, Params[time_zone]='+00:00'; asserts SELECT @@session.time_zone=+00:00 after Ping (startup fatal). - migrations/embed.go: //go:embed *.sql exposes var FS embed.FS. - internal/store/migrate.go: MigrateUp/MigrateDown/MigrateVersion backed by golang-migrate iofs source + mysql driver; ErrNoChange treated as success. - cmd/migrate/main.go: filled — up/down/version subcommands, reads config.Load() + store.Open. - cmd/server/main.go: startup sequence Load → store.Open (UTC assert) → MigrateUp (if PANGOLIN_AUTO_MIGRATE=true) → HTTP listen; structured slog output at each step. - internal/store/mysql_test.go: pure-function unit tests for buildDSN (empty-fields case + conflicting params overridden case); both pass. - internal/store/mysql_integration_test.go: //go:build integration; testcontainers mysql:8 — UTC assertion + MigrateUp idempotency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
108 lines
3.4 KiB
Go
108 lines
3.4 KiB
Go
// Package config loads and validates server configuration from environment
|
|
// variables and optional config files. It provides a single Config struct
|
|
// consumed by all other packages at startup.
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
)
|
|
|
|
// Config holds all application-level configuration.
|
|
// All environment variables use the PANGOLIN_ prefix (12-factor app style).
|
|
type Config struct {
|
|
// HTTPAddr is the public HTTP API listen address.
|
|
// Env: PANGOLIN_HTTP_ADDR Default: :8080
|
|
HTTPAddr string
|
|
|
|
// AdminAddr is the internal admin gRPC listen address.
|
|
// Placeholder for task #5.
|
|
// Env: PANGOLIN_ADMIN_ADDR Default: :9090
|
|
AdminAddr string
|
|
|
|
// GRPCAddr is the data-plane gRPC listen address.
|
|
// Placeholder for task #7.
|
|
// Env: PANGOLIN_GRPC_ADDR Default: :9091
|
|
GRPCAddr string
|
|
|
|
// MySQL connection. Set either MySQLDSN or the individual fields.
|
|
// If MySQLDSN is set the individual fields are ignored.
|
|
MySQLDSN string // PANGOLIN_MYSQL_DSN
|
|
MySQLHost string // PANGOLIN_MYSQL_HOST (required when no DSN)
|
|
MySQLPort string // PANGOLIN_MYSQL_PORT Default: 3306
|
|
MySQLUser string // PANGOLIN_MYSQL_USER (required when no DSN)
|
|
MySQLPassword string // PANGOLIN_MYSQL_PASSWORD
|
|
MySQLDBName string // PANGOLIN_MYSQL_DBNAME (required when no DSN)
|
|
|
|
// RedisAddr is the Redis server address.
|
|
// Env: PANGOLIN_REDIS_ADDR Default: 127.0.0.1:6379
|
|
RedisAddr string
|
|
|
|
// AutoMigrate controls whether database migrations run at startup.
|
|
// Env: PANGOLIN_AUTO_MIGRATE=true|false Default: false
|
|
AutoMigrate bool
|
|
|
|
// JWTSecret is the HS256 signing key.
|
|
// Placeholder for task #2 (auth middleware).
|
|
// Env: PANGOLIN_JWT_SECRET
|
|
JWTSecret string
|
|
|
|
// WebhookSecret is the HMAC-SHA256 shared secret for card-store webhooks.
|
|
// Env: PANGOLIN_WEBHOOK_SECRET
|
|
WebhookSecret string
|
|
}
|
|
|
|
// Load reads Config from environment variables.
|
|
// Returns an error listing any missing required variables.
|
|
func Load() (Config, error) {
|
|
c := Config{
|
|
HTTPAddr: envOr("PANGOLIN_HTTP_ADDR", ":8080"),
|
|
AdminAddr: envOr("PANGOLIN_ADMIN_ADDR", ":9090"),
|
|
GRPCAddr: envOr("PANGOLIN_GRPC_ADDR", ":9091"),
|
|
MySQLDSN: os.Getenv("PANGOLIN_MYSQL_DSN"),
|
|
MySQLHost: os.Getenv("PANGOLIN_MYSQL_HOST"),
|
|
MySQLPort: envOr("PANGOLIN_MYSQL_PORT", "3306"),
|
|
MySQLUser: os.Getenv("PANGOLIN_MYSQL_USER"),
|
|
MySQLPassword: os.Getenv("PANGOLIN_MYSQL_PASSWORD"),
|
|
MySQLDBName: os.Getenv("PANGOLIN_MYSQL_DBNAME"),
|
|
RedisAddr: envOr("PANGOLIN_REDIS_ADDR", "127.0.0.1:6379"),
|
|
JWTSecret: os.Getenv("PANGOLIN_JWT_SECRET"),
|
|
WebhookSecret: os.Getenv("PANGOLIN_WEBHOOK_SECRET"),
|
|
}
|
|
|
|
if v := os.Getenv("PANGOLIN_AUTO_MIGRATE"); v != "" {
|
|
b, err := strconv.ParseBool(v)
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("config: PANGOLIN_AUTO_MIGRATE=%q: %w", v, err)
|
|
}
|
|
c.AutoMigrate = b
|
|
}
|
|
|
|
// Validate: either full DSN or individual connection fields must be set.
|
|
if c.MySQLDSN == "" {
|
|
var missing []string
|
|
if c.MySQLHost == "" {
|
|
missing = append(missing, "PANGOLIN_MYSQL_HOST")
|
|
}
|
|
if c.MySQLUser == "" {
|
|
missing = append(missing, "PANGOLIN_MYSQL_USER")
|
|
}
|
|
if c.MySQLDBName == "" {
|
|
missing = append(missing, "PANGOLIN_MYSQL_DBNAME")
|
|
}
|
|
if len(missing) > 0 {
|
|
return Config{}, fmt.Errorf("config: required env vars not set: %v (or set PANGOLIN_MYSQL_DSN)", missing)
|
|
}
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
func envOr(key, def string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|