支持环境变量覆盖运行配置
This commit is contained in:
parent
8ba644d7b3
commit
fc2397935b
@ -42,6 +42,41 @@ go run ./cmd/scert serve --base-dir C:\ScertData
|
||||
|
||||
SMTP 通知是 Scert 的出站连接,不需要为 Scert 开放 SMTP 入站端口。公网防火墙通常只需放行 Nginx 使用的 `443`,若配置 HTTP 跳转再放行 `80`;内部端口 `9004` 和 Nginx 测试端口 `9003` 都不应对公网开放。
|
||||
|
||||
### systemd 环境变量覆盖
|
||||
|
||||
服务启动时可以使用 `SCERT_*` 环境变量覆盖当前进程的配置文件值,适合在 systemd 的 `EnvironmentFile` 中配置端口和部署环境。覆盖只在本次进程生效,不会修改 `scert.json`,敏感凭据仍应使用加密凭据引用,不要直接写入日志。
|
||||
|
||||
常用变量如下:
|
||||
|
||||
```text
|
||||
SCERT_SERVER_HOST=127.0.0.1
|
||||
SCERT_SERVER_PORT=9004
|
||||
SCERT_SERVER_HTTPS_MODE=proxy
|
||||
SCERT_WEB_HOSTNAME=web.example.com
|
||||
SCERT_WEB_PUBLIC_IP=192.168.2.3
|
||||
SCERT_DATABASE_DRIVER=sqlite
|
||||
SCERT_DATABASE_DSN=/scert/data/scert.db
|
||||
SCERT_ACME_ENVIRONMENT=staging
|
||||
SCERT_ACME_ACCOUNT_EMAIL=admin@example.com
|
||||
SCERT_ACME_RENEWAL_THRESHOLD_DAYS=6
|
||||
SCERT_ACME_PROPAGATION_TIMEOUT_SECS=900
|
||||
SCERT_ACME_PROPAGATION_POLLING_INTERVAL_SECS=10
|
||||
SCERT_DNS_PROVIDER=cloudflare
|
||||
SCERT_DNS_CREDENTIAL_REF=secret://dns/cloudflare
|
||||
SCERT_NOTIFICATION_HTTP_CREDENTIAL_REF=secret://notification/http
|
||||
SCERT_NOTIFICATION_SMTP_CREDENTIAL_REF=secret://notification/smtp
|
||||
```
|
||||
|
||||
例如 Scert 内部服务使用 9004、Nginx 使用 9003 时,Scert 的环境文件可以写成:
|
||||
|
||||
```text
|
||||
SCERT_SERVER_HOST=127.0.0.1
|
||||
SCERT_SERVER_PORT=9004
|
||||
SCERT_SERVER_HTTPS_MODE=proxy
|
||||
```
|
||||
|
||||
修改环境文件后必须重启 Scert 服务;Nginx 仍按现有配置监听 9003 并代理到 `https://127.0.0.1:9004`。
|
||||
|
||||
## 测试和构建
|
||||
|
||||
```powershell
|
||||
|
||||
@ -31,7 +31,7 @@ func main() {
|
||||
if path == "" {
|
||||
path = filepath.Join(config.DefaultPaths(*baseDir).Config, "scert.json")
|
||||
}
|
||||
cfg, err := config.Load(path)
|
||||
cfg, err := config.LoadWithEnv(path)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "读取配置失败:", err)
|
||||
os.Exit(1)
|
||||
|
||||
@ -855,7 +855,7 @@ func printJSON(cmd *cobra.Command, value any) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func loadConfig() (config.Config, error) { return config.Load(resolveConfigPath()) }
|
||||
func loadConfig() (config.Config, error) { return config.LoadWithEnv(resolveConfigPath()) }
|
||||
|
||||
func resolveConfigPath() string {
|
||||
if configPath != "" {
|
||||
|
||||
@ -209,6 +209,73 @@ func Load(path string) (Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// LoadWithEnv 读取配置文件,并应用受控的 SCERT_* 环境变量覆盖。
|
||||
// 环境变量只影响当前进程,不会回写配置文件。
|
||||
func LoadWithEnv(path string) (Config, error) {
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if err := cfg.ApplyEnvironment(); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// ApplyEnvironment 将部署环境常用配置映射到当前配置。
|
||||
// 未设置的变量保持配置文件中的值,敏感值只从环境读取,不写入日志或配置文件。
|
||||
func (c *Config) ApplyEnvironment() error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("配置不能为空")
|
||||
}
|
||||
stringOverrides := []struct {
|
||||
name string
|
||||
target *string
|
||||
}{
|
||||
{"SCERT_SERVER_HOST", &c.Server.Host},
|
||||
{"SCERT_SERVER_HTTPS_MODE", &c.Server.HTTPSMode},
|
||||
{"SCERT_WEB_HOSTNAME", &c.Web.Hostname},
|
||||
{"SCERT_WEB_PUBLIC_IP", &c.Web.PublicIP},
|
||||
{"SCERT_DATABASE_DRIVER", &c.Database.Driver},
|
||||
{"SCERT_DATABASE_DSN", &c.Database.DSN},
|
||||
{"SCERT_ACME_ENVIRONMENT", &c.ACME.Environment},
|
||||
{"SCERT_ACME_ACCOUNT_EMAIL", &c.ACME.AccountEmail},
|
||||
{"SCERT_DNS_PROVIDER", &c.DNS.Provider},
|
||||
{"SCERT_DNS_CREDENTIAL_REF", &c.DNS.CredentialRef},
|
||||
{"SCERT_NOTIFICATION_HTTP_CREDENTIAL_REF", &c.Notification.HTTPCredentialRef},
|
||||
{"SCERT_NOTIFICATION_SMTP_CREDENTIAL_REF", &c.Notification.SMTPCredentialRef},
|
||||
}
|
||||
for _, override := range stringOverrides {
|
||||
if value, ok := os.LookupEnv(override.name); ok {
|
||||
*override.target = strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
intOverrides := []struct {
|
||||
name string
|
||||
target *int
|
||||
}{
|
||||
{"SCERT_SERVER_PORT", &c.Server.Port},
|
||||
{"SCERT_ACME_RENEWAL_THRESHOLD_DAYS", &c.ACME.RenewalThresholdDays},
|
||||
{"SCERT_ACME_PROPAGATION_TIMEOUT_SECS", &c.ACME.PropagationTimeoutSecs},
|
||||
{"SCERT_ACME_PROPAGATION_POLLING_INTERVAL_SECS", &c.ACME.PropagationPollingIntervalSecs},
|
||||
}
|
||||
for _, override := range intOverrides {
|
||||
value, ok := os.LookupEnv(override.name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
parsed, err := strconv.Atoi(strings.TrimSpace(value))
|
||||
if err != nil {
|
||||
return fmt.Errorf("环境变量 %s 必须是整数:%w", override.name, err)
|
||||
}
|
||||
*override.target = parsed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) Normalize() {
|
||||
if c == nil {
|
||||
return
|
||||
|
||||
@ -208,3 +208,47 @@ func TestSaveAndLoadACMEPropagationSettings(t *testing.T) {
|
||||
t.Fatalf("DNS 传播参数持久化不一致:%+v", loaded.ACME)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadWithEnvOverridesRuntimeSettings(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := Default(dir)
|
||||
path := filepath.Join(dir, "config", "scert.json")
|
||||
if err := cfg.Save(path); err != nil {
|
||||
t.Fatalf("保存配置失败:%v", err)
|
||||
}
|
||||
t.Setenv("SCERT_SERVER_HOST", "127.0.0.1")
|
||||
t.Setenv("SCERT_SERVER_PORT", "9004")
|
||||
t.Setenv("SCERT_SERVER_HTTPS_MODE", "proxy")
|
||||
t.Setenv("SCERT_WEB_PUBLIC_IP", "192.168.2.3")
|
||||
t.Setenv("SCERT_ACME_PROPAGATION_TIMEOUT_SECS", "1200")
|
||||
loaded, err := LoadWithEnv(path)
|
||||
if err != nil {
|
||||
t.Fatalf("带环境覆盖读取配置失败:%v", err)
|
||||
}
|
||||
if loaded.Server.Host != "127.0.0.1" || loaded.Server.Port != 9004 || loaded.Server.HTTPSMode != "proxy" {
|
||||
t.Fatalf("服务运行配置覆盖不正确:%+v", loaded.Server)
|
||||
}
|
||||
if loaded.Web.PublicIP != "192.168.2.3" || loaded.ACME.PropagationTimeoutSecs != 1200 {
|
||||
t.Fatalf("运行时辅助配置覆盖不正确:web=%+v acme=%+v", loaded.Web, loaded.ACME)
|
||||
}
|
||||
fileConfig, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("重新读取原配置失败:%v", err)
|
||||
}
|
||||
if fileConfig.Server.Port != 8080 || fileConfig.Web.PublicIP != "" {
|
||||
t.Fatalf("环境覆盖不应回写配置文件:%+v", fileConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadWithEnvRejectsInvalidInteger(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := Default(dir)
|
||||
path := filepath.Join(dir, "config", "scert.json")
|
||||
if err := cfg.Save(path); err != nil {
|
||||
t.Fatalf("保存配置失败:%v", err)
|
||||
}
|
||||
t.Setenv("SCERT_SERVER_PORT", "九千零四")
|
||||
if _, err := LoadWithEnv(path); err == nil {
|
||||
t.Fatal("无效端口环境变量未被拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user