- 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
242 lines
6.7 KiB
Go
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{},
|
|
}
|
|
}
|