Обновления: кнопка «Обновить» показывает настоящий результат, устаревшие данные не предлагаются
Жалоба со стенда: в «Обновлениях» висит ЛК 0.2.0, нажимаешь «Обновить» — ничего не происходит. На деле обновление уходило в фон и падало («манифест HTTP 404: релизов в канале нет» — стенд на канале stable, релизы выкладываются в beta), а отказ оседал в незаметной строке «Последняя проверка». - Модули обновляются на месте и во flash показывается фактический результат; в фон уходит только обновление ядра, где ответа установщика всё равно не дождаться (bj-server перезапускается раньше). - Строка результата при отказе показывается предупреждением, а не серым текстом. - Если последняя проверка не удалась, кнопки установки скрыты, у версии — пояснение «нажмите „Проверить обновления“»: предлагать установку по данным неудачной проверки нельзя. - Версии запоминаются вместе с каналом (components_channel); при смене канала прошлые данные забываются — они относились к другому каналу. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
@@ -96,6 +96,10 @@ type UpdateSettings struct {
|
|||||||
Notes string `json:"notes,omitempty"` // заметки доступного релиза
|
Notes string `json:"notes,omitempty"` // заметки доступного релиза
|
||||||
// Components — версии компонентов в последнем проверенном релизе (имя → версия).
|
// Components — версии компонентов в последнем проверенном релизе (имя → версия).
|
||||||
Components map[string]string `json:"components,omitempty"`
|
Components map[string]string `json:"components,omitempty"`
|
||||||
|
// ComponentsChannel — канал, в котором эти версии были найдены. Если канал
|
||||||
|
// сменили, данные прошлой проверки к новому каналу не относятся: предлагать
|
||||||
|
// по ним обновление нельзя (кнопка «Обновить» уходила бы в пустоту).
|
||||||
|
ComponentsChannel string `json:"components_channel,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewsSettings — лента новостей (события системы, окна техработ, обновления
|
// NewsSettings — лента новостей (события системы, окна техработ, обновления
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ type SetupData struct {
|
|||||||
LKPreview LKPreviewStatus // кабинет на этом сервере для просмотра
|
LKPreview LKPreviewStatus // кабинет на этом сервере для просмотра
|
||||||
LKData LKDataView // курсы валют и ежедневный срез стоимости
|
LKData LKDataView // курсы валют и ежедневный срез стоимости
|
||||||
Units []UnitView // службы bj и её модулей
|
Units []UnitView // службы bj и её модулей
|
||||||
|
UpdateFailed bool // последняя попытка обновления закончилась отказом
|
||||||
Versions SystemVersions // версии компонентов bj и внешних систем
|
Versions SystemVersions // версии компонентов bj и внешних систем
|
||||||
Modules map[string]bool // какие лицензируемые модули разрешены (ключ = признак)
|
Modules map[string]bool // какие лицензируемые модули разрешены (ключ = признак)
|
||||||
Features []LicenseFeatureView // признаки лицензии для карточки
|
Features []LicenseFeatureView // признаки лицензии для карточки
|
||||||
@@ -394,6 +395,7 @@ func (h *setupHandlers) renderSetup(w http.ResponseWriter, req *http.Request, fl
|
|||||||
LKPreview: lkPreviewStatus(h.rc, req),
|
LKPreview: lkPreviewStatus(h.rc, req),
|
||||||
LKData: lkDataView(h.rc),
|
LKData: lkDataView(h.rc),
|
||||||
Units: unitViews(h.rc),
|
Units: unitViews(h.rc),
|
||||||
|
UpdateFailed: updateResultFailed(s.Update.LastResult),
|
||||||
LKKeyMask: lkKeyForPage(s.LK.APIKey),
|
LKKeyMask: lkKeyForPage(s.LK.APIKey),
|
||||||
Versions: systemVersions(h.rc),
|
Versions: systemVersions(h.rc),
|
||||||
Modules: modulesAllowed(h.rc),
|
Modules: modulesAllowed(h.rc),
|
||||||
@@ -1243,10 +1245,16 @@ func (h *setupHandlers) saveUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
cfg := h.rc.Snapshot().Update
|
cfg := h.rc.Snapshot().Update
|
||||||
|
was := updateChannel(cfg)
|
||||||
cfg.BaseURL = strings.TrimSpace(r.FormValue("base_url"))
|
cfg.BaseURL = strings.TrimSpace(r.FormValue("base_url"))
|
||||||
cfg.Channel = strings.TrimSpace(r.FormValue("channel"))
|
cfg.Channel = strings.TrimSpace(r.FormValue("channel"))
|
||||||
cfg.PublicKey = strings.TrimSpace(r.FormValue("public_key"))
|
cfg.PublicKey = strings.TrimSpace(r.FormValue("public_key"))
|
||||||
cfg.AutoCheck = r.FormValue("auto_check") != ""
|
cfg.AutoCheck = r.FormValue("auto_check") != ""
|
||||||
|
if updateChannel(cfg) != was {
|
||||||
|
// Версии из прошлого канала к новому не относятся — забываем их до проверки.
|
||||||
|
cfg.Components, cfg.ComponentsChannel, cfg.Available, cfg.Notes = nil, "", "", ""
|
||||||
|
cfg.LastResult = "канал сменён на «" + updateChannel(cfg) + "» — нажмите «Проверить обновления»"
|
||||||
|
}
|
||||||
if err := h.rc.SaveUpdateSettings(cfg); err != nil {
|
if err := h.rc.SaveUpdateSettings(cfg); err != nil {
|
||||||
setupFlash(w, r, "обновления: ошибка сохранения: "+err.Error())
|
setupFlash(w, r, "обновления: ошибка сохранения: "+err.Error())
|
||||||
return
|
return
|
||||||
@@ -1277,24 +1285,35 @@ func (h *setupHandlers) applyUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
_ = r.ParseForm()
|
_ = r.ParseForm()
|
||||||
names := r.Form["component"]
|
names := r.Form["component"]
|
||||||
rc := h.rc
|
rc := h.rc
|
||||||
// В фоне: при обновлении ядра bj-server перезапустится раньше, чем придёт ответ установщика.
|
save := func(msg string, err error) string {
|
||||||
go func() {
|
|
||||||
msg, err := NewUpdater(rc).ApplyUpdate(context.Background(), names)
|
|
||||||
cfg := rc.Snapshot().Update
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg = "Обновление не применено: " + err.Error()
|
msg = "Обновление не применено: " + err.Error()
|
||||||
} else {
|
} else {
|
||||||
msg = "Обновление: " + msg
|
msg = "Обновление: " + msg
|
||||||
}
|
}
|
||||||
log.Printf("lk-gateway: %s", msg)
|
log.Printf("lk-gateway: %s", msg)
|
||||||
|
cfg := rc.Snapshot().Update
|
||||||
cfg.LastResult, cfg.LastCheck = msg, time.Now().UTC()
|
cfg.LastResult, cfg.LastCheck = msg, time.Now().UTC()
|
||||||
_ = rc.SaveUpdateSettings(cfg)
|
_ = rc.SaveUpdateSettings(cfg)
|
||||||
}()
|
return msg
|
||||||
what := "всех компонентов с новой версией"
|
|
||||||
if len(names) > 0 {
|
|
||||||
what = strings.Join(names, ", ")
|
|
||||||
}
|
}
|
||||||
setupFlash(w, r, "Обновление "+what+" запущено: файлы скачиваются и проверяются, затем перезапускаются службы. Итог — в строке «Последняя проверка» через минуту; при обновлении ядра страница временно не отвечает.")
|
// Обновление ядра — только в фоне: bj-server перезапустится раньше, чем придёт
|
||||||
|
// ответ установщика, и страница всё равно не дождётся. Модули обновляем на месте
|
||||||
|
// и показываем настоящий результат: иначе отказ («в канале нет релизов», нет
|
||||||
|
// лицензии, не сходится подпись) виден только строкой «Последняя проверка»,
|
||||||
|
// и со стороны выглядит так, будто кнопка ничего не делает.
|
||||||
|
if updateTouchesCore(rc, names) {
|
||||||
|
go func() { save(NewUpdater(rc).ApplyUpdate(context.Background(), names)) }()
|
||||||
|
what := "всех компонентов с новой версией"
|
||||||
|
if len(names) > 0 {
|
||||||
|
what = strings.Join(names, ", ")
|
||||||
|
}
|
||||||
|
setupFlash(w, r, "Обновление "+what+" запущено: файлы скачиваются и проверяются, затем перезапускаются службы. Итог — в строке «Последняя проверка» через минуту; при обновлении ядра страница временно не отвечает.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
setupFlash(w, r, save(NewUpdater(rc).ApplyUpdate(ctx, names)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// uploadUpdate — POST /admin/setup/update/upload: пакет обновления файлом (закрытый контур).
|
// uploadUpdate — POST /admin/setup/update/upload: пакет обновления файлом (закрытый контур).
|
||||||
|
|||||||
@@ -119,6 +119,16 @@ func (u *Updater) CheckForUpdate(ctx context.Context) (UpdateStatus, error) {
|
|||||||
return st, nil
|
return st, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateChannel — канал обновлений установки (пусто = stable).
|
||||||
|
func updateChannel(s UpdateSettings) string {
|
||||||
|
if c := strings.TrimSpace(s.Channel); c != "" {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return "stable"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Updater) channel() string { return updateChannel(u.rc.Snapshot().Update) }
|
||||||
|
|
||||||
func (u *Updater) saveCheckResult(st UpdateStatus, avail map[string]string) {
|
func (u *Updater) saveCheckResult(st UpdateStatus, avail map[string]string) {
|
||||||
cfg := u.rc.Snapshot().Update
|
cfg := u.rc.Snapshot().Update
|
||||||
cfg.LastCheck = time.Now().UTC()
|
cfg.LastCheck = time.Now().UTC()
|
||||||
@@ -127,6 +137,7 @@ func (u *Updater) saveCheckResult(st UpdateStatus, avail map[string]string) {
|
|||||||
cfg.Notes = st.Notes
|
cfg.Notes = st.Notes
|
||||||
if avail != nil {
|
if avail != nil {
|
||||||
cfg.Components = avail
|
cfg.Components = avail
|
||||||
|
cfg.ComponentsChannel = u.channel()
|
||||||
}
|
}
|
||||||
if err := u.rc.SaveUpdateSettings(cfg); err != nil {
|
if err := u.rc.SaveUpdateSettings(cfg); err != nil {
|
||||||
log.Printf("lk-gateway: сохранение результата проверки обновления: %v", err)
|
log.Printf("lk-gateway: сохранение результата проверки обновления: %v", err)
|
||||||
@@ -284,7 +295,13 @@ func (u *Updater) ensureSplitModules(ctx context.Context) {
|
|||||||
// установленное на этом сервере и версии из последней проверки.
|
// установленное на этом сервере и версии из последней проверки.
|
||||||
func updateComponentRows(rc *RuntimeConfig) ([]release.ComponentState, bool) {
|
func updateComponentRows(rc *RuntimeConfig) ([]release.ComponentState, bool) {
|
||||||
rows := release.Plan("", nil, release.LoadInstalled(release.InstalledFile), BuildVersion)
|
rows := release.Plan("", nil, release.LoadInstalled(release.InstalledFile), BuildVersion)
|
||||||
avail := rc.Snapshot().Update.Components
|
set := rc.Snapshot().Update
|
||||||
|
avail := set.Components
|
||||||
|
// Версии из проверки в другом канале к текущему каналу не относятся: по ним
|
||||||
|
// «Обновить» уходило бы в пустоту («релизов в канале нет»).
|
||||||
|
if set.ComponentsChannel != "" && set.ComponentsChannel != updateChannel(set) {
|
||||||
|
avail = nil
|
||||||
|
}
|
||||||
var out []release.ComponentState
|
var out []release.ComponentState
|
||||||
any := false
|
any := false
|
||||||
vers := systemVersions(rc)
|
vers := systemVersions(rc)
|
||||||
@@ -299,6 +316,16 @@ func updateComponentRows(rc *RuntimeConfig) ([]release.ComponentState, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
release.MarkSplitModules(out, func(f string) bool { return moduleAllowed(rc, f) })
|
release.MarkSplitModules(out, func(f string) bool { return moduleAllowed(rc, f) })
|
||||||
|
// Последняя проверка не удалась — предлагать установку по старым данным нельзя:
|
||||||
|
// кнопка ушла бы в тот же отказ. Показываем причину и просим проверить заново.
|
||||||
|
if updateResultFailed(set.LastResult) {
|
||||||
|
for i := range out {
|
||||||
|
if out[i].Update && out[i].Available != "" {
|
||||||
|
out[i].Blocked = "проверка обновлений не удалась — нажмите «Проверить обновления»"
|
||||||
|
}
|
||||||
|
out[i].Update = false
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, r := range out {
|
for _, r := range out {
|
||||||
any = any || r.Update
|
any = any || r.Update
|
||||||
}
|
}
|
||||||
@@ -460,3 +487,30 @@ func (u *Updater) ApplyBundle(ctx context.Context, zipPath string) (string, erro
|
|||||||
}
|
}
|
||||||
return msg, nil
|
return msg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateTouchesCore — затронет ли обновление ядро (тогда ответа установщика не дождаться).
|
||||||
|
func updateTouchesCore(rc *RuntimeConfig, names []string) bool {
|
||||||
|
if len(names) == 0 {
|
||||||
|
// «Обновить всё»: ядро попадёт в набор, если для него есть новая версия
|
||||||
|
rows, _ := updateComponentRows(rc)
|
||||||
|
for _, r := range rows {
|
||||||
|
if r.Core && r.Update {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, n := range names {
|
||||||
|
if c, ok := release.ComponentByName(n); ok && c.Core {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateResultFailed — последняя строка результата говорит об отказе.
|
||||||
|
func updateResultFailed(result string) bool {
|
||||||
|
r := strings.ToLower(result)
|
||||||
|
return strings.Contains(r, "не применено") || strings.Contains(r, "ошибка") ||
|
||||||
|
strings.Contains(r, "не удалось") || strings.Contains(r, "недоступ")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package lkgateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// После неудачной проверки нельзя предлагать установку по старым данным: кнопка
|
||||||
|
// «Обновить» уходила бы в тот же отказ, и со стороны это выглядит как «ничего не
|
||||||
|
// происходит» (жалоба 18.09 по стенду: ЛК 0.2.0, канал stable пуст).
|
||||||
|
func TestUpdateRowsAfterFailedCheck(t *testing.T) {
|
||||||
|
rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cfg := rc.Snapshot().Update
|
||||||
|
cfg.Channel = "stable"
|
||||||
|
cfg.Components = map[string]string{"bj-lk": "0.2.0"}
|
||||||
|
cfg.ComponentsChannel = "stable"
|
||||||
|
cfg.LastResult = "Обновление не применено: манифест: release: манифест HTTP 404: релизов в канале нет"
|
||||||
|
if err := rc.SaveUpdateSettings(cfg); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rows, any := updateComponentRows(rc)
|
||||||
|
if any {
|
||||||
|
t.Error("после отказа проверки предлагается обновление")
|
||||||
|
}
|
||||||
|
for _, r := range rows {
|
||||||
|
if r.Update {
|
||||||
|
t.Errorf("%s: кнопка обновления осталась", r.Name)
|
||||||
|
}
|
||||||
|
if r.Name == "bj-lk" && r.Available == "0.2.0" && r.Blocked == "" {
|
||||||
|
t.Error("не объяснено, почему обновление недоступно")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Версии, найденные в другом канале, к текущему не относятся.
|
||||||
|
cfg.LastResult = "доступны обновления: bj-lk 0.2.0"
|
||||||
|
cfg.ComponentsChannel = "beta"
|
||||||
|
if err := rc.SaveUpdateSettings(cfg); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, r := range updateComponentRowsOnly(rc) {
|
||||||
|
if r.Available != "" {
|
||||||
|
t.Errorf("%s: показана версия из чужого канала (%s)", r.Name, r.Available)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !updateResultFailed("Обновление не применено: нет лицензии") || updateResultFailed("все компоненты актуальны") {
|
||||||
|
t.Error("определение отказа работает неверно")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateComponentRowsOnly(rc *RuntimeConfig) []componentRow {
|
||||||
|
rows, _ := updateComponentRows(rc)
|
||||||
|
out := make([]componentRow, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
out = append(out, componentRow{Name: r.Name, Available: r.Available})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
type componentRow struct{ Name, Available string }
|
||||||
@@ -1115,7 +1115,7 @@ function hbBind(inp,box,list,onPick){
|
|||||||
</div>
|
</div>
|
||||||
{{if .Settings.Update.Notes}}<p class="muted" style="margin-top:10px">Что нового в релизе {{.Settings.Update.Available}}: {{.Settings.Update.Notes}}</p>{{end}}
|
{{if .Settings.Update.Notes}}<p class="muted" style="margin-top:10px">Что нового в релизе {{.Settings.Update.Available}}: {{.Settings.Update.Notes}}</p>{{end}}
|
||||||
<p class="muted" style="margin-top:8px;font-size:12px"><b>Закрытый контур.</b> Если у сервера нет выхода к серверу обновлений, скачайте пакет обновления на портале клиента (lic.zetit.ru → Установка → «Пакет обновления») и загрузите его кнопкой «Обновить из файла». Система сама распознаёт, что внутри — ядро, модули или всё вместе — и ставит в правильном порядке. Модуль, которому нужно более новое ядро, чем установлено, будет пропущен с подсказкой обновить сначала ядро.</p>
|
<p class="muted" style="margin-top:8px;font-size:12px"><b>Закрытый контур.</b> Если у сервера нет выхода к серверу обновлений, скачайте пакет обновления на портале клиента (lic.zetit.ru → Установка → «Пакет обновления») и загрузите его кнопкой «Обновить из файла». Система сама распознаёт, что внутри — ядро, модули или всё вместе — и ставит в правильном порядке. Модуль, которому нужно более новое ядро, чем установлено, будет пропущен с подсказкой обновить сначала ядро.</p>
|
||||||
{{if not .Settings.Update.LastCheck.IsZero}}<p class="muted" style="margin-top:6px;font-size:12px">Последняя проверка: {{.Settings.Update.LastCheck.Format "02.01.2006 15:04"}}. {{.Settings.Update.LastResult}}</p>{{end}}
|
{{if not .Settings.Update.LastCheck.IsZero}}{{if .UpdateFailed}}<div class="alert alert-warn" style="margin-top:10px">{{.Settings.Update.LastResult}} <span class="muted">({{.Settings.Update.LastCheck.Format "02.01.2006 15:04"}})</span></div>{{else}}<p class="muted" style="margin-top:6px;font-size:12px">Последняя проверка: {{.Settings.Update.LastCheck.Format "02.01.2006 15:04"}}. {{.Settings.Update.LastResult}}</p>{{end}}{{end}}
|
||||||
{{if and .License.Present (not .License.AllowsUpdates)}}<p class="muted" style="margin-top:6px;font-size:12px;color:var(--warn)">Текущий план «{{.License.Plan}}» не включает обновления.</p>{{end}}
|
{{if and .License.Present (not .License.AllowsUpdates)}}<p class="muted" style="margin-top:6px;font-size:12px;color:var(--warn)">Текущий план «{{.License.Plan}}» не включает обновления.</p>{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user