922f506fe5
F1: record rolled-back targets per service (endpointID/containerName + remote
digest) and skip auto-update during a 24h cooldown unless the remote digest
changes — breaks the infinite update→rollback loop on a persistently
unhealthy image, without blocking a genuinely new image.
F2: unit-test applyContainerUpdate dispatch/payload mapping.
F3: settings_update.go comments mention auto-heal AND auto-update.
F4: drop stale '(future M4)' TS docs; primitives are frontend-only.
F5: replace the anonymous ContainerAutomation settings struct with named
types (identical JSON tags).
F6: drop parseEnable (duplicate of boolLabel).
F7: remove the unused gitService dependency.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
87 lines
3.3 KiB
TypeScript
87 lines
3.3 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 frontend code path behind the
|
|
* "Update now" button and the bulk "Update selected" action, guaranteeing both
|
|
* manual flows behave identically. The backend auto-update daemon does NOT call
|
|
* this primitive: it is a separate Go implementation
|
|
* (resolveContainerUpdateRouting / groupContainersForUpdate in
|
|
* api/containerautomation) that mirrors the same routing on the server.
|
|
*
|
|
* 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);
|
|
}
|
|
}
|