472 lines
18 KiB
Go
Executable file
472 lines
18 KiB
Go
Executable file
package main
|
||
|
||
import (
|
||
"flag"
|
||
"log"
|
||
"os"
|
||
"strings"
|
||
)
|
||
|
||
// makeSPAlertsConfig создаёт конфиг для SP алертов с отдельной группой и contact point.
|
||
func makeSPAlertsConfig(config Config) Config {
|
||
spConfig := config
|
||
spConfig.AlertsGroup = config.SPAlertsGroup
|
||
spConfig.AlertsReceiver = config.SPAlertsReceiver
|
||
return spConfig
|
||
}
|
||
|
||
// makeSPConfig создаёт конфиг для SP дашборда на основе основного конфига.
|
||
func makeSPConfig(config Config) Config {
|
||
spConfig := config
|
||
spConfig.VLDashboardTitle = config.SPDashboardTitle
|
||
spConfig.VLDashboardUID = config.SPDashboardUID
|
||
spConfig.VLGrafanaFolder = config.SPGrafanaFolder
|
||
return spConfig
|
||
}
|
||
|
||
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)
|
||
validateAndFixTemplate(template)
|
||
|
||
// 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)
|
||
}
|
||
|
||
// Разделяем клиентов по waf_provider
|
||
spikClients := map[string]ClientData{}
|
||
spClients := map[string]ClientData{}
|
||
for k, v := range validClients {
|
||
switch strings.ToLower(v.WafProvider) {
|
||
case "spik":
|
||
spikClients[k] = v
|
||
case "sp":
|
||
spClients[k] = v
|
||
}
|
||
}
|
||
|
||
// Хэш шаблона алертов — считается независимо от дашборда
|
||
alertTemplateHash := hashAlertTemplate(config.TemplatesPath)
|
||
alertsChanged := stateManager.AlertsNeedUpdate(spikClients) || 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 spik clients
|
||
log.Printf("Generating single dashboard: %s", config.DashboardTitle)
|
||
log.Printf("Including %d spik clients with %d total domains", len(spikClients), countTotalDomains(spikClients))
|
||
|
||
dashboard := generateSingleDashboard(spikClients, template, config)
|
||
|
||
if config.DryRun {
|
||
if config.DashboardOSEnabled {
|
||
logDryRun(config.DashboardTitle, dashboard)
|
||
}
|
||
if alertTmpl != nil {
|
||
log.Printf("DRY RUN: Generating alert rules preview...")
|
||
if config.AlertsOSEnabled {
|
||
_ = generateAndSendAlerts(spikClients, *alertTmpl, config, true)
|
||
}
|
||
if config.AlertsVLEnabled {
|
||
_, _ = generateAndSendAlertsVL(spikClients, *alertTmpl, config, true)
|
||
}
|
||
}
|
||
log.Printf("=== Summary ===")
|
||
log.Printf("Spik clients: %d, SP clients: %d", len(spikClients), len(spClients))
|
||
log.Printf("Skipped: %d", skippedCount)
|
||
log.Printf("DRY RUN: Dashboard generated but not sent to Grafana")
|
||
if config.VLDatasourceUID != "" && config.DashboardVLEnabled {
|
||
vlDashboard := generateVLDashboard(spikClients, template, config)
|
||
logDryRun(config.VLDashboardTitle, vlDashboard)
|
||
if len(spClients) > 0 && config.SPDashboardTitle != "" {
|
||
spConfig := makeSPConfig(config)
|
||
spDashboard := generateVLDashboard(spClients, template, spConfig)
|
||
logDryRun(config.SPDashboardTitle, spDashboard)
|
||
}
|
||
}
|
||
os.Exit(0)
|
||
}
|
||
|
||
// Send OS dashboard to Grafana
|
||
if config.DashboardOSEnabled {
|
||
if err := sendToGrafana(dashboard, config); err != nil {
|
||
log.Printf("Error sending dashboard: %v", err)
|
||
log.Printf("=== Summary ===")
|
||
log.Printf("Spik clients: %d", len(spikClients))
|
||
log.Printf("Skipped: %d", skippedCount)
|
||
log.Printf("Status: FAILED")
|
||
os.Exit(1)
|
||
}
|
||
log.Printf("Successfully created/updated dashboard: %s", config.DashboardTitle)
|
||
} else {
|
||
log.Printf("Dashboard OS: disabled (DASHBOARD_OS_ENABLED=false)")
|
||
}
|
||
|
||
// Generate VictoriaLogs dashboard for spik clients
|
||
if config.VLDatasourceUID != "" && config.DashboardVLEnabled {
|
||
log.Printf("Generating VictoriaLogs dashboard: %s", config.VLDashboardTitle)
|
||
vlDashboard := generateVLDashboard(spikClients, template, config)
|
||
if err := sendVLDashboardToGrafana(vlDashboard, config); err != nil {
|
||
log.Printf("Warning: VictoriaLogs dashboard failed: %v", err)
|
||
} else {
|
||
log.Printf("Successfully created/updated VL dashboard: %s", config.VLDashboardTitle)
|
||
}
|
||
} else if config.VLDatasourceUID != "" {
|
||
log.Printf("Dashboard VL: disabled (DASHBOARD_VL_ENABLED=false)")
|
||
}
|
||
|
||
// Generate SP VictoriaLogs dashboard for sp clients
|
||
if config.VLDatasourceUID != "" && config.SPDashboardTitle != "" && len(spClients) > 0 {
|
||
log.Printf("Generating SP VictoriaLogs dashboard: %s (%d clients)", config.SPDashboardTitle, len(spClients))
|
||
spConfig := makeSPConfig(config)
|
||
spDashboard := generateVLDashboard(spClients, template, spConfig)
|
||
if err := sendVLDashboardToGrafana(spDashboard, spConfig); err != nil {
|
||
log.Printf("Warning: SP dashboard failed: %v", err)
|
||
} else {
|
||
log.Printf("Successfully created/updated SP dashboard: %s", config.SPDashboardTitle)
|
||
}
|
||
}
|
||
|
||
// Generate and send alert rules
|
||
if alertTmpl != nil {
|
||
if alertsChanged || config.ForceRun {
|
||
osErr := false
|
||
vlErr := false
|
||
if config.AlertsOSEnabled {
|
||
if err := generateAndSendAlerts(spikClients, *alertTmpl, config, false); err != nil {
|
||
log.Printf("Warning: alerts generation failed: %v", err)
|
||
osErr = true
|
||
}
|
||
} else {
|
||
log.Printf("Alerts OS: disabled (ALERTS_OS_ENABLED=false)")
|
||
}
|
||
allActiveUIDs := map[string]bool{}
|
||
if config.AlertsVLEnabled {
|
||
spikUIDs, err := generateAndSendAlertsVL(spikClients, *alertTmpl, config, false)
|
||
if err != nil {
|
||
log.Printf("Warning: VL alerts generation failed: %v", err)
|
||
vlErr = true
|
||
}
|
||
for _, uid := range spikUIDs {
|
||
allActiveUIDs[uid] = true
|
||
}
|
||
// SP алерты — для sp клиентов с отдельным contact point и группой
|
||
if len(spClients) > 0 {
|
||
spAlertsConfig := makeSPAlertsConfig(config)
|
||
spUIDs, err := generateAndSendAlertsVL(spClients, *alertTmpl, spAlertsConfig, false)
|
||
if err != nil {
|
||
log.Printf("Warning: SP VL alerts generation failed: %v", err)
|
||
}
|
||
for _, uid := range spUIDs {
|
||
allActiveUIDs[uid] = true
|
||
}
|
||
}
|
||
// Статические алерты — отправляем один раз с основным конфигом
|
||
// (не с SP конфигом, чтобы попали в правильную группу [SYS] PTAF Grafana)
|
||
if !config.AlertsOSEnabled {
|
||
if err := sendStaticAlerts(*alertTmpl, config, false); err != nil {
|
||
log.Printf("Warning: VL static alerts error: %v", err)
|
||
}
|
||
for _, a := range alertTmpl.StaticAlerts {
|
||
if uid, ok := a["uid"].(string); ok {
|
||
allActiveUIDs[uid] = true
|
||
}
|
||
}
|
||
}
|
||
if folderUID, err := ensureAlertFolder(config); err == nil {
|
||
deleteObsoleteAlerts(allActiveUIDs, folderUID, config)
|
||
}
|
||
} else {
|
||
log.Printf("Alerts VL: disabled (ALERTS_VL_ENABLED=false)")
|
||
}
|
||
// Сохраняем хэш если не было критических ошибок
|
||
// Частичные ошибки отдельных алертов не блокируют сохранение хэша
|
||
if !osErr && !vlErr {
|
||
stateManager.UpdateAlertsHash(spikClients)
|
||
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("Spik clients: %d, SP clients: %d", len(spikClients), len(spClients))
|
||
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")
|
||
|
||
// VictoriaLogs second dashboard flags
|
||
vlDatasourceUID := getEnvOrDefault("VL_DATASOURCE_UID", DefaultVLDatasourceUID)
|
||
vlDashboardTitle := getEnvOrDefault("VL_DASHBOARD_TITLE", DefaultVLDashboardTitle)
|
||
vlGrafanaFolder := getEnvOrDefault("VL_GRAFANA_FOLDER", DefaultVLGrafanaFolder)
|
||
vlDashboardUID := getEnvOrDefault("VL_DASHBOARD_UID", DefaultVLDashboardUID)
|
||
flag.StringVar(&config.VLDatasourceUID, "vl-datasource-uid", vlDatasourceUID, "VictoriaLogs datasource UID (empty = skip VL dashboard)")
|
||
flag.StringVar(&config.VLDashboardTitle, "vl-dashboard-title", vlDashboardTitle, "VictoriaLogs dashboard title")
|
||
flag.StringVar(&config.VLGrafanaFolder, "vl-grafana-folder", vlGrafanaFolder, "Grafana folder for VictoriaLogs dashboard (empty = same as main)")
|
||
flag.StringVar(&config.VLDashboardUID, "vl-dashboard-uid", vlDashboardUID, "VictoriaLogs dashboard UID in Grafana")
|
||
|
||
// SP dashboard flags
|
||
spDashboardTitle := getEnvOrDefault("SP_DASHBOARD_TITLE", DefaultSPDashboardTitle)
|
||
spDashboardUID := getEnvOrDefault("SP_DASHBOARD_UID", DefaultSPDashboardUID)
|
||
spGrafanaFolder := getEnvOrDefault("SP_GRAFANA_FOLDER", DefaultSPGrafanaFolder)
|
||
flag.StringVar(&config.SPDashboardTitle, "sp-dashboard-title", spDashboardTitle, "SP VictoriaLogs dashboard title")
|
||
flag.StringVar(&config.SPDashboardUID, "sp-dashboard-uid", spDashboardUID, "SP VictoriaLogs dashboard UID in Grafana")
|
||
flag.StringVar(&config.SPGrafanaFolder, "sp-grafana-folder", spGrafanaFolder, "Grafana folder for SP dashboard (empty = same as main)")
|
||
config.SPAlertsGroup = getEnvOrDefault("SP_ALERTS_GROUP", DefaultSPAlertsGroup)
|
||
config.SPAlertsReceiver = getEnvOrDefault("SP_ALERTS_RECEIVER", DefaultSPAlertsReceiver)
|
||
|
||
// 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")
|
||
config.AlertsOSEnabled = getEnvOrDefault("ALERTS_OS_ENABLED", "true") != "false"
|
||
config.AlertsVLEnabled = getEnvOrDefault("ALERTS_VL_ENABLED", "true") != "false"
|
||
config.DashboardOSEnabled = getEnvOrDefault("DASHBOARD_OS_ENABLED", "true") != "false"
|
||
config.DashboardVLEnabled = getEnvOrDefault("DASHBOARD_VL_ENABLED", "true") != "false"
|
||
|
||
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
|
||
}
|