From f93d0939282ec97f573573a6ccc9adad5a2f2da7 Mon Sep 17 00:00:00 2001 From: "Eric 1549169735@qq.com" <1549169735@qq.com> Date: Wed, 19 Aug 2026 22:01:54 +0800 Subject: [PATCH] build: support selecting environment config file --- Makefile | 16 ++++++++++++++-- README.md | 13 +++++++++++++ internal/config/config.go | 19 ++++++++++++++++++- internal/config/config_test.go | 18 ++++++++++++++++++ main.go | 3 ++- 5 files changed, 65 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 3de5c3f..d4c0997 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,12 @@ WEB_DIR := web SDK_DIR := sdk ARCH ?= $(shell go env GOARCH) GOOS ?= $(shell go env GOOS) +CONFIG_DIR ?= configs +CONFIG_FILE ?= +ENV_FILE ?= config.yml +BUILD_DIR ?= bin -.PHONY: web-install web-build sdk-install sdk-build build test vet check seed-pet-doctor docker-config docker-up docker-down docker-logs +.PHONY: web-install web-build sdk-install sdk-build build run test vet check seed-pet-doctor docker-config docker-up docker-down docker-logs web-install: cd $(WEB_DIR) && npm install @@ -18,7 +22,15 @@ sdk-build: cd $(SDK_DIR) && npm run build build: web-build sdk-build - CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(ARCH) go build -o bin/main . + @test -f $(CONFIG_DIR)/config.yml || (echo "missing base config: $(CONFIG_DIR)/config.yml"; exit 1) + @test -f $(CONFIG_DIR)/$(ENV_FILE) || (echo "missing environment file: $(CONFIG_DIR)/$(ENV_FILE)"; exit 1) + mkdir -p $(BUILD_DIR)/configs + cp $(CONFIG_DIR)/config.yml $(BUILD_DIR)/configs/config.yml + @if [ "$(ENV_FILE)" != "config.yml" ]; then cp $(CONFIG_DIR)/$(ENV_FILE) $(BUILD_DIR)/configs/$(ENV_FILE); fi + CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(ARCH) go build -o $(BUILD_DIR)/main . + +run: build + ./$(BUILD_DIR)/main -config-dir $(if $(CONFIG_FILE),$(CONFIG_DIR),$(BUILD_DIR)/configs) $(if $(CONFIG_FILE),-config-file $(CONFIG_FILE),) test: go test ./... diff --git a/README.md b/README.md index 899a30c..a7263a0 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,19 @@ make build ARCH=arm64 如需交叉编译其他操作系统,可同时设置 `GOOS`,例如 `make build GOOS=linux ARCH=arm64`。 +构建时可以指定随产物携带的环境文件。基础配置和该文件会复制到 `bin/configs/`: + +```bash +make build ENV_FILE=config.test.yml +make build ENV_FILE=config.prod.yml ARCH=arm64 +``` + +使用构建产物启动指定环境时,环境变量仍会覆盖文件中的配置: + +```bash +./bin/main -config-dir bin/configs -env prod +``` + 默认配置位于 `configs/config.yml`,可以通过环境变量覆盖。 ## Docker Compose 运行 diff --git a/internal/config/config.go b/internal/config/config.go index 37f334e..50a8209 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -93,6 +93,7 @@ type MultiTableTables struct { type LoadOptions struct { Environment string ConfigDir string + ConfigFile string } func Load(options LoadOptions) (Config, error) { @@ -105,6 +106,9 @@ func Load(options LoadOptions) (Config, error) { if environment == "" { environment = normalizeEnvironment(os.Getenv("APP_ENV")) } + if options.ConfigFile != "" && environment == "" { + environment = normalizeEnvironment(environmentFromConfigFile(options.ConfigFile)) + } var cfg Config basePath := filepath.Join(configDir, "config.yml") @@ -112,7 +116,15 @@ func Load(options LoadOptions) (Config, error) { return Config{}, fmt.Errorf("load base config %s: %w", basePath, err) } - if environment == "test" || environment == "prod" { + if options.ConfigFile != "" { + profilePath := options.ConfigFile + if !filepath.IsAbs(profilePath) { + profilePath = filepath.Join(configDir, profilePath) + } + if err := cleanenv.ReadConfig(profilePath, &cfg); err != nil { + return Config{}, fmt.Errorf("load config file %s: %w", profilePath, err) + } + } else if environment == "test" || environment == "prod" { profilePath := filepath.Join(configDir, "config."+environment+".yml") if err := cleanenv.ReadConfig(profilePath, &cfg); err != nil { return Config{}, fmt.Errorf("load profile config %s: %w", profilePath, err) @@ -139,6 +151,11 @@ func Load(options LoadOptions) (Config, error) { return cfg, nil } +func environmentFromConfigFile(path string) string { + name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) + return strings.TrimPrefix(name, "config.") +} + func normalizeEnvironment(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case "production", "prod": diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a6f4ef9..0ff0917 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -51,6 +51,24 @@ auth: } } +func TestLoadExplicitConfigFile(t *testing.T) { + dir := writeConfigs(t, ` +app: + env: test +server: + port: 8081 +database: + name: explicit_db +`) + cfg, err := Load(LoadOptions{ConfigDir: dir, ConfigFile: "config.test.yml"}) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if cfg.App.Env != "test" || cfg.Server.Port != 8081 || cfg.Database.Name != "explicit_db" || cfg.Database.User != "base_user" { + t.Fatalf("unexpected explicit config: %+v", cfg) + } +} + func TestLoadProductionRequiresEnvironmentSecrets(t *testing.T) { dir := writeConfigs(t, "app:\n env: production\n") t.Setenv("APP_DATABASE_PASSWORD", "") diff --git a/main.go b/main.go index bb0ef58..5fb27b0 100644 --- a/main.go +++ b/main.go @@ -33,9 +33,10 @@ var sdkFiles embed.FS func main() { environment := flag.String("env", "", "runtime environment: development, test, prod") configDir := flag.String("config-dir", "configs", "configuration directory") + configFile := flag.String("config-file", "", "explicit configuration file; relative paths are resolved from config-dir") flag.Parse() - cfg, err := config.Load(config.LoadOptions{Environment: *environment, ConfigDir: *configDir}) + cfg, err := config.Load(config.LoadOptions{Environment: *environment, ConfigDir: *configDir, ConfigFile: *configFile}) if err != nil { fmt.Fprintf(os.Stderr, "load configuration: %v\n", err) os.Exit(1)