diff --git a/api/http/handler/system/system_upgrade.go b/api/http/handler/system/system_upgrade.go index cbf600260..66bb27852 100644 --- a/api/http/handler/system/system_upgrade.go +++ b/api/http/handler/system/system_upgrade.go @@ -8,7 +8,6 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/rs/zerolog/log" ) type systemUpgradePayload struct { @@ -43,12 +42,10 @@ func (handler *Handler) systemUpgrade(w http.ResponseWriter, r *http.Request) *h return httperror.BadRequest("Invalid request payload", err) } - go func() { - err = handler.upgradeService.Upgrade(payload.License) - if err != nil { - log.Error().Err(err).Msg("Failed to upgrade Portainer") - } - }() + err = handler.upgradeService.Upgrade(payload.License) + if err != nil { + return httperror.InternalServerError("Failed to upgrade Portainer", err) + } return response.Empty(w) } diff --git a/api/internal/upgrade/upgrade.go b/api/internal/upgrade/upgrade.go index c07739882..bb4bf5721 100644 --- a/api/internal/upgrade/upgrade.go +++ b/api/internal/upgrade/upgrade.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "os" + "strings" "time" "github.com/cbroglie/mustache" @@ -17,8 +18,8 @@ import ( ) const ( - // mustacheUpgradeStandaloneTemplateFile represents the name of the template file for the standalone upgrade - mustacheUpgradeStandaloneTemplateFile = "upgrade-standalone.yml.mustache" + // mustacheUpgradeDockerTemplateFile represents the name of the template file for the docker upgrade + mustacheUpgradeDockerTemplateFile = "upgrade-docker.yml.mustache" // portainerImagePrefixEnvVar represents the name of the environment variable used to define the image prefix for portainer-updater // useful if there's a need to test PR images @@ -60,19 +61,20 @@ func (service *service) Upgrade(licenseKey string) error { switch service.platform { case platform.PlatformDockerStandalone: - return service.UpgradeDockerStandalone(licenseKey, portainer.APIVersion) - // case platform.PlatformDockerSwarm: + return service.upgradeDocker(licenseKey, portainer.APIVersion, "standalone") + case platform.PlatformDockerSwarm: + return service.upgradeDocker(licenseKey, portainer.APIVersion, "swarm") // case platform.PlatformKubernetes: // case platform.PlatformPodman: // case platform.PlatformNomad: // default: } - return errors.New("unsupported platform") + return fmt.Errorf("unsupported platform %s", service.platform) } -func (service *service) UpgradeDockerStandalone(licenseKey, version string) error { - templateName := filesystem.JoinPaths(service.assetsPath, "mustache-templates", mustacheUpgradeStandaloneTemplateFile) +func (service *service) upgradeDocker(licenseKey, version, envType string) error { + templateName := filesystem.JoinPaths(service.assetsPath, "mustache-templates", mustacheUpgradeDockerTemplateFile) portainerImagePrefix := os.Getenv(portainerImagePrefixEnvVar) if portainerImagePrefix == "" { @@ -88,6 +90,7 @@ func (service *service) UpgradeDockerStandalone(licenseKey, version string) erro "skip_pull_image": skipPullImage, "updater_image": os.Getenv(updaterImageEnvVar), "license": licenseKey, + "envType": envType, }) log.Debug(). @@ -99,7 +102,8 @@ func (service *service) UpgradeDockerStandalone(licenseKey, version string) erro } tmpDir := os.TempDir() - filePath := filesystem.JoinPaths(tmpDir, fmt.Sprintf("upgrade-%d.yml", time.Now().Unix())) + timeId := time.Now().Unix() + filePath := filesystem.JoinPaths(tmpDir, fmt.Sprintf("upgrade-%d.yml", timeId)) r := bytes.NewReader([]byte(composeFile)) @@ -108,18 +112,28 @@ func (service *service) UpgradeDockerStandalone(licenseKey, version string) erro return errors.Wrap(err, "failed to create upgrade compose file") } + projectName := fmt.Sprintf( + "portainer-upgrade-%d-%s", + timeId, + strings.Replace(version, ".", "-", -1)) + err = service.composeDeployer.Deploy( context.Background(), []string{filePath}, libstack.DeployOptions{ ForceRecreate: true, AbortOnContainerExit: true, + Options: libstack.Options{ + ProjectName: projectName, + }, }, ) + // optimally, server was restarted by the updater, so we should not reach this point + if err != nil { return errors.Wrap(err, "failed to deploy upgrade stack") } - return nil + return errors.New("upgrade failed: server should have been restarted by the updater") } diff --git a/app/portainer/services/axios.ts b/app/portainer/services/axios.ts index 5448feb48..a26d1559d 100644 --- a/app/portainer/services/axios.ts +++ b/app/portainer/services/axios.ts @@ -74,3 +74,9 @@ function defaultErrorParser(axiosError: AxiosError) { const error = new Error(message); return { error, details }; } + +export function isAxiosError< + ResponseType = { message: string; details: string } +>(error: unknown): error is AxiosError { + return axiosOrigin.isAxiosError(error); +} diff --git a/app/react/portainer/system/useSystemStatus.ts b/app/react/portainer/system/useSystemStatus.ts index 092ce7eef..3817c4ed3 100644 --- a/app/react/portainer/system/useSystemStatus.ts +++ b/app/react/portainer/system/useSystemStatus.ts @@ -41,6 +41,7 @@ export function useSystemStatus({ select, enabled, retry, + retryDelay: 1000, onSuccess, }); } diff --git a/app/react/portainer/system/useUpgradeEditionMutation.ts b/app/react/portainer/system/useUpgradeEditionMutation.ts index a0c5bd329..c453e377a 100644 --- a/app/react/portainer/system/useUpgradeEditionMutation.ts +++ b/app/react/portainer/system/useUpgradeEditionMutation.ts @@ -1,6 +1,9 @@ import { useMutation } from 'react-query'; -import axios, { parseAxiosError } from '@/portainer/services/axios'; +import axios, { + isAxiosError, + parseAxiosError, +} from '@/portainer/services/axios'; import { withError } from '@/react-tools/react-query'; import { buildUrl } from './build-url'; @@ -15,6 +18,15 @@ async function upgradeEdition({ license }: { license: string }) { try { await axios.post(buildUrl('upgrade'), { license }); } catch (error) { - throw parseAxiosError(error as Error); + if (!isAxiosError(error)) { + throw error; + } + + // if error is because the server disconnected, then everything went well + if (!error.response || !error.response.status) { + return; + } + + throw parseAxiosError(error); } } diff --git a/app/react/sidebar/UpgradeBEBanner/UpgradeBEBanner.tsx b/app/react/sidebar/UpgradeBEBanner/UpgradeBEBanner.tsx index 8decf339e..8134454e7 100644 --- a/app/react/sidebar/UpgradeBEBanner/UpgradeBEBanner.tsx +++ b/app/react/sidebar/UpgradeBEBanner/UpgradeBEBanner.tsx @@ -19,7 +19,10 @@ export const UpgradeBEBannerWrapper = withHideOnExtension( withEdition(UpgradeBEBanner, 'CE') ); -const enabledPlatforms: Array = ['Docker Standalone']; +const enabledPlatforms: Array = [ + 'Docker Standalone', + 'Docker Swarm', +]; function UpgradeBEBanner() { const { isAdmin } = useUser(); diff --git a/gruntfile.js b/gruntfile.js index 641ed29a0..83489b33f 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -208,7 +208,7 @@ function shell_download_docker_compose_binary(platform, arch) { var binaryVersion = '<%= binaries.dockerComposePluginVersion %>'; return ` - if [ -f dist/docker-compose.plugin ] || [ -f dist/docker-compose.plugin.exe ]; then + if [ -f dist/docker-compose ] || [ -f dist/docker-compose.exe ]; then echo "docker compose binary exists"; else build/download_docker_compose_binary.sh ${platform} ${arch} ${binaryVersion}; diff --git a/mustache-templates/upgrade-standalone.yml.mustache b/mustache-templates/upgrade-docker.yml.mustache similarity index 76% rename from mustache-templates/upgrade-standalone.yml.mustache rename to mustache-templates/upgrade-docker.yml.mustache index a7f85aa19..5b931b01f 100644 --- a/mustache-templates/upgrade-standalone.yml.mustache +++ b/mustache-templates/upgrade-docker.yml.mustache @@ -5,7 +5,7 @@ services: image: {{updater_image}}{{^updater_image}}portainer/portainer-updater:latest{{/updater_image}} command: ["portainer", "--image", "{{image}}{{^image}}portainer/portainer-ee:latest{{/image}}", - "--env-type", "standalone", + "--env-type", "{{envType}}{{^envType}}standalone{{/envType}}", "--license", "{{license}}" ] {{#skip_pull_image}}