feat(update): независимые обновления компонентов bj
- internal/release/components.go: список компонентов (ядро bj-server, криптосервис, bj-lk, bj-npr, bj-quikexport) с путём и службой, установленные версии в components.json, расчёт обновлений по версии каждого файла и sha256 (без отката, модули — только если уже установлены на сервере) - cmd/bj-component-install: root-установщик — сам перепроверяет подпись манифеста ключом /etc/bj/update.pub и sha256, копия прежнего файла, перезапуск только затронутых служб - updater: bj-server (без прав root) скачивает в update-staging и передаёт установку root-воркеру (действие component-install); установка в фоне - настройки: таблица компонентов с версиями, «Обновить» и «Обновить всё» - bj-release: версия файла из <файл>.version; bj-keymedia-worker: component-install Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
package release
|
||||
|
||||
// Компоненты bj, которые обновляются независимо. У каждого свой файл в манифесте релиза и своя
|
||||
// версия (Artifact.Version), путь установки и служба systemd. Список общий для bj-server (что
|
||||
// показывать и предлагать к обновлению), root-установщика cmd/bj-component-install (что разрешено
|
||||
// ставить и куда) и cmd/bj-release (какие файлы исполняемые).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Component — независимо обновляемая часть bj.
|
||||
type Component struct {
|
||||
Name string // логическое имя артефакта в манифесте
|
||||
Title string
|
||||
Path string // куда устанавливать
|
||||
Unit string // служба systemd, перезапускаемая после установки ("" — без перезапуска)
|
||||
Exec bool // исполняемый файл
|
||||
Core bool // ядро: ставится всегда; модули — только если уже установлены на этом сервере
|
||||
}
|
||||
|
||||
// Components — все компоненты bj. Порядок — порядок показа и установки (ядро перезапускается последним).
|
||||
var Components = []Component{
|
||||
{Name: "bj-server", Title: "Ядро bj: переводы M2M, DEPO, QUIK МНП, ЛК ЕСИА", Path: "/opt/bj/bj-server", Unit: "bj-server", Exec: true, Core: true},
|
||||
{Name: "crypto-service.jar", Title: "Криптосервис", Path: "/opt/bj/crypto-service.jar", Unit: "bj-crypto"},
|
||||
{Name: "bj-lk", Title: "Личный кабинет клиента", Path: "/opt/bj/bj-lk", Unit: "bj-lk", Exec: true},
|
||||
{Name: "bj-npr", Title: "Риск-монитор НПР / УДС", Path: "/opt/bj/bj-npr", Unit: "bj-npr", Exec: true},
|
||||
{Name: "bj-quikexport", Title: "Экспорт QUIK", Path: "/opt/bj/bj-quikexport", Unit: "bj-quikexport", Exec: true},
|
||||
}
|
||||
|
||||
// ComponentByName — компонент по имени артефакта.
|
||||
func ComponentByName(name string) (Component, bool) {
|
||||
for _, c := range Components {
|
||||
if c.Name == name {
|
||||
return c, true
|
||||
}
|
||||
}
|
||||
return Component{}, false
|
||||
}
|
||||
|
||||
// PathAt — путь установки относительно корня root ("" — настоящий корень; иначе проверка в песочнице).
|
||||
func (c Component) PathAt(root string) string {
|
||||
if root == "" {
|
||||
return c.Path
|
||||
}
|
||||
return filepath.Join(root, c.Path)
|
||||
}
|
||||
|
||||
// InstalledFile — установленные версии компонентов; пишет bj-component-install.
|
||||
const InstalledFile = "/var/lib/bj/.bj/components.json"
|
||||
|
||||
// Installed — запись об установленном компоненте.
|
||||
type Installed struct {
|
||||
Version string `json:"version"`
|
||||
SHA256 string `json:"sha256"`
|
||||
InstalledAt time.Time `json:"installed_at"`
|
||||
}
|
||||
|
||||
// LoadInstalled — установленные версии (пустая карта, если файла нет).
|
||||
func LoadInstalled(path string) map[string]Installed {
|
||||
m := map[string]Installed{}
|
||||
if b, err := os.ReadFile(path); err == nil {
|
||||
_ = json.Unmarshal(b, &m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// SaveInstalled — атомарная запись установленных версий.
|
||||
func SaveInstalled(path string, m map[string]Installed) error {
|
||||
b, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, b, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, path)
|
||||
}
|
||||
|
||||
// ComponentState — компонент на этом сервере против манифеста.
|
||||
type ComponentState struct {
|
||||
Component
|
||||
Present bool // файл установлен на этом сервере
|
||||
Installed string // установленная версия ("" — неизвестна)
|
||||
Available string // версия в манифесте ("" — компонента нет в релизе)
|
||||
SameFile bool // установленный файл совпадает с файлом релиза
|
||||
Update bool // можно обновить
|
||||
}
|
||||
|
||||
// Plan — что можно обновить. m == nil — только состояние установленного (без сети).
|
||||
// selfVersion — версия работающего bj-server, если записи об установке ещё нет.
|
||||
func Plan(root string, m *Manifest, installed map[string]Installed, selfVersion string) []ComponentState {
|
||||
var out []ComponentState
|
||||
for _, c := range Components {
|
||||
st := ComponentState{Component: c}
|
||||
sha := ""
|
||||
if fi, err := os.Stat(c.PathAt(root)); err == nil && fi.Mode().IsRegular() {
|
||||
st.Present = true
|
||||
sha = cachedHash(c.PathAt(root), fi)
|
||||
}
|
||||
if inst, ok := installed[c.Name]; ok && inst.SHA256 != "" && inst.SHA256 == sha {
|
||||
st.Installed = inst.Version
|
||||
}
|
||||
if st.Installed == "" && c.Core && st.Present {
|
||||
st.Installed = selfVersion
|
||||
}
|
||||
if m != nil {
|
||||
for _, a := range m.Artifacts {
|
||||
if a.Name != c.Name {
|
||||
continue
|
||||
}
|
||||
st.Available = a.Version
|
||||
if st.Available == "" {
|
||||
st.Available = m.Version
|
||||
}
|
||||
st.SameFile = sha != "" && a.SHA256 == sha
|
||||
st.Update = (st.Present || c.Core) && !st.SameFile && (st.Installed == "" || IsNewer(st.Available, st.Installed))
|
||||
}
|
||||
}
|
||||
out = append(out, st)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// cachedHash — sha256 файла с кэшем по размеру и времени изменения: страница настроек
|
||||
// не пересчитывает десятки мегабайт на каждый показ.
|
||||
var hashCache = struct {
|
||||
sync.Mutex
|
||||
m map[string]hashEntry
|
||||
}{m: map[string]hashEntry{}}
|
||||
|
||||
type hashEntry struct {
|
||||
size int64
|
||||
mod time.Time
|
||||
sha string
|
||||
}
|
||||
|
||||
func cachedHash(path string, fi os.FileInfo) string {
|
||||
hashCache.Lock()
|
||||
e, ok := hashCache.m[path]
|
||||
hashCache.Unlock()
|
||||
if ok && e.size == fi.Size() && e.mod.Equal(fi.ModTime()) {
|
||||
return e.sha
|
||||
}
|
||||
sha, _, err := HashFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
hashCache.Lock()
|
||||
hashCache.m[path] = hashEntry{size: fi.Size(), mod: fi.ModTime(), sha: sha}
|
||||
hashCache.Unlock()
|
||||
return sha
|
||||
}
|
||||
Reference in New Issue
Block a user