e63d2ffe9b
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>
65 lines
1.8 KiB
TypeScript
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',
|
|
});
|
|
});
|
|
});
|