Files
Bridge-and-Join-s/internal/lkgateway/updater.go
T
zuevav a9cb055504 Обновление: компоненты, которых не знает прежний 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
2026-10-06 23:44:55 +03:00

815 lines
34 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lkgateway
// Обновления bj по компонентам (internal/release/components.go). У каждого компонента своя версия в
// манифесте артефактории: ядро, криптосервис, ЛК клиента, НПР, экспорт QUIK обновляются независимо.
//
// bj-server работает без прав root, поэтому сам только проверяет манифест, скачивает выбранные
// файлы в /var/lib/bj/update-staging и передаёт установку root-воркеру (действие
// component-install → /usr/local/sbin/bj-component-install). Установщик заново проверяет подпись и
// sha256, ставит файлы и перезапускает службы; при обновлении ядра bj-server перезапустится сам.
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"sync"
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/release"
)
// BuildVersion — версия bj-server. Переопределяется при сборке:
//
// go build -ldflags "-X .../lkgateway.BuildVersion=1.2.0"
var BuildVersion = "0.1.0"
// DefaultUpdatePublicKey — публичный ключ артефактории, зашитый в релиз.
// Открытая часть ключа подписи релизов. Если задан в
// настройках (UpdateSettings.PublicKey) — приоритет у настроек (для проверки
// в bj; root-установщик верит только /etc/bj/update.pub).
var DefaultUpdatePublicKey = release.DefaultPublicKey
const updateStagingDir = "/var/lib/bj/update-staging"
// DefaultUpdateURL — сервер обновлений по умолчанию (встроен в сервер лицензий).
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} }
type UpdateStatus struct {
Configured bool
CurrentVersion string
Available string // версия релиза
HasUpdate bool // есть что обновить хотя бы в одном компоненте
Channel string
Notes string
Message string
Components []release.ComponentState
}
func (u *Updater) updateClient() (*release.Client, error) {
cfg := u.rc.Snapshot().Update
pub := cfg.PublicKey
if pub == "" {
pub = DefaultUpdatePublicKey
}
base := cfg.BaseURL
if base == "" {
base = DefaultUpdateURL
}
if pub == "" {
return nil, fmt.Errorf("обновления не настроены: в этой сборке нет открытого ключа подписи релизов")
}
channel := cfg.Channel
if channel == "" {
channel = "stable"
}
cl, err := release.NewClient(base, channel, pub)
if err != nil {
return nil, err
}
cl.Token = strings.TrimSpace(u.rc.Snapshot().License.Key) // лицензионный токен установки
return cl, nil
}
// CheckForUpdate — манифест и состояние каждого компонента.
func (u *Updater) CheckForUpdate(ctx context.Context) (UpdateStatus, error) {
st := UpdateStatus{CurrentVersion: BuildVersion, Channel: u.rc.Snapshot().Update.Channel}
cl, err := u.updateClient()
if err != nil {
st.Message = err.Error()
return st, nil // не настроено — не ошибка
}
st.Configured = true
m, err := cl.FetchManifest(ctx)
if err != nil {
st.Message = "проверка не удалась: " + err.Error()
u.saveCheckResult(st, nil)
return st, err
}
st.Available, st.Notes = m.Version, m.Notes
syncSameFileVersions(m)
st.Components = release.Plan("", m, release.LoadInstalled(release.InstalledFile), BuildVersion)
release.MarkSplitModules(st.Components, u.allowed)
off := markUnlicensed(st.Components, u.allowed)
var upd []string
avail := map[string]string{}
for _, c := range st.Components {
if c.Available != "" {
avail[c.Name] = c.Available
}
if c.New {
upd = append(upd, c.Name+" "+c.Available+" (установка модуля)")
} else if c.Update {
upd = append(upd, c.Name+" "+c.Available)
}
}
st.HasUpdate = len(upd) > 0
if st.HasUpdate {
st.Message = "доступны обновления: " + strings.Join(upd, ", ")
} else {
st.Message = "все установленные компоненты актуальны (релиз " + m.Version + ")"
}
st.Message += unlicensedNote(off)
u.saveCheckResult(st, avail)
return st, nil
}
// syncSameFileVersions — если файл компонента на сервере побайтно совпадает с файлом выпуска, а
// записана другая версия (компонент не менялся, а номер выпуска вырос, или файл выложен вручную),
// записываем версию выпуска: иначе список предлагает «обновить» то, что установщик отклонит как
// «обновление не требуется».
func syncSameFileVersions(m *release.Manifest) {
installed := release.LoadInstalled(release.InstalledFile)
changed := false
for _, a := range m.Artifacts {
c, ok := release.ComponentByName(a.Name)
if !ok || a.SHA256 == "" {
continue
}
v := a.Version
if v == "" {
v = m.Version
}
if cur, ok := installed[a.Name]; ok && cur.Version == v {
continue
}
sum, err := fileSHA256(c.Path)
if err != nil || !strings.EqualFold(sum, a.SHA256) {
continue
}
installed[a.Name] = release.Installed{Version: v, SHA256: sum, InstalledAt: time.Now().UTC()}
changed = true
}
if changed {
if err := release.SaveInstalled(release.InstalledFile, installed); err != nil {
log.Printf("lk-gateway: запись версий совпадающих компонентов: %v", err)
}
}
}
func fileSHA256(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// markUnlicensed — модули, установленные на сервере, но снятые с лицензии: не обновляются (сервер
// обновлений их файлы не отдаёт), в списке — с пояснением. Возвращает названия таких модулей.
func markUnlicensed(states []release.ComponentState, allowed func(string) bool) []string {
var disabled map[string]string
if globalRC != nil {
disabled = globalRC.Snapshot().Update.DisabledModules
}
var off []string
for i := range states {
c := &states[i]
if c.Core || c.Feature == "" || c.Feature == "updates" || allowed(c.Feature) {
continue
}
c.Update, c.New = false, false
if _, already := disabled[c.Name]; c.Present && !already { // об отключённом уже сказано один раз
c.Blocked = "не входит в лицензию — модуль отключён и не обновляется; чтобы подключить, свяжитесь с ZETIT"
off = append(off, "«"+c.Title+"»")
}
}
return off
}
func unlicensedNote(off []string) string {
if len(off) == 0 {
return ""
}
if len(off) == 1 {
return "; модуль " + off[0] + " не входит в лицензию — отключён и не обновлялся; чтобы подключить, свяжитесь с ZETIT"
}
return "; модули " + strings.Join(off, ", ") + " не входят в лицензию — отключены и не обновлялись; чтобы подключить, свяжитесь с ZETIT"
}
// updateChannel — канал обновлений установки (пусто = stable).
func updateChannel(s UpdateSettings) string {
if c := strings.TrimSpace(s.Channel); c != "" {
return c
}
return "stable"
}
func (u *Updater) channel() string { return updateChannel(u.rc.Snapshot().Update) }
func (u *Updater) saveCheckResult(st UpdateStatus, avail map[string]string) {
cfg := u.rc.Snapshot().Update
cfg.LastCheck = time.Now().UTC()
cfg.LastResult = st.Message
cfg.CheckFailed = avail == nil // nil — манифест получить не удалось
cfg.Available = st.Available
cfg.Notes = st.Notes
if avail != nil {
cfg.Components = avail
cfg.ComponentsChannel = u.channel()
}
if err := u.rc.SaveUpdateSettings(cfg); err != nil {
log.Printf("lk-gateway: сохранение результата проверки обновления: %v", err)
}
}
// ApplyUpdate — скачать выбранные компоненты (names пусто — все, для которых есть обновление) и
// передать установку root-установщику. Если среди них ядро, bj-server перезапустится до ответа.
func (u *Updater) ApplyUpdate(ctx context.Context, names []string) (string, error) {
progressBegin(4)
progressStage(1, "Проверка выпуска на сервере лицензий")
msg, err := u.applyUpdate(ctx, names)
progressEnd(msg, err)
if err == nil { // с обновлением мог прийти новый проверенный дистрибутив ИШ — обновить шлюз
go igateAutoUpgrade(context.Background(), u.rc)
}
return msg, err
}
func (u *Updater) applyUpdate(ctx context.Context, names []string) (string, error) {
if licensingEnabled(u.rc) {
ls := licenseStatus(u.rc)
if !ls.Valid {
return "", fmt.Errorf("обновления заблокированы — лицензия: %s", ls.Message)
}
if !ls.AllowsUpdates {
return "", fmt.Errorf("обновления не входят в план %q", ls.Plan)
}
}
cl, err := u.updateClient()
if err != nil {
return "", err
}
sm, m, err := cl.FetchSigned(ctx)
if err != nil {
return "", fmt.Errorf("манифест: %w", err)
}
syncSameFileVersions(m)
plan := release.Plan("", m, release.LoadInstalled(release.InstalledFile), BuildVersion)
release.MarkSplitModules(plan, u.allowed)
off := markUnlicensed(plan, u.allowed)
want := map[string]bool{}
for _, n := range names {
want[n] = true
}
var selected []release.ComponentState
for _, c := range plan {
switch {
case len(want) > 0 && !want[c.Name]:
case c.Update:
selected = append(selected, c)
case want[c.Name] && c.Blocked != "":
return "", fmt.Errorf("%s: %s", c.Title, c.Blocked)
case want[c.Name]:
return "", fmt.Errorf("%s: обновление не требуется (установлена %s, в релизе %s)", c.Title, pickVersion(c.Installed), pickVersion(c.Available))
}
}
if len(selected) == 0 {
if len(off) > 0 {
return "обновлять нечего: компоненты лицензии актуальны (релиз " + m.Version + ")" + unlicensedNote(off), nil
}
return "", fmt.Errorf("нечего обновлять: все установленные компоненты актуальны (релиз %s)", m.Version)
}
// Очень старый root-установщик (до 25.09.2026) не знает bj-installer и не ставит впервые выделенные
// из ядра модули: пока он сам не обновлён, такие компоненты откладываются (второй заход). Установщик
// новее этого умеет всё — обновление одним заходом, даже если вместе с ним обновляется и он сам
// (раньше любое его обновление делило установку на два раза: «скачал 24 МБ — снова „Обновить“ — ещё 6»).
selected, deferred := deferForInstaller(plan, selected)
if len(selected) == 0 {
return "", fmt.Errorf("сначала обновите установщик обновлений (bj-component-install): текущий не ставит %s", strings.Join(deferred, ", "))
}
cleanupStaging(24 * time.Hour)
staging := filepath.Join(updateStagingDir, fmt.Sprintf("%d", time.Now().UnixNano()))
if err := os.MkdirAll(staging, 0o750); err != nil {
return "", fmt.Errorf("каталог подготовки: %w", err)
}
b, _ := json.Marshal(sm)
if err := os.WriteFile(filepath.Join(staging, "manifest.json"), b, 0o640); err != nil {
return "", err
}
var list []string
var req struct {
Names []string `json:"names"`
New []string `json:"new,omitempty"`
}
var total int64
for _, c := range selected {
for _, a := range m.Artifacts {
if a.Name == c.Name {
total += a.Size
}
}
if c.Name == "bj-server" {
progressTarget(c.Available)
}
}
progressNote(strings.TrimPrefix(unlicensedNote(off), "; "))
progressStage(2, "Скачивание файлов")
stopWatch := progressWatchDir(staging, total)
refused := false
for _, c := range selected {
skip := false
for _, a := range m.Artifacts {
if a.Name == c.Name {
if fromLocalBundle(a, staging) {
log.Printf("lk-gateway: %s %s — из комплекта образа, без скачивания", c.Name, pickVersion(c.Available))
continue
}
if _, err := cl.DownloadArtifact(ctx, a, staging); err != nil {
// Модуль сняли с лицензии, а bj ещё помнит прежний её состав (узнаёт при продлении):
// сервер обновлений файл не отдаёт — пропускаем модуль, остальное ставим.
if !c.Core && c.Feature != "updates" && strings.Contains(err.Error(), "HTTP 403") {
off = append(off, "«"+c.Title+"»")
refused, skip = true, true
_ = os.Remove(filepath.Join(staging, filepath.Base(a.File)))
continue
}
stopWatch()
return "", fmt.Errorf("скачивание %s: %w", c.Name, err)
}
}
}
if skip {
continue
}
req.Names = append(req.Names, c.Name)
if c.New {
req.New = append(req.New, c.Name)
}
list = append(list, c.Name+" "+c.Available)
}
stopWatch()
if refused {
// состав лицензии устарел — обновляем сразу, чтобы разделы снятых модулей пропали
lctx, lcancel := context.WithTimeout(ctx, 30*time.Second)
if err := LicenseRenew(lctx, u.rc); err != nil {
log.Printf("lk-gateway: продление лицензии после отказа в модуле: %v", err)
}
lcancel()
progressNote(strings.TrimPrefix(unlicensedNote(off), "; "))
if len(req.Names) == 0 {
_ = os.RemoveAll(staging)
return "обновлять нечего: компоненты лицензии актуальны (релиз " + m.Version + ")" + unlicensedNote(off), nil
}
}
rb, _ := json.Marshal(req)
if err := os.WriteFile(filepath.Join(staging, "request.json"), rb, 0o640); err != nil {
return "", err
}
progressStage(3, "Проверка подписи и установка, затем перезапуск служб")
log.Printf("lk-gateway: обновление компонентов: %s — передано root-установщику", strings.Join(list, ", "))
wctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
res, err := runKeymediaOp(wctx, "component-install", map[string]string{"staging": staging})
_ = os.RemoveAll(staging)
if err != nil {
return "", fmt.Errorf("установка: %w", err)
}
msg := "установлено: " + strings.Join(list, ", ")
if r, ok := res["restarted"].([]any); ok && len(r) > 0 {
var units []string
for _, x := range r {
units = append(units, fmt.Sprint(x))
}
msg += "; перезапущены службы: " + strings.Join(units, ", ")
}
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
}
// allowed — разрешён ли лицензией признак компонента.
func (u *Updater) allowed(feature string) bool { return moduleAllowed(u.rc, feature) }
var splitModulesMu sync.Mutex
// ensureSplitModules — выделенные из ядра модули, которые входят в лицензию, но ещё не стоят на этом
// сервере (например, ядро обновилось прежним установщиком), ставятся сами: без своих служб разделы
// этих модулей в bj не работают. Раз за запуск bj-server; ошибки — только в журнал и «Последнюю проверку».
func (u *Updater) ensureSplitModules(ctx context.Context) (string, error) {
splitModulesMu.Lock() // запуск bj, активация ключа и «Подготовка модулей» — по очереди: второй увидит, что всё уже стоит
defer splitModulesMu.Unlock()
var names []string
installed := release.LoadInstalled(release.InstalledFile)
for _, c := range release.Plan("", nil, installed, BuildVersion) {
if c.FromCore && !c.Present && u.allowed(c.Feature) {
names = append(names, c.Name)
}
}
// bj-installer обновлением ещё не ставился (сервер с образа или обновлён прежним установщиком
// обновлений): ставим сразу — с ним приезжают свежие службы и скрипты. Только если на сервере
// обновлений он новее, иначе ApplyUpdate вернёт «обновление не требуется» — это не ошибка.
if _, ok := installed["bj-installer"]; !ok && licenseStatus(u.rc).AllowsUpdates {
if st, err := u.CheckForUpdate(ctx); err == nil {
for _, c := range st.Components {
if c.Name == "bj-installer" && c.Update {
names = append(names, c.Name)
}
}
}
}
if len(names) == 0 {
return "", nil
}
cctx, cancel := context.WithTimeout(ctx, 30*time.Minute) // с дистрибутивом ИШ (160 МБ) скачивание дольше
defer cancel()
msg, err := u.ApplyUpdate(cctx, names)
if err != nil {
msg = "Установка модулей " + strings.Join(names, ", ") + " не выполнена: " + err.Error()
} else {
msg = "Установка модулей: " + msg
}
log.Printf("lk-gateway: %s", msg)
cfg := u.rc.Snapshot().Update
cfg.LastResult, cfg.LastCheck = msg, time.Now().UTC()
_ = u.rc.SaveUpdateSettings(cfg)
return msg, err
}
// localBundleDir — комплект компонентов, с которого сервер ставился с образа (bj-firstboot).
var localBundleDir = "/var/lib/bj/bundle"
// fromLocalBundle — файл компонента из комплекта образа, если это тот же файл, что в выпуске (совпадает
// sha256): модуль, который уже лежит на диске, не качается заново — после ввода ключа он ставится сразу.
func fromLocalBundle(a release.Artifact, staging string) bool {
src := filepath.Join(localBundleDir, filepath.Base(a.File))
sum, _, err := release.HashFile(src)
if err != nil || !strings.EqualFold(sum, a.SHA256) {
return false
}
in, err := os.Open(src)
if err != nil {
return false
}
defer in.Close()
dst := filepath.Join(staging, filepath.Base(a.File))
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o640)
if err != nil {
return false
}
if _, err := io.Copy(out, in); err != nil {
out.Close()
_ = os.Remove(dst)
return false
}
return out.Close() == nil
}
// updateComponentRows — компоненты для раздела «Обновления» без обращения к артефактории:
// установленное на этом сервере и версии из последней проверки.
func updateComponentRows(rc *RuntimeConfig) ([]release.ComponentState, bool) {
rows := release.Plan("", nil, release.LoadInstalled(release.InstalledFile), BuildVersion)
set := rc.Snapshot().Update
avail := set.Components
// Версии из проверки в другом канале к текущему каналу не относятся: по ним
// «Обновить» уходило бы в пустоту («релизов в канале нет»).
if set.ComponentsChannel != "" && set.ComponentsChannel != updateChannel(set) {
avail = nil
}
var out []release.ComponentState
any := false
vers := systemVersions(rc)
for _, r := range rows {
if r.Present && r.Installed == "" {
r.Installed = vers.Get(r.Name).Version // выложен вручную — версия со слов самого компонента
}
r.Available = avail[r.Name]
r.Update = r.Available != "" && (r.Present || r.Core) && (r.Installed == "" || release.IsNewer(r.Available, r.Installed))
// Модуль, которого нет на сервере и нет в лицензии, не показываем: поставить его нельзя,
// а в списке он выглядит так, будто входит в поставку.
// Модуль вне лицензии не показываем вовсе (и установленный: его служба отключена), пока
// лицензия действует; без действующей лицензии список как есть.
unlicensed := !r.Core && r.Feature != "" && r.Feature != "updates" && licenseStatus(rc).Valid && !moduleAllowed(rc, r.Feature)
if !unlicensed && (r.Present || r.Core || (r.Available != "" && moduleAllowed(rc, r.Feature))) {
out = append(out, r)
}
}
release.MarkSplitModules(out, func(f string) bool { return moduleAllowed(rc, f) })
markUnlicensed(out, func(f string) bool { return moduleAllowed(rc, f) })
// Последняя проверка не удалась — предлагать установку по старым данным нельзя:
// кнопка ушла бы в тот же отказ. Показываем причину и просим проверить заново.
if set.CheckFailed {
for i := range out {
if out[i].Update && out[i].Available != "" {
out[i].Blocked = "проверка обновлений не удалась — нажмите «Проверить обновления»"
}
out[i].Update = false
}
}
for _, r := range out {
any = any || r.Update
}
return out, any
}
func pickVersion(v string) string {
if v == "" {
return "неизвестна"
}
return v
}
// cleanupStaging — удалить брошенные каталоги подготовки (например, если bj перезапустился при обновлении ядра).
func cleanupStaging(olderThan time.Duration) {
entries, err := os.ReadDir(updateStagingDir)
if err != nil {
return
}
for _, e := range entries {
if info, err := e.Info(); err == nil && e.IsDir() && time.Since(info.ModTime()) > olderThan {
_ = os.RemoveAll(filepath.Join(updateStagingDir, e.Name()))
}
}
}
// updateLoop — фоновая авто-проверка обновлений (если включена).
func (u *Updater) updateLoop(ctx context.Context) {
ticker := time.NewTicker(6 * time.Hour)
defer ticker.Stop()
check := func() {
if !u.rc.Snapshot().Update.AutoCheck {
return
}
cctx, cancel := context.WithTimeout(ctx, 90*time.Second)
defer cancel()
if st, err := u.CheckForUpdate(cctx); err == nil && st.HasUpdate {
log.Printf("lk-gateway: %s", st.Message)
u.notifyUpdate(st)
}
}
// первая проверка через минуту после старта
select {
case <-ctx.Done():
return
case <-time.After(time.Minute):
cleanupStaging(time.Hour)
enforceModuleLicense(ctx, u.rc)
_, _ = u.ensureSplitModules(ctx)
u.applyPending(ctx)
check()
}
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
check()
}
}
}
// 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
if st.Available == "" || cfg.Notified == st.Available {
return
}
cfg.Notified = st.Available
if err := u.rc.SaveUpdateSettings(cfg); err != nil {
return
}
body := "Вышел выпуск bj " + st.Available + ", на этом сервере он ещё не установлен: " + strings.TrimPrefix(st.Message, "доступны обновления: ") +
". Установить — Настройка → Обновления, кнопка «Обновить»; обмен на время установки прерывается на минуту-две."
if n := strings.TrimSpace(st.Notes); n != "" {
body += "\n\nЧто нового:\n" + n
}
raiseSystemAlert(u.rc, "Доступно обновление bj "+st.Available, body)
}
// ApplyBundle — обновление из файла для закрытого контура: пакет с портала клиента
// (подписанный манифест + файлы компонентов). Что ставить, решает тот же план, что и при
// обновлении по сети: всё, что новее установленного, в порядке компонентов — ядро первым,
// модули за ним. Модуль, которому нужно более новое ядро, чем будет после установки,
// пропускается с объяснением.
func (u *Updater) ApplyBundle(ctx context.Context, zipPath string) (string, error) {
progressBegin(4)
progressStage(1, "Распаковка и проверка пакета")
msg, err := u.applyBundle(ctx, zipPath)
progressEnd(msg, err)
return msg, err
}
func (u *Updater) applyBundle(ctx context.Context, zipPath string) (string, error) {
defer os.Remove(zipPath)
if licensingEnabled(u.rc) {
ls := licenseStatus(u.rc)
if !ls.Valid {
return "", fmt.Errorf("обновления заблокированы — лицензия: %s", ls.Message)
}
if !ls.AllowsUpdates {
return "", fmt.Errorf("обновления не входят в план %q", ls.Plan)
}
}
pubB64 := u.rc.Snapshot().Update.PublicKey
if pubB64 == "" {
pubB64 = DefaultUpdatePublicKey
}
pub, err := release.ParsePublicKey(pubB64)
if err != nil {
return "", fmt.Errorf("открытый ключ подписи релизов: %w", err)
}
cleanupStaging(24 * time.Hour)
staging := filepath.Join(updateStagingDir, fmt.Sprintf("%d", time.Now().UnixNano()))
files, err := release.ExtractBundle(zipPath, staging)
if err != nil {
_ = os.RemoveAll(staging)
return "", err
}
sm, m, err := release.ReadStagedManifest(staging, pub)
if err != nil {
_ = os.RemoveAll(staging)
return "", err
}
_ = sm
have := map[string]bool{}
for _, f := range files {
have[f] = true
}
plan := release.Plan("", m, release.LoadInstalled(release.InstalledFile), BuildVersion)
release.MarkSplitModules(plan, u.allowed)
markUnlicensed(plan, u.allowed)
var req struct {
Names []string `json:"names"`
New []string `json:"new,omitempty"`
}
var list, skipped []string
for _, c := range plan {
if c.Blocked != "" {
skipped = append(skipped, c.Title+": "+c.Blocked)
continue
}
if !c.Update {
continue
}
if !have[c.Name] { // в пакете может не быть файла модуля (не входит в лицензию клиента)
continue
}
if !u.allowed(c.Feature) && c.Feature != "updates" {
skipped = append(skipped, c.Title+": не входит в лицензию")
continue
}
req.Names = append(req.Names, c.Name)
if c.New {
req.New = append(req.New, c.Name)
}
list = append(list, c.Name+" "+c.Available)
}
if len(req.Names) == 0 {
_ = os.RemoveAll(staging)
msg := fmt.Sprintf("в пакете релиз %s: нечего обновлять, все компоненты актуальны", m.Version)
if len(skipped) > 0 {
msg += "; пропущено: " + strings.Join(skipped, "; ")
}
return "", errors.New(msg)
}
rb, _ := json.Marshal(req)
if err := os.WriteFile(filepath.Join(staging, "request.json"), rb, 0o640); err != nil {
return "", err
}
for _, n := range req.Names {
if n == "bj-server" {
for _, c := range plan {
if c.Name == n {
progressTarget(c.Available)
}
}
}
}
progressStage(3, "Проверка подписи и установка, затем перезапуск служб")
log.Printf("lk-gateway: обновление из файла (релиз %s): %s — передано root-установщику", m.Version, strings.Join(list, ", "))
wctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
defer cancel()
res, err := runKeymediaOp(wctx, "component-install", map[string]string{"staging": staging})
_ = os.RemoveAll(staging)
if err != nil {
return "", fmt.Errorf("установка: %w", err)
}
msg := fmt.Sprintf("из файла (релиз %s) установлено: %s", m.Version, strings.Join(list, ", "))
if r, ok := res["restarted"].([]any); ok && len(r) > 0 {
var units []string
for _, x := range r {
units = append(units, fmt.Sprint(x))
}
msg += "; перезапущены службы: " + strings.Join(units, ", ")
}
if len(skipped) > 0 {
msg += "; пропущено: " + strings.Join(skipped, "; ")
}
return msg, nil
}
// updateTouchesCore — затронет ли обновление ядро (тогда ответа установщика не дождаться).
func updateTouchesCore(rc *RuntimeConfig, names []string) bool {
if len(names) == 0 {
// «Обновить всё»: ядро попадёт в набор, если для него есть новая версия
rows, _ := updateComponentRows(rc)
for _, r := range rows {
if r.Core && r.Update {
return true
}
}
return false
}
for _, n := range names {
if c, ok := release.ComponentByName(n); ok && c.Core {
return true
}
}
return false
}
// updateResultFailed — последняя строка результата говорит об отказе.
func updateResultFailed(result string) bool {
r := strings.ToLower(result)
return strings.Contains(r, "не применено") || strings.Contains(r, "ошибка") ||
strings.Contains(r, "не удалось") || strings.Contains(r, "недоступ")
}