// Команда netpulse-gitsync — переливання історії конфігів у Git. // // Потрібна двічі за життя інсталяції. // // Коли версіювання вмикають на системі, яка вже місяцями збирає конфіги: // без переливання репозиторій починається з наступної зміни, і вся // накопичена історія лишається невидимою саме тоді, коли її вперше // захотіли подивитись. // // І коли диск із репозиторієм втрачено. Тіла конфігів лежать // зашифрованими в базі, тож Git повністю відтворюваний з неї — // зворотне невірно, і саме тому база лишається джерелом істини. // // netpulse-gitsync -dsn ... -dek np1= -git-root /var/lib/netpulse/git // // Повторний запуск безпечний: незмінений вміст нового коміту не створює. package main import ( "context" "flag" "fmt" "os" "os/signal" "syscall" "time" "github.com/netpulse/netpulse/server/internal/crypto" "github.com/netpulse/netpulse/server/internal/gitstore" "github.com/netpulse/netpulse/server/internal/store" ) func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, "netpulse-gitsync:", err) os.Exit(1) } } func run() error { var ( dsn = flag.String("dsn", os.Getenv("NETPULSE_DSN"), "DSN PostgreSQL") keys = flag.String("dek", os.Getenv("NETPULSE_DEK"), "ключі шифрування: key_id=[,...]") gitRoot = flag.String("git-root", envOr("NETPULSE_GIT_ROOT", "/var/lib/netpulse/git"), "корінь сховища версій") tenant = flag.String("tenant", "", "uuid тенанта; порожньо — усі") dryRun = flag.Bool("dry-run", false, "лише показати, що буде записано") quiet = flag.Bool("quiet", false, "без переліку версій") ) flag.Parse() if *dsn == "" { return fmt.Errorf("не вказано -dsn") } if *gitRoot == "" { return fmt.Errorf("не вказано -git-root") } ring, err := crypto.ParseKeyring(*keys) if err != nil { return fmt.Errorf("-dek: %w", err) } if ring == nil { return fmt.Errorf("не вказано -dek: тіла конфігів зашифровані, без ключа їх не прочитати") } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() st, err := store.New(ctx, *dsn) if err != nil { return fmt.Errorf("підключення до БД: %w", err) } defer st.Close() st.UseGit(gitstore.New(*gitRoot)) tenants := []string{*tenant} if *tenant == "" { tenants, err = st.TenantIDs(ctx) if err != nil { return err } } var total store.GitSyncStat for _, id := range tenants { var onProgress func(string, string, time.Time, gitstore.Result) if !*quiet { onProgress = func(device, kind string, at time.Time, res gitstore.Result) { mark := "+" if res.Unchanged { mark = "=" } fmt.Printf(" %s %s %s %s %s\n", mark, at.Format("2006-01-02 15:04"), device, kind, short(res.CommitSHA)) } } fmt.Printf("тенант %s\n", id) stat, err := st.SyncGit(ctx, id, ring, *dryRun, onProgress) if err != nil { return fmt.Errorf("тенант %s: %w", id, err) } total.Total += stat.Total total.Written += stat.Written total.Unchanged += stat.Unchanged total.Skipped += stat.Skipped } verb := "записано" if *dryRun { verb = "буде записано" } fmt.Printf("\nверсій %d, %s %d, без змін %d, пропущено %d\n", total.Total, verb, total.Written, total.Unchanged, total.Skipped) if total.Skipped > 0 { fmt.Println("пропущені версії — це записи без збереженого тіла або з чужим ключем шифрування") } return nil } func envOr(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } func short(sha string) string { if len(sha) > 12 { return sha[:12] } return sha }