324 lines
11 KiB
Go
324 lines
11 KiB
Go
package main
|
||
|
||
import (
|
||
"flag"
|
||
"log"
|
||
"os"
|
||
)
|
||
|
||
func main() {
|
||
// Load environment variables from .env file before parsing flags
|
||
// This allows .env file to provide defaults that can be overridden by flags
|
||
loadEnvFileWithFallback()
|
||
|
||
config := parseFlags()
|
||
|
||
// Initialize state manager
|
||
stateManager := NewStateManager(config.StateFile)
|
||
if err := stateManager.Load(); err != nil {
|
||
log.Printf("Warning: Failed to load state: %v", err)
|
||
}
|
||
|
||
// Show last run info
|
||
if lastRun := stateManager.GetLastRun(); !lastRun.IsZero() {
|
||
log.Printf("Last successful run: %s", lastRun.Format("2006-01-02 15:04:05"))
|
||
clientCount, domainCount, sidCount := stateManager.GetStats()
|
||
log.Printf("Previous state: %d clients, %d domains, %d SIDs", clientCount, domainCount, sidCount)
|
||
}
|
||
|
||
// Update templates from Git
|
||
var templateCommit string
|
||
if !config.SkipGitPull {
|
||
commit, err := updateTemplates(config)
|
||
if err != nil {
|
||
log.Printf("Warning: Failed to update templates from Git: %v", err)
|
||
log.Printf("Continuing with existing templates...")
|
||
}
|
||
templateCommit = commit
|
||
} else {
|
||
// Get current commit without pulling
|
||
templateCommit = getCurrentCommit(config.TemplatesPath)
|
||
}
|
||
|
||
// Load dashboard template
|
||
template, err := loadTemplate(config.TemplatesPath)
|
||
if err != nil {
|
||
log.Fatalf("Failed to load template: %v", err)
|
||
}
|
||
|
||
log.Printf("Loaded template version: %s", template.Version)
|
||
|
||
// Load alert rules template
|
||
alertTmpl, err := loadAlertTemplate(config.TemplatesPath)
|
||
if err != nil {
|
||
log.Printf("Warning: Failed to load alert template: %v", err)
|
||
log.Printf("Alert rules will not be generated")
|
||
}
|
||
|
||
// Connect to database
|
||
db, err := connectDB(config)
|
||
if err != nil {
|
||
log.Fatalf("Failed to connect to database: %v", err)
|
||
}
|
||
defer db.Close()
|
||
|
||
// Fetch clients data
|
||
clients, err := fetchClientsData(db)
|
||
if err != nil {
|
||
log.Fatalf("Failed to fetch clients data: %v", err)
|
||
}
|
||
|
||
log.Printf("Found %d clients", len(clients))
|
||
|
||
// Check if there are any clients
|
||
if len(clients) == 0 {
|
||
log.Printf("No clients found in database. Nothing to generate.")
|
||
log.Printf("This is not an error - database is empty or no data matches criteria.")
|
||
os.Exit(0)
|
||
}
|
||
|
||
// Filter out clients without domains
|
||
validClients := make(map[string]ClientData)
|
||
skippedCount := 0
|
||
for clientTitle, clientData := range clients {
|
||
if len(clientData.Domains) == 0 {
|
||
log.Printf("Warning: Client %s has no domains, skipping", clientTitle)
|
||
skippedCount++
|
||
continue
|
||
}
|
||
validClients[clientTitle] = clientData
|
||
}
|
||
|
||
if len(validClients) == 0 {
|
||
log.Printf("No valid clients with domains found. Nothing to generate.")
|
||
os.Exit(0)
|
||
}
|
||
|
||
// Хэш шаблона алертов — считается независимо от дашборда
|
||
alertTemplateHash := hashAlertTemplate(config.TemplatesPath)
|
||
alertsChanged := stateManager.AlertsNeedUpdate(validClients) || stateManager.AlertTemplateChanged(alertTemplateHash)
|
||
|
||
// Check for changes
|
||
hasChanges, changeReason := stateManager.CheckChanges(templateCommit, template.Version, clients)
|
||
|
||
if !hasChanges && !alertsChanged && !config.ForceRun {
|
||
log.Printf("=== No Changes Detected ===")
|
||
log.Printf("No changes in templates or database since last run.")
|
||
log.Printf("Skipping dashboard generation.")
|
||
log.Printf("")
|
||
log.Printf("Use -force flag to regenerate anyway.")
|
||
os.Exit(0)
|
||
}
|
||
|
||
if hasChanges {
|
||
log.Printf("=== Changes Detected ===")
|
||
log.Printf("%s", changeReason)
|
||
log.Printf("")
|
||
} else if alertsChanged {
|
||
log.Printf("=== Alert Rules Changed ===")
|
||
log.Printf("")
|
||
}
|
||
|
||
if config.ForceRun {
|
||
log.Printf("Force run enabled - regenerating dashboard")
|
||
}
|
||
|
||
// Generate single dashboard with all clients
|
||
log.Printf("Generating single dashboard: %s", config.DashboardTitle)
|
||
log.Printf("Including %d clients with %d total domains", len(validClients), countTotalDomains(validClients))
|
||
|
||
dashboard := generateSingleDashboard(validClients, template, config)
|
||
|
||
if config.DryRun {
|
||
logDryRun(config.DashboardTitle, dashboard)
|
||
if alertTmpl != nil {
|
||
log.Printf("DRY RUN: Generating alert rules preview...")
|
||
_ = generateAndSendAlerts(validClients, *alertTmpl, config, true)
|
||
}
|
||
log.Printf("=== Summary ===")
|
||
log.Printf("Clients: %d", len(validClients))
|
||
log.Printf("Skipped: %d", skippedCount)
|
||
log.Printf("DRY RUN: Dashboard generated but not sent to Grafana")
|
||
os.Exit(0)
|
||
}
|
||
|
||
// Send to Grafana
|
||
if err := sendToGrafana(dashboard, config); err != nil {
|
||
log.Printf("Error sending dashboard: %v", err)
|
||
log.Printf("=== Summary ===")
|
||
log.Printf("Clients: %d", len(validClients))
|
||
log.Printf("Skipped: %d", skippedCount)
|
||
log.Printf("Status: FAILED")
|
||
os.Exit(1)
|
||
}
|
||
|
||
log.Printf("Successfully created/updated dashboard: %s", config.DashboardTitle)
|
||
|
||
// Generate and send alert rules
|
||
if alertTmpl != nil {
|
||
if alertsChanged || config.ForceRun {
|
||
if err := generateAndSendAlerts(validClients, *alertTmpl, config, false); err != nil {
|
||
log.Printf("Warning: alerts generation failed: %v", err)
|
||
} else {
|
||
stateManager.UpdateAlertsHash(validClients)
|
||
stateManager.UpdateAlertTemplateHash(alertTemplateHash)
|
||
}
|
||
} else {
|
||
log.Printf("Alerts: no changes in rps_limit, skipping")
|
||
}
|
||
}
|
||
|
||
// Summary
|
||
log.Printf("=== Summary ===")
|
||
log.Printf("Dashboard: %s", config.DashboardTitle)
|
||
log.Printf("Clients: %d", len(validClients))
|
||
if skippedCount > 0 {
|
||
log.Printf("Skipped: %d", skippedCount)
|
||
}
|
||
log.Printf("Total domains: %d", countTotalDomains(validClients))
|
||
log.Printf("Status: SUCCESS")
|
||
|
||
// Update state
|
||
stateManager.Update(templateCommit, template.Version, clients, changeReason)
|
||
if err := stateManager.Save(); err != nil {
|
||
log.Printf("Warning: Failed to save state: %v", err)
|
||
} else {
|
||
log.Printf("State saved to %s", config.StateFile)
|
||
}
|
||
}
|
||
|
||
func countTotalDomains(clients map[string]ClientData) int {
|
||
count := 0
|
||
for _, client := range clients {
|
||
count += len(client.Domains)
|
||
}
|
||
return count
|
||
}
|
||
|
||
func parseFlags() Config {
|
||
config := Config{}
|
||
|
||
// Database flags - resolve from env vars with fallback to defaults
|
||
dbUser := getEnvOrDefault("DB_USER", DefaultDBUser)
|
||
dbPassword := getEnvOrDefault("DB_PASSWORD", DefaultDBPassword)
|
||
|
||
// Grafana settings - resolve from env vars with fallback to defaults
|
||
grafanaFolder := getEnvOrDefault("GRAFANA_FOLDER", DefaultGrafanaFolder)
|
||
dashboardTitle := getEnvOrDefault("DASHBOARD_TITLE", DefaultDashboardTitle)
|
||
|
||
flag.StringVar(&config.DBHost, "db-host", DefaultPrimaryDBHost, "PostgreSQL host")
|
||
flag.IntVar(&config.DBPort, "db-port", DefaultDBPort, "PostgreSQL port")
|
||
flag.StringVar(&config.DBUser, "db-user", dbUser, "PostgreSQL user")
|
||
flag.StringVar(&config.DBPassword, "db-password", dbPassword, "PostgreSQL password")
|
||
flag.StringVar(&config.DBName, "db-name", DefaultDBName, "PostgreSQL database name")
|
||
|
||
// Grafana flags
|
||
var grafanaURLFlag, grafanaAPIKeyFlag string
|
||
flag.StringVar(&grafanaURLFlag, "grafana-url", "", "Grafana URL")
|
||
flag.StringVar(&grafanaAPIKeyFlag, "grafana-api-key", "", "Grafana API key")
|
||
flag.StringVar(&config.GrafanaFolder, "grafana-folder", grafanaFolder, "Grafana folder for dashboards")
|
||
flag.StringVar(&config.DashboardTitle, "dashboard-title", dashboardTitle, "Dashboard title")
|
||
|
||
// Git flags
|
||
var gitTokenFlag string
|
||
flag.StringVar(&gitTokenFlag, "git-token", "", "Gitea token (overrides env var and hardcoded value)")
|
||
flag.StringVar(&config.TemplatesRepo, "templates-repo", DefaultTemplatesRepo, "Git repository URL for templates")
|
||
flag.StringVar(&config.TemplatesBranch, "templates-branch", DefaultTemplatesBranch, "Git branch for templates")
|
||
flag.StringVar(&config.TemplatesPath, "templates-path", DefaultTemplatesPath, "Local path for templates")
|
||
flag.BoolVar(&config.SkipGitPull, "skip-git-pull", false, "Skip updating templates from Git")
|
||
|
||
// Alerts flags
|
||
alertsFolder := getEnvOrDefault("ALERTS_FOLDER", DefaultAlertsFolder)
|
||
alertsReceiver := getEnvOrDefault("ALERTS_RECEIVER", DefaultAlertsReceiver)
|
||
alertsGroup := getEnvOrDefault("ALERTS_GROUP", DefaultAlertsGroup)
|
||
alertsDatasource := getEnvOrDefault("ALERTS_DATASOURCE_UID", DefaultAlertsDatasource)
|
||
flag.StringVar(&config.AlertsFolder, "alerts-folder", alertsFolder, "Grafana folder for alert rules")
|
||
flag.StringVar(&config.AlertsReceiver, "alerts-receiver", alertsReceiver, "Grafana notification receiver for alerts")
|
||
flag.StringVar(&config.AlertsGroup, "alerts-group", alertsGroup, "Grafana alert rule group name")
|
||
flag.StringVar(&config.AlertsDatasourceUID, "alerts-datasource-uid", alertsDatasource, "OpenSearch datasource UID for alerts")
|
||
|
||
// Other flags
|
||
flag.BoolVar(&config.DryRun, "dry-run", false, "Generate JSON but don't send to Grafana")
|
||
flag.StringVar(&config.StateFile, "state-file", DefaultStateFile, "Path to state file")
|
||
flag.BoolVar(&config.ForceRun, "force", false, "Force regeneration even if no changes detected")
|
||
|
||
flag.Parse()
|
||
|
||
// Resolve tokens with priority
|
||
config.GitToken = resolveToken("Git", gitTokenFlag, "GIT_TOKEN", DefaultGitToken)
|
||
config.GrafanaAPIKey = resolveToken("Grafana API", grafanaAPIKeyFlag, "GRAFANA_API_KEY", DefaultGrafanaAPIKey)
|
||
config.GrafanaURL = resolveValue("Grafana URL", grafanaURLFlag, "GRAFANA_URL", DefaultGrafanaURL)
|
||
|
||
// Validate required parameters
|
||
if config.GrafanaURL == "" {
|
||
log.Fatal("Error: grafana-url is required (use -grafana-url flag, GRAFANA_URL env var, .env file, or set DefaultGrafanaURL in config.go)")
|
||
}
|
||
if config.GrafanaAPIKey == "" {
|
||
log.Fatal("Error: grafana-api-key is required (use -grafana-api-key flag, GRAFANA_API_KEY env var, .env file, or set DefaultGrafanaAPIKey in config.go)")
|
||
}
|
||
if config.DBUser == "" {
|
||
log.Fatal("Error: db-user is required (use -db-user flag, DB_USER env var, or .env file)")
|
||
}
|
||
if config.DBPassword == "" {
|
||
log.Fatal("Error: db-password is required (use -db-password flag, DB_PASSWORD env var, or .env file)")
|
||
}
|
||
|
||
return config
|
||
}
|
||
|
||
// resolveToken определяет токен с приоритетом:
|
||
// 1. Флаг командной строки
|
||
// 2. Переменная окружения
|
||
// 3. Hardcoded константа
|
||
func resolveToken(name, flagValue, envVar, defaultValue string) string {
|
||
// Приоритет 1: флаг командной строки
|
||
if flagValue != "" {
|
||
log.Printf("Using %s token from command line flag", name)
|
||
return flagValue
|
||
}
|
||
|
||
// Приоритет 2: переменная окружения
|
||
if envValue := os.Getenv(envVar); envValue != "" {
|
||
log.Printf("Using %s token from %s environment variable", name, envVar)
|
||
return envValue
|
||
}
|
||
|
||
// Приоритет 3: hardcoded в config.go
|
||
if defaultValue != "" {
|
||
log.Printf("Using %s token from config.go (hardcoded)", name)
|
||
return defaultValue
|
||
}
|
||
|
||
// Токен не задан
|
||
return ""
|
||
}
|
||
|
||
// resolveValue определяет значение с приоритетом (для non-secret параметров)
|
||
func resolveValue(name, flagValue, envVar, defaultValue string) string {
|
||
// Приоритет 1: флаг командной строки
|
||
if flagValue != "" {
|
||
return flagValue
|
||
}
|
||
|
||
// Приоритет 2: переменная окружения
|
||
if envValue := os.Getenv(envVar); envValue != "" {
|
||
log.Printf("Using %s from %s environment variable", name, envVar)
|
||
return envValue
|
||
}
|
||
|
||
// Приоритет 3: hardcoded в config.go
|
||
if defaultValue != "" {
|
||
log.Printf("Using %s from config.go (hardcoded)", name)
|
||
return defaultValue
|
||
}
|
||
|
||
return ""
|
||
}
|
||
|
||
// getEnvOrDefault возвращает переменную окружения или default значение
|
||
func getEnvOrDefault(envVar, defaultValue string) string {
|
||
if value := os.Getenv(envVar); value != "" {
|
||
return value
|
||
}
|
||
return defaultValue
|
||
}
|