Files
portainer/app/react/docker/containers/update/applyContainerUpdate.test.ts
T
vvzvlad e63d2ffe9b fix(automation): update a single container instead of redeploying its stack
Clicking "Update" on a stack member (and the native auto-update daemon
updating one) redeployed the whole compose stack instead of updating just
that container. Match Watchtower behaviour: always recreate the single
container with a re-pull. The recreate endpoint preserves config + compose
labels, so the container stays part of its project.

Collapse all update surfaces to a single-container recreate and drop the
now-dead stack-aware routing:
- frontend: "Update now" button, list badge and bulk "Update selected" now
  recreate each container individually; remove standalone/stack/external
  routing, the external refusal, the PortainerStackUpdate gate and the
  stack-update confirm dialog.
- daemon: route every outdated candidate through updateStandalone; remove
  updateStack, the stack/external grouping and the stackDeployer dependency.
- add a regression test asserting a Portainer-managed compose-stack member is
  recreated individually, not stack-redeployed.

Behavioural notes: git/external compose containers are now auto-updated too
(were detect-only), and updating a stack member no longer requires
PortainerStackUpdate (same auth as the normal Recreate action).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:21:29 +03:00

65 lines
1.8 KiB
TypeScript

import { vi, describe, it, expect, beforeEach } from 'vitest';
import { COMPOSE_STACK_NAME_LABEL } from '@/react/constants';
import { applyContainerUpdate } from './applyContainerUpdate';
import { ContainerUpdateContext } from './types';
// Mock the recreate mutation so we assert the dispatch and payload mapping
// without touching the network.
const recreateContainer = vi.fn();
vi.mock('../containers.service', () => ({
recreateContainer: (...args: unknown[]) => recreateContainer(...args),
}));
function buildContext(
overrides: Partial<ContainerUpdateContext> = {}
): ContainerUpdateContext {
return {
id: 'abc123',
name: 'my-container',
image: 'nginx:latest',
environmentId: 3,
nodeName: 'node-1',
...overrides,
};
}
describe('applyContainerUpdate', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('recreates a plain container with pull and node', async () => {
await applyContainerUpdate(buildContext());
expect(recreateContainer).toHaveBeenCalledWith(3, 'abc123', true, {
nodeName: 'node-1',
});
});
it('recreates a compose-managed container the same way (never redeploys a stack)', async () => {
// A container carrying compose labels must ALSO recreate: the recreate
// endpoint preserves those labels, so it stays part of its project.
await applyContainerUpdate(
buildContext({
labels: { [COMPOSE_STACK_NAME_LABEL]: 'my-stack' },
})
);
expect(recreateContainer).toHaveBeenCalledWith(3, 'abc123', true, {
nodeName: 'node-1',
});
expect(recreateContainer).toHaveBeenCalledTimes(1);
});
it('passes pullImage=false through to the recreate', async () => {
await applyContainerUpdate(buildContext(), { pullImage: false });
expect(recreateContainer).toHaveBeenCalledWith(3, 'abc123', false, {
nodeName: 'node-1',
});
});
});