Агенты подхватывают изменения с сервера: каталог и самообновление
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
@@ -0,0 +1,112 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"gitea.hsrv.site/mrleo1nid/backup-master/internal/catalog"
|
||||
)
|
||||
|
||||
func TestAgentConfigCatalogAndUpdate(t *testing.T) {
|
||||
dist := t.TempDir()
|
||||
bin := []byte("fake-backupctl-binary")
|
||||
if err := os.WriteFile(filepath.Join(dist, "backupctl-linux-amd64"), bin, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s, err := New(Config{DataDir: t.TempDir(), DistDir: dist, Version: "v0.6.0"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
tok := newToken()
|
||||
id, err := s.st.CreateClient("x", "x.example.com", hashToken(tok))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, _ := s.st.GetClient(id)
|
||||
if !c.AutoUpdate {
|
||||
t.Fatal("автообновление должно быть включено по умолчанию")
|
||||
}
|
||||
c.ConfigYAML = "categories:\n - {name: scripts, disabled: true}\n"
|
||||
if err := s.st.UpdateClientSettings(c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ts := newTestHTTP(t, s)
|
||||
var cfg AgentConfigResponse
|
||||
decode(t, call(t, "GET", ts+"/api/v1/agent/config?arch=amd64&version=v0.5.0", tok, nil), &cfg)
|
||||
|
||||
cat, err := catalog.Parse([]byte(cfg.CatalogYAML))
|
||||
if err != nil || cat.Validate() != nil {
|
||||
t.Fatalf("каталог не читается: %v", err)
|
||||
}
|
||||
if cat.Find("xui") == nil || !cat.Find("scripts").Disabled {
|
||||
t.Fatal("в каталоге нет встроенных категорий или переопределений клиента")
|
||||
}
|
||||
sum := sha256.Sum256(bin)
|
||||
if cfg.Update == nil || cfg.Update.Version != "v0.6.0" || cfg.Update.Path != "/dl/backupctl-linux-amd64" || cfg.Update.SHA256 != hex.EncodeToString(sum[:]) {
|
||||
t.Fatalf("update: %+v", cfg.Update)
|
||||
}
|
||||
if c, _ := s.st.GetClient(id); c.AgentVersion != "v0.5.0" {
|
||||
t.Fatalf("версия агента не записана: %q", c.AgentVersion)
|
||||
}
|
||||
|
||||
// актуальная версия, неизвестная архитектура, выключенное автообновление — без обновления
|
||||
for _, q := range []string{"arch=amd64&version=v0.6.0", "arch=riscv64&version=v0.5.0"} {
|
||||
cfg = AgentConfigResponse{}
|
||||
decode(t, call(t, "GET", ts+"/api/v1/agent/config?"+q, tok, nil), &cfg)
|
||||
if cfg.Update != nil {
|
||||
t.Fatalf("%s: обновление не ожидалось", q)
|
||||
}
|
||||
}
|
||||
c, _ = s.st.GetClient(id)
|
||||
c.AutoUpdate = false
|
||||
_ = s.st.UpdateClientSettings(c)
|
||||
cfg = AgentConfigResponse{}
|
||||
decode(t, call(t, "GET", ts+"/api/v1/agent/config?arch=amd64&version=v0.5.0", tok, nil), &cfg)
|
||||
if cfg.Update != nil {
|
||||
t.Fatal("автообновление выключено, а обновление предложено")
|
||||
}
|
||||
}
|
||||
|
||||
// Базы первых релизов без новых колонок мигрируются при открытии.
|
||||
func TestStoreMigration(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "old.db")
|
||||
db, err := sql.Open("sqlite", "file:"+path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = db.Exec(`CREATE TABLE clients (id INTEGER PRIMARY KEY, name TEXT NOT NULL, domain TEXT NOT NULL UNIQUE,
|
||||
token_hash TEXT NOT NULL UNIQUE, config_yaml TEXT NOT NULL DEFAULT '', keep_daily INTEGER NOT NULL DEFAULT 7,
|
||||
keep_weekly INTEGER NOT NULL DEFAULT 4, keep_monthly INTEGER NOT NULL DEFAULT 6, heavy_keep INTEGER NOT NULL DEFAULT 1,
|
||||
heavy_every_days INTEGER NOT NULL DEFAULT 7, stale_hours INTEGER NOT NULL DEFAULT 26, created_at INTEGER NOT NULL,
|
||||
last_backup_at INTEGER NOT NULL DEFAULT 0, last_heavy_at INTEGER NOT NULL DEFAULT 0, last_seen_at INTEGER NOT NULL DEFAULT 0,
|
||||
last_error TEXT NOT NULL DEFAULT '', last_error_at INTEGER NOT NULL DEFAULT 0, stale_notified INTEGER NOT NULL DEFAULT 0,
|
||||
disabled INTEGER NOT NULL DEFAULT 0);
|
||||
INSERT INTO clients (name, domain, token_hash, created_at) VALUES ('a', 'a.example.com', 'h', 1);`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db.Close()
|
||||
|
||||
st, err := OpenStore(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer st.Close()
|
||||
cs, err := st.ListClients()
|
||||
if err != nil || len(cs) != 1 || !cs[0].AutoUpdate {
|
||||
t.Fatalf("после миграции: %v %+v", err, cs)
|
||||
}
|
||||
// повторное открытие не падает на уже добавленных колонках
|
||||
st.Close()
|
||||
if st2, err := OpenStore(path); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
st2.Close()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user