Обновление: компоненты, которых не знает прежний root-установщик (igate-dist), — вторым заходом, сразу или после перезапуска

Старый bj-component-install отклонял обновление целиком из-за незнакомого igate-dist — вместе со своей
новой версией (рабочий сервер: установщик .06.2). installerSince: с какой версии установщик знает
компонент; отложенное ставится тем же нажатием после нового установщика, а если перезапускался
bj-server — само после старта (Update.Pending). Тест TestLKAgentEndToEnd: ждёт счётчик агента (гонка).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-10-06 23:44:55 +03:00
parent efa135b513
commit a9cb055504
4 changed files with 103 additions and 24 deletions
+7 -1
View File
@@ -63,7 +63,13 @@ func TestLKAgentEndToEnd(t *testing.T) {
if resp.StatusCode != http.StatusOK || !out.OK || out.Mode != "agent" || out.BJ["service"] != "bj-server" {
t.Fatalf("ответ ядра через очередь: код %d, %+v", resp.StatusCode, out)
}
if st := lkAgentStatus(rc.Snapshot()); !st.Connected || st.Served == 0 {
// счётчик агент увеличивает после того, как отдал ответ кабинету, — клиент может получить ответ раньше
st := lkAgentStatus(rc.Snapshot())
for i := 0; i < 40 && (!st.Connected || st.Served == 0); i++ {
time.Sleep(50 * time.Millisecond)
st = lkAgentStatus(rc.Snapshot())
}
if !st.Connected || st.Served == 0 {
t.Fatalf("состояние канала: %+v", st)
}
+22
View File
@@ -1,6 +1,7 @@
package lkgateway
import (
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/release"
"net/http"
"net/http/httptest"
"path/filepath"
@@ -184,3 +185,24 @@ func TestCreateUserLoginTaken(t *testing.T) {
t.Fatal("один администратор — шаг не выполнен")
}
}
// Старый root-установщик не знает igate-dist (рабочий сервер 06.10.2026: установщик .06.2) — сначала ставится
// новый установщик, igate-dist откладывается на второй заход; новый установщик — всё одним заходом.
func TestDeferForInstaller(t *testing.T) {
st := func(name, inst string, upd, isNew bool) release.ComponentState {
return release.ComponentState{Component: release.Component{Name: name}, Installed: inst, Update: upd, New: isNew}
}
plan := []release.ComponentState{st("bj-component-install", "2026.10.06.2", true, false), st("igate-dist", "", true, true), st("bj-server", "2026.10.06.26", true, false)}
kept, deferred := deferForInstaller(plan, plan)
if len(kept) != 2 || len(deferred) != 1 || deferred[0] != "igate-dist" {
t.Fatalf("старый установщик: оставлено %v, отложено %v", kept, deferred)
}
plan[0] = st("bj-component-install", "2026.10.06.15", true, false)
if kept, deferred := deferForInstaller(plan, plan); len(kept) != 3 || len(deferred) != 0 {
t.Fatalf("установщик знает igate-dist: оставлено %d, отложено %v", len(kept), deferred)
}
plan[0] = st("bj-component-install", "2026.10.06.2", false, false) // установщик не обновляется — откладывать нечем
if kept, deferred := deferForInstaller(plan, plan); len(kept) != 3 || len(deferred) != 0 {
t.Fatalf("без нового установщика: оставлено %d, отложено %v", len(kept), deferred)
}
}
+2
View File
@@ -113,6 +113,8 @@ type UpdateSettings struct {
ComponentsChannel string `json:"components_channel,omitempty"`
// Notified — выпуск, о котором уже написали администратору («Доступно обновление bj»): по разу на выпуск.
Notified string `json:"notified,omitempty"`
// Pending — компоненты, отложенные до нового установщика обновлений (ставятся после перезапуска bj-server).
Pending []string `json:"pending,omitempty"`
}
// NewsSettings — лента новостей (события системы, окна техработ, обновления
+71 -22
View File
@@ -45,6 +45,12 @@ const DefaultUpdateURL = "https://lic.zetit.ru/updates"
// installerKnowsAll — с этой версии root-установщик обновлений знает bj-installer и ставит выделенные модули.
const installerKnowsAll = "2026.09.25"
// installerSince — с какой версии root-установщик (bj-component-install) знает компонент. Старый установщик
// отклоняет обновление целиком, если в нём есть незнакомый компонент — вместе с собственной новой версией
// (06.10.2026 на рабочем сервере: установщик .06.2, «неизвестный компонент "igate-dist"»). Такие компоненты
// ставятся вторым заходом, уже новым установщиком — сразу или после перезапуска bj-server.
var installerSince = map[string]string{"bj-installer": installerKnowsAll, "igate-dist": "2026.10.06.15"}
type Updater struct{ rc *RuntimeConfig }
func NewUpdater(rc *RuntimeConfig) *Updater { return &Updater{rc: rc} }
@@ -294,26 +300,9 @@ func (u *Updater) applyUpdate(ctx context.Context, names []string) (string, erro
// из ядра модули: пока он сам не обновлён, такие компоненты откладываются (второй заход). Установщик
// новее этого умеет всё — обновление одним заходом, даже если вместе с ним обновляется и он сам
// (раньше любое его обновление делило установку на два раза: «скачал 24 МБ — снова „Обновить“ — ещё 6»).
installerOutdated := false
for _, c := range plan {
if c.Name == "bj-component-install" && c.Update && (c.Installed == "" || release.IsNewer(installerKnowsAll, c.Installed)) {
installerOutdated = true
}
}
var deferred []string
if installerOutdated {
kept := selected[:0]
for _, c := range selected {
if c.New || c.Name == "bj-installer" { // bj-installer прежний установщик обновлений не знает
deferred = append(deferred, c.Name)
continue
}
kept = append(kept, c)
}
selected = kept
selected, deferred := deferForInstaller(plan, selected)
if len(selected) == 0 {
return "", fmt.Errorf("сначала обновите установщик обновлений (bj-component-install): текущий не ставит модули %s", strings.Join(deferred, ", "))
}
return "", fmt.Errorf("сначала обновите установщик обновлений (bj-component-install): текущий не ставит %s", strings.Join(deferred, ", "))
}
cleanupStaging(24 * time.Hour)
@@ -411,10 +400,25 @@ func (u *Updater) applyUpdate(ctx context.Context, names []string) (string, erro
}
msg += "; перезапущены службы: " + strings.Join(units, ", ")
}
if len(deferred) > 0 {
msg += "; модули " + strings.Join(deferred, ", ") + " будут установлены после перезапуска bj-server новым установщиком"
}
msg += unlicensedNote(off)
if len(deferred) > 0 {
core := false
for _, n := range req.Names {
core = core || n == "bj-server"
}
if core { // bj-server перезапускается — доставит отложенное сам после старта (updateLoop)
cfg := u.rc.Snapshot().Update
cfg.Pending = deferred
_ = u.rc.SaveUpdateSettings(cfg)
return msg + "; " + strings.Join(deferred, ", ") + " — сразу после перезапуска bj-server, новым установщиком", nil
}
log.Printf("lk-gateway: установщик обновлён — второй заход: %s", strings.Join(deferred, ", "))
more, err := u.applyUpdate(ctx, deferred)
if err != nil {
return msg + "; " + strings.Join(deferred, ", ") + " не установлены: " + err.Error(), nil
}
return msg + "; затем " + more, nil
}
return msg, nil
}
@@ -585,6 +589,7 @@ func (u *Updater) updateLoop(ctx context.Context) {
cleanupStaging(time.Hour)
enforceModuleLicense(ctx, u.rc)
_, _ = u.ensureSplitModules(ctx)
u.applyPending(ctx)
check()
}
for {
@@ -597,6 +602,50 @@ func (u *Updater) updateLoop(ctx context.Context) {
}
}
// deferForInstaller — что из выбранного прежний root-установщик не поставит (installerSince, впервые
// выделенные модули): откладывается до второго захода, когда в первом обновится сам установщик.
func deferForInstaller(plan, selected []release.ComponentState) (kept []release.ComponentState, deferred []string) {
installerUpd, installerVer := false, ""
for _, c := range plan {
if c.Name == "bj-component-install" {
installerUpd, installerVer = c.Update, c.Installed
}
}
if !installerUpd {
return selected, nil
}
older := func(since string) bool { return installerVer == "" || release.IsNewer(since, installerVer) }
for _, c := range selected {
since, known := installerSince[c.Name]
if (known && older(since)) || (c.New && older(installerKnowsAll)) {
deferred = append(deferred, c.Name)
continue
}
kept = append(kept, c)
}
return kept, deferred
}
// applyPending — компоненты, отложенные прошлым обновлением до нового установщика (обновление шло вместе с
// bj-server и перезапустило его): ставятся сами, без второго нажатия «Обновить».
func (u *Updater) applyPending(ctx context.Context) {
cfg := u.rc.Snapshot().Update
if len(cfg.Pending) == 0 {
return
}
names := cfg.Pending
cfg.Pending = nil // один заход: не получилось — человек увидит компонент в списке обновлений
_ = u.rc.SaveUpdateSettings(cfg)
actx, cancel := context.WithTimeout(ctx, 30*time.Minute)
defer cancel()
msg, err := u.ApplyUpdate(actx, names)
if err != nil {
log.Printf("lk-gateway: отложенные компоненты %s не установлены: %v", strings.Join(names, ", "), err)
return
}
log.Printf("lk-gateway: отложенные компоненты: %s", msg)
}
// notifyUpdate — администратору системы: вышел выпуск, который на этом сервере ещё не установлен (по разу на выпуск).
func (u *Updater) notifyUpdate(st UpdateStatus) {
cfg := u.rc.Snapshot().Update