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
parent
3cb484f06a
commit
6f84317e7a
2
Makefile
2
Makefile
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ function UpgradeBEBanner() {
|
|||
|
||||
if (
|
||||
!enabledPlatforms.includes(systemInfo.platform) &&
|
||||
process.env.NODE_ENV !== 'development'
|
||||
process.env.FORCE_SHOW_UPGRADE_BANNER !== ''
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue