feat: load explicit configuration references and remove implicit env overlays
This commit is contained in:
@@ -10,11 +10,13 @@ DEMO_CLIENT_TIMEOUT=500ms DEMO_MAX_ITEMS=0 go run ./examples/configuration
|
||||
DEMO_MAX_ITEMS=wrong go run ./examples/configuration
|
||||
```
|
||||
|
||||
前三次都输出 `timeout=2s maxItems=100 allowed=[json xml]`。环境覆盖允许显式零值。
|
||||
前三次都输出 `timeout=2s maxItems=100 allowed=[json xml]`。文件显式引用允许零值。
|
||||
最后一条输出定位 `maxItems` / `DEMO_MAX_ITEMS` 的安全错误,不打印输入原值。
|
||||
也可 `-config /path/to/app.yaml` 读取自己的文件。示例没有建立网络连接。
|
||||
|
||||
`Config` 定义字段,`envDefault` 定义缺省值,`Validate` 用 `config.Invalid` 返回字段与公开原因。
|
||||
`Config` 定义字段,`Default()` 定义初始值,`Validate` 用 `config.Invalid` 返回字段与公开原因。
|
||||
文件中的 `${NAME}` 必填,`${NAME:-value}` 支持空/未设置的备用值,`$${NAME}` 保留字面量;字符串不裁剪,也不二次展开。没有出现在文件引用中的变量不会被读取。
|
||||
|
||||
`newLoader` 额外注册 `.custom`(演示复用 JSON decoder);同一个 `load` 可读取所有格式。
|
||||
JSON/YAML/XML 直接可用,无需注册。新增格式只修改解析器注册,不改业务结构或校验。
|
||||
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
{
|
||||
"client": {"endpoint": "https://example.com", "timeout": "2s"},
|
||||
"maxItems": 100,
|
||||
"allowed": ["json", "xml"]
|
||||
}
|
||||
{"client":{"endpoint":"https://${DEMO_HOST:-example.com}","timeout":"${DEMO_CLIENT_TIMEOUT:-2s}","token":"${DEMO_CLIENT_TOKEN:-}"},"maxItems":"${DEMO_MAX_ITEMS:-100}","allowed":["json","xml"]}
|
||||
|
||||
@@ -1,8 +1 @@
|
||||
<config>
|
||||
<client>
|
||||
<endpoint>https://example.com</endpoint>
|
||||
<timeout>2s</timeout>
|
||||
</client>
|
||||
<maxItems>100</maxItems>
|
||||
<allowed type="array"><item>json</item><item>xml</item></allowed>
|
||||
</config>
|
||||
<config><client><endpoint>https://${DEMO_HOST:-example.com}</endpoint><timeout>${DEMO_CLIENT_TIMEOUT:-2s}</timeout><token>${DEMO_CLIENT_TOKEN:-}</token></client><maxItems>${DEMO_MAX_ITEMS:-100}</maxItems><allowed type="array"><item>json</item><item>xml</item></allowed></config>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
client:
|
||||
endpoint: https://example.com
|
||||
timeout: 2s
|
||||
maxItems: 100
|
||||
endpoint: "https://${DEMO_HOST:-example.com}"
|
||||
timeout: "${DEMO_CLIENT_TIMEOUT:-2s}"
|
||||
token: "${DEMO_CLIENT_TOKEN:-}"
|
||||
maxItems: "${DEMO_MAX_ITEMS:-100}"
|
||||
allowed: [json, xml]
|
||||
|
||||
@@ -17,16 +17,18 @@ var examples embed.FS
|
||||
|
||||
type ClientConfig struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Timeout time.Duration `json:"timeout" envDefault:"3s"`
|
||||
// This field is deliberately unavailable to configuration files.
|
||||
Token string `json:"-" env:"DEMO_CLIENT_TOKEN,secret"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
// The file chooses a fixed value or an explicit environment reference.
|
||||
Token string `json:"token"`
|
||||
}
|
||||
type Config struct {
|
||||
Client ClientConfig `json:"client"`
|
||||
MaxItems int `json:"maxItems" envDefault:"20"`
|
||||
MaxItems int `json:"maxItems"`
|
||||
Allowed []string `json:"allowed"`
|
||||
}
|
||||
|
||||
func Default() Config { return Config{Client: ClientConfig{Timeout: 3 * time.Second}, MaxItems: 20} }
|
||||
|
||||
func (c Config) Validate() error {
|
||||
if c.Client.Timeout <= 0 {
|
||||
return config.Invalid("client.timeout", "must be positive")
|
||||
@@ -48,7 +50,7 @@ func load(path string) (Config, error) {
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return config.LoadProject(path, Config{}, config.EnvOptions{Prefix: "DEMO_", Empty: "reject"}, loader)
|
||||
return config.LoadProject(path, Default(), loader)
|
||||
}
|
||||
func run() error {
|
||||
path := flag.String("config", "", "external configuration file")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"generatorConfig": true,
|
||||
"templateVersion": "v0.20.0-preview",
|
||||
"templateVersion": "v0.22.0-preview",
|
||||
"profile": {
|
||||
"database": "none",
|
||||
"storage": "none",
|
||||
@@ -12,7 +12,7 @@
|
||||
"entry": "api/managed.api",
|
||||
"generated": {
|
||||
"docs/openapi.json": "0370fd6bd6a387e2ef21f6b42b009ce66749846b8bea3d481009a34f3af039a5",
|
||||
"internal/config/loadGen.go": "5d630b894abdbbd66ddfa7d7d30f7573fc4ff995020a32a9f9157cd4af3a5655",
|
||||
"internal/config/loadGen.go": "d85e3123e4c96c95cb0886e60e4760230426efca4d2bbbfc981822da08b90d13",
|
||||
"internal/handler/appGen.go": "f5251b4dc33e2312b0bdb71ead70d2171977d851265e124f640458d705a2cc9d",
|
||||
"internal/handler/demo/handlersGen.go": "3c76d85dbe89eb5127d9d134dc625203ecf9719dd11d174c1f335691b99e1375",
|
||||
"internal/handler/openapiGen.go": "f24cab3abbb8557a3fc4dd93fba23cc0b093f3e0c944d3ba1bd5b0eeff2d96f3",
|
||||
@@ -22,7 +22,7 @@
|
||||
"internal/svc/runtimeGen.go": "29f6afb6614881935099c1845ecb3f7902ee773caa077940f566cb8dfdbcdbb1",
|
||||
"internal/types/dtoManagedd28645a2af1cGen.go": "7586e86fa5d67051283178cc578c890f6c02fd2d5c383170a71a39c2e77ce344",
|
||||
"internal/types/typesGen.go": "372d67f23a313b3e77d2a3b7a236315db9721bc63350bf1f71db1293e57570d1",
|
||||
"main.go": "64e26d97de7e7c6c17f47186785edc73d43c23d6c20b50bfc7fff6ef2e914f17"
|
||||
"main.go": "10cb6a84b2eb1b68dc10c2dca949c9dd7c68a9f78a8e6d8f56ce9336d4b2d125"
|
||||
},
|
||||
"contract": {
|
||||
"routes": {
|
||||
|
||||
@@ -46,6 +46,6 @@ Configuration, API contracts, svc fields/Init/AppOptions, Logic, Repository and
|
||||
middleware implementations remain developer-owned. Regeneration preserves them.
|
||||
Use `go-omni api format` to format local contracts, then `api generate` and
|
||||
`api generate --check`. See the [framework development guide](../../DEVELOPMENT.md)
|
||||
for array/null responses, typed multipart, full configuration environment overrides,
|
||||
for array/null responses, typed multipart, explicit configuration references and -check-config,
|
||||
shared contracts and v0.13 upgrade steps. This example keeps its raw callback,
|
||||
streaming upload and download to exercise those existing extension points.
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
# Non-secret settings only. Environment variables override these values.
|
||||
server:
|
||||
address: '127.0.0.1:8080'
|
||||
enableLogger: true
|
||||
slowRequestThreshold: 500ms
|
||||
shutdownTimeout: 10s
|
||||
endpoints:
|
||||
liveness: {enabled: true, path: /health/live, responseMode: app}
|
||||
readiness: {enabled: true, path: /health/ready, responseMode: app}
|
||||
openapi: {enabled: true, path: /openapi.json}
|
||||
log:
|
||||
service: "example.com/managed"
|
||||
level: info
|
||||
format: json
|
||||
output: stderr
|
||||
redactKeys: []
|
||||
address: "${GO_OMNI_HTTP_ADDR:-127.0.0.1:8080}"
|
||||
transport: "${GO_OMNI_TRANSPORT:-nethttp}"
|
||||
|
||||
@@ -2,7 +2,7 @@ module example.com/managed
|
||||
|
||||
go 1.22.0
|
||||
|
||||
require git.yuntuo.hk/k2safe/go-omni v0.21.0-preview
|
||||
require git.yuntuo.hk/k2safe/go-omni v0.22.0-preview
|
||||
|
||||
require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
|
||||
@@ -3,14 +3,12 @@ package config
|
||||
|
||||
import omniConfig "git.yuntuo.hk/k2safe/go-omni/config"
|
||||
|
||||
// Load is maintained by the generator. Extend Config/Default/Validate in
|
||||
// config.go, or NewLoader in loader.go; do not duplicate the loading pipeline.
|
||||
// Load resolves explicit file references, binds onto Default and validates.
|
||||
// Ordinary projects only edit Config, Default, Validate and the selected file.
|
||||
func Load(path string) (Config, error) {
|
||||
loader, err := NewLoader()
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return omniConfig.LoadProject(path, Default(), omniConfig.EnvOptions{
|
||||
Prefix: "GO_OMNI_", Empty: "ignore", Aliases: omniConfig.ServerEnvAliases("GO_OMNI_", "server"),
|
||||
}, loader)
|
||||
return omniConfig.LoadProject(path, Default(), loader)
|
||||
}
|
||||
|
||||
@@ -17,8 +17,15 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
path := flag.String("config", "etc/config.yaml", "configuration file; empty uses defaults and environment")
|
||||
path := flag.String("config", "etc/config.yaml", "configuration file; empty uses only Default()")
|
||||
check := flag.Bool("check-config", false, "validate configuration and exit without starting dependencies")
|
||||
flag.Parse()
|
||||
if *check {
|
||||
if _, err := config.Load(*path); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
if err := run(ctx, *path); err != nil {
|
||||
|
||||
@@ -7,7 +7,7 @@ go run ./examples/modules
|
||||
```
|
||||
|
||||
默认监听 `127.0.0.1:8080`,使用仅限开发的 `Bearer dev-token`。
|
||||
配置 `GO_OMNI_TRANSPORT=native` 可直接切换自研网络层,TLS 配置与主框架一致。
|
||||
默认读取 `examples/modules/config.yaml`,也可用 `-config` 选择一个其他文件。文件显式引用了 `GO_OMNI_TRANSPORT`,设置为 native 可切换自研网络层;TLS 在文件的 `tls.certFile` / `tls.keyFile` 中配置。此示例直接加载框架 Config,没有生成项目的 server 外层。
|
||||
|
||||
```sh
|
||||
curl -H 'Authorization: Bearer dev-token' http://127.0.0.1:8080/api/catalog
|
||||
|
||||
2
examples/modules/config.yaml
Normal file
2
examples/modules/config.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
address: "${GO_OMNI_HTTP_ADDR:-127.0.0.1:8080}"
|
||||
transport: "${GO_OMNI_TRANSPORT:-nethttp}"
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -106,13 +107,11 @@ func newApp(settings omni.Config, token, upstream string) (application *omni.App
|
||||
}
|
||||
|
||||
func run(ctx context.Context) error {
|
||||
settings, err := omni.LoadConfig("")
|
||||
settings, err := omni.LoadConfig(*configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if os.Getenv("GO_OMNI_HTTP_ADDR") == "" {
|
||||
settings.Address = "127.0.0.1:8080"
|
||||
}
|
||||
|
||||
token := os.Getenv("GO_OMNI_DEV_TOKEN")
|
||||
if token == "" {
|
||||
token = "dev-token"
|
||||
@@ -124,7 +123,10 @@ func run(ctx context.Context) error {
|
||||
return application.RunContext(ctx)
|
||||
}
|
||||
|
||||
var configPath = flag.String("config", "examples/modules/config.yaml", "configuration file")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
if err := run(ctx); err != nil {
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestModulesConfiguredTransports(t *testing.T) {
|
||||
io.WriteString(w, `{"name":"upstream catalog"}`)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
settings, err := omni.LoadConfig("MODULES_TEST_")
|
||||
settings, err := omni.LoadConfig("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -88,16 +88,9 @@ func newApp(cfg omni.Config, d dependencies) (*omni.App, error) {
|
||||
return app, nil
|
||||
}
|
||||
func run(ctx context.Context, configFile, root string) error {
|
||||
var cfg config.Config
|
||||
var err error
|
||||
if configFile != "" {
|
||||
cfg, err = config.LoadFile(configFile, "")
|
||||
} else {
|
||||
cfg, err = config.LoadFromEnv("")
|
||||
if os.Getenv("GO_OMNI_HTTP_ADDR") == "" {
|
||||
cfg.Address = "127.0.0.1:8080"
|
||||
}
|
||||
}
|
||||
defaults := config.Default()
|
||||
defaults.Address = "127.0.0.1:8080"
|
||||
cfg, err := config.LoadProject(configFile, defaults)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
go run ./examples/reference
|
||||
```
|
||||
|
||||
默认监听 `127.0.0.1:8080`,可通过环境变量调整:
|
||||
默认读取 `examples/reference/config.yaml`,监听 `127.0.0.1:8080`。文件已显式引用地址变量:
|
||||
|
||||
```sh
|
||||
GO_OMNI_ADDR=127.0.0.1:8081 go run ./examples/reference
|
||||
GO_OMNI_HTTP_ADDR=127.0.0.1:8081 go run ./examples/reference
|
||||
```
|
||||
|
||||
如果终端提示找不到 `go`,需要先把 Go 的 `bin` 目录加入 `PATH`。不要与 `examples/basic` 同时占用同一个端口。
|
||||
@@ -133,14 +133,10 @@ curl -i http://127.0.0.1:8080/api/v1/users/1
|
||||
- 列表按 ID 排序、返回总数,空列表使用 `[]`。
|
||||
- 使用传入的 `context.Context` 执行查询,保留创建时间,修改时更新时间。
|
||||
|
||||
MySQL 运行配置通过环境变量传入:
|
||||
此手写示例显式读取 `GO_OMNI_DB_DSN`;驱动固定 mysql,连接池使用 database.DefaultConfig()。项目可将数据库配置放入自己的 Config 并通过 LoadProject 统一加载(见 [配置指南](../../CONFIG.md)),框架不再自动读取数据库环境变量:
|
||||
|
||||
```sh
|
||||
export GO_OMNI_DB_DRIVER=mysql
|
||||
export GO_OMNI_DB_DSN='root:password@tcp(127.0.0.1:3306)/go_omni?parseTime=true&loc=UTC'
|
||||
export GO_OMNI_DB_MAX_OPEN_CONNS=10
|
||||
export GO_OMNI_DB_MAX_IDLE_CONNS=5
|
||||
export GO_OMNI_DB_PING_TIMEOUT=5s
|
||||
go run ./examples/reference
|
||||
```
|
||||
|
||||
|
||||
9
examples/reference/config.yaml
Normal file
9
examples/reference/config.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
address: "${GO_OMNI_HTTP_ADDR:-127.0.0.1:8080}"
|
||||
transport: "${GO_OMNI_TRANSPORT:-nethttp}"
|
||||
tls:
|
||||
certFile: "${GO_OMNI_TLS_CERT_FILE:-}"
|
||||
keyFile: "${GO_OMNI_TLS_KEY_FILE:-}"
|
||||
log:
|
||||
service: reference-api
|
||||
environment: local
|
||||
output: stdout
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -42,6 +43,7 @@ func registerRoutes(application *omni.App, users service.UserRepository, logger
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
if err := runReference(ctx); err != nil {
|
||||
@@ -49,29 +51,9 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func referenceConfig() (omni.Config, error) {
|
||||
settings, err := omni.LoadConfig("")
|
||||
if err != nil {
|
||||
return settings, err
|
||||
}
|
||||
// Preserve the example's loopback default and legacy address variable.
|
||||
if strings.TrimSpace(os.Getenv("GO_OMNI_HTTP_ADDR")) == "" {
|
||||
settings.Address = strings.TrimSpace(os.Getenv("GO_OMNI_ADDR"))
|
||||
if settings.Address == "" {
|
||||
settings.Address = "127.0.0.1:8080"
|
||||
}
|
||||
}
|
||||
if settings.Log.Service == "" {
|
||||
settings.Log.Service = "reference-api"
|
||||
}
|
||||
if settings.Log.Environment == "" {
|
||||
settings.Log.Environment = "local"
|
||||
}
|
||||
if strings.TrimSpace(os.Getenv("GO_OMNI_LOG_OUTPUT")) == "" {
|
||||
settings.Log.Output = "stdout"
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
var configPath = flag.String("config", "examples/reference/config.yaml", "configuration file")
|
||||
|
||||
func referenceConfig() (omni.Config, error) { return omni.LoadConfig(*configPath) }
|
||||
|
||||
func runReference(ctx context.Context) error {
|
||||
settings, err := referenceConfig()
|
||||
@@ -106,10 +88,9 @@ func openUsers(ctx context.Context, logger *logx.Logger) (service.UserRepository
|
||||
if strings.TrimSpace(os.Getenv("GO_OMNI_DB_DSN")) == "" {
|
||||
return repository.NewMemoryUsers(), func() error { return nil }, "memory", nil
|
||||
}
|
||||
cfg, err := database.ConfigFromEnv("GO_OMNI_DB_")
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
cfg := database.DefaultConfig()
|
||||
cfg.Driver = "mysql"
|
||||
cfg.DSN = os.Getenv("GO_OMNI_DB_DSN")
|
||||
db, err := database.Open(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -99,7 +101,11 @@ func TestBusinessConfiguredTransports(t *testing.T) {
|
||||
t.Setenv("REFERENCE_TEST_TLS_CERT_FILE", cert)
|
||||
t.Setenv("REFERENCE_TEST_TLS_KEY_FILE", key)
|
||||
}
|
||||
settings, err := omni.LoadConfig("REFERENCE_TEST_")
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
if err := os.WriteFile(path, []byte("transport: ${REFERENCE_TEST_TRANSPORT}\nshutdownTimeout: 3s\ntls:\n certFile: \"${REFERENCE_TEST_TLS_CERT_FILE:-}\"\n keyFile: \"${REFERENCE_TEST_TLS_KEY_FILE:-}\""), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings, err := omni.LoadConfig(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -159,24 +165,26 @@ func TestBusinessConfiguredTransports(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferenceAddressCompatibility(t *testing.T) {
|
||||
func TestReferenceExplicitConfig(t *testing.T) {
|
||||
previous := *configPath
|
||||
*configPath = "config.yaml"
|
||||
defer func() { *configPath = previous }()
|
||||
t.Setenv("GO_OMNI_TRANSPORT", "native")
|
||||
t.Setenv("GO_OMNI_HTTP_ADDR", "")
|
||||
t.Setenv("GO_OMNI_TLS_CERT_FILE", "")
|
||||
t.Setenv("GO_OMNI_TLS_KEY_FILE", "")
|
||||
t.Setenv("GO_OMNI_HTTP_ADDR", "")
|
||||
t.Setenv("GO_OMNI_ADDR", "")
|
||||
settings, err := referenceConfig()
|
||||
if err != nil || settings.Address != "127.0.0.1:8080" || settings.Transport != "native" {
|
||||
t.Fatalf("default: %+v %v", settings, err)
|
||||
c, err := referenceConfig()
|
||||
if err != nil || c.Address != "127.0.0.1:8080" || c.Transport != "native" {
|
||||
t.Fatal(c, err)
|
||||
}
|
||||
t.Setenv("GO_OMNI_ADDR", "127.0.0.1:8081")
|
||||
settings, err = referenceConfig()
|
||||
if err != nil || settings.Address != "127.0.0.1:8081" {
|
||||
t.Fatalf("legacy: %+v %v", settings, err)
|
||||
t.Setenv("GO_OMNI_ADDR", "127.0.0.1:1234")
|
||||
c, err = referenceConfig()
|
||||
if err != nil || c.Address != "127.0.0.1:8080" {
|
||||
t.Fatal("implicit alias", err)
|
||||
}
|
||||
t.Setenv("GO_OMNI_HTTP_ADDR", "127.0.0.1:8082")
|
||||
settings, err = referenceConfig()
|
||||
if err != nil || settings.Address != "127.0.0.1:8082" {
|
||||
t.Fatalf("standard: %+v %v", settings, err)
|
||||
c, err = referenceConfig()
|
||||
if err != nil || c.Address != "127.0.0.1:8082" {
|
||||
t.Fatal(c, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 表,缺失时拒绝启动,不自动改表。
|
||||
|
||||
## 接口
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user