package lkgateway import ( "context" "fmt" "log" "os" "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) — приоритет у настроек. var DefaultUpdatePublicKey = "" // installPaths — куда устанавливать артефакты по логическому имени. // Файлы не из этого списка при авто-обновлении пропускаются (скрипты/SQL // обновляются отдельно, не на лету). var installPaths = map[string]string{ "bj-server": "/opt/bj/bj-server", "crypto-service.jar": "/opt/bj/crypto-service.jar", } // Updater — авто-обновление bj-server из артефактории (работает поверх rc). type Updater struct{ rc *RuntimeConfig } // NewUpdater создаёт Updater на текущем runtime-конфиге. func NewUpdater(rc *RuntimeConfig) *Updater { return &Updater{rc: rc} } // UpdateStatus — сводка для UI/handler. type UpdateStatus struct { Configured bool CurrentVersion string Available string HasUpdate bool Channel string Notes string Message string } func (u *Updater) updateClient() (*release.Client, error) { cfg := u.rc.Snapshot().Update pub := cfg.PublicKey if pub == "" { pub = DefaultUpdatePublicKey } if cfg.BaseURL == "" || pub == "" { return nil, fmt.Errorf("обновления не настроены (нужны URL артефактории и публичный ключ)") } channel := cfg.Channel if channel == "" { channel = "stable" } return release.NewClient(cfg.BaseURL, channel, pub) } // 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) return st, err } st.Available = m.Version st.Notes = m.Notes st.HasUpdate = release.IsNewer(m.Version, BuildVersion) if st.HasUpdate { st.Message = fmt.Sprintf("доступна версия %s (текущая %s)", m.Version, BuildVersion) } else { st.Message = "установлена актуальная версия " + BuildVersion } u.saveCheckResult(st) return st, nil } func (u *Updater) saveCheckResult(st UpdateStatus) { cfg := u.rc.Snapshot().Update cfg.LastCheck = time.Now().UTC() cfg.LastResult = st.Message cfg.Available = st.Available cfg.Notes = st.Notes if err := u.rc.SaveUpdateSettings(cfg); err != nil { log.Printf("lk-gateway: сохранение результата проверки обновления: %v", err) } } // ApplyUpdate скачивает обновлённые артефакты (с проверкой подписи манифеста // и sha256 каждого файла), атомарно заменяет бинари и завершает процесс с // ненулевым кодом — systemd (Restart=on-failure) поднимает новую версию. func (u *Updater) ApplyUpdate(ctx context.Context) error { // Гейт лицензией: если лицензирование включено — требуется валидная // лицензия с фичей updates. Без лицензирования (открытый режим) — пропускаем. 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 } m, err := cl.FetchManifest(ctx) if err != nil { return fmt.Errorf("манифест: %w", err) } if !release.IsNewer(m.Version, BuildVersion) { return fmt.Errorf("обновление не требуется (текущая %s, доступна %s)", BuildVersion, m.Version) } updated := 0 for _, a := range m.Artifacts { dst, ok := installPaths[a.Name] if !ok { continue // скрипты/SQL не обновляем на лету } dir := dirOf(dst) path, err := cl.DownloadArtifact(ctx, a, dir) if err != nil { return fmt.Errorf("скачивание %s: %w", a.Name, err) } // DownloadArtifact кладёт файл под именем a.File; если целевое имя // иное — переименуем атомарно. if path != dst { if err := os.Rename(path, dst); err != nil { return fmt.Errorf("установка %s: %w", a.Name, err) } } log.Printf("lk-gateway: обновлён %s → %s (%s)", a.Name, dst, m.Version) updated++ } if updated == 0 { return fmt.Errorf("в манифесте %s нет обновляемых бинарей", m.Version) } log.Printf("lk-gateway: обновление до %s применено (%d файлов), перезапуск через systemd…", m.Version, updated) // Завершаемся с ненулевым кодом — systemd Restart=on-failure поднимет // новый бинарь. Даём пару секунд на флаш логов/ответа. go func() { time.Sleep(800 * time.Millisecond) os.Exit(42) }() return nil } // 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 (текущая %s)", st.Available, st.CurrentVersion) } } // первая проверка через минуту после старта select { case <-ctx.Done(): return case <-time.After(time.Minute): check() } for { select { case <-ctx.Done(): return case <-ticker.C: check() } } } func dirOf(path string) string { for i := len(path) - 1; i >= 0; i-- { if path[i] == '/' { return path[:i] } } return "." }