Reference in New Issue
Block a user
Delete Branch "feat/8-autoheal"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Нативный авто-хил демон — фоновый scheduler-джоб перезапускает контейнеры в состоянии healthcheck
unhealthy, без сайдкараwillfarrell/autoheal. Closes #8 (эпик #3, майлстоун M1). Бэкенд Go + фронт React, чистый CE (без registry/pull).Бэкенд — новый пакет
api/containerautomation/:service.go— жизненный цикл:NewService/Start()/Reload(), mutex-guarded (ре)планирование джоба, in-memory retry-state с очисткой.Reload()перезапускает/останавливает джоб по новым настройкам (паттернStartAutoupdate/StopAutoupdate).autoheal.go—heal(): фильтр Docker-эндпоинтов, серверный фильтрhealth=unhealthy, рестарт со stop-timeout; чистая функцияdecideRestart(cooldown 60s / окно 10m / max-retries).labels.go— чистое разрешение меток/scope (io.portainer.autoheal.*+ алиасыautoheal*), толерантно к мусору.Бэкенд — настройки:
Settings.ContainerAutomation.AutoHeal {Enabled, CheckInterval, Scope}(api/portainer.go); дефолты fresh-install (api/datastore/init.go: disabled/30s/labeled); миграция backfill в группе 2.43.0 (migrate_2_43_0.go, golden-файл обновлён, MigratorCount 2→3); settings-handler читает/валидирует/применяет + пост-коммитReload()через локальный интерфейсContainerAutomationReloader(без import-цикла, конкретный сервис инжектится черезserver.go); bootstrap вmain.goпослеStartStackSchedules.Фронт: глобальная панель
AutoHealPanelвSettingsView(master switch / interval / scope,useUpdateSettingsMutation) + read-only строкаAutoHealRowв деталях контейнера (резолвит состояние из иммутабельных меток, поясняет что opt-in задаётся в форме Create/Edit + глоб.настройках). TS-типы синхронизированы с Go JSON. БезisBE, EE не тронут.How verified
go build ./api/...— чисто.go vet(containerautomation/settings/datastore/http/cmd) — чисто.gofmt -l— чисто.go test ./api/containerautomation/... ./api/datastore/ ./api/http/handler/settings/...—ok. Тесты:TestInScope,TestStopTimeout,TestMaxRetries,TestDecideRestart(cooldown/окно/max-retries/reset).pnpm typecheck— чисто.pnpm lint— 0 ошибок.pnpm test— новые 7 зелёных (AutoHealPanel,AutoHealRow); единственный фейлCreateStackForm— пред-существующий 5s-таймаут-флейк под полной параллельной нагрузкой, в изоляции проходит, не связан с правкой.Только ручная проверка: реальный рестарт unhealthy-контейнера на живом Docker-эндпоинте, перезапуск джоба на изменение настроек, поведение cooldown в проде.
Review checklist
Add a native, CE-only auto-heal daemon that restarts Docker containers whose healthcheck reports "unhealthy", replacing the willfarrell/autoheal sidecar. Backend: - New package api/containerautomation (service lifecycle + scheduler job, per-endpoint heal pass, label/scope parsing, in-memory cooldown/retry state). - Settings.ContainerAutomation.AutoHeal {Enabled, CheckInterval, Scope} with fresh-install defaults and a 2.43.0 migration backfilling existing installs. - Settings update handler reloads/stops the job via a small Reloader interface (no import cycle); service bootstrapped from main.go after stack schedules. Frontend: - Global AutoHealPanel in SettingsView (enable / interval / scope) via useUpdateSettingsMutation, plus settings TS types. - Read-only per-container Auto-heal row in the container details view (Docker labels are immutable at runtime; opt-in is set via Create/Edit form labels). Tests: Go unit tests for label/scope resolution and the cooldown/retry decision; vitest for the panel and the per-container row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>F1 [warning]
api/containerautomation/autoheal.go:119-120,164-169— HTTP-таймаут Docker-клиента обрезает рестарт контейнеров с длинным stop-timeout. Клиент создаётся сclientTimeout := endpointTimeout(30s), которое CreateClient кладёт вhttp.Client.Timeout(api/docker/client/client.go:205-212) — это жёсткий потолок на ВСЕ запросы клиента, включаяcli.ContainerRestart(стр. 166). Но restart-контекст осознанно считается какrestartTimeout = time.Duration(stopTimeout)*time.Second + restartTimeoutBuffer(стр. 164), чтобы дать graceful-stop время. При labelio.portainer.autoheal.stop-timeout> ~15s (напр. 60s у БД) дедлайн (75s) превышает 30s клиента → запрос рестарта рвётся на 30s, рестарт логируется как failed (стр. 169), а попытка уже засчитана в decideRestart. Итог: контейнеры с долгим graceful-stop — ровно те, ради которых ввели настраиваемый stop-timeout — авто-хилом надёжно НЕ лечатся. Дефолт (stop-timeout 10 → 25s < 30s) маскирует, поэтому тесты/типовой случай зелёные.Fix: не задавать Docker-клиенту короткий общий http.Client.Timeout, конфликтующий с per-call контекстами — создавать клиент с таймаутом ≥ максимально возможного restartTimeout (или nil/большой, полагаясь на listCtx/restartCtx как границы каждого вызова), либо отдельный клиент для рестарта с таймаутом ≥ restartTimeout.
F2 [warning]
api/http/handler/settings/settings_update.go:121-131— новая серверная валидация auto-heal не покрыта тестом (файла settings_update_test.go нет). Ветки: CheckInterval с ошибкой time.ParseDuration или d<=0 → «Invalid auto-heal check interval…»; Scope вне {labeled,all} → «Invalid auto-heal scope…». Это нетривиальная валидация на trust boundary (UI обходится), а проект систематически тестирует Validate() пейлоадов (endpoint_create_test.go, gitops sources create_git_test.go, stacks/*). Без теста легко регрессировать (принять отрицательную длительность/невалидный scope), что прямо влияет на планирование демона.Fix: добавить settings_update_test.go с табличным тестом на settingsUpdatePayload.Validate: валидный (CheckInterval=30s, Scope=labeled/all → nil); невалидный интервал (abc); неположительный (0s/-5s); невалидный scope (weird); ContainerAutomation==nil (валидация пропускается).
F3 [suggestion]
app/react/portainer/settings/SettingsView/AutoHealPanel/validation.ts:8-22— yup-схема (durationPattern/^(\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+$/+ oneOf(['labeled','all'])) без теста: AutoHealPanel.test.tsx проверяет только рендер и дефолты, невалидный интервал не вводит и форму не сабмитит. Регулярка вручную зеркалит единицы Go time.ParseDuration — легко сломать (пропустить голое число без единицы или отвергнуть валидный 1m30s). В репозитории есть прецедент тестирования yup-схем validation.ts (gitops sources, docker stacks).Fix: добавить validation.test.ts: принимаемые «30s»,«1m30s»,«2h»,«500ms»; отвергаемые «30»,«abc»,«»; scope labeled/all (ок) vs weird (ошибка).
F4 [suggestion]
api/portainer.go:1216-1222— поле ContainerAutomation объявлено дважды вложенной анонимной структурой прямо в Settings, что расходится с устойчивой конвенцией файла: все прочие сложносоставные поля Settings ссылаются на именованные типы из блокаtype ( … )(Edge Edge, LDAPSettings, OAuthSettings, GlobalDeploymentOptions, InternalAuthSettings). ContainerAutomation — единственное исключение. Анонимный тип нельзя переиспользовать по имени из пакета containerautomation и обработчиков, из-за чего payload-типы дублируются; на фронте для тех же данных уже заведены именованные AutoHealSettings/ContainerAutomationSettings (types.ts:488-498).Fix: объявить именованные типы в блоке
type ( … )(AutoHealSettings{Enabled,CheckInterval,Scope}, ContainerAutomationSettings{AutoHeal AutoHealSettings}) с теми же json-тегами/example и заменить поле наContainerAutomation ContainerAutomationSettings, по образцуEdge Edge.Ревью (head см. маркер) — раунд 1 (ПОЛНЫЕ 8 аспектов, отдельный субагент на каждый: security, stability, regressions, test-coverage, conventions, documentation, simplification, architecture). Вердикт: CHANGES.
ПРИМЕЧАНИЕ: предыдущий «ревью»-маркер и находки от аккаунта agent_coder не учитываются — по решению мейнтейнера единственный ревьюер — agent_reviewer; это независимое ревью, маркер перезаписан.
Раскладка: security (admin-gate на настройки, label-opt-in только на свои контейнеры, безопасный парсинг) / architecture (отдельный сервис containerautomation, чистая развязка, расширяемый конфиг под след. milestone'ы) / regressions (миграция аддитивна/идемпотентна, демон off по умолчанию, частичный PUT совместим) / simplification / documentation — LGTM.
Открыто:
Эскалаций/блокеров нет (F1 — конкретный фикс таймаута; дефолтная конфигурация работает).
Объединено в один PR на эпик #3 → #19 (M1 вошёл целиком). Этот PR закрываю как superseded; ветка будет удалена (все коммиты в feat/3-auto-update).
Pull request closed