Files
portainer/app/react/docker/containers/ItemView/ContainerDetailsSection/AutoUpdateRow.test.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

70 lines
2.1 KiB
TypeScript

import { render, screen } from '@testing-library/react';
import { AutoUpdateRow } from './AutoUpdateRow';
function renderRow(labels?: Record<string, string>) {
return render(
<table>
<tbody>
<AutoUpdateRow labels={labels} />
</tbody>
</table>
);
}
describe('AutoUpdateRow', () => {
it('shows enabled when the primary label is true', () => {
renderRow({ 'io.portainer.update.enable': 'true' });
expect(screen.getByText('Enabled')).toBeInTheDocument();
});
it('shows enabled via the watchtower alias', () => {
renderRow({ 'com.centurylinklabs.watchtower.enable': 'true' });
expect(screen.getByText('Enabled')).toBeInTheDocument();
});
it.each(['TRUE', 'True', 'T', 't', '1'])(
'parses the truthy value %s like strconv.ParseBool',
(value) => {
renderRow({ 'io.portainer.update.enable': value });
expect(screen.getByText('Enabled')).toBeInTheDocument();
}
);
it('treats a present-but-invalid value as opted out', () => {
renderRow({ 'io.portainer.update.enable': 'soon' });
expect(screen.getByText('Disabled (opted out)')).toBeInTheDocument();
});
it('shows opted out when the label is false', () => {
renderRow({ 'io.portainer.update.enable': 'false' });
expect(screen.getByText('Disabled (opted out)')).toBeInTheDocument();
});
it('shows the global-scope fallback when no label is set', () => {
renderRow({});
expect(
screen.getByText('Not labeled (follows global scope)')
).toBeInTheDocument();
});
it('surfaces the monitor-only note (primary label)', () => {
renderRow({
'io.portainer.update.enable': 'true',
'io.portainer.update.monitor-only': 'true',
});
expect(
screen.getByText(/Monitor-only: updates are detected but never applied/i)
).toBeInTheDocument();
});
it('surfaces the monitor-only note via the watchtower alias', () => {
renderRow({
'com.centurylinklabs.watchtower.monitor-only': 'true',
});
expect(
screen.getByText(/Monitor-only: updates are detected but never applied/i)
).toBeInTheDocument();
});
});