Files
portainer/app/react/docker/containers/ItemView/ContainerDetailsSection/AutoUpdateRow.tsx
T
claude code agent b3ae5f3659 feat(automation): native auto-update daemon (#11, epic #3 M4)
Add an optional periodic auto-update daemon that detects outdated container
images and applies updates, replacing the containrrr/watchtower sidecar. It
extends M1's containerautomation service/scheduler/labels infrastructure and
reuses the existing zlib image-detection engine, the standalone Recreate path
and the stack deployer.

Backend:
- api/containerautomation/autoupdate.go: scheduler job iterating Docker
  (non-edge) endpoints -> in-scope running containers -> ContainerImageStatus;
  for Outdated: standalone -> ContainerService.Recreate(pull); stack-managed ->
  one stack redeploy-with-pull per stack per tick (git via RedeployWhenChanged,
  file via the deployer directly); external compose -> detect only. Monitor-only
  containers are status-checked (warms the badge cache) but never applied.
  Overlap guard (atomic), pull/registry-auth failure -> leave running container
  untouched, conservative cleanup of the dangling old image on the Cleanup flag
  (non-forced ImageRemove only succeeds when truly unused).
- labels.go: update enable / monitor-only labels with watchtower aliases,
  InUpdateScope, IsMonitorOnly, and pure resolveContainerUpdateRouting /
  groupContainersForUpdate (Go analogue of M3's TS routing + grouping).
- service.go: run both jobs, Reload restarts/stops each per settings; NewService
  also takes ContainerService, StackDeployer and GitService.
- Settings.ContainerAutomation.AutoUpdate {Enabled, PollInterval, Scope,
  Cleanup} with fresh-install defaults and a 2.43.0 backfill (extends M1's
  migration; golden test data updated). settings handler validates + reloads.

Frontend:
- Global AutoUpdatePanel in SettingsView (enable / poll interval / scope /
  cleanup) via useUpdateSettingsMutation, plus settings TS types.
- Read-only per-container Auto-update row in the container details view
  (Docker labels are immutable at runtime), surfacing monitor-only.

Tests: Go unit tests for the update label aliases, scope, monitor-only, the
routing decision and the one-redeploy-per-stack grouping; vitest for the panel
and the per-container row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 10:04:09 +03:00

66 lines
2.3 KiB
TypeScript

import { DetailsTable } from '@@/DetailsTable';
interface Props {
labels?: Record<string, string>;
}
// Label keys (with watchtower aliases) mirroring the backend
// (api/containerautomation/labels.go).
const ENABLE_LABEL = 'io.portainer.update.enable';
const ENABLE_LABEL_ALIAS = 'com.centurylinklabs.watchtower.enable';
const MONITOR_ONLY_LABEL = 'io.portainer.update.monitor-only';
const MONITOR_ONLY_LABEL_ALIAS = 'com.centurylinklabs.watchtower.monitor-only';
function parseBool(value?: string) {
if (value === undefined) {
return undefined;
}
// Mirror Go's strconv.ParseBool (used by the backend label parser): accept
// 1/t/true case-insensitively as truthy. Any other present-but-invalid value
// (including 0/f/false and garbage) counts as present & false, matching how
// the backend treats an unparseable label.
const normalized = value.toLowerCase();
return normalized === '1' || normalized === 't' || normalized === 'true';
}
/**
* AutoUpdateRow shows the per-container auto-update opt-in state, resolved from
* the container's immutable Docker labels. It is read-only: because labels
* cannot be changed on a running container, opt-in is set through the
* Create/Edit form labels and the global behavior is controlled in Settings.
*/
export function AutoUpdateRow({ labels }: Props) {
const enabled =
parseBool(labels?.[ENABLE_LABEL]) ?? parseBool(labels?.[ENABLE_LABEL_ALIAS]);
const monitorOnly =
parseBool(labels?.[MONITOR_ONLY_LABEL]) ??
parseBool(labels?.[MONITOR_ONLY_LABEL_ALIAS]);
let stateLabel: string;
if (enabled === true) {
stateLabel = 'Enabled';
} else if (enabled === false) {
stateLabel = 'Disabled (opted out)';
} else {
stateLabel = 'Not labeled (follows global scope)';
}
return (
<DetailsTable.Row label="Auto-update">
<div className="space-y-1">
<div>{stateLabel}</div>
{monitorOnly === true && (
<div className="small text-muted">
Monitor-only: updates are detected but never applied automatically.
</div>
)}
<div className="small text-muted">
Set via the <code>{ENABLE_LABEL}</code> label (immutable at runtime;
edit through the container Create/Edit form). Global behavior is
configured in Settings.
</div>
</div>
</DetailsTable.Row>
);
}