4135435b51
- cmd/bj-installer (вместо веб-мастера): проверка Debian 13 / Astra, пакеты, пользователь и каталоги, активация лицензии ключом продукта, PostgreSQL и миграции m2m_core, компоненты по лицензии с проверкой подписи, root-скрипты и службы, одноразовый код и итоговый экран с адресом http://<сервер>:8080/first-run; проверка в песочнице (--steps, --root) - bj: /first-run — пока нет пользователей и установщик оставил код, по коду создаётся первый администратор (прежние установки без файла работают как раньше) - internal/release/install.go — общая установка компонентов (bj-component-install и установщик); bj-component-install стал компонентом ядра - открытые ключи вынесены в internal/license и internal/release; миграции и файлы развёртывания встроены (migrations/embed.go, deploy/embed.go); unit bj-server исправлен - scripts/build-release.sh: сборка с версиями, подпись, публикация на сервер обновлений Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
165 lines
6.7 KiB
Go
165 lines
6.7 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 // ядро: ставится всегда; модули — только если уже установлены на этом сервере
|
|
// 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"},
|
|
{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, Feature: "npr"},
|
|
{Name: "bj-quikexport", Title: "Экспорт QUIK", Path: "/opt/bj/bj-quikexport", Unit: "bj-quikexport", Exec: 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 // можно обновить
|
|
}
|
|
|
|
// 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
|
|
}
|