Files
ocdp-go/backend/internal/bootstrap/config.go
Ivan087 0144e9cab7 fix: auto-enable cluster bootstrap, add init-db.sql to postgres volumes
- Auto-enable cluster seeding when BOOTSTRAP_CLUSTERS is set
  (no longer requires separate BOOTSTRAP_ENABLE_CLUSTERS=true)
- Add BOOTSTRAP_ENABLE_CLUSTERS to README .env template
- Mount init-db.sql in postgres service volumes
2026-05-20 18:00:49 +08:00

242 lines
6.7 KiB
Go

package bootstrap
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
)
// BootstrapConfig 预注入配置
type BootstrapConfig struct {
Enabled bool `json:"enabled"`
Users []UserSeed `json:"users"`
Registries []RegistrySeed `json:"registries"`
Clusters []ClusterSeed `json:"clusters"`
}
// UserSeed 用户预注入数据
type UserSeed struct {
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
Role string `json:"role"`
}
// RegistrySeed Registry 预注入数据
type RegistrySeed struct {
Name string `json:"name"`
URL string `json:"url"`
Description string `json:"description"`
Username string `json:"username"`
Password string `json:"password"`
Insecure bool `json:"insecure"`
}
// ClusterSeed Cluster 预注入数据
type ClusterSeed struct {
Name string `json:"name"`
Host string `json:"host"`
Description string `json:"description"`
CAData string `json:"ca_data"`
CertData string `json:"cert_data"`
KeyData string `json:"key_data"`
Token string `json:"token,omitempty"`
}
// LoadBootstrapConfig 加载预注入配置
// 支持从文件或环境变量加载
//
// 加载优先级:
// 1. 环境变量 BOOTSTRAP_CONFIG_JSON (最高优先级)
// 2. 环境变量 BOOTSTRAP_* (root .env / container env)
// 3. Mock 模式: 配置文件 config/bootstrap.json
// 4. 未提供任何 bootstrap 配置时禁用预注入
func LoadBootstrapConfig() (*BootstrapConfig, error) {
// 1. 优先从环境变量加载
if configJSON := os.Getenv("BOOTSTRAP_CONFIG_JSON"); configJSON != "" {
var config BootstrapConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return nil, fmt.Errorf("failed to parse BOOTSTRAP_CONFIG_JSON: %w", err)
}
return &config, nil
}
if config, ok := loadBootstrapConfigFromEnv(); ok {
return config, nil
}
// 2. 检查适配器模式
adapterMode := os.Getenv("ADAPTER_MODE")
// Mock 模式: 使用配置文件(假数据)
if adapterMode == "mock" {
configPath := os.Getenv("BOOTSTRAP_CONFIG_FILE")
if configPath == "" {
configPath = filepath.Join("config", "bootstrap.json")
}
// 检查文件是否存在
if _, err := os.Stat(configPath); os.IsNotExist(err) {
// 配置文件不存在,不预注入任何数据
return GetDefaultBootstrapConfig(), nil
}
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read bootstrap config file %s: %w", configPath, err)
}
var config BootstrapConfig
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse bootstrap config: %w", err)
}
return &config, nil
}
// 3. 真实模式: 未显式配置时不预注入任何数据
return GetDefaultBootstrapConfig(), nil
}
func loadBootstrapConfigFromEnv() (*BootstrapConfig, bool) {
if !hasBootstrapEnv() {
return nil, false
}
config := &BootstrapConfig{
Enabled: true,
Users: make([]UserSeed, 0, 1),
Registries: make([]RegistrySeed, 0, 1),
Clusters: make([]ClusterSeed, 0),
}
adminUser := strings.TrimSpace(os.Getenv("BOOTSTRAP_ADMIN_USER"))
adminPass := strings.TrimSpace(os.Getenv("BOOTSTRAP_ADMIN_PASS"))
if adminUser != "" && adminPass != "" {
config.Users = append(config.Users, UserSeed{
Username: adminUser,
Password: adminPass,
Email: getEnv("BOOTSTRAP_ADMIN_EMAIL", adminUser+"@example.local"),
Role: "admin",
})
}
if registryURL := os.Getenv("BOOTSTRAP_REGISTRY_URL"); registryURL != "" {
registryUser := getEnv("BOOTSTRAP_REGISTRY_ROBOT_USER", getEnv("BOOTSTRAP_REGISTRY_USER", ""))
registryPass := getEnv("BOOTSTRAP_REGISTRY_ROBOT_PASS", getEnv("BOOTSTRAP_REGISTRY_PASS", ""))
config.Registries = append(config.Registries, RegistrySeed{
Name: getEnv("BOOTSTRAP_REGISTRY_NAME", "harbor"),
URL: registryURL,
Description: getEnv("BOOTSTRAP_REGISTRY_DESC", ""),
Username: registryUser,
Password: registryPass,
Insecure: parseBoolEnv("BOOTSTRAP_REGISTRY_INSECURE", false),
})
}
enableClusters := parseBoolEnv("BOOTSTRAP_ENABLE_CLUSTERS", false) ||
os.Getenv("BOOTSTRAP_CLUSTERS") != ""
if enableClusters {
for _, clusterName := range discoverBootstrapClusters() {
prefix := "BOOTSTRAP_CLUSTER_" + normalizeEnvName(clusterName) + "_"
host := os.Getenv(prefix + "HOST")
if host == "" {
continue
}
config.Clusters = append(config.Clusters, ClusterSeed{
Name: strings.ToLower(clusterName),
Host: host,
Description: os.Getenv(prefix + "DESC"),
CAData: os.Getenv(prefix + "CA"),
CertData: os.Getenv(prefix + "CERT"),
KeyData: os.Getenv(prefix + "KEY"),
Token: os.Getenv(prefix + "TOKEN"),
})
}
}
return config, true
}
func hasBootstrapEnv() bool {
for _, env := range os.Environ() {
if strings.HasPrefix(env, "BOOTSTRAP_") {
return true
}
}
return false
}
func discoverBootstrapClusters() []string {
names := make(map[string]struct{})
if configured := os.Getenv("BOOTSTRAP_CLUSTERS"); configured != "" {
for _, name := range strings.Split(configured, ",") {
name = strings.TrimSpace(name)
if name != "" {
names[normalizeEnvName(name)] = struct{}{}
}
}
}
for _, env := range os.Environ() {
key, _, ok := strings.Cut(env, "=")
if !ok || !strings.HasPrefix(key, "BOOTSTRAP_CLUSTER_") || !strings.HasSuffix(key, "_HOST") {
continue
}
name := strings.TrimSuffix(strings.TrimPrefix(key, "BOOTSTRAP_CLUSTER_"), "_HOST")
if name != "" {
names[name] = struct{}{}
}
}
result := make([]string, 0, len(names))
for name := range names {
result = append(result, name)
}
sort.Strings(result)
return result
}
func normalizeEnvName(name string) string {
replacer := strings.NewReplacer("-", "_", ".", "_", " ", "_")
return strings.ToUpper(replacer.Replace(strings.TrimSpace(name)))
}
func parseBoolEnv(key string, defaultValue bool) bool {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return defaultValue
}
parsed, err := strconv.ParseBool(value)
if err != nil {
return defaultValue
}
return parsed
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// GetDefaultBootstrapConfig 返回安全的空默认配置。
//
// 这里不能包含真实或示例账号密码、Registry 或集群凭据。预注入数据必须来自
// BOOTSTRAP_CONFIG_JSON、BOOTSTRAP_* 环境变量,或显式提供的 bootstrap 配置文件。
func GetDefaultBootstrapConfig() *BootstrapConfig {
return &BootstrapConfig{
Enabled: false,
Users: []UserSeed{},
Registries: []RegistrySeed{},
Clusters: []ClusterSeed{},
}
}