Files
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

80 lines
2.2 KiB
TypeScript

import { Download } from 'lucide-react';
import { useRouter } from '@uirouter/react';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { useContainerImageStatus } from '@/react/docker/containers/queries/useContainerImageStatus';
import { useApplyContainerImageUpdate } from '@/react/docker/containers/update';
import { ButtonGroup, LoadingButton } from '@@/buttons';
import { ContainerId } from '../../../types';
interface UpdateNowButtonProps {
environmentId: EnvironmentId;
containerId: ContainerId;
nodeName?: string;
containerImage: string;
containerName: string;
labels?: Record<string, string>;
isPortainer: boolean;
}
/**
* "Update now" surfaces a discoverable per-container apply action ONLY when the
* image is `outdated`. It routes through the shared update primitive, which
* always recreates just this one container with a fresh image pull (the recreate
* endpoint preserves config + compose labels, so a stack member stays part of
* its project). The button is disabled only for Portainer's own container, which
* can't recreate itself.
*/
export function UpdateNowButton({
environmentId,
containerId,
nodeName,
containerImage,
containerName,
labels,
isPortainer,
}: UpdateNowButtonProps) {
const router = useRouter();
const statusQuery = useContainerImageStatus(
environmentId,
containerId,
nodeName
);
const { apply, isLoading, canApply } = useApplyContainerImageUpdate({
environmentId,
containerId,
nodeName,
containerImage,
containerName,
labels,
isPortainer,
// The details view reloads to reflect the recreated container.
onSuccess: () =>
router.stateService.go('docker.containers', {}, { reload: true }),
});
// Only meaningful when a newer image is actually available.
if (statusQuery.data?.Status !== 'outdated') {
return null;
}
const button = (
<LoadingButton
color="primary"
size="small"
onClick={apply}
disabled={!canApply}
isLoading={isLoading}
loadingText="Updating..."
data-cy="update-now-button"
icon={Download}
>
Update now
</LoadingButton>
);
return <ButtonGroup>{button}</ButtonGroup>;
}