f7cb0f3241
Add a discoverable per-container "Update now" action, shown only when the
image status is `outdated`, plus a bulk "Update selected" action in the
containers list.
Both manual paths share ONE apply primitive (applyContainerUpdate /
useUpdateContainerImage) that also backs the future M4 auto-update job:
- standalone container -> recreate-with-pull (existing recreate endpoint)
- stack-managed -> stack redeploy-with-pull (existing git/file stack
update mutations), so the container stays in its
stack and is never recreated out-of-band
- externally-managed -> refused; the details button is disabled with an
compose explanatory tooltip and the bulk action skips it
Decision logic lives in the pure, unit-tested resolveContainerUpdatePath /
groupContainersForUpdate helpers. The bulk action filters to outdated
containers and redeploys each owning stack exactly once even when several of
its containers are selected, reporting per-item success/failure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
84 lines
3.1 KiB
TypeScript
84 lines
3.1 KiB
TypeScript
import { Stack } from '@/react/common/stacks/types';
|
|
import { getStackFile } from '@/react/common/stacks/queries/useStackFile';
|
|
import { updateStack } from '@/react/docker/stacks/useUpdateStack';
|
|
import { updateGitStack } from '@/react/portainer/gitops/queries/useUpdateGitStack';
|
|
|
|
import { recreateContainer } from '../containers.service';
|
|
|
|
import { resolveContainerUpdatePath } from './resolveContainerUpdatePath';
|
|
import { ContainerUpdateContext, ContainerUpdateKind } from './types';
|
|
|
|
/** Thrown when an update is attempted on an externally-managed compose container. */
|
|
export const EXTERNAL_STACK_UPDATE_ERROR =
|
|
'This container is part of a compose project that is not managed by Portainer, so it cannot be updated from here.';
|
|
|
|
/**
|
|
* Redeploy a Portainer stack, forcing a re-pull, while preserving its current
|
|
* file content, environment variables and options. Reuses the exact mutations
|
|
* the stack page uses for "pull and redeploy" so we never invent a new path.
|
|
*/
|
|
async function redeployStackWithPull(stack: Stack, pullImage: boolean) {
|
|
if (stack.GitConfig) {
|
|
await updateGitStack(stack.Id, stack.EndpointId, {
|
|
RepullImageAndRedeploy: pullImage,
|
|
Env: stack.Env ?? [],
|
|
Prune: stack.Option?.Prune,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// File/standalone compose stack: redeploy with its current file unchanged.
|
|
const file = await getStackFile({ stackId: stack.Id });
|
|
await updateStack({
|
|
stackId: stack.Id,
|
|
environmentId: stack.EndpointId,
|
|
payload: {
|
|
stackFileContent: file.StackFileContent,
|
|
env: stack.Env ?? [],
|
|
prune: stack.Option?.Prune,
|
|
repullImageAndRedeploy: pullImage,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Shared "apply an image update" primitive. Decides standalone-vs-stack-vs-external
|
|
* and runs the matching mutation. This is the single code path behind the
|
|
* "Update now" button, the bulk "Update selected" action and (future M4) the
|
|
* auto-update job, guaranteeing manual and automatic updates behave identically.
|
|
*
|
|
* A stack-managed container is ALWAYS routed through stack redeploy so it stays
|
|
* part of its stack; an externally-managed compose container is refused rather
|
|
* than recreated out-of-band (which would detach it).
|
|
*/
|
|
export async function applyContainerUpdate(
|
|
context: ContainerUpdateContext,
|
|
stacks: Stack[],
|
|
{ pullImage = true }: { pullImage?: boolean } = {}
|
|
): Promise<ContainerUpdateKind> {
|
|
const path = resolveContainerUpdatePath(context, stacks);
|
|
|
|
switch (path.kind) {
|
|
case 'standalone':
|
|
await recreateContainer(context.environmentId, context.id, pullImage, {
|
|
nodeName: context.nodeName,
|
|
});
|
|
return 'standalone';
|
|
|
|
case 'stack': {
|
|
const stack = stacks.find((s) => s.Id === path.stackId);
|
|
if (!stack) {
|
|
// Should not happen (resolve found it), but never fall back to a bare
|
|
// recreate: that would orphan the container from its stack.
|
|
throw new Error(EXTERNAL_STACK_UPDATE_ERROR);
|
|
}
|
|
await redeployStackWithPull(stack, pullImage);
|
|
return 'stack';
|
|
}
|
|
|
|
case 'external':
|
|
default:
|
|
throw new Error(EXTERNAL_STACK_UPDATE_ERROR);
|
|
}
|
|
}
|