feat(update): независимые обновления компонентов bj

- internal/release/components.go: список компонентов (ядро bj-server, криптосервис, bj-lk,
  bj-npr, bj-quikexport) с путём и службой, установленные версии в components.json,
  расчёт обновлений по версии каждого файла и sha256 (без отката, модули — только если
  уже установлены на сервере)
- cmd/bj-component-install: root-установщик — сам перепроверяет подпись манифеста ключом
  /etc/bj/update.pub и sha256, копия прежнего файла, перезапуск только затронутых служб
- updater: bj-server (без прав root) скачивает в update-staging и передаёт установку
  root-воркеру (действие component-install); установка в фоне
- настройки: таблица компонентов с версиями, «Обновить» и «Обновить всё»
- bj-release: версия файла из <файл>.version; bj-keymedia-worker: component-install

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-15 20:16:36 +03:00
parent 5115ed7223
commit 2908697363
11 changed files with 720 additions and 114 deletions
+27 -5
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/release"
"io"
"log"
"net/http"
@@ -291,7 +292,9 @@ type SetupData struct {
ReadyCount int
TotalCount int
Certificates []cryptocli.Certificate
CurrentVersion string // версия bj-server (для раздела «Обновления»)
CurrentVersion string // версия bj-server (для раздела «Обновления»)
Components []release.ComponentState // компоненты и их версии (раздел «Обновления»)
UpdatesAvailable bool
License LicenseStatus // статус лицензии
Health []HealthStatus // здоровье фоновых компонентов
AdvancedUnlocked bool // открыты ли перспективные модули (QUIK, Fancy)
@@ -338,8 +341,11 @@ func (h *setupHandlers) renderSetup(w http.ResponseWriter, req *http.Request, fl
ready++
}
}
comps, anyUpdate := updateComponentRows(h.rc)
data := SetupData{
page: nowPage("Настройка", "setup"),
Components: comps,
UpdatesAvailable: anyUpdate,
Settings: s,
Readiness: r,
ReadyCount: ready,
@@ -1225,11 +1231,27 @@ func (h *setupHandlers) applyUpdate(w http.ResponseWriter, r *http.Request) {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
if err := NewUpdater(h.rc).ApplyUpdate(r.Context()); err != nil {
setupFlash(w, r, "Обновление не применено: "+err.Error())
return
_ = r.ParseForm()
names := r.Form["component"]
rc := h.rc
// В фоне: при обновлении ядра bj-server перезапустится раньше, чем придёт ответ установщика.
go func() {
msg, err := NewUpdater(rc).ApplyUpdate(context.Background(), names)
cfg := rc.Snapshot().Update
if err != nil {
msg = "Обновление не применено: " + err.Error()
} else {
msg = "Обновление: " + msg
}
log.Printf("lk-gateway: %s", msg)
cfg.LastResult, cfg.LastCheck = msg, time.Now().UTC()
_ = rc.SaveUpdateSettings(cfg)
}()
what := "всех компонентов с новой версией"
if len(names) > 0 {
what = strings.Join(names, ", ")
}
setupFlash(w, r, "Обновление скачано и применено — bj-server перезапускается, обновите страницу через 10 секунд")
setupFlash(w, r, "Обновление "+what+" запущено: файлы скачиваются и проверяются, затем перезапускаются службы. Итог — в строке «Последняя проверка» через минуту; при обновлении ядра страница временно не отвечает.")
}
// runTestClaim делает CreateClaim + ждёт финального состояния через GetClaim.