mirror of https://github.com/portainer/portainer
183 lines
5.0 KiB
Go
183 lines
5.0 KiB
Go
package upgrade
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cbroglie/mustache"
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/filters"
|
|
"github.com/pkg/errors"
|
|
libstack "github.com/portainer/docker-compose-wrapper"
|
|
portainer "github.com/portainer/portainer/api"
|
|
"github.com/portainer/portainer/api/docker"
|
|
"github.com/portainer/portainer/api/filesystem"
|
|
"github.com/portainer/portainer/api/platform"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
const (
|
|
// 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
|
|
portainerImagePrefixEnvVar = "UPGRADE_PORTAINER_IMAGE_PREFIX"
|
|
// skipPullImageEnvVar represents the name of the environment variable used to define if the image pull should be skipped
|
|
// useful if there's a need to test local images
|
|
skipPullImageEnvVar = "UPGRADE_SKIP_PULL_PORTAINER_IMAGE"
|
|
// updaterImageEnvVar represents the name of the environment variable used to define the updater image
|
|
// useful if there's a need to test a different updater
|
|
updaterImageEnvVar = "UPGRADE_UPDATER_IMAGE"
|
|
)
|
|
|
|
type Service interface {
|
|
Upgrade(licenseKey string) error
|
|
}
|
|
|
|
type service struct {
|
|
composeDeployer libstack.Deployer
|
|
isUpdating bool
|
|
platform platform.ContainerPlatform
|
|
assetsPath string
|
|
}
|
|
|
|
func NewService(
|
|
assetsPath string,
|
|
composeDeployer libstack.Deployer,
|
|
) (Service, error) {
|
|
platform, err := platform.DetermineContainerPlatform()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to determine container platform")
|
|
}
|
|
|
|
return &service{
|
|
assetsPath: assetsPath,
|
|
composeDeployer: composeDeployer,
|
|
platform: platform,
|
|
}, nil
|
|
}
|
|
|
|
func (service *service) Upgrade(licenseKey string) error {
|
|
service.isUpdating = true
|
|
|
|
switch service.platform {
|
|
case platform.PlatformDockerStandalone:
|
|
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 fmt.Errorf("unsupported platform %s", service.platform)
|
|
}
|
|
|
|
func (service *service) upgradeDocker(licenseKey, version, envType string) error {
|
|
ctx := context.TODO()
|
|
templateName := filesystem.JoinPaths(service.assetsPath, "mustache-templates", mustacheUpgradeDockerTemplateFile)
|
|
|
|
portainerImagePrefix := os.Getenv(portainerImagePrefixEnvVar)
|
|
if portainerImagePrefix == "" {
|
|
portainerImagePrefix = "portainer/portainer-ee"
|
|
}
|
|
|
|
image := fmt.Sprintf("%s:%s", portainerImagePrefix, version)
|
|
|
|
skipPullImage := os.Getenv(skipPullImageEnvVar)
|
|
|
|
if err := service.checkImage(ctx, image, skipPullImage != ""); err != nil {
|
|
return err
|
|
}
|
|
|
|
composeFile, err := mustache.RenderFile(templateName, map[string]string{
|
|
"image": image,
|
|
"skip_pull_image": skipPullImage,
|
|
"updater_image": os.Getenv(updaterImageEnvVar),
|
|
"license": licenseKey,
|
|
"envType": envType,
|
|
})
|
|
|
|
log.Debug().
|
|
Str("composeFile", composeFile).
|
|
Msg("Compose file for upgrade")
|
|
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to render upgrade template")
|
|
}
|
|
|
|
tmpDir := os.TempDir()
|
|
timeId := time.Now().Unix()
|
|
filePath := filesystem.JoinPaths(tmpDir, fmt.Sprintf("upgrade-%d.yml", timeId))
|
|
|
|
r := bytes.NewReader([]byte(composeFile))
|
|
|
|
err = filesystem.CreateFile(filePath, r)
|
|
if err != nil {
|
|
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(
|
|
ctx,
|
|
[]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 errors.New("upgrade failed: server should have been restarted by the updater")
|
|
}
|
|
|
|
func (service *service) checkImage(ctx context.Context, image string, skipPullImage bool) error {
|
|
cli, err := docker.CreateClientFromEnv()
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to create docker client")
|
|
}
|
|
|
|
if skipPullImage {
|
|
filters := filters.NewArgs()
|
|
filters.Add("reference", image)
|
|
images, err := cli.ImageList(ctx, types.ImageListOptions{
|
|
Filters: filters,
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to list images")
|
|
}
|
|
|
|
if len(images) == 0 {
|
|
return errors.Errorf("image %s not found locally", image)
|
|
}
|
|
|
|
return nil
|
|
} else {
|
|
// check if available on registry
|
|
_, err := cli.DistributionInspect(ctx, image, "")
|
|
if err != nil {
|
|
return errors.Errorf("image %s not found on registry", image)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|