Агенты подхватывают изменения с сервера: каталог и самообновление
CI / test (push) Successful in 1m29s
CI / test (push) Successful in 1m29s
- сервер отдаёт агенту итоговый каталог (встроенный каталог сервера + переопределения клиента); если каталог не подходит версии агента — встроенный каталог агента и предупреждение - самообновление перед бэкапом: версия сервера новее → скачивание с того же сервера (только /dl/...), проверка sha256 и запуска новой версии, подмена с откатом, перезапуск через exec; при ошибке бэкап идёт текущей версией - галочка «Автообновление» у клиента (по умолчанию включена), версия агента в списке и карточке, отставшие подсвечены; версия сервера в шапке - миграция существующей базы: колонки auto_update и agent_version - backupctl backup --no-update Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1947b26913
commit
488abc2d34
21 files changed
+674
-51
No files matched your search
+72
-23
@@ -11,6 +11,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -27,43 +28,73 @@ type Options struct {
|
||||
Output string
|
||||
// ForceHeavy — включить heavy-части независимо от расписания.
|
||||
ForceHeavy bool
|
||||
// NoUpdate — не обновлять агента, даже если сервер предлагает.
|
||||
NoUpdate bool
|
||||
}
|
||||
|
||||
// prepare загружает конфиг, получает переопределения с сервера и собирает план.
|
||||
func prepare(ctx context.Context, opts Options, spool string) (*Plan, *api.Client, error) {
|
||||
// session — подключение к серверу и его ответ для одного запуска агента.
|
||||
type session struct {
|
||||
cfg *Config
|
||||
client *api.Client
|
||||
remote *api.AgentConfig
|
||||
}
|
||||
|
||||
// connect читает конфиг и спрашивает у сервера настройки (каталог, обновление).
|
||||
func connect(ctx context.Context, opts Options) (*session, error) {
|
||||
cfg, err := LoadConfig(opts.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
client := api.NewClient(cfg.Server, cfg.Token)
|
||||
remote, err := client.AgentConfig(ctx)
|
||||
remote, err := client.AgentConfig(ctx, runtime.GOARCH, opts.AgentVersion)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("конфиг с сервера: %w", err)
|
||||
return nil, fmt.Errorf("конфиг с сервера: %w", err)
|
||||
}
|
||||
return &session{cfg: cfg, client: client, remote: remote}, nil
|
||||
}
|
||||
|
||||
// catalogFor выбирает каталог: итоговый с сервера, а если его нет (старый сервер)
|
||||
// или он не читается этой версией агента — встроенный + переопределения клиента.
|
||||
func catalogFor(remote *api.AgentConfig) (*catalog.Config, []string, error) {
|
||||
var warns []string
|
||||
if strings.TrimSpace(remote.CatalogYAML) != "" {
|
||||
cat, err := catalog.Parse([]byte(remote.CatalogYAML))
|
||||
if err == nil {
|
||||
err = cat.Validate()
|
||||
}
|
||||
if err == nil {
|
||||
return cat, nil, nil
|
||||
}
|
||||
warns = append(warns, fmt.Sprintf("каталог с сервера не подошёл этой версии агента (%v) — использую встроенный", err))
|
||||
}
|
||||
cat, err := catalog.Default()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, warns, err
|
||||
}
|
||||
if strings.TrimSpace(remote.ConfigYAML) != "" {
|
||||
over, err := catalog.Parse([]byte(remote.ConfigYAML))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("конфиг клиента с сервера: %w", err)
|
||||
return nil, warns, fmt.Errorf("конфиг клиента с сервера: %w", err)
|
||||
}
|
||||
cat = catalog.Merge(cat, over)
|
||||
}
|
||||
if err := cat.Validate(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if spool == "" {
|
||||
spool = cfg.SpoolDir
|
||||
}
|
||||
plan, err := Collect(ctx, cat, spool, remote.HeavyDue || opts.ForceHeavy)
|
||||
return cat, warns, cat.Validate()
|
||||
}
|
||||
|
||||
// prepare собирает план бэкапа по каталогу.
|
||||
func (s *session) prepare(ctx context.Context, opts Options, spool string, warns []string) (*Plan, error) {
|
||||
cat, catWarns, err := catalogFor(s.remote)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
plan.Manifest.Domain = remote.Domain
|
||||
plan, err := Collect(ctx, cat, spool, s.remote.HeavyDue || opts.ForceHeavy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plan.Manifest.Domain = s.remote.Domain
|
||||
plan.Manifest.AgentVersion = opts.AgentVersion
|
||||
return plan, client, nil
|
||||
plan.Manifest.Warnings = append(append(warns, catWarns...), plan.Manifest.Warnings...)
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
func newSpool(base string) (string, error) {
|
||||
@@ -73,20 +104,24 @@ func newSpool(base string) (string, error) {
|
||||
|
||||
// PrintPlan выводит, что попадёт в бэкап. Дампы БД при этом реально выполняются (во временный каталог).
|
||||
func PrintPlan(ctx context.Context, opts Options, w io.Writer) error {
|
||||
cfg, err := LoadConfig(opts.ConfigPath)
|
||||
sess, err := connect(ctx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
spool, err := newSpool(cfg.SpoolDir)
|
||||
spool, err := newSpool(sess.cfg.SpoolDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(spool)
|
||||
plan, _, err := prepare(ctx, opts, spool)
|
||||
plan, err := sess.prepare(ctx, opts, spool, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m := &plan.Manifest
|
||||
fmt.Fprintf(w, "Агент %s, сервер %s\n", opts.AgentVersion, sess.remote.ServerVersion)
|
||||
if u := sess.remote.Update; u != nil {
|
||||
fmt.Fprintf(w, "Доступно обновление агента до %s — установится при следующем бэкапе\n", u.Version)
|
||||
}
|
||||
fmt.Fprintf(w, "Домен: %s, hostname: %s, %s (%s)\n", m.Domain, m.Hostname, m.OS.PrettyName, m.OS.Arch)
|
||||
if len(m.Packages) > 0 {
|
||||
fmt.Fprintf(w, "Пакеты:")
|
||||
@@ -161,18 +196,32 @@ func Backup(ctx context.Context, opts Options) (err error) {
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
cfg, err := LoadConfig(opts.ConfigPath)
|
||||
sess, err := connect(ctx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
spool, err := newSpool(cfg.SpoolDir)
|
||||
// Самообновление — до сбора: при успехе процесс перезапускается новой версией
|
||||
// (exec не возвращается), при ошибке бэкап идёт текущей версией с предупреждением.
|
||||
var warns []string
|
||||
if u := sess.remote.Update; u != nil && !opts.NoUpdate && os.Getenv(updatedEnv) == "" {
|
||||
log.Printf("обновление агента %s → %s", opts.AgentVersion, u.Version)
|
||||
if err := selfUpdate(ctx, sess.client, u, opts.AgentVersion); err != nil {
|
||||
log.Printf("! обновление не удалось: %v", err)
|
||||
warns = append(warns, fmt.Sprintf("обновление агента до %s не удалось: %v", u.Version, err))
|
||||
}
|
||||
}
|
||||
if from := os.Getenv(updatedEnv); from != "" {
|
||||
log.Printf("агент обновлён: %s → %s", from, opts.AgentVersion)
|
||||
}
|
||||
client := sess.client
|
||||
spool, err := newSpool(sess.cfg.SpoolDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(spool)
|
||||
|
||||
start := time.Now()
|
||||
plan, client, err := prepare(ctx, opts, spool)
|
||||
plan, err := sess.prepare(ctx, opts, spool, warns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user