Files
LokiStack/backend/config/config.go
T
opsx f67b86e6bb
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
style: gofmt config.go
2026-11-01 04:08:50 +08:00

159 lines
4.7 KiB
Go

package config
import (
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"sync"
)
var (
instance *Config
once sync.Once
)
type Config struct {
ServerPort string
DatabasePath string
JWTSecret string
EncryptionKey string // 独立加密密钥,必须与JWT_SECRET分离
DataDir string
KubeDir string
LogLevel string // 新增:日志级别 debug/info/warn/error
HostKeyDir string // 新增:SSH主机密钥存储目录
LicenseAPIBase string // opsx-licence API 根(须以 /api 结尾)
}
// resolveSecrets 密钥解析(所有部署形态统一的零配置体验):
// 显式环境变量优先;缺失时读取 DATA_DIR/.secrets.env;两者皆无则生成随机密钥
// 并持久化(0600),重启/升级保持不变。无法持久化时拒绝启动,绝不使用易失密钥。
func resolveSecrets(dataDir string) (jwt, enc string) {
jwt = os.Getenv("JWT_SECRET")
enc = os.Getenv("ENCRYPTION_KEY")
if jwt != "" && enc != "" {
return jwt, enc
}
secretsFile := filepath.Join(dataDir, ".secrets.env")
if data, err := os.ReadFile(secretsFile); err == nil {
for _, line := range strings.Split(string(data), "\n") {
if v, ok := strings.CutPrefix(line, "JWT_SECRET="); ok && jwt == "" && v != "" {
jwt = v
}
if v, ok := strings.CutPrefix(line, "ENCRYPTION_KEY="); ok && enc == "" && v != "" {
enc = v
}
}
}
if jwt != "" && enc != "" {
return jwt, enc
}
gen := func() string {
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
log.Fatal("[SECURITY] 生成随机密钥失败: ", err)
}
return hex.EncodeToString(buf)
}
if jwt == "" {
jwt = gen()
log.Printf("[SECURITY] JWT_SECRET 未设置,已生成随机密钥并持久化到 %s", secretsFile)
}
if enc == "" {
enc = gen()
log.Printf("[SECURITY] ENCRYPTION_KEY 未设置,已生成随机密钥并持久化到 %s", secretsFile)
}
if err := os.MkdirAll(dataDir, 0o700); err != nil {
log.Fatal("[SECURITY] 无法创建数据目录以持久化密钥: ", err)
}
if err := os.WriteFile(secretsFile, []byte(fmt.Sprintf(
"# LokiStack auto-generated. Delete to regenerate.\nJWT_SECRET=%s\nENCRYPTION_KEY=%s\n", jwt, enc,
)), 0o600); err != nil {
log.Fatal("[SECURITY] 密钥持久化失败(请显式设置 JWT_SECRET / ENCRYPTION_KEY): ", err)
}
return jwt, enc
}
func GetConfig() *Config {
once.Do(func() {
dataDir := getEnv("DATA_DIR", "./data")
jwtSecret, encKey := resolveSecrets(dataDir)
// ENCRYPTION_KEY 长度校验:至少32字节(256位)
if len(encKey) < 32 {
log.Fatal(fmt.Sprintf("[SECURITY] ENCRYPTION_KEY must be at least 32 bytes, got %d bytes", len(encKey)))
}
instance = &Config{
ServerPort: getEnv("SERVER_PORT", "8080"),
DatabasePath: databasePath(dataDir),
JWTSecret: jwtSecret,
EncryptionKey: encKey,
DataDir: dataDir,
KubeDir: filepath.Join(dataDir, "kubeconfigs"),
LogLevel: getEnv("LOG_LEVEL", "info"),
HostKeyDir: filepath.Join(dataDir, "known_hosts"),
LicenseAPIBase: resolveLicenseAPIBase(),
}
_ = os.MkdirAll(instance.KubeDir, 0700)
_ = os.MkdirAll(instance.HostKeyDir, 0700)
})
return instance
}
// resolveLicenseAPIBase 国内主节点默认;LICENSE_API_BASE / OPSX_LICENSE_API_BASE 可覆盖。
// 备灾节点 l123 不写死进默认路径,需显式配置。
func resolveLicenseAPIBase() string {
for _, key := range []string{"LICENSE_API_BASE", "OPSX_LICENSE_API_BASE"} {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return normalizeLicenseAPIBase(v)
}
}
return "https://licence.rlwj.cn/api"
}
func normalizeLicenseAPIBase(raw string) string {
base := strings.TrimRight(strings.TrimSpace(raw), "/")
if base == "" {
return "https://licence.rlwj.cn/api"
}
if strings.HasSuffix(base, "/api/v1") {
return strings.TrimSuffix(base, "/v1")
}
if strings.HasSuffix(base, "/v1") && !strings.HasSuffix(base, "/api/v1") {
return strings.TrimSuffix(base, "/v1") + "/api"
}
if !strings.HasSuffix(base, "/api") {
return base + "/api"
}
return base
}
func getEnv(key, defaultVal string) string {
if val := os.Getenv(key); val != "" {
return val
}
return defaultVal
}
// databasePath 新名优先;历史命名的数据文件存在且新文件不存在时沿用旧文件
// (数据平滑迁移,不搬移文件、零丢失风险)
func databasePath(dataDir string) string {
newPath := filepath.Join(dataDir, "lokistack.json")
if _, err := os.Stat(newPath); err == nil {
return newPath
}
for _, legacy := range []string{"opsxloki.json", "lokideploy.json"} {
legacyPath := filepath.Join(dataDir, legacy)
if _, err := os.Stat(legacyPath); err == nil {
return legacyPath
}
}
return newPath
}