Skip to content

Commit

Permalink
feat(system): upgrade portainer on kubernetes [EE-4625] (#8448)
Browse files Browse the repository at this point in the history
  • Loading branch information
chiptus authored Mar 8, 2023
1 parent 0669ad7 commit 4c86be7
Showing 8 changed files with 394 additions and 139 deletions.
2 changes: 1 addition & 1 deletion api/cmd/portainer/main.go
Original file line number Diff line number Diff line change
@@ -684,7 +684,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatal().Msg("failed to fetch SSL settings from DB")
}

upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer)
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer, kubernetesClientFactory)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing upgrade service")
}
42 changes: 40 additions & 2 deletions api/http/handler/system/system_upgrade.go
Original file line number Diff line number Diff line change
@@ -8,6 +8,8 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/platform"
)

type systemUpgradePayload struct {
@@ -28,24 +30,60 @@ func (payload *systemUpgradePayload) Validate(r *http.Request) error {
return nil
}

var platformToEndpointType = map[platform.ContainerPlatform]portainer.EndpointType{
platform.PlatformDockerStandalone: portainer.DockerEnvironment,
platform.PlatformDockerSwarm: portainer.DockerEnvironment,
platform.PlatformKubernetes: portainer.KubernetesLocalEnvironment,
}

// @id systemUpgrade
// @summary Upgrade Portainer to BE
// @description Upgrade Portainer to BE
// @description **Access policy**: administrator
// @tags system
// @produce json
// @success 200 {object} status "Success"
// @success 204 {object} status "Success"
// @router /system/upgrade [post]
func (handler *Handler) systemUpgrade(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
payload, err := request.GetPayload[systemUpgradePayload](r)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
}

err = handler.upgradeService.Upgrade(payload.License)
environment, err := handler.guessLocalEndpoint()
if err != nil {
return httperror.InternalServerError("Failed to guess local endpoint", err)
}

err = handler.upgradeService.Upgrade(environment, payload.License)
if err != nil {
return httperror.InternalServerError("Failed to upgrade Portainer", err)
}

return response.Empty(w)
}

func (handler *Handler) guessLocalEndpoint() (*portainer.Endpoint, error) {
platform, err := platform.DetermineContainerPlatform()
if err != nil {
return nil, errors.Wrap(err, "failed to determine container platform")
}

endpointType, ok := platformToEndpointType[platform]
if !ok {
return nil, errors.New("failed to determine endpoint type")
}

endpoints, err := handler.dataStore.Endpoint().Endpoints()
if err != nil {
return nil, errors.Wrap(err, "failed to retrieve endpoints")
}

for _, endpoint := range endpoints {
if endpoint.Type == endpointType {
return &endpoint, nil
}
}

return nil, errors.New("failed to find local endpoint")
}
143 changes: 17 additions & 126 deletions api/internal/upgrade/upgrade.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
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/kubernetes/cli"
"github.com/portainer/portainer/api/platform"
"github.com/rs/zerolog/log"
)

const (
@@ -36,147 +26,48 @@ const (
)

type Service interface {
Upgrade(licenseKey string) error
Upgrade(environment *portainer.Endpoint, licenseKey string) error
}

type service struct {
composeDeployer libstack.Deployer
isUpdating bool
platform platform.ContainerPlatform
assetsPath string
composeDeployer libstack.Deployer
kubernetesClientFactory *cli.ClientFactory

isUpdating bool
platform platform.ContainerPlatform

assetsPath string
}

func NewService(
assetsPath string,
composeDeployer libstack.Deployer,
kubernetesClientFactory *cli.ClientFactory,
) (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,
assetsPath: assetsPath,
composeDeployer: composeDeployer,
kubernetesClientFactory: kubernetesClientFactory,
platform: platform,
}, nil
}

func (service *service) Upgrade(licenseKey string) error {
func (service *service) Upgrade(environment *portainer.Endpoint, 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:
case platform.PlatformKubernetes:
return service.upgradeKubernetes(environment, licenseKey, portainer.APIVersion)
}

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
}
}
121 changes: 121 additions & 0 deletions api/internal/upgrade/upgrade_docker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
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/docker/docker/client"
"github.com/pkg/errors"
libstack "github.com/portainer/docker-compose-wrapper"
"github.com/portainer/portainer/api/filesystem"
"github.com/rs/zerolog/log"
)

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.checkImageForDocker(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) checkImageForDocker(ctx context.Context, image string, skipPullImage bool) error {
cli, err := client.NewClientWithOpts(client.FromEnv)
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
}
}
Loading

0 comments on commit 4c86be7

Please sign in to comment.