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" "encoding/json" "fmt" "log" "os" "path/filepath" "strings" "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 st.Components = release.Plan("", m, release.LoadInstalled(release.InstalledFile), BuildVersion) release.MarkSplitModules(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 + ")" } u.saveCheckResult(st, avail) return st, nil } 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.Available = st.Available cfg.Notes = st.Notes if avail != nil { cfg.Components = avail } 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) { 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) } plan := release.Plan("", m, release.LoadInstalled(release.InstalledFile), BuildVersion) release.MarkSplitModules(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]: return "", fmt.Errorf("%s: обновление не требуется (установлена %s, в релизе %s)", c.Title, pickVersion(c.Installed), pickVersion(c.Available)) } } if len(selected) == 0 { 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 { 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"` } for _, c := range selected { for _, a := range m.Artifacts { if a.Name == c.Name { if _, err := cl.DownloadArtifact(ctx, a, staging); err != nil { return "", fmt.Errorf("скачивание %s: %w", c.Name, err) } } } req.Names = append(req.Names, c.Name) if c.New { req.New = append(req.New, c.Name) } list = append(list, c.Name+" "+c.Available) } rb, _ := json.Marshal(req) if err := os.WriteFile(filepath.Join(staging, "request.json"), rb, 0o640); err != nil { return "", err } 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 новым установщиком" } return msg, nil } // allowed — разрешён ли лицензией признак компонента. func (u *Updater) allowed(feature string) bool { return moduleAllowed(u.rc, feature) } // ensureSplitModules — выделенные из ядра модули, которые входят в лицензию, но ещё не стоят на этом // сервере (например, ядро обновилось прежним установщиком), ставятся сами: без своих служб разделы // этих модулей в bj не работают. Раз за запуск bj-server; ошибки — только в журнал и «Последнюю проверку». func (u *Updater) ensureSplitModules(ctx context.Context) { var names []string for _, c := range release.Plan("", nil, release.LoadInstalled(release.InstalledFile), BuildVersion) { if c.FromCore && !c.Present && u.allowed(c.Feature) { names = append(names, c.Name) } } if len(names) == 0 { return } cctx, cancel := context.WithTimeout(ctx, 10*time.Minute) 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) } // updateComponentRows — компоненты для раздела «Обновления» без обращения к артефактории: // установленное на этом сервере и версии из последней проверки. func updateComponentRows(rc *RuntimeConfig) ([]release.ComponentState, bool) { rows := release.Plan("", nil, release.LoadInstalled(release.InstalledFile), BuildVersion) avail := rc.Snapshot().Update.Components 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)) if r.Present || r.Core || r.Available != "" { out = append(out, r) } } release.MarkSplitModules(out, func(f string) bool { return moduleAllowed(rc, f) }) 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) u.ensureSplitModules(ctx) check() } for { select { case <-ctx.Done(): return case <-ticker.C: check() } } }