Files
Bridge-and-Join-s/internal/release/components.go
T
zuevav fdd70c06dd Обновления несут установщик: bj-installer — компонент ядра, refresh-system переписывает службы и скрипты; мастер предлагает обновиться сразу после ввода ключа
Правки установки приезжают обновлением с сервера лицензий, образ ISO пересобирать не нужно.
Прежний установщик обновлений bj-installer не знает — он откладывается и ставится после
перезапуска bj-server (ensureSplitModules).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-09-24 17:44:28 +03:00

210 lines
9.5 KiB
Go

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 // ядро: ставится всегда; модули — только если уже установлены на этом сервере
// FromCore — модуль выделен из bj-server: обновление ставит его и там, где его ещё нет, если он
// входит в лицензию (иначе после обновления ядра раздел модуля перестал бы работать).
FromCore bool
// Feature — признак лицензии, без которого сервер обновлений не отдаёт файл компонента.
Feature string
}
// 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, Feature: "updates"},
{Name: "bj-component-install", Title: "Установщик обновлений", Path: "/usr/local/sbin/bj-component-install", Exec: true, Core: true, Feature: "updates"},
// Установщик несёт системные файлы (службы systemd, служебные скрипты): после его обновления
// они переписываются командой «bj-installer refresh-system» — правки установки приезжают
// обновлением, образ ISO пересобирать не нужно.
{Name: "bj-installer", Title: "Установщик и системные файлы (службы, скрипты)", Path: "/usr/local/sbin/bj-installer", Exec: true, Core: true, Feature: "updates"},
{Name: "crypto-service.jar", Title: "Криптосервис", Path: "/opt/bj/crypto-service.jar", Unit: "bj-crypto", Feature: "updates"},
{Name: "bj-lk", Title: "Личный кабинет клиента", Path: "/opt/bj/bj-lk", Unit: "bj-lk", Exec: true, Feature: "web-cabinet"},
{Name: "bj-npr", Title: "Риск-монитор НПР / УДС", Path: "/opt/bj/bj-npr", Unit: "bj-npr", Exec: true, FromCore: true, Feature: "npr"},
{Name: "bj-quikexport", Title: "Экспорт QUIK", Path: "/opt/bj/bj-quikexport", Unit: "bj-quikexport", Exec: true, FromCore: true, Feature: "quik-export"},
}
// 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 // можно обновить
New bool // модуль выделен из ядра и ставится впервые (FromCore)
// Blocked — почему обновлять нельзя: модулю нужно более новое ядро, чем будет после установки.
Blocked string
}
// Plan — что можно обновить. m == nil — только состояние установленного (без сети).
// selfVersion — версия работающего bj-server, если записи об установке ещё нет.
func Plan(root string, m *Manifest, installed map[string]Installed, selfVersion string) []ComponentState {
var out []ComponentState
// Версия ядра после установки: если ядро тоже обновляется этим релизом — его новая версия,
// иначе — установленная. С ней сверяются требования модулей (Artifact.MinCore).
coreAfter := selfVersion
if inst, ok := installed["bj-server"]; ok && inst.Version != "" {
coreAfter = inst.Version
}
if m != nil {
for _, a := range m.Artifacts {
if a.Name == "bj-server" {
v := a.Version
if v == "" {
v = m.Version
}
if coreAfter == "" || IsNewer(v, coreAfter) {
coreAfter = v
}
}
}
}
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))
if !c.Core && a.MinCore != "" && coreAfter != "" && IsNewer(a.MinCore, coreAfter) {
// Модулю нужно ядро новее, чем будет на сервере: сначала обновляется ядро.
st.Blocked = "нужно ядро не ниже " + a.MinCore + " (будет " + coreAfter + ") — сначала обновите ядро"
st.Update = false
}
}
}
out = append(out, st)
}
return out
}
// MarkSplitModules — отметить к установке выделенные из ядра модули (FromCore), которых ещё нет на
// этом сервере, если они есть в релизе и разрешены лицензией (allowed — по признаку компонента).
func MarkSplitModules(states []ComponentState, allowed func(feature string) bool) {
for i := range states {
s := &states[i]
if s.FromCore && !s.Present && s.Available != "" && s.Blocked == "" && allowed(s.Feature) {
s.Update, s.New = true, true
}
}
}
// 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
}