Обновления из файла для закрытого контура: пакет с портала → проверка подписи → ядро, потом модули

- release: пакет обновления (ExtractBundle/ReadStagedManifest/WriteBundle), Artifact.MinCore,
  план блокирует модуль, которому нужно ядро новее, чем будет после установки
- bj-release: модули получают min_core = версия релиза
- настройки → Обновления: «Обновить из файла» (zip до 2 ГБ), фон как у сетевого обновления,
  причина блокировки в таблице компонентов

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
zuevav
2026-09-18 11:27:23 +03:00
parent 5bafd60df6
commit 17d8ec3988
8 changed files with 443 additions and 2 deletions
+48
View File
@@ -78,6 +78,7 @@ func registerSetup(mux *http.ServeMux, a *admin, rc *RuntimeConfig, svc *Service
mux.HandleFunc("/admin/setup/update", h.saveUpdate)
mux.HandleFunc("/admin/setup/update/check", h.checkUpdate)
mux.HandleFunc("/admin/setup/update/apply", h.applyUpdate)
mux.HandleFunc("/admin/setup/update/upload", h.uploadUpdate)
mux.HandleFunc("/admin/setup/license", h.saveLicense)
mux.HandleFunc("/admin/setup/license/activate", h.licenseActivate)
mux.HandleFunc("/admin/setup/license/renew", h.licenseRenewNow)
@@ -1287,6 +1288,53 @@ func (h *setupHandlers) applyUpdate(w http.ResponseWriter, r *http.Request) {
setupFlash(w, r, "Обновление "+what+" запущено: файлы скачиваются и проверяются, затем перезапускаются службы. Итог — в строке «Последняя проверка» через минуту; при обновлении ядра страница временно не отвечает.")
}
// uploadUpdate — POST /admin/setup/update/upload: пакет обновления файлом (закрытый контур).
// Файл сохраняется в каталог подготовки, дальше — в фоне, как и обновление по сети.
func (h *setupHandlers) uploadUpdate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 2<<30)
f, hdr, err := r.FormFile("bundle")
if err != nil {
setupFlash(w, r, "Обновление из файла: файл не получен ("+err.Error()+")")
return
}
defer f.Close()
if err := os.MkdirAll(updateStagingDir, 0o750); err != nil {
setupFlash(w, r, "Обновление из файла: каталог подготовки: "+err.Error())
return
}
dst := filepath.Join(updateStagingDir, fmt.Sprintf("upload-%d.zip", time.Now().UnixNano()))
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o640)
if err != nil {
setupFlash(w, r, "Обновление из файла: "+err.Error())
return
}
n, err := io.Copy(out, f)
out.Close()
if err != nil {
_ = os.Remove(dst)
setupFlash(w, r, "Обновление из файла: не удалось сохранить: "+err.Error())
return
}
rc := h.rc
go func() {
msg, err := NewUpdater(rc).ApplyBundle(context.Background(), dst)
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)
}()
setupFlash(w, r, fmt.Sprintf("Файл %s (%d МБ) принят: проверяется подпись, затем ставятся компоненты новее текущих — ядро, потом модули. Итог — в строке «Последняя проверка» через минуту; при обновлении ядра страница временно не отвечает.", hdr.Filename, n>>20))
}
// runTestClaim делает CreateClaim + ждёт финального состояния через GetClaim.
func (h *setupHandlers) runTestClaim() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)