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-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
yarn storybook:build

View File

@ -45,6 +45,7 @@ import (
"github.com/portainer/portainer/api/pendingactions"
"github.com/portainer/portainer/api/pendingactions/actions"
"github.com/portainer/portainer/api/pendingactions/handlers"
"github.com/portainer/portainer/api/platform"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks/deployments"
"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")
}
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 {
log.Fatal().Err(err).Msg("failed initializing upgrade service")
}
@ -589,6 +603,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
UpgradeService: upgradeService,
AdminCreationDone: adminCreationDone,
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
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)
if err != nil {
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,
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")
}

View File

@ -60,7 +60,7 @@ func Test_UpAndDown(t *testing.T) {
ctx := context.TODO()
err = w.Up(ctx, stack, endpoint, false)
err = w.Up(ctx, stack, endpoint, portainer.ComposeUpOptions{})
if err != nil {
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.ComposeStackManager.Up(context.TODO(), stack, endpoint, false)
return handler.ComposeStackManager.Up(context.TODO(), stack, endpoint, portainer.ComposeUpOptions{})
case portainer.DockerSwarmStack:
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/http/security"
"github.com/portainer/portainer/api/internal/upgrade"
"github.com/portainer/portainer/api/platform"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/gorilla/mux"
@ -15,22 +16,25 @@ import (
// Handler is the HTTP handler used to handle status operations.
type Handler struct {
*mux.Router
status *portainer.Status
dataStore dataservices.DataStore
upgradeService upgrade.Service
status *portainer.Status
dataStore dataservices.DataStore
upgradeService upgrade.Service
platformService platform.Service
}
// NewHandler creates a handler to manage status operations.
func NewHandler(bouncer security.BouncerService,
status *portainer.Status,
dataStore dataservices.DataStore,
platformService platform.Service,
upgradeService upgrade.Service) *Handler {
h := &Handler{
Router: mux.NewRouter(),
dataStore: dataStore,
status: status,
upgradeService: upgradeService,
Router: mux.NewRouter(),
dataStore: dataStore,
status: status,
upgradeService: upgradeService,
platformService: platformService,
}
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) {
edgeAgents++
}
}
platform, err := plf.DetermineContainerPlatform()
platform, err := handler.platformService.GetPlatform()
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{

View File

@ -4,8 +4,6 @@ import (
"net/http"
"regexp"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/platform"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
@ -31,12 +29,6 @@ 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
@ -51,40 +43,20 @@ func (handler *Handler) systemUpgrade(w http.ResponseWriter, r *http.Request) *h
return httperror.BadRequest("Invalid request payload", err)
}
environment, err := handler.guessLocalEndpoint()
environment, err := handler.platformService.GetLocalEnvironment()
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 {
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")
}

View File

@ -39,7 +39,7 @@ func Test_getSystemVersion(t *testing.T) {
apiKeyService := apikey.NewAPIKeyService(store.APIKeyRepository(), store.User())
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
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"
"github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/pendingactions"
"github.com/portainer/portainer/api/platform"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/pkg/libhelm"
@ -111,6 +112,7 @@ type Server struct {
UpgradeService upgrade.Service
AdminCreationDone chan struct{}
PendingActionsService *pendingactions.PendingActionsService
PlatformService platform.Service
}
// Start starts the HTTP server
@ -265,6 +267,7 @@ func (server *Server) Start() error {
var systemHandler = system.NewHandler(requestBouncer,
server.Status,
server.DataStore,
server.PlatformService,
server.UpgradeService)
var templatesHandler = templates.NewHandler(requestBouncer)

View File

@ -19,8 +19,11 @@ func (manager *composeStackManager) ComposeSyntaxMaxVersion() string {
func (manager *composeStackManager) NormalizeStackName(name string) string {
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
}

View File

@ -3,11 +3,13 @@ package upgrade
import (
"fmt"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/platform"
"github.com/portainer/portainer/pkg/libstack"
"github.com/portainer/portainer/api/dataservices"
dockerclient "github.com/portainer/portainer/api/docker/client"
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 (
@ -26,48 +28,54 @@ const (
)
type Service interface {
Upgrade(environment *portainer.Endpoint, licenseKey string) error
Upgrade(platform plf.ContainerPlatform, environment *portainer.Endpoint, licenseKey string) error
}
type service struct {
composeDeployer libstack.Deployer
kubernetesClientFactory *cli.ClientFactory
kubernetesClientFactory *kubecli.ClientFactory
dockerClientFactory *dockerclient.ClientFactory
dockerComposeStackManager portainer.ComposeStackManager
fileService portainer.FileService
isUpdating bool
platform platform.ContainerPlatform
assetsPath string
}
func NewService(
assetsPath string,
composeDeployer libstack.Deployer,
kubernetesClientFactory *cli.ClientFactory,
kubernetesClientFactory *kubecli.ClientFactory,
dockerClientFactory *dockerclient.ClientFactory,
dockerComposeStackManager portainer.ComposeStackManager,
dataStore dataservices.DataStore,
fileService portainer.FileService,
stackDeployer deployments.StackDeployer,
) (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,
kubernetesClientFactory: kubernetesClientFactory,
platform: platform,
assetsPath: assetsPath,
kubernetesClientFactory: kubernetesClientFactory,
dockerClientFactory: dockerClientFactory,
dockerComposeStackManager: dockerComposeStackManager,
fileService: fileService,
}, 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
log.Debug().
Str("platform", string(platform)).
Msg("Starting upgrade process")
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:
switch platform {
case plf.PlatformDockerStandalone:
return service.upgradeDocker(environment, licenseKey, portainer.APIVersion, "standalone")
case plf.PlatformDockerSwarm:
return service.upgradeDocker(environment, licenseKey, portainer.APIVersion, "swarm")
case plf.PlatformKubernetes:
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
import (
"bytes"
"context"
"fmt"
"os"
"strings"
"time"
"github.com/docker/docker/api/types"
"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/pkg/libstack"
"github.com/cbroglie/mustache"
"github.com/pkg/errors"
"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()
templateName := filesystem.JoinPaths(service.assetsPath, "mustache-templates", mustacheUpgradeDockerTemplateFile)
portainerImagePrefix := os.Getenv(portainerImagePrefixEnvVar)
@ -30,15 +29,16 @@ func (service *service) upgradeDocker(licenseKey, version, envType string) error
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
}
composeFile, err := mustache.RenderFile(templateName, map[string]string{
"image": image,
"skip_pull_image": skipPullImage,
"skip_pull_image": skipPullImageEnv,
"updater_image": os.Getenv(updaterImageEnvVar),
"license": licenseKey,
"envType": envType,
@ -52,13 +52,10 @@ func (service *service) upgradeDocker(licenseKey, version, envType string) error
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))
fileName := fmt.Sprintf("upgrade-%d.yml", timeId)
r := bytes.NewReader([]byte(composeFile))
err = filesystem.CreateFile(filePath, r)
filePath, err := service.fileService.StoreStackFileFromBytes("upgrade", fileName, []byte(composeFile))
if err != nil {
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(
"portainer-upgrade-%d-%s",
timeId,
strings.ReplaceAll(version, ".", "-"))
err = service.composeDeployer.Deploy(
ctx,
[]string{filePath},
libstack.DeployOptions{
ForceRecreate: true,
AbortOnContainerExit: true,
Options: libstack.Options{
ProjectName: projectName,
},
},
strings.ReplaceAll(version, ".", "-"),
)
// 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 {
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 {
cli, err := client.NewClientWithOpts(
client.FromEnv,
client.WithAPIVersionNegotiation(),
)
func (service *service) checkImageForDocker(ctx context.Context, environment *portainer.Endpoint, imageName string, skipPullImage bool) error {
cli, err := service.dockerClientFactory.CreateClient(environment, "", nil)
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.Add("reference", imageName)
images, err := cli.ImageList(ctx, image.ListOptions{
Filters: filters,
})
if err != nil {
@ -109,15 +101,15 @@ func (service *service) checkImageForDocker(ctx context.Context, image string, s
}
if len(images) == 0 {
return errors.Errorf("image %s not found locally", image)
return errors.Errorf("image %s not found locally", imageName)
}
return nil
} else {
// check if available on registry
_, err := cli.DistributionInspect(ctx, image, "")
_, err := cli.DistributionInspect(ctx, imageName, "")
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

View File

@ -1,14 +1,7 @@
package platform
import (
"context"
"os"
dockerclient "github.com/portainer/portainer/api/docker/client"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
const (
@ -20,6 +13,8 @@ const (
type ContainerPlatform string
const (
// PlatformDocker represent the Docker platform (Unknown)
PlatformDocker = ContainerPlatform("Docker")
// PlatformDockerStandalone represent the Docker platform (Standalone)
PlatformDockerStandalone = ContainerPlatform("Docker Standalone")
// PlatformDockerSwarm represent the Docker platform (Swarm)
@ -34,43 +29,22 @@ const (
// or KUBERNETES_SERVICE_HOST environment variable to determine if
// the container is running on Podman or inside the Kubernetes platform.
// Defaults to Docker otherwise.
func DetermineContainerPlatform() (ContainerPlatform, error) {
func DetermineContainerPlatform() ContainerPlatform {
podmanModeEnvVar := os.Getenv(PodmanMode)
if podmanModeEnvVar == "1" {
return PlatformPodman, nil
return PlatformPodman
}
serviceHostKubernetesEnvVar := os.Getenv(KubernetesServiceHost)
if serviceHostKubernetesEnvVar != "" {
return PlatformKubernetes, nil
return PlatformKubernetes
}
if !isRunningInContainer() {
return "", nil
return ""
}
dockerCli, err := dockerclient.CreateSimpleClient()
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
return PlatformDocker
}
// 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
}
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 interface {
ComposeSyntaxMaxVersion() 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
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 {
d.composeStackManager.Down(context.TODO(), stack, endpoint)
}

View File

@ -2,7 +2,8 @@ package deployments
import (
"fmt"
"log"
"github.com/rs/zerolog/log"
"github.com/pkg/errors"
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) {
user, err := dataStore.User().Read(securityContext.UserID)
if err != nil {
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 {
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")
}

View File

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

View File

@ -158,11 +158,12 @@ func (wrapper *PluginWrapper) command(command composeCommand, options libstack.O
cmd.Env = append(cmd.Env, options.Env...)
executedCommand := cmd.String()
log.Debug().
Str("command", program).
Strs("args", args).
Str("command", executedCommand).
Interface("env", cmd.Env).
Msg("run command")
Msg("execute command")
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
Remove(ctx context.Context, projectName string, 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
WaitForStatus(ctx context.Context, name string, status Status) <-chan WaitResult
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
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
}