From 4b4aef7ef81be44f7952a1df09d6491a86e26fdd Mon Sep 17 00:00:00 2001 From: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:17:57 +1300 Subject: [PATCH] fix(stack): apply new stack manual redeployment filed name to regular stack [BE-12384] (#1375) --- api/edge/edge.go | 5 +++++ api/http/handler/stacks/stack_update.go | 17 +++++++++++++---- .../handler/stacks/stack_update_git_redeploy.go | 11 +++++++---- app/portainer/services/api/stackService.js | 4 ++-- .../views/stacks/edit/stackController.js | 2 +- .../components/PrivateRegistryFieldset.tsx | 8 ++++++++ 6 files changed, 36 insertions(+), 11 deletions(-) diff --git a/api/edge/edge.go b/api/edge/edge.go index 9d9d51b88..e4acbaa69 100644 --- a/api/edge/edge.go +++ b/api/edge/edge.go @@ -65,6 +65,11 @@ type ( ForceUpdate bool DeployerOptionsPayload DeployerOptionsPayload + + // Used only for EE async edge agent + // ReadyRePullImage is a flag to indicate whether the auto update is trigger to re-pull image + // Deprecated(2.36): use DeployerOptionsPayload.ForceRecreate instead + ReadyRePullImage bool } DeployerOptionsPayload struct { diff --git a/api/http/handler/stacks/stack_update.go b/api/http/handler/stacks/stack_update.go index 685a114c0..0c51dcb4f 100644 --- a/api/http/handler/stacks/stack_update.go +++ b/api/http/handler/stacks/stack_update.go @@ -24,6 +24,10 @@ type updateComposeStackPayload struct { StackFileContent string `example:"version: 3\n services:\n web:\n image:nginx"` // A list of environment(endpoint) variables used during stack deployment Env []portainer.Pair + // RepullImageAndRedeploy indicates whether to force repulling images and redeploying the stack + RepullImageAndRedeploy bool + + // Deprecated(2.36): use RepullImageAndRedeploy instead for cleaner responsibility // Force a pulling to current image with the original tag though the image is already the latest PullImage bool `example:"false"` } @@ -43,6 +47,10 @@ type updateSwarmStackPayload struct { Env []portainer.Pair // Prune services that are no longer referenced (only available for Swarm stacks) Prune bool `example:"true"` + // RepullImageAndRedeploy indicates whether to force repulling images and redeploying the stack + RepullImageAndRedeploy bool + + // Deprecated(2.36): use RepullImageAndRedeploy instead for cleaner responsibility // Force a pulling to current image with the original tag though the image is already the latest PullImage bool `example:"false"` } @@ -206,6 +214,7 @@ func (handler *Handler) updateComposeStack(tx dataservices.DataStoreTx, r *http. return httperror.BadRequest("Invalid request payload", err) } + payload.RepullImageAndRedeploy = payload.RepullImageAndRedeploy || payload.PullImage stack.Env = payload.Env if stack.GitConfig != nil { @@ -233,8 +242,8 @@ func (handler *Handler) updateComposeStack(tx dataservices.DataStoreTx, r *http. endpoint, handler.FileService, handler.StackDeployer, - payload.PullImage, - false) + payload.RepullImageAndRedeploy, + payload.RepullImageAndRedeploy) if err != nil { if rollbackErr := handler.FileService.RollbackStackFile(stackFolder, stack.EntryPoint); rollbackErr != nil { log.Warn().Err(rollbackErr).Msg("rollback stack file error") @@ -271,7 +280,7 @@ func (handler *Handler) updateSwarmStack(tx dataservices.DataStoreTx, r *http.Re if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil { return httperror.BadRequest("Invalid request payload", err) } - + payload.RepullImageAndRedeploy = payload.RepullImageAndRedeploy || payload.PullImage stack.Env = payload.Env if stack.GitConfig != nil { @@ -300,7 +309,7 @@ func (handler *Handler) updateSwarmStack(tx dataservices.DataStoreTx, r *http.Re handler.FileService, handler.StackDeployer, payload.Prune, - payload.PullImage) + payload.RepullImageAndRedeploy) if err != nil { if rollbackErr := handler.FileService.RollbackStackFile(stackFolder, stack.EntryPoint); rollbackErr != nil { log.Warn().Err(rollbackErr).Msg("rollback stack file error") diff --git a/api/http/handler/stacks/stack_update_git_redeploy.go b/api/http/handler/stacks/stack_update_git_redeploy.go index c595808aa..fc2cdbfcb 100644 --- a/api/http/handler/stacks/stack_update_git_redeploy.go +++ b/api/http/handler/stacks/stack_update_git_redeploy.go @@ -27,10 +27,13 @@ type stackGitRedployPayload struct { RepositoryAuthorizationType gittypes.GitCredentialAuthType Env []portainer.Pair Prune bool - // Force a pulling to current image with the original tag though the image is already the latest - PullImage bool `example:"false"` + // RepullImageAndRedeploy indicates whether to force repulling images and redeploying the stack + RepullImageAndRedeploy bool StackName string + // Deprecated(2.36): use RepullImageAndRedeploy instead for cleaner responsibility + // Force a pulling to current image with the original tag though the image is already the latest + PullImage bool `example:"false"` } func (payload *stackGitRedployPayload) Validate(r *http.Request) error { @@ -124,7 +127,7 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request) if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil { return httperror.BadRequest("Invalid request payload", err) } - + payload.RepullImageAndRedeploy = payload.RepullImageAndRedeploy || payload.PullImage stack.GitConfig.ReferenceName = payload.RepositoryReferenceName stack.Env = payload.Env if stack.Type == portainer.DockerSwarmStack { @@ -168,7 +171,7 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request) defer clean() - if err := handler.deployStack(r, stack, payload.PullImage, endpoint); err != nil { + if err := handler.deployStack(r, stack, payload.RepullImageAndRedeploy, endpoint); err != nil { return err } diff --git a/app/portainer/services/api/stackService.js b/app/portainer/services/api/stackService.js index 5d347111d..22a228c99 100644 --- a/app/portainer/services/api/stackService.js +++ b/app/portainer/services/api/stackService.js @@ -227,7 +227,7 @@ angular.module('portainer.app').factory('StackService', [ return deferred.promise; }; - service.updateStack = function (stack, stackFile, env, prune, pullImage) { + service.updateStack = function (stack, stackFile, env, prune, repullImageAndRedeploy) { return Stack.update( { endpointId: stack.EndpointId }, { @@ -235,7 +235,7 @@ angular.module('portainer.app').factory('StackService', [ StackFileContent: stackFile, Env: env, Prune: prune, - PullImage: pullImage, + RepullImageAndRedeploy: repullImageAndRedeploy, } ).$promise; }; diff --git a/app/portainer/views/stacks/edit/stackController.js b/app/portainer/views/stacks/edit/stackController.js index 8c0280f3e..56b291c4a 100644 --- a/app/portainer/views/stacks/edit/stackController.js +++ b/app/portainer/views/stacks/edit/stackController.js @@ -188,7 +188,7 @@ angular.module('portainer.app').controller('StackController', [ } $scope.state.actionInProgress = true; - StackService.updateStack(stack, stackFile, env, prune, result.pullImage) + StackService.updateStack(stack, stackFile, env, prune, result.repullImageAndRedeploy) .then(function success() { Notifications.success('Success', 'Stack successfully deployed'); $scope.state.isEditorDirty = false; diff --git a/app/react/edge/edge-stacks/components/PrivateRegistryFieldset.tsx b/app/react/edge/edge-stacks/components/PrivateRegistryFieldset.tsx index 7a1703ffc..8ac50fc17 100644 --- a/app/react/edge/edge-stacks/components/PrivateRegistryFieldset.tsx +++ b/app/react/edge/edge-stacks/components/PrivateRegistryFieldset.tsx @@ -36,6 +36,7 @@ export function PrivateRegistryFieldset({ }: Props) { const [checked, setChecked] = useState(isActive || false); const [selected, setSelected] = useState(value); + const [isInitialMount, setIsInitialMount] = useState(true); const tooltipMessage = 'This allows you to provide credentials when using a private registry that requires authentication'; @@ -47,6 +48,13 @@ export function PrivateRegistryFieldset({ }, [isActive]); useEffect(() => { + // Skip onChange call on initial mount when checkbox is already checked + // to preserve the saved registry value + if (isInitialMount) { + setIsInitialMount(false); + return; + } + if (checked) { onChange(); } else {