Files
Bridge-and-Join-s/internal/lkgateway/updater.go
T
zuevav 648a5ae05f ИШ обновляется через сервер лицензий вместе с bj; наглядный ход установки ИШ
- Дистрибутив ИШ — компонент выпуска igate-dist (версия = версия ИШ). В выпуск
  попадает только одобренный архив: deploy/release/igate-approved (версия и
  sha256), сборка сверяет контрольную сумму. Клиент получает его обновлением;
  bj-server после обновления (и при запуске) сам обновляет ИШ, если на сервере
  он старее; итог — событие в «Новостях». Настройки, каналы и база ИШ
  сохраняются, прежняя версия — в /var/backups.
- Установка «с сервера» берёт самый новый проверенный дистрибутив: с образа или из
  обновления. С сайта НРД — только в режиме разработки.
- Большие файлы: одна прежняя копия вместо пяти; предел скачивания — по размеру.
- Ход установки ИШ — этапами: пройденные, текущий («идёт 0:41 · обычно до 1:30»,
  дольше обычного — предупреждение), журнал под раскрытием; кнопка на время
  установки выключена.
- Установщик: уникальные этапы; igate-cli и сессия ключницы с принудительным
  завершением (timeout -k); зависшая установка (ход не менялся 15 минут)
  снимается повторным запуском; служба установки — не дольше 40 минут.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-06 17:42:54 +03:00

742 lines
29 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"
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-установщик не умеет впервые ставить выделенные из ядра модули: пока он сам не
// обновлён, такие модули откладываются — их поставит ensureSplitModules после перезапуска.
installerOutdated := false
for _, c := range plan {
if c.Name == "bj-component-install" && c.Update {
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
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, ", ")
}
if len(deferred) > 0 {
msg += "; модули " + strings.Join(deferred, ", ") + " будут установлены после перезапуска bj-server новым установщиком"
}
msg += unlicensedNote(off)
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)
}
}
// первая проверка через минуту после старта
select {
case <-ctx.Done():
return
case <-time.After(time.Minute):
cleanupStaging(time.Hour)
enforceModuleLicense(ctx, u.rc)
_, _ = u.ensureSplitModules(ctx)
check()
}
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
check()
}
}
}
// 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, "недоступ")
}