Netpulse_SasS/server/cmd/netpulse-gitsync/main.go
byrsapty 65b37a28ff
Some checks are pending
CI / web (push) Waiting to run
CI / server (push) Waiting to run
CI / agent (push) Waiting to run
Git-двигун NCM: версіювання конфігів, переливання історії, довільне порівняння
go-git, чистий Go без cgo. Голий репозиторій на тенанта, гілка на
пристрій за ідентифікатором (ім'я змінюють, історія не має від цього
розсипатись), файл за іменем. Однаковий вміст нового коміту не створює.

netpulse-gitsync переливає накопичену історію й відтворює втрачений
репозиторій із бази — тіла конфігів там і так лежать зашифрованими.

У вебі з'явився вибір версії, з якою порівнювати: сервер це вмів
(?from=), інтерфейс — ні.

Дорогою виправлено чотири тести, які CI запустив уперше з базою.
Серед них справжня помилка: машинний токен не міг читати мапи —
ACL мап отримував порожній рядок замість uuid і давав 500.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:15:14 +03:00

133 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Команда netpulse-gitsync — переливання історії конфігів у Git.
//
// Потрібна двічі за життя інсталяції.
//
// Коли версіювання вмикають на системі, яка вже місяцями збирає конфіги:
// без переливання репозиторій починається з наступної зміни, і вся
// накопичена історія лишається невидимою саме тоді, коли її вперше
// захотіли подивитись.
//
// І коли диск із репозиторієм втрачено. Тіла конфігів лежать
// зашифрованими в базі, тож Git повністю відтворюваний з неї —
// зворотне невірно, і саме тому база лишається джерелом істини.
//
// netpulse-gitsync -dsn ... -dek np1=<hex> -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=<hex|base64>[,...]")
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
}