feat(system): upgrade on swarm [EE-5848] (#11728)

Co-authored-by: Chaim Lev-Ari <chaim.levi-ari@portainer.io>
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
pull/12236/head
Chaim Lev-Ari 2024-09-20 19:00:38 +03:00 committed by GitHub
parent 3cb484f06a
commit 6f84317e7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 362 additions and 158 deletions

View File

@ -30,7 +30,7 @@ build-server: init-dist ## Build the server binary
./build/build_binary.sh "$(PLATFORM)" "$(ARCH)" ./build/build_binary.sh "$(PLATFORM)" "$(ARCH)"
build-image: build-all ## Build the Portainer image locally build-image: build-all ## Build the Portainer image locally
docker buildx build --load -t portainerci/portainer:$(TAG) -f build/linux/Dockerfile . docker buildx build --load -t portainerci/portainer-ce:$(TAG) -f build/linux/Dockerfile .
build-storybook: ## Build and serve the storybook files build-storybook: ## Build and serve the storybook files
yarn storybook:build yarn storybook:build

View File

@ -45,6 +45,7 @@ import (
"github.com/portainer/portainer/api/pendingactions" "github.com/portainer/portainer/api/pendingactions"
"github.com/portainer/portainer/api/pendingactions/actions" "github.com/portainer/portainer/api/pendingactions/actions"
"github.com/portainer/portainer/api/pendingactions/handlers" "github.com/portainer/portainer/api/pendingactions/handlers"
"github.com/portainer/portainer/api/platform"
"github.com/portainer/portainer/api/scheduler" "github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks/deployments" "github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/pkg/featureflags" "github.com/portainer/portainer/pkg/featureflags"
@ -532,7 +533,20 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatal().Msg("failed to fetch SSL settings from DB") log.Fatal().Msg("failed to fetch SSL settings from DB")
} }
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer, kubernetesClientFactory) platformService, err := platform.NewService(dataStore)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing platform service")
}
upgradeService, err := upgrade.NewService(
*flags.Assets,
kubernetesClientFactory,
dockerClientFactory,
composeStackManager,
dataStore,
fileService,
stackDeployer,
)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("failed initializing upgrade service") log.Fatal().Err(err).Msg("failed initializing upgrade service")
} }
@ -589,6 +603,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
UpgradeService: upgradeService, UpgradeService: upgradeService,
AdminCreationDone: adminCreationDone, AdminCreationDone: adminCreationDone,
PendingActionsService: pendingActionsService, PendingActionsService: pendingActionsService,
PlatformService: platformService,
} }
} }

View File

@ -38,7 +38,7 @@ func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
} }
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command // Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, forceRecreate bool) error { func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, options portainer.ComposeUpOptions) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint) url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to fetch environment proxy") return errors.Wrap(err, "failed to fetch environment proxy")
@ -61,7 +61,39 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
Host: url, Host: url,
ProjectName: stack.Name, ProjectName: stack.Name,
}, },
ForceRecreate: forceRecreate, ForceRecreate: options.ForceRecreate,
AbortOnContainerExit: options.AbortOnContainerExit,
})
return errors.Wrap(err, "failed to deploy a stack")
}
// Run runs a one-off command on a service. Wraps `docker-compose run` command
func (manager *ComposeStackManager) Run(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, serviceName string, options portainer.ComposeRunOptions) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil {
return errors.Wrap(err, "failed to fetch environment proxy")
}
if proxy != nil {
defer proxy.Close()
}
envFilePath, err := createEnvFile(stack)
if err != nil {
return errors.Wrap(err, "failed to create env file")
}
filePaths := stackutils.GetStackFilePaths(stack, true)
err = manager.deployer.Run(ctx, filePaths, serviceName, libstack.RunOptions{
Options: libstack.Options{
WorkingDir: stack.ProjectPath,
EnvFilePath: envFilePath,
Host: url,
ProjectName: stack.Name,
},
Remove: options.Remove,
Args: options.Args,
Detached: options.Detached,
}) })
return errors.Wrap(err, "failed to deploy a stack") return errors.Wrap(err, "failed to deploy a stack")
} }

View File

@ -60,7 +60,7 @@ func Test_UpAndDown(t *testing.T) {
ctx := context.TODO() ctx := context.TODO()
err = w.Up(ctx, stack, endpoint, false) err = w.Up(ctx, stack, endpoint, portainer.ComposeUpOptions{})
if err != nil { if err != nil {
t.Fatalf("Error calling docker-compose up: %s", err) t.Fatalf("Error calling docker-compose up: %s", err)
} }

View File

@ -161,7 +161,7 @@ func (handler *Handler) startStack(
return handler.StackDeployer.StartRemoteComposeStack(stack, endpoint, filteredRegistries) return handler.StackDeployer.StartRemoteComposeStack(stack, endpoint, filteredRegistries)
} }
return handler.ComposeStackManager.Up(context.TODO(), stack, endpoint, false) return handler.ComposeStackManager.Up(context.TODO(), stack, endpoint, portainer.ComposeUpOptions{})
case portainer.DockerSwarmStack: case portainer.DockerSwarmStack:
stack.Name = handler.SwarmStackManager.NormalizeStackName(stack.Name) stack.Name = handler.SwarmStackManager.NormalizeStackName(stack.Name)

View File

@ -7,6 +7,7 @@ import (
"github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/upgrade" "github.com/portainer/portainer/api/internal/upgrade"
"github.com/portainer/portainer/api/platform"
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -15,22 +16,25 @@ import (
// Handler is the HTTP handler used to handle status operations. // Handler is the HTTP handler used to handle status operations.
type Handler struct { type Handler struct {
*mux.Router *mux.Router
status *portainer.Status status *portainer.Status
dataStore dataservices.DataStore dataStore dataservices.DataStore
upgradeService upgrade.Service upgradeService upgrade.Service
platformService platform.Service
} }
// NewHandler creates a handler to manage status operations. // NewHandler creates a handler to manage status operations.
func NewHandler(bouncer security.BouncerService, func NewHandler(bouncer security.BouncerService,
status *portainer.Status, status *portainer.Status,
dataStore dataservices.DataStore, dataStore dataservices.DataStore,
platformService platform.Service,
upgradeService upgrade.Service) *Handler { upgradeService upgrade.Service) *Handler {
h := &Handler{ h := &Handler{
Router: mux.NewRouter(), Router: mux.NewRouter(),
dataStore: dataStore, dataStore: dataStore,
status: status, status: status,
upgradeService: upgradeService, upgradeService: upgradeService,
platformService: platformService,
} }
router := h.PathPrefix("/system").Subrouter() router := h.PathPrefix("/system").Subrouter()

View File

@ -42,12 +42,11 @@ func (handler *Handler) systemInfo(w http.ResponseWriter, r *http.Request) *http
if endpointutils.IsEdgeEndpoint(&environment) { if endpointutils.IsEdgeEndpoint(&environment) {
edgeAgents++ edgeAgents++
} }
} }
platform, err := plf.DetermineContainerPlatform() platform, err := handler.platformService.GetPlatform()
if err != nil { if err != nil {
return httperror.InternalServerError("Unable to determine container platform", err) return httperror.InternalServerError("Failed to get platform", err)
} }
return response.JSON(w, &systemInfoResponse{ return response.JSON(w, &systemInfoResponse{

View File

@ -4,8 +4,6 @@ import (
"net/http" "net/http"
"regexp" "regexp"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/platform"
httperror "github.com/portainer/portainer/pkg/libhttp/error" httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response" "github.com/portainer/portainer/pkg/libhttp/response"
@ -31,12 +29,6 @@ func (payload *systemUpgradePayload) Validate(r *http.Request) error {
return nil return nil
} }
var platformToEndpointType = map[platform.ContainerPlatform]portainer.EndpointType{
platform.PlatformDockerStandalone: portainer.DockerEnvironment,
platform.PlatformDockerSwarm: portainer.DockerEnvironment,
platform.PlatformKubernetes: portainer.KubernetesLocalEnvironment,
}
// @id systemUpgrade // @id systemUpgrade
// @summary Upgrade Portainer to BE // @summary Upgrade Portainer to BE
// @description Upgrade Portainer to BE // @description Upgrade Portainer to BE
@ -51,40 +43,20 @@ func (handler *Handler) systemUpgrade(w http.ResponseWriter, r *http.Request) *h
return httperror.BadRequest("Invalid request payload", err) return httperror.BadRequest("Invalid request payload", err)
} }
environment, err := handler.guessLocalEndpoint() environment, err := handler.platformService.GetLocalEnvironment()
if err != nil { if err != nil {
return httperror.InternalServerError("Failed to guess local endpoint", err) return httperror.InternalServerError("Failed to get local environment", err)
} }
err = handler.upgradeService.Upgrade(environment, payload.License) platform, err := handler.platformService.GetPlatform()
if err != nil {
return httperror.InternalServerError("Failed to get platform", err)
}
err = handler.upgradeService.Upgrade(platform, environment, payload.License)
if err != nil { if err != nil {
return httperror.InternalServerError("Failed to upgrade Portainer", err) return httperror.InternalServerError("Failed to upgrade Portainer", err)
} }
return response.Empty(w) 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")
}

View File

@ -39,7 +39,7 @@ func Test_getSystemVersion(t *testing.T) {
apiKeyService := apikey.NewAPIKeyService(store.APIKeyRepository(), store.User()) apiKeyService := apikey.NewAPIKeyService(store.APIKeyRepository(), store.User())
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService) requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
h := NewHandler(requestBouncer, &portainer.Status{}, store, nil) h := NewHandler(requestBouncer, &portainer.Status{}, store, nil, nil)
// generate standard and admin user tokens // generate standard and admin user tokens
jwt, _, _ := jwtService.GenerateToken(&portainer.TokenData{ID: adminUser.ID, Username: adminUser.Username, Role: adminUser.Role}) jwt, _, _ := jwtService.GenerateToken(&portainer.TokenData{ID: adminUser.ID, Username: adminUser.Username, Role: adminUser.Role})

View File

@ -65,6 +65,7 @@ import (
k8s "github.com/portainer/portainer/api/kubernetes" k8s "github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/api/kubernetes/cli" "github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/pendingactions" "github.com/portainer/portainer/api/pendingactions"
"github.com/portainer/portainer/api/platform"
"github.com/portainer/portainer/api/scheduler" "github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks/deployments" "github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/pkg/libhelm" "github.com/portainer/portainer/pkg/libhelm"
@ -111,6 +112,7 @@ type Server struct {
UpgradeService upgrade.Service UpgradeService upgrade.Service
AdminCreationDone chan struct{} AdminCreationDone chan struct{}
PendingActionsService *pendingactions.PendingActionsService PendingActionsService *pendingactions.PendingActionsService
PlatformService platform.Service
} }
// Start starts the HTTP server // Start starts the HTTP server
@ -265,6 +267,7 @@ func (server *Server) Start() error {
var systemHandler = system.NewHandler(requestBouncer, var systemHandler = system.NewHandler(requestBouncer,
server.Status, server.Status,
server.DataStore, server.DataStore,
server.PlatformService,
server.UpgradeService) server.UpgradeService)
var templatesHandler = templates.NewHandler(requestBouncer) var templatesHandler = templates.NewHandler(requestBouncer)

View File

@ -19,8 +19,11 @@ func (manager *composeStackManager) ComposeSyntaxMaxVersion() string {
func (manager *composeStackManager) NormalizeStackName(name string) string { func (manager *composeStackManager) NormalizeStackName(name string) string {
return name return name
} }
func (manager *composeStackManager) Run(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, serviceName string, options portainer.ComposeRunOptions) error {
return nil
}
func (manager *composeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, forceRecreate bool) error { func (manager *composeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, options portainer.ComposeUpOptions) error {
return nil return nil
} }

View File

@ -3,11 +3,13 @@ package upgrade
import ( import (
"fmt" "fmt"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/kubernetes/cli" "github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/platform" dockerclient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/pkg/libstack" kubecli "github.com/portainer/portainer/api/kubernetes/cli"
plf "github.com/portainer/portainer/api/platform"
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/rs/zerolog/log"
) )
const ( const (
@ -26,48 +28,54 @@ const (
) )
type Service interface { type Service interface {
Upgrade(environment *portainer.Endpoint, licenseKey string) error Upgrade(platform plf.ContainerPlatform, environment *portainer.Endpoint, licenseKey string) error
} }
type service struct { type service struct {
composeDeployer libstack.Deployer kubernetesClientFactory *kubecli.ClientFactory
kubernetesClientFactory *cli.ClientFactory dockerClientFactory *dockerclient.ClientFactory
dockerComposeStackManager portainer.ComposeStackManager
fileService portainer.FileService
isUpdating bool isUpdating bool
platform platform.ContainerPlatform
assetsPath string assetsPath string
} }
func NewService( func NewService(
assetsPath string, assetsPath string,
composeDeployer libstack.Deployer, kubernetesClientFactory *kubecli.ClientFactory,
kubernetesClientFactory *cli.ClientFactory, dockerClientFactory *dockerclient.ClientFactory,
dockerComposeStackManager portainer.ComposeStackManager,
dataStore dataservices.DataStore,
fileService portainer.FileService,
stackDeployer deployments.StackDeployer,
) (Service, error) { ) (Service, error) {
platform, err := platform.DetermineContainerPlatform()
if err != nil {
return nil, errors.Wrap(err, "failed to determine container platform")
}
return &service{ return &service{
assetsPath: assetsPath, assetsPath: assetsPath,
composeDeployer: composeDeployer, kubernetesClientFactory: kubernetesClientFactory,
kubernetesClientFactory: kubernetesClientFactory, dockerClientFactory: dockerClientFactory,
platform: platform, dockerComposeStackManager: dockerComposeStackManager,
fileService: fileService,
}, nil }, nil
} }
func (service *service) Upgrade(environment *portainer.Endpoint, licenseKey string) error { func (service *service) Upgrade(platform plf.ContainerPlatform, environment *portainer.Endpoint, licenseKey string) error {
service.isUpdating = true service.isUpdating = true
log.Debug().
Str("platform", string(platform)).
Msg("Starting upgrade process")
switch service.platform { switch platform {
case platform.PlatformDockerStandalone: case plf.PlatformDockerStandalone:
return service.upgradeDocker(licenseKey, portainer.APIVersion, "standalone") return service.upgradeDocker(environment, licenseKey, portainer.APIVersion, "standalone")
case platform.PlatformDockerSwarm: case plf.PlatformDockerSwarm:
return service.upgradeDocker(licenseKey, portainer.APIVersion, "swarm") return service.upgradeDocker(environment, licenseKey, portainer.APIVersion, "swarm")
case platform.PlatformKubernetes: case plf.PlatformKubernetes:
return service.upgradeKubernetes(environment, licenseKey, portainer.APIVersion) return service.upgradeKubernetes(environment, licenseKey, portainer.APIVersion)
} }
return fmt.Errorf("unsupported platform %s", service.platform) service.isUpdating = false
return fmt.Errorf("unsupported platform %s", platform)
} }

View File

@ -1,26 +1,25 @@
package upgrade package upgrade
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"time" "time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client" "github.com/docker/docker/api/types/image"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem" "github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/pkg/libstack"
"github.com/cbroglie/mustache" "github.com/cbroglie/mustache"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func (service *service) upgradeDocker(licenseKey, version, envType string) error { func (service *service) upgradeDocker(environment *portainer.Endpoint, licenseKey, version string, envType string) error {
ctx := context.TODO() ctx := context.TODO()
templateName := filesystem.JoinPaths(service.assetsPath, "mustache-templates", mustacheUpgradeDockerTemplateFile) templateName := filesystem.JoinPaths(service.assetsPath, "mustache-templates", mustacheUpgradeDockerTemplateFile)
portainerImagePrefix := os.Getenv(portainerImagePrefixEnvVar) portainerImagePrefix := os.Getenv(portainerImagePrefixEnvVar)
@ -30,15 +29,16 @@ func (service *service) upgradeDocker(licenseKey, version, envType string) error
image := fmt.Sprintf("%s:%s", portainerImagePrefix, version) image := fmt.Sprintf("%s:%s", portainerImagePrefix, version)
skipPullImage := os.Getenv(skipPullImageEnvVar) skipPullImageEnv := os.Getenv(skipPullImageEnvVar)
skipPullImage := skipPullImageEnv != ""
if err := service.checkImageForDocker(ctx, image, skipPullImage != ""); err != nil { if err := service.checkImageForDocker(ctx, environment, image, skipPullImage); err != nil {
return err return err
} }
composeFile, err := mustache.RenderFile(templateName, map[string]string{ composeFile, err := mustache.RenderFile(templateName, map[string]string{
"image": image, "image": image,
"skip_pull_image": skipPullImage, "skip_pull_image": skipPullImageEnv,
"updater_image": os.Getenv(updaterImageEnvVar), "updater_image": os.Getenv(updaterImageEnvVar),
"license": licenseKey, "license": licenseKey,
"envType": envType, "envType": envType,
@ -52,13 +52,10 @@ func (service *service) upgradeDocker(licenseKey, version, envType string) error
return errors.Wrap(err, "failed to render upgrade template") return errors.Wrap(err, "failed to render upgrade template")
} }
tmpDir := os.TempDir()
timeId := time.Now().Unix() timeId := time.Now().Unix()
filePath := filesystem.JoinPaths(tmpDir, fmt.Sprintf("upgrade-%d.yml", timeId)) fileName := fmt.Sprintf("upgrade-%d.yml", timeId)
r := bytes.NewReader([]byte(composeFile)) filePath, err := service.fileService.StoreStackFileFromBytes("upgrade", fileName, []byte(composeFile))
err = filesystem.CreateFile(filePath, r)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to create upgrade compose file") return errors.Wrap(err, "failed to create upgrade compose file")
} }
@ -66,42 +63,37 @@ func (service *service) upgradeDocker(licenseKey, version, envType string) error
projectName := fmt.Sprintf( projectName := fmt.Sprintf(
"portainer-upgrade-%d-%s", "portainer-upgrade-%d-%s",
timeId, timeId,
strings.ReplaceAll(version, ".", "-")) strings.ReplaceAll(version, ".", "-"),
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 tempStack := &portainer.Stack{
Name: projectName,
ProjectPath: filePath,
EntryPoint: fileName,
}
err = service.dockerComposeStackManager.Run(ctx, tempStack, environment, "updater", portainer.ComposeRunOptions{
Remove: true,
Detached: true,
})
if err != nil { if err != nil {
return errors.Wrap(err, "failed to deploy upgrade stack") return errors.Wrap(err, "failed to deploy upgrade stack")
} }
return errors.New("upgrade failed: server should have been restarted by the updater") return nil
} }
func (service *service) checkImageForDocker(ctx context.Context, image string, skipPullImage bool) error { func (service *service) checkImageForDocker(ctx context.Context, environment *portainer.Endpoint, imageName string, skipPullImage bool) error {
cli, err := client.NewClientWithOpts( cli, err := service.dockerClientFactory.CreateClient(environment, "", nil)
client.FromEnv,
client.WithAPIVersionNegotiation(),
)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to create docker client") return errors.Wrap(err, "failed to create docker client")
} }
if skipPullImage { if skipPullImage {
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("reference", image) filters.Add("reference", imageName)
images, err := cli.ImageList(ctx, types.ImageListOptions{ images, err := cli.ImageList(ctx, image.ListOptions{
Filters: filters, Filters: filters,
}) })
if err != nil { if err != nil {
@ -109,15 +101,15 @@ func (service *service) checkImageForDocker(ctx context.Context, image string, s
} }
if len(images) == 0 { if len(images) == 0 {
return errors.Errorf("image %s not found locally", image) return errors.Errorf("image %s not found locally", imageName)
} }
return nil return nil
} else { } else {
// check if available on registry // check if available on registry
_, err := cli.DistributionInspect(ctx, image, "") _, err := cli.DistributionInspect(ctx, imageName, "")
if err != nil { if err != nil {
return errors.Errorf("image %s not found on registry", image) return errors.Errorf("image %s not found on registry", imageName)
} }
return nil return nil

View File

@ -1,14 +1,7 @@
package platform package platform
import ( import (
"context"
"os" "os"
dockerclient "github.com/portainer/portainer/api/docker/client"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
) )
const ( const (
@ -20,6 +13,8 @@ const (
type ContainerPlatform string type ContainerPlatform string
const ( const (
// PlatformDocker represent the Docker platform (Unknown)
PlatformDocker = ContainerPlatform("Docker")
// PlatformDockerStandalone represent the Docker platform (Standalone) // PlatformDockerStandalone represent the Docker platform (Standalone)
PlatformDockerStandalone = ContainerPlatform("Docker Standalone") PlatformDockerStandalone = ContainerPlatform("Docker Standalone")
// PlatformDockerSwarm represent the Docker platform (Swarm) // PlatformDockerSwarm represent the Docker platform (Swarm)
@ -34,43 +29,22 @@ const (
// or KUBERNETES_SERVICE_HOST environment variable to determine if // or KUBERNETES_SERVICE_HOST environment variable to determine if
// the container is running on Podman or inside the Kubernetes platform. // the container is running on Podman or inside the Kubernetes platform.
// Defaults to Docker otherwise. // Defaults to Docker otherwise.
func DetermineContainerPlatform() (ContainerPlatform, error) { func DetermineContainerPlatform() ContainerPlatform {
podmanModeEnvVar := os.Getenv(PodmanMode) podmanModeEnvVar := os.Getenv(PodmanMode)
if podmanModeEnvVar == "1" { if podmanModeEnvVar == "1" {
return PlatformPodman, nil return PlatformPodman
} }
serviceHostKubernetesEnvVar := os.Getenv(KubernetesServiceHost) serviceHostKubernetesEnvVar := os.Getenv(KubernetesServiceHost)
if serviceHostKubernetesEnvVar != "" { if serviceHostKubernetesEnvVar != "" {
return PlatformKubernetes, nil return PlatformKubernetes
} }
if !isRunningInContainer() { if !isRunningInContainer() {
return "", nil return ""
} }
dockerCli, err := dockerclient.CreateSimpleClient() return PlatformDocker
if err != nil {
return "", errors.WithMessage(err, "failed to create docker client")
}
defer dockerCli.Close()
info, err := dockerCli.Info(context.Background())
if err != nil {
if client.IsErrConnectionFailed(err) {
log.Warn().Err(err).Msg("failed to retrieve docker info")
return "", nil
}
return "", errors.WithMessage(err, "failed to retrieve docker info")
}
if info.Swarm.NodeID == "" {
return PlatformDockerStandalone, nil
}
return PlatformDockerSwarm, nil
} }
// isRunningInContainer returns true if the process is running inside a container // isRunningInContainer returns true if the process is running inside a container

113
api/platform/service.go Normal file
View File

@ -0,0 +1,113 @@
package platform
import (
"errors"
"fmt"
"slices"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/rs/zerolog/log"
)
type Service interface {
GetLocalEnvironment() (*portainer.Endpoint, error)
GetPlatform() (ContainerPlatform, error)
}
type service struct {
dataStore dataservices.DataStore
environment *portainer.Endpoint
platform ContainerPlatform
}
func NewService(dataStore dataservices.DataStore) (Service, error) {
return &service{
dataStore: dataStore,
}, nil
}
func (service *service) GetLocalEnvironment() (*portainer.Endpoint, error) {
if service.environment == nil {
environment, platform, err := guessLocalEnvironment(service.dataStore)
if err != nil {
return nil, err
}
service.environment = environment
service.platform = platform
}
return service.environment, nil
}
func (service *service) GetPlatform() (ContainerPlatform, error) {
if service.environment == nil {
environment, platform, err := guessLocalEnvironment(service.dataStore)
if err != nil {
return "", err
}
service.environment = environment
service.platform = platform
}
return service.platform, nil
}
var platformToEndpointType = map[ContainerPlatform][]portainer.EndpointType{
PlatformDocker: {portainer.AgentOnDockerEnvironment, portainer.DockerEnvironment},
PlatformKubernetes: {portainer.KubernetesLocalEnvironment},
}
func guessLocalEnvironment(dataStore dataservices.DataStore) (*portainer.Endpoint, ContainerPlatform, error) {
platform := DetermineContainerPlatform()
if !slices.Contains([]ContainerPlatform{PlatformDocker, PlatformKubernetes}, platform) {
log.Debug().
Str("platform", string(platform)).
Msg("environment not supported for upgrade")
return nil, "", nil
}
endpoints, err := dataStore.Endpoint().Endpoints()
if err != nil {
return nil, "", fmt.Errorf("failed to retrieve endpoints: %w", err)
}
endpointTypes, ok := platformToEndpointType[platform]
if !ok {
return nil, "", errors.New("failed to determine endpoint type")
}
for _, endpoint := range endpoints {
if slices.Contains(endpointTypes, endpoint.Type) {
if platform != PlatformDocker {
return &endpoint, platform, nil
}
dockerPlatform := checkDockerEnvTypeForUpgrade(&endpoint)
if dockerPlatform != "" {
return &endpoint, dockerPlatform, nil
}
}
}
return nil, "", errors.New("failed to find local endpoint")
}
func checkDockerEnvTypeForUpgrade(environment *portainer.Endpoint) ContainerPlatform {
if endpointutils.IsLocalEndpoint(environment) { // standalone
return PlatformDockerStandalone
}
if strings.HasPrefix(environment.URL, "tcp://tasks.") { // swarm
return PlatformDockerSwarm
}
return ""
}

View File

@ -1357,11 +1357,31 @@ type (
ValidateFlags(flags *CLIFlags) error ValidateFlags(flags *CLIFlags) error
} }
ComposeUpOptions struct {
// ForceRecreate forces to recreate containers
ForceRecreate bool
// AbortOnContainerExit will stop the deployment if a container exits.
// This is useful when running a onetime task.
//
// When this is set, docker compose will output its logs to stdout
AbortOnContainerExit bool
}
ComposeRunOptions struct {
// Remove will remove the container after it has stopped
Remove bool
// Args are the arguments to pass to the container
Args []string
// Detached will run the container in the background
Detached bool
}
// ComposeStackManager represents a service to manage Compose stacks // ComposeStackManager represents a service to manage Compose stacks
ComposeStackManager interface { ComposeStackManager interface {
ComposeSyntaxMaxVersion() string ComposeSyntaxMaxVersion() string
NormalizeStackName(name string) string NormalizeStackName(name string) string
Up(ctx context.Context, stack *Stack, endpoint *Endpoint, forceRecreate bool) error Run(ctx context.Context, stack *Stack, endpoint *Endpoint, serviceName string, options ComposeRunOptions) error
Up(ctx context.Context, stack *Stack, endpoint *Endpoint, options ComposeUpOptions) error
Down(ctx context.Context, stack *Stack, endpoint *Endpoint) error Down(ctx context.Context, stack *Stack, endpoint *Endpoint) error
Pull(ctx context.Context, stack *Stack, endpoint *Endpoint) error Pull(ctx context.Context, stack *Stack, endpoint *Endpoint) error
} }

View File

@ -69,7 +69,9 @@ func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *por
} }
} }
err := d.composeStackManager.Up(context.TODO(), stack, endpoint, forceRecreate) err := d.composeStackManager.Up(context.TODO(), stack, endpoint, portainer.ComposeUpOptions{
ForceRecreate: forceRecreate,
})
if err != nil { if err != nil {
d.composeStackManager.Down(context.TODO(), stack, endpoint) d.composeStackManager.Down(context.TODO(), stack, endpoint)
} }

View File

@ -2,7 +2,8 @@ package deployments
import ( import (
"fmt" "fmt"
"log"
"github.com/rs/zerolog/log"
"github.com/pkg/errors" "github.com/pkg/errors"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
@ -24,6 +25,7 @@ type ComposeStackDeploymentConfig struct {
} }
func CreateComposeStackDeploymentConfig(securityContext *security.RestrictedRequestContext, stack *portainer.Stack, endpoint *portainer.Endpoint, dataStore dataservices.DataStore, fileService portainer.FileService, deployer StackDeployer, forcePullImage, forceCreate bool) (*ComposeStackDeploymentConfig, error) { func CreateComposeStackDeploymentConfig(securityContext *security.RestrictedRequestContext, stack *portainer.Stack, endpoint *portainer.Endpoint, dataStore dataservices.DataStore, fileService portainer.FileService, deployer StackDeployer, forcePullImage, forceCreate bool) (*ComposeStackDeploymentConfig, error) {
user, err := dataStore.User().Read(securityContext.UserID) user, err := dataStore.User().Read(securityContext.UserID)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to load user information from the database: %w", err) return nil, fmt.Errorf("unable to load user information from the database: %w", err)
@ -60,7 +62,7 @@ func (config *ComposeStackDeploymentConfig) GetUsername() string {
func (config *ComposeStackDeploymentConfig) Deploy() error { func (config *ComposeStackDeploymentConfig) Deploy() error {
if config.FileService == nil || config.StackDeployer == nil { if config.FileService == nil || config.StackDeployer == nil {
log.Println("[deployment, compose] file service or stack deployer is not initialised") log.Debug().Msg("file service or stack deployer is not initialized")
return errors.New("file service or stack deployer cannot be nil") return errors.New("file service or stack deployer cannot be nil")
} }

View File

@ -59,7 +59,7 @@ function UpgradeBEBanner() {
if ( if (
!enabledPlatforms.includes(systemInfo.platform) && !enabledPlatforms.includes(systemInfo.platform) &&
process.env.NODE_ENV !== 'development' process.env.FORCE_SHOW_UPGRADE_BANNER !== ''
) { ) {
return null; return null;
} }

View File

@ -158,11 +158,12 @@ func (wrapper *PluginWrapper) command(command composeCommand, options libstack.O
cmd.Env = append(cmd.Env, options.Env...) cmd.Env = append(cmd.Env, options.Env...)
executedCommand := cmd.String()
log.Debug(). log.Debug().
Str("command", program). Str("command", executedCommand).
Strs("args", args).
Interface("env", cmd.Env). Interface("env", cmd.Env).
Msg("run command") Msg("execute command")
cmd.Stderr = &stderr cmd.Stderr = &stderr

View File

@ -0,0 +1,53 @@
package composeplugin
import (
"context"
"github.com/portainer/portainer/pkg/libstack"
"github.com/rs/zerolog/log"
)
func (wrapper *PluginWrapper) Run(ctx context.Context, filePaths []string, serviceName string, options libstack.RunOptions) error {
output, err := wrapper.command(newRunCommand(filePaths, serviceName, runOptions{
remove: options.Remove,
args: options.Args,
detached: options.Detached,
}), options.Options)
if len(output) != 0 {
if err != nil {
return err
}
log.Info().Msg("Stack run successful")
log.Debug().
Str("output", string(output)).
Msg("docker compose")
}
return err
}
type runOptions struct {
remove bool
args []string
detached bool
}
func newRunCommand(filePaths []string, serviceName string, options runOptions) composeCommand {
args := []string{"run"}
if options.remove {
args = append(args, "--rm")
}
if options.detached {
args = append(args, "-d")
}
args = append(args, serviceName)
args = append(args, options.args...)
return newCommand(args, filePaths)
}

View File

@ -12,6 +12,7 @@ type Deployer interface {
// if projectName is supplied filePaths will be ignored // if projectName is supplied filePaths will be ignored
Remove(ctx context.Context, projectName string, filePaths []string, options Options) error Remove(ctx context.Context, projectName string, filePaths []string, options Options) error
Pull(ctx context.Context, filePaths []string, options Options) error Pull(ctx context.Context, filePaths []string, options Options) error
Run(ctx context.Context, filePaths []string, serviceName string, options RunOptions) error
Validate(ctx context.Context, filePaths []string, options Options) error Validate(ctx context.Context, filePaths []string, options Options) error
WaitForStatus(ctx context.Context, name string, status Status) <-chan WaitResult WaitForStatus(ctx context.Context, name string, status Status) <-chan WaitResult
Config(ctx context.Context, filePaths []string, options Options) ([]byte, error) Config(ctx context.Context, filePaths []string, options Options) ([]byte, error)
@ -61,3 +62,13 @@ type DeployOptions struct {
// When this is set, docker compose will output its logs to stdout // When this is set, docker compose will output its logs to stdout
AbortOnContainerExit bool `` AbortOnContainerExit bool ``
} }
type RunOptions struct {
Options
// Automatically remove the container when it exits
Remove bool
// A list of arguments to pass to the container
Args []string
// Run the container in detached mode
Detached bool
}