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:
zuevav
2026-09-15 20:16:36 +03:00
parent 5115ed7223
commit 2908697363
11 changed files with 720 additions and 114 deletions
+14 -6
View File
@@ -39,24 +39,32 @@ func NewClient(baseURL, channel, pubB64 string) (*Client, error) {
// FetchManifest скачивает /v1/<channel>/manifest.json и проверяет подпись.
// Возвращает доверенный Manifest (или ошибку, если подпись не сошлась).
func (c *Client) FetchManifest(ctx context.Context) (*Manifest, error) {
_, m, err := c.FetchSigned(ctx)
return m, err
}
// FetchSigned — подписанный манифест как есть (для повторной проверки root-установщиком) и
// проверенный Manifest.
func (c *Client) FetchSigned(ctx context.Context) (*SignedManifest, *Manifest, error) {
url := fmt.Sprintf("%s/v1/%s/manifest.json", c.BaseURL, c.Channel)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
return nil, nil, err
}
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, fmt.Errorf("release: запрос манифеста: %w", err)
return nil, nil, fmt.Errorf("release: запрос манифеста: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("release: манифест HTTP %d", resp.StatusCode)
return nil, nil, fmt.Errorf("release: манифест HTTP %d", resp.StatusCode)
}
var sm SignedManifest
if err := json.NewDecoder(resp.Body).Decode(&sm); err != nil {
return nil, fmt.Errorf("release: разбор манифеста: %w", err)
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&sm); err != nil {
return nil, nil, fmt.Errorf("release: разбор манифеста: %w", err)
}
return Verify(&sm, c.Pub) // здесь проверяется подпись
m, err := Verify(&sm, c.Pub) // здесь проверяется подпись
return &sm, m, err
}
// DownloadArtifact скачивает артефакт в destDir, проверяет sha256/размер по