package main import ( "context" "encoding/json" "log" "os" "os/signal" "strconv" "strings" "syscall" "time" controlplane "git.ipao.vip/rogee/wx-win-agent/control-plane" ) func main() { config := controlplane.DefaultServerConfig() if value := os.Getenv("WXAGENT_CONTROL_PLANE_ADDR"); value != "" { config.ListenAddr = value } if value := os.Getenv("WXAGENT_CONTROL_PLANE_DATA"); value != "" { config.DataFile = value } config.BackupDir = os.Getenv("WXAGENT_CONTROL_PLANE_BACKUP_DIR") config.BackupCount = readIntEnv("WXAGENT_CONTROL_PLANE_BACKUP_COUNT", config.BackupCount) config.BackupInterval = readDurationEnv("WXAGENT_CONTROL_PLANE_BACKUP_INTERVAL", config.BackupInterval) config.TaskRetention = readDurationEnv("WXAGENT_CONTROL_PLANE_TASK_RETENTION", config.TaskRetention) config.EventRetention = readDurationEnv("WXAGENT_CONTROL_PLANE_EVENT_RETENTION", config.EventRetention) config.AuditRetention = readDurationEnv("WXAGENT_CONTROL_PLANE_AUDIT_RETENTION", config.AuditRetention) config.AccountRetention = readDurationEnv("WXAGENT_ACCOUNT_DATA_RETENTION", config.AccountRetention) config.AccountMaxShardBytes = readInt64Env("WXAGENT_ACCOUNT_MAX_SHARD_BYTES", config.AccountMaxShardBytes) config.AccountMaxBatchBytes = readInt64Env("WXAGENT_ACCOUNT_MAX_BATCH_BYTES", config.AccountMaxBatchBytes) config.AccountMaintenanceInterval = readDurationEnv("WXAGENT_ACCOUNT_MAINTENANCE_INTERVAL", config.AccountMaintenanceInterval) config.TLSCertFile = os.Getenv("WXAGENT_CONTROL_PLANE_TLS_CERT_FILE") config.TLSKeyFile = os.Getenv("WXAGENT_CONTROL_PLANE_TLS_KEY_FILE") config.MTLSClientCAFile = os.Getenv("WXAGENT_MTLS_CLIENT_CA_FILE") config.MTLSRequireNodeCert = readBoolEnv("WXAGENT_MTLS_REQUIRE_NODE_CERT", false) config.MTLSRevokedCertsFile = os.Getenv("WXAGENT_MTLS_REVOKED_CERTS_FILE") config.AIBaseURL = os.Getenv("WXAGENT_AI_BASE_URL") config.AIAPIKey = readSecret("WXAGENT_AI_API_KEY", "WXAGENT_AI_API_KEY_FILE") if value := os.Getenv("WXAGENT_AI_MODEL"); value != "" { config.AIModel = value } config.AITimeout = readDurationEnv("WXAGENT_AI_TIMEOUT", config.AITimeout) config.AISchedulerInterval = readDurationEnv("WXAGENT_AI_SCHEDULER_INTERVAL", config.AISchedulerInterval) config.NodeTokens = readMapSecret("WXAGENT_NODE_TOKENS", "WXAGENT_NODE_TOKENS_FILE") if len(config.NodeTokens) == 0 { nodeID, nodeToken := os.Getenv("WXAGENT_NODE_ID"), readSecret("WXAGENT_NODE_TOKEN", "WXAGENT_NODE_TOKEN_FILE") if strings.TrimSpace(nodeID) != "" && nodeToken != "" { config.NodeTokens = map[string]string{nodeID: nodeToken} } } config.WebUsers = readMapSecret("WXAGENT_WEB_USERS", "WXAGENT_WEB_USERS_FILE") if len(config.WebUsers) == 0 { webUser, webPassword := os.Getenv("WXAGENT_WEB_USER"), readSecret("WXAGENT_WEB_PASSWORD", "WXAGENT_WEB_PASSWORD_FILE") if strings.TrimSpace(webUser) != "" && webPassword != "" { config.WebUsers = map[string]string{webUser: webPassword} } } if len(config.NodeTokens) == 0 || len(config.WebUsers) == 0 { log.Fatal("configure node and web credentials with environment variables or *_FILE secret files") } server, err := controlplane.NewServer(config) if err != nil { log.Fatal(err) } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() log.Printf("wxagent control plane listening on %s; data=%s", config.ListenAddr, config.DataFile) if err := server.ListenAndServe(ctx); err != nil { log.Printf("control plane stopped: %v", err) os.Exit(1) } } func readSecret(valueName, fileName string) string { if path := strings.TrimSpace(os.Getenv(fileName)); path != "" { data, err := os.ReadFile(path) if err != nil { log.Fatalf("read %s: %v", fileName, err) } return strings.TrimRight(string(data), "\r\n") } return os.Getenv(valueName) } func readMapSecret(valueName, fileName string) map[string]string { value := readSecret(valueName, fileName) if value == "" { return nil } result := make(map[string]string) if err := json.Unmarshal([]byte(value), &result); err != nil { log.Fatalf("%s must contain a JSON object", fileName) } return result } func readBoolEnv(name string, fallback bool) bool { value := strings.TrimSpace(os.Getenv(name)) if value == "" { return fallback } parsed, err := strconv.ParseBool(value) if err != nil { log.Fatalf("%s must be true or false", name) } return parsed } func readIntEnv(name string, fallback int) int { value := strings.TrimSpace(os.Getenv(name)) if value == "" { return fallback } parsed, err := strconv.Atoi(value) if err != nil || parsed < 1 { log.Fatalf("%s must be a positive integer", name) } return parsed } func readInt64Env(name string, fallback int64) int64 { value := strings.TrimSpace(os.Getenv(name)) if value == "" { return fallback } parsed, err := strconv.ParseInt(value, 10, 64) if err != nil || parsed < 0 { log.Fatalf("%s must be a non-negative integer", name) } return parsed } func readDurationEnv(name string, fallback time.Duration) time.Duration { value := strings.TrimSpace(os.Getenv(name)) if value == "" { return fallback } parsed, err := time.ParseDuration(value) if err != nil || parsed < 0 { log.Fatalf("%s must be a non-negative duration", name) } return parsed }