build: support selecting environment config file
This commit is contained in:
16
Makefile
16
Makefile
@@ -2,8 +2,12 @@ WEB_DIR := web
|
|||||||
SDK_DIR := sdk
|
SDK_DIR := sdk
|
||||||
ARCH ?= $(shell go env GOARCH)
|
ARCH ?= $(shell go env GOARCH)
|
||||||
GOOS ?= $(shell go env GOOS)
|
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:
|
web-install:
|
||||||
cd $(WEB_DIR) && npm install
|
cd $(WEB_DIR) && npm install
|
||||||
@@ -18,7 +22,15 @@ sdk-build:
|
|||||||
cd $(SDK_DIR) && npm run build
|
cd $(SDK_DIR) && npm run build
|
||||||
|
|
||||||
build: web-build sdk-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:
|
test:
|
||||||
go test ./...
|
go test ./...
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -20,6 +20,19 @@ make build ARCH=arm64
|
|||||||
|
|
||||||
如需交叉编译其他操作系统,可同时设置 `GOOS`,例如 `make build GOOS=linux 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`,可以通过环境变量覆盖。
|
默认配置位于 `configs/config.yml`,可以通过环境变量覆盖。
|
||||||
|
|
||||||
## Docker Compose 运行
|
## Docker Compose 运行
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ type MultiTableTables struct {
|
|||||||
type LoadOptions struct {
|
type LoadOptions struct {
|
||||||
Environment string
|
Environment string
|
||||||
ConfigDir string
|
ConfigDir string
|
||||||
|
ConfigFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load(options LoadOptions) (Config, error) {
|
func Load(options LoadOptions) (Config, error) {
|
||||||
@@ -105,6 +106,9 @@ func Load(options LoadOptions) (Config, error) {
|
|||||||
if environment == "" {
|
if environment == "" {
|
||||||
environment = normalizeEnvironment(os.Getenv("APP_ENV"))
|
environment = normalizeEnvironment(os.Getenv("APP_ENV"))
|
||||||
}
|
}
|
||||||
|
if options.ConfigFile != "" && environment == "" {
|
||||||
|
environment = normalizeEnvironment(environmentFromConfigFile(options.ConfigFile))
|
||||||
|
}
|
||||||
|
|
||||||
var cfg Config
|
var cfg Config
|
||||||
basePath := filepath.Join(configDir, "config.yml")
|
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)
|
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")
|
profilePath := filepath.Join(configDir, "config."+environment+".yml")
|
||||||
if err := cleanenv.ReadConfig(profilePath, &cfg); err != nil {
|
if err := cleanenv.ReadConfig(profilePath, &cfg); err != nil {
|
||||||
return Config{}, fmt.Errorf("load profile config %s: %w", profilePath, err)
|
return Config{}, fmt.Errorf("load profile config %s: %w", profilePath, err)
|
||||||
@@ -139,6 +151,11 @@ func Load(options LoadOptions) (Config, error) {
|
|||||||
return cfg, nil
|
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 {
|
func normalizeEnvironment(value string) string {
|
||||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||||
case "production", "prod":
|
case "production", "prod":
|
||||||
|
|||||||
@@ -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) {
|
func TestLoadProductionRequiresEnvironmentSecrets(t *testing.T) {
|
||||||
dir := writeConfigs(t, "app:\n env: production\n")
|
dir := writeConfigs(t, "app:\n env: production\n")
|
||||||
t.Setenv("APP_DATABASE_PASSWORD", "")
|
t.Setenv("APP_DATABASE_PASSWORD", "")
|
||||||
|
|||||||
3
main.go
3
main.go
@@ -33,9 +33,10 @@ var sdkFiles embed.FS
|
|||||||
func main() {
|
func main() {
|
||||||
environment := flag.String("env", "", "runtime environment: development, test, prod")
|
environment := flag.String("env", "", "runtime environment: development, test, prod")
|
||||||
configDir := flag.String("config-dir", "configs", "configuration directory")
|
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()
|
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 {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "load configuration: %v\n", err)
|
fmt.Fprintf(os.Stderr, "load configuration: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user