b3ae5f3659
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>
70 lines
2.1 KiB
TypeScript
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();
|
|
});
|
|
});
|