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 } // igateUpgradeNote — с обновлением пришёл дистрибутив ИШ: сам шлюз bj обновит чуть позже, отдельно. func igateUpgradeNote(list []string) string { for _, x := range list { if v, ok := strings.CutPrefix(x, "igate-dist "); ok { return "; пришёл дистрибутив ИШ " + v + " — сам шлюз bj обновит автоматически в течение нескольких минут (около 2 минут шлюз недоступен; настройки и каналы сохраняются)" } } return "" } 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, ", ") + igateUpgradeNote(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, ", ")) + igateUpgradeNote(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, "недоступ") }