go-git, чистий Go без cgo. Голий репозиторій на тенанта, гілка на пристрій за ідентифікатором (ім'я змінюють, історія не має від цього розсипатись), файл за іменем. Однаковий вміст нового коміту не створює. netpulse-gitsync переливає накопичену історію й відтворює втрачений репозиторій із бази — тіла конфігів там і так лежать зашифрованими. У вебі з'явився вибір версії, з якою порівнювати: сервер це вмів (?from=), інтерфейс — ні. Дорогою виправлено чотири тести, які CI запустив уперше з базою. Серед них справжня помилка: машинний токен не міг читати мапи — ACL мап отримував порожній рядок замість uuid і давав 500. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
133 lines
4.2 KiB
Go
133 lines
4.2 KiB
Go
// Команда 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
|
||
}
|