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>
99 lines
2.9 KiB
TypeScript
99 lines
2.9 KiB
TypeScript
import { List } from 'lucide-react';
|
|
|
|
import { ContainerDetailsViewModel } from '@/docker/models/containerDetails';
|
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
|
import { joinCommand } from '@/docker/filters/utils';
|
|
|
|
import { DetailsTable } from '@@/DetailsTable';
|
|
import { Widget } from '@@/Widget';
|
|
|
|
import { RestartPolicy } from '../../CreateView/RestartPolicyTab/types';
|
|
import { RestartPolicySection } from '../RestartPolicySection/RestartPolicySection';
|
|
|
|
import { AutoHealRow } from './AutoHealRow';
|
|
import { AutoUpdateRow } from './AutoUpdateRow';
|
|
import { ImageRow } from './ImageRow';
|
|
import { PortConfigurationRow } from './PortConfigurationRow';
|
|
import { EnvironmentVariablesRow } from './EnvironmentVariablesRow';
|
|
import { LabelsRow } from './LabelsRow';
|
|
import { SysctlsRow } from './SysctlsRow';
|
|
import { SecurityOptRow } from './SecurityOptRow';
|
|
import { GpuRow } from './GpuRow';
|
|
|
|
interface Props {
|
|
environmentId: EnvironmentId;
|
|
container: ContainerDetailsViewModel;
|
|
nodeName?: string;
|
|
onUpdateSuccess?(): void;
|
|
}
|
|
|
|
export function ContainerDetailsSection({
|
|
environmentId,
|
|
container,
|
|
nodeName,
|
|
onUpdateSuccess,
|
|
}: Props) {
|
|
const config = container.Config;
|
|
const hostConfig = container.HostConfig;
|
|
|
|
if (!config || !hostConfig || !container.Id) {
|
|
return null;
|
|
}
|
|
|
|
const restartPolicyName = hostConfig.RestartPolicy?.Name as
|
|
| RestartPolicy
|
|
| undefined;
|
|
|
|
return (
|
|
<Widget>
|
|
<Widget.Title icon={List} title="Container details" />
|
|
<Widget.Body>
|
|
<DetailsTable dataCy="container-details-table">
|
|
<ImageRow
|
|
image={config.Image || ''}
|
|
imageHash={container.Image || ''}
|
|
nodeName={nodeName}
|
|
/>
|
|
|
|
<PortConfigurationRow ports={container.NetworkSettings?.Ports} />
|
|
|
|
<DetailsTable.Row label="CMD">
|
|
<code>{joinCommand(config.Cmd)}</code>
|
|
</DetailsTable.Row>
|
|
|
|
<DetailsTable.Row label="ENTRYPOINT">
|
|
<code>
|
|
{config.Entrypoint ? joinCommand(config.Entrypoint) : 'null'}
|
|
</code>
|
|
</DetailsTable.Row>
|
|
|
|
<EnvironmentVariablesRow variables={config.Env} />
|
|
|
|
<LabelsRow labels={config.Labels} />
|
|
|
|
<DetailsTable.Row label="Restart policies">
|
|
<RestartPolicySection
|
|
environmentId={environmentId}
|
|
containerId={container.Id}
|
|
nodeName={nodeName}
|
|
name={restartPolicyName}
|
|
maximumRetryCount={hostConfig.RestartPolicy?.MaximumRetryCount}
|
|
onUpdateSuccess={onUpdateSuccess}
|
|
/>
|
|
</DetailsTable.Row>
|
|
|
|
<AutoHealRow labels={config.Labels} />
|
|
|
|
<AutoUpdateRow labels={config.Labels} />
|
|
|
|
<SysctlsRow sysctls={hostConfig.Sysctls} />
|
|
|
|
<SecurityOptRow securityOpts={hostConfig.SecurityOpt} />
|
|
|
|
<GpuRow deviceRequests={hostConfig.DeviceRequests} />
|
|
</DetailsTable>
|
|
</Widget.Body>
|
|
</Widget>
|
|
);
|
|
}
|