feat: load explicit configuration references and remove implicit env overlays

This commit is contained in:
2026-09-26 16:14:54 +08:00
parent 4bf6e88cfe
commit d598b2ebee
94 changed files with 1355 additions and 1759 deletions

View File

@@ -18,13 +18,12 @@ go-omni migrate --dir migrations --apply
go run ./cmd/server
```
源码示例的 go.mod 使用 `replace ... => ../..`。生成代码使用 `v0.8.0-preview` 响应策略入口。
源码示例的 go.mod 使用 `replace ... => ../..`。生成代码使用 `v0.22.0-preview` 显式配置加载入口。
复制到独立仓库时删除 replace,保留同版本固定依赖,再执行 go mod tidy;CLI 也固定到同一版本。
远程版本验证脚本会检查没有本地替换。
`GO_OMNI_TRANSPORT=native` 可切换为 native,业务代码无需调整。
配置是默认值 → etc/config.yaml → 环境变量。日志默认写入 var/log/server.log 并轮转,
`GO_OMNI_LOG_OUTPUT=stderr` 可以切换为标准错误输出。MySQL 默认最多 10 条连接。
配置是 Default() → etc/config.yaml 的显式引用 → 类型绑定与校验。文件里的固定值不会被环境自动覆盖。日志默认写入 var/log/server.log 并轮转;要改用 stderr,在文件明确设置 log.file 为空、log.rotation.enabled 为 false、log.output 为 stderr。MySQL 默认最多 10 条连接。
应用检查 users 和 user_audit 表,缺失时拒绝启动,不自动改表。
## 接口

View File

@@ -1,6 +1,7 @@
# Non-secret settings only. Environment variables override these values.
# Defaults are overridden only by fields in this file.
server:
address: '127.0.0.1:8080'
transport: "${GO_OMNI_TRANSPORT:-nethttp}"
address: "${GO_OMNI_HTTP_ADDR:-127.0.0.1:8080}"
enableLogger: true
slowRequestThreshold: 500ms
shutdownTimeout: 10s
@@ -17,16 +18,19 @@ server:
compress: true
redactKeys: []
storage:
filesToken: "${GO_OMNI_FILES_TOKEN:-}"
signingKey: "${GO_OMNI_STORAGE_SIGNING_KEY}"
provider: local
maxBytes: 33554432
contentTypes: ['text/plain', 'image/png', 'image/jpeg', 'application/pdf']
linkTtl: 5m
requestTimeout: 30s
root: './var/files'
publicUrl: 'http://127.0.0.1:8080/storage/object'
publicUrl: "${GO_OMNI_STORAGE_PUBLIC_URL:-http://127.0.0.1:8080/storage/object}"
database:
dsn: "${GO_OMNI_DB_DSN}"
driver: mysql
maxOpenConns: 10
maxIdleConns: 5

View File

@@ -3,7 +3,7 @@ module example.com/userService
go 1.22.0
require (
git.yuntuo.hk/k2safe/go-omni v0.8.0-preview
git.yuntuo.hk/k2safe/go-omni v0.22.0-preview
github.com/go-sql-driver/mysql v1.9.3
)

View File

@@ -15,10 +15,9 @@ func TestTransactions(t *testing.T) {
if os.Getenv("USER_SERVICE_TEST_DATABASE") != "1" {
t.Skip("isolated MySQL required")
}
cfg, err := database.ConfigFromEnv("")
if err != nil {
t.Fatal(err)
}
cfg := database.DefaultConfig()
cfg.Driver = "mysql"
cfg.DSN = os.Getenv("GO_OMNI_DB_DSN")
db, err := database.Open(context.Background(), cfg)
if err != nil {
t.Fatal(err)

View File

@@ -3,14 +3,11 @@ package settings
import (
"errors"
"fmt"
"git.yuntuo.hk/k2safe/go-omni/config"
"git.yuntuo.hk/k2safe/go-omni/database"
"git.yuntuo.hk/k2safe/go-omni/logx"
"git.yuntuo.hk/k2safe/go-omni/storage"
"net/url"
"os"
"strconv"
"strings"
"time"
)
@@ -38,40 +35,7 @@ func Default() Config {
}}
}
// Load uses defaults -> strict YAML/JSON -> nonempty environment variables.
// Pass an empty path to use only defaults and environment.
func Load(path string) (Config, error) {
cfg := Default()
var err error
if path != "" {
cfg, err = config.DecodeFile(path, cfg)
if err != nil {
return Config{}, err
}
}
cfg.Server, err = config.ApplyEnv(cfg.Server, "")
if err != nil {
return Config{}, err
}
// A stream explicitly selected by environment overrides a file profile.
if output := strings.TrimSpace(os.Getenv("GO_OMNI_LOG_OUTPUT")); output != "" && strings.TrimSpace(os.Getenv("GO_OMNI_LOG_FILE")) == "" {
cfg.Server.Log.File = ""
cfg.Server.Log.Rotation = logx.RotationConfig{}
}
if err = cfg.Storage.applyEnv(); err != nil {
return Config{}, err
}
cfg.Database, err = database.ApplyEnv(cfg.Database, "")
if err != nil {
return Config{}, err
}
if err = cfg.Validate(); err != nil {
return Config{}, err
}
return cfg, nil
}
func Load(path string) (Config, error) { return config.LoadProject(path, Default()) }
func (c Config) Validate() error {
if err := c.Database.Validate(); err != nil {
@@ -108,62 +72,17 @@ type StorageConfig struct {
ContentTypes []string `json:"contentTypes"`
LinkTTL time.Duration `json:"linkTtl"`
RequestTimeout time.Duration `json:"requestTimeout"`
// Secrets are environment-only and cannot be populated by config files.
FilesToken string `json:"-"`
SigningKey string `json:"-"`
AccessKey string `json:"-"`
SecretKey string `json:"-"`
SessionToken string `json:"-"`
// The file explicitly selects secret environment references.
FilesToken string `json:"filesToken"`
SigningKey string `json:"signingKey"`
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
SessionToken string `json:"sessionToken"`
}
func (s StorageConfig) Policy() storage.UploadPolicy {
return storage.UploadPolicy{MaxBytes: s.MaxBytes, ContentTypes: s.ContentTypes}
}
func (s *StorageConfig) applyEnv() error {
values := map[string]*string{
"GO_OMNI_STORAGE_ROOT": &s.Root, "GO_OMNI_STORAGE_PUBLIC_URL": &s.PublicURL,
"GO_OMNI_STORAGE_ENDPOINT": &s.Endpoint, "GO_OMNI_STORAGE_REGION": &s.Region,
"GO_OMNI_STORAGE_BUCKET": &s.Bucket, "GO_OMNI_STORAGE_ACCOUNT_ID": &s.AccountID,
"GO_OMNI_STORAGE_SIGNING_KEY": &s.SigningKey, "GO_OMNI_FILES_TOKEN": &s.FilesToken,
"GO_OMNI_STORAGE_ACCESS_KEY": &s.AccessKey, "GO_OMNI_STORAGE_SECRET_KEY": &s.SecretKey,
"GO_OMNI_STORAGE_SESSION_TOKEN": &s.SessionToken,
}
for name, target := range values {
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
*target = value
}
}
for name, target := range map[string]*time.Duration{"GO_OMNI_STORAGE_LINK_TTL": &s.LinkTTL, "GO_OMNI_STORAGE_REQUEST_TIMEOUT": &s.RequestTimeout} {
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
v, err := time.ParseDuration(value)
if err != nil {
return fmt.Errorf("%s must be a duration", name)
}
*target = v
}
}
if value := os.Getenv("GO_OMNI_STORAGE_MAX_BYTES"); value != "" {
v, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return errors.New("GO_OMNI_STORAGE_MAX_BYTES must be an integer")
}
s.MaxBytes = v
}
if value := os.Getenv("GO_OMNI_STORAGE_SECURE"); value != "" {
v, err := strconv.ParseBool(value)
if err != nil {
return errors.New("GO_OMNI_STORAGE_SECURE must be a boolean")
}
s.Secure = v
}
if value := strings.TrimSpace(os.Getenv("GO_OMNI_STORAGE_CONTENT_TYPES")); value != "" {
s.ContentTypes = strings.Split(value, ",")
for i := range s.ContentTypes {
s.ContentTypes[i] = strings.TrimSpace(s.ContentTypes[i])
}
}
return nil
}
func (s StorageConfig) Validate() error {
if s.Provider != "local" {
return errors.New("storage.provider must match the adapter selected at generation")
@@ -186,7 +105,7 @@ func (s StorageConfig) Validate() error {
return errors.New("storage.publicUrl must be an absolute HTTP(S) URL ending in /storage/object")
}
if len(s.SigningKey) < 32 {
return errors.New("set GO_OMNI_STORAGE_SIGNING_KEY to a stable secret of at least 32 bytes")
return errors.New("storage.signingKey requires a stable secret of at least 32 bytes")
}
return nil