1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-24 15:59:41 +02:00

refactor(stack): stack build process backend only [EE-4342] (#7750)

This commit is contained in:
Oscar Zhou 2022-10-05 22:33:59 +13:00 committed by GitHub
parent 83a1ce9d2a
commit e9de484c3e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 2270 additions and 942 deletions

View file

@ -0,0 +1,35 @@
package deployments
import (
"time"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/scheduler"
"github.com/rs/zerolog/log"
)
func StartAutoupdate(stackID portainer.StackID, interval string, scheduler *scheduler.Scheduler, stackDeployer StackDeployer, datastore dataservices.DataStore, gitService portainer.GitService) (jobID string, e *httperror.HandlerError) {
d, err := time.ParseDuration(interval)
if err != nil {
return "", httperror.BadRequest("Unable to parse stack's auto update interval", err)
}
jobID = scheduler.StartJobEvery(d, func() error {
return RedeployWhenChanged(stackID, stackDeployer, datastore, gitService)
})
return jobID, nil
}
func StopAutoupdate(stackID portainer.StackID, jobID string, scheduler *scheduler.Scheduler) {
if jobID == "" {
return
}
if err := scheduler.StopJob(jobID); err != nil {
log.Warn().Int("stack_id", int(stackID)).Msg("could not stop the job for the stack")
}
}

View file

@ -1,4 +1,4 @@
package stacks
package deployments
import (
"fmt"

View file

@ -1,4 +1,4 @@
package stacks
package deployments
import (
"errors"

View file

@ -1,14 +1,12 @@
package stacks
package deployments
import (
"context"
"os"
"sync"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/stackutils"
k "github.com/portainer/portainer/api/kubernetes"
)
@ -83,13 +81,12 @@ func (d *stackDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *
appLabels.Kind = "git"
}
manifestFilePaths, tempDir, err := stackutils.CreateTempK8SDeploymentFiles(stack, d.kubernetesDeployer, appLabels)
k8sDeploymentConfig, err := CreateKubernetesStackDeploymentConfig(stack, d.kubernetesDeployer, appLabels, user, endpoint)
if err != nil {
return errors.Wrap(err, "failed to create temp kub deployment files")
}
defer os.RemoveAll(tempDir)
_, err = d.kubernetesDeployer.Deploy(user.ID, endpoint, manifestFilePaths, stack.Namespace)
err = k8sDeploymentConfig.Deploy()
if err != nil {
return errors.Wrap(err, "failed to deploy kubernetes application")
}

View file

@ -0,0 +1,93 @@
package deployments
import (
"fmt"
"log"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/stacks/stackutils"
)
type ComposeStackDeploymentConfig struct {
stack *portainer.Stack
endpoint *portainer.Endpoint
registries []portainer.Registry
isAdmin bool
user *portainer.User
forcePullImage bool
ForceCreate bool
FileService portainer.FileService
StackDeployer StackDeployer
}
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().User(securityContext.UserID)
if err != nil {
return nil, fmt.Errorf("unable to load user information from the database: %w", err)
}
registries, err := dataStore.Registry().Registries()
if err != nil {
return nil, fmt.Errorf("unable to retrieve registries from the database: %w", err)
}
filteredRegistries := security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
config := &ComposeStackDeploymentConfig{
stack: stack,
endpoint: endpoint,
registries: filteredRegistries,
isAdmin: securityContext.IsAdmin,
user: user,
forcePullImage: forcePullImage,
ForceCreate: forceCreate,
FileService: fileService,
StackDeployer: deployer,
}
return config, nil
}
func (config *ComposeStackDeploymentConfig) GetUsername() string {
if config.user != nil {
return config.user.Username
}
return ""
}
func (config *ComposeStackDeploymentConfig) Deploy() error {
if config.FileService == nil || config.StackDeployer == nil {
log.Println("[deployment, compose] file service or stack deployer is not initialised")
return errors.New("file service or stack deployer cannot be nil")
}
isAdminOrEndpointAdmin, err := stackutils.UserIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil {
return errors.Wrap(err, "failed to validate user admin privileges")
}
securitySettings := &config.endpoint.SecuritySettings
if (!securitySettings.AllowBindMountsForRegularUsers ||
!securitySettings.AllowPrivilegedModeForRegularUsers ||
!securitySettings.AllowHostNamespaceForRegularUsers ||
!securitySettings.AllowDeviceMappingForRegularUsers ||
!securitySettings.AllowSysctlSettingForRegularUsers ||
!securitySettings.AllowContainerCapabilitiesForRegularUsers) &&
!isAdminOrEndpointAdmin {
err = stackutils.ValidateStackFiles(config.stack, securitySettings, config.FileService)
if err != nil {
return err
}
}
return config.StackDeployer.DeployComposeStack(config.stack, config.endpoint, config.registries, config.forcePullImage, config.ForceCreate)
}
func (config *ComposeStackDeploymentConfig) GetResponse() string {
return ""
}

View file

@ -0,0 +1,7 @@
package deployments
type StackDeploymentConfiger interface {
GetUsername() string
Deploy() error
GetResponse() string
}

View file

@ -0,0 +1,89 @@
package deployments
import (
"fmt"
"io/ioutil"
"os"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
k "github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/api/stacks/stackutils"
)
type KubernetesStackDeploymentConfig struct {
stack *portainer.Stack
kuberneteDeployer portainer.KubernetesDeployer
appLabels k.KubeAppLabels
user *portainer.User
endpoint *portainer.Endpoint
output string
}
func CreateKubernetesStackDeploymentConfig(stack *portainer.Stack, kubeDeployer portainer.KubernetesDeployer, appLabels k.KubeAppLabels, user *portainer.User, endpoint *portainer.Endpoint) (*KubernetesStackDeploymentConfig, error) {
return &KubernetesStackDeploymentConfig{
stack: stack,
kuberneteDeployer: kubeDeployer,
appLabels: appLabels,
user: user,
endpoint: endpoint,
}, nil
}
func (config *KubernetesStackDeploymentConfig) GetUsername() string {
return config.user.Username
}
func (config *KubernetesStackDeploymentConfig) Deploy() error {
fileNames := stackutils.GetStackFilePaths(config.stack, false)
manifestFilePaths := make([]string, len(fileNames))
tmpDir, err := ioutil.TempDir("", "kub_deployment")
if err != nil {
return errors.Wrap(err, "failed to create temp kub deployment directory")
}
defer os.RemoveAll(tmpDir)
for _, fileName := range fileNames {
manifestFilePath := filesystem.JoinPaths(tmpDir, fileName)
manifestContent, err := ioutil.ReadFile(filesystem.JoinPaths(config.stack.ProjectPath, fileName))
if err != nil {
return errors.Wrap(err, "failed to read manifest file")
}
if config.stack.IsComposeFormat {
manifestContent, err = config.kuberneteDeployer.ConvertCompose(manifestContent)
if err != nil {
return errors.Wrap(err, "failed to convert docker compose file to a kube manifest")
}
}
manifestContent, err = k.AddAppLabels(manifestContent, config.appLabels.ToMap())
if err != nil {
return errors.Wrap(err, "failed to add application labels")
}
err = filesystem.WriteToFile(manifestFilePath, []byte(manifestContent))
if err != nil {
return errors.Wrap(err, "failed to create temp manifest file")
}
manifestFilePaths = append(manifestFilePaths, manifestFilePath)
}
output, err := config.kuberneteDeployer.Deploy(config.user.ID, config.endpoint, manifestFilePaths, config.stack.Namespace)
if err != nil {
return fmt.Errorf("failed to deploy kubernete stack: %w", err)
}
config.output = output
return nil
}
func (config *KubernetesStackDeploymentConfig) GetResponse() string {
return config.output
}

View file

@ -0,0 +1,86 @@
package deployments
import (
"fmt"
"log"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/stacks/stackutils"
)
type SwarmStackDeploymentConfig struct {
stack *portainer.Stack
endpoint *portainer.Endpoint
registries []portainer.Registry
prune bool
isAdmin bool
user *portainer.User
pullImage bool
FileService portainer.FileService
StackDeployer StackDeployer
}
func CreateSwarmStackDeploymentConfig(securityContext *security.RestrictedRequestContext, stack *portainer.Stack, endpoint *portainer.Endpoint, dataStore dataservices.DataStore, fileService portainer.FileService, deployer StackDeployer, prune bool, pullImage bool) (*SwarmStackDeploymentConfig, error) {
user, err := dataStore.User().User(securityContext.UserID)
if err != nil {
return nil, fmt.Errorf("unable to load user information from the database: %w", err)
}
registries, err := dataStore.Registry().Registries()
if err != nil {
return nil, fmt.Errorf("unable to retrieve registries from the database: %w", err)
}
filteredRegistries := security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
config := &SwarmStackDeploymentConfig{
stack: stack,
endpoint: endpoint,
registries: filteredRegistries,
prune: prune,
isAdmin: securityContext.IsAdmin,
user: user,
pullImage: pullImage,
FileService: fileService,
StackDeployer: deployer,
}
return config, nil
}
func (config *SwarmStackDeploymentConfig) GetUsername() string {
if config.user != nil {
return config.user.Username
}
return ""
}
func (config *SwarmStackDeploymentConfig) Deploy() error {
if config.FileService == nil || config.StackDeployer == nil {
log.Println("[deployment, swarm] file service or stack deployer is not initialised")
return errors.New("file service or stack deployer cannot be nil")
}
isAdminOrEndpointAdmin, err := stackutils.UserIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil {
return errors.Wrap(err, "failed to validate user admin privileges")
}
settings := &config.endpoint.SecuritySettings
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
err = stackutils.ValidateStackFiles(config.stack, settings, config.FileService)
if err != nil {
return err
}
}
return config.StackDeployer.DeploySwarmStack(config.stack, config.endpoint, config.registries, config.prune, config.pullImage)
}
func (config *SwarmStackDeploymentConfig) GetResponse() string {
return ""
}

View file

@ -1,4 +1,4 @@
package stacks
package deployments
import (
"time"

View file

@ -0,0 +1,81 @@
package stackbuilders
import (
"strconv"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/stacks/deployments"
)
type ComposeStackFileContentBuilder struct {
FileContentMethodStackBuilder
SecurityContext *security.RestrictedRequestContext
}
// CreateComposeStackFileContentBuilder creates a builder for the compose stack (docker standalone) that will be deployed by file content method
func CreateComposeStackFileContentBuilder(securityContext *security.RestrictedRequestContext,
dataStore dataservices.DataStore,
fileService portainer.FileService,
stackDeployer deployments.StackDeployer) *ComposeStackFileContentBuilder {
return &ComposeStackFileContentBuilder{
FileContentMethodStackBuilder: FileContentMethodStackBuilder{
StackBuilder: CreateStackBuilder(dataStore, fileService, stackDeployer),
},
SecurityContext: securityContext,
}
}
func (b *ComposeStackFileContentBuilder) SetGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint) FileContentMethodStackBuildProcess {
b.FileContentMethodStackBuilder.SetGeneralInfo(payload, endpoint)
return b
}
func (b *ComposeStackFileContentBuilder) SetUniqueInfo(payload *StackPayload) FileContentMethodStackBuildProcess {
if b.hasError() {
return b
}
b.stack.Name = payload.Name
b.stack.Type = portainer.DockerComposeStack
b.stack.EntryPoint = filesystem.ComposeFileDefaultName
b.stack.Env = payload.Env
b.stack.FromAppTemplate = payload.FromAppTemplate
return b
}
func (b *ComposeStackFileContentBuilder) SetFileContent(payload *StackPayload) FileContentMethodStackBuildProcess {
if b.hasError() {
return b
}
stackFolder := strconv.Itoa(int(b.stack.ID))
projectPath, err := b.fileService.StoreStackFileFromBytes(stackFolder, b.stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
b.err = httperror.InternalServerError("Unable to persist Compose file on disk", err)
return b
}
b.stack.ProjectPath = projectPath
return b
}
func (b *ComposeStackFileContentBuilder) Deploy(payload *StackPayload, endpoint *portainer.Endpoint) FileContentMethodStackBuildProcess {
if b.hasError() {
return b
}
composeDeploymentConfig, err := deployments.CreateComposeStackDeploymentConfig(b.SecurityContext, b.stack, endpoint, b.dataStore, b.fileService, b.stackDeployer, false, false)
if err != nil {
b.err = httperror.InternalServerError(err.Error(), err)
return b
}
b.deploymentConfiger = composeDeploymentConfig
b.stack.CreatedBy = b.deploymentConfiger.GetUsername()
return b.FileContentMethodStackBuilder.Deploy(payload, endpoint)
}

View file

@ -0,0 +1,72 @@
package stackbuilders
import (
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/stacks/deployments"
)
type ComposeStackFileUploadBuilder struct {
FileUploadMethodStackBuilder
SecurityContext *security.RestrictedRequestContext
}
// CreateComposeStackFileUploadBuilder creates a builder for the compose stack (docker standalone) that will be deployed by file upload method
func CreateComposeStackFileUploadBuilder(securityContext *security.RestrictedRequestContext,
dataStore dataservices.DataStore,
fileService portainer.FileService,
stackDeployer deployments.StackDeployer) *ComposeStackFileUploadBuilder {
return &ComposeStackFileUploadBuilder{
FileUploadMethodStackBuilder: FileUploadMethodStackBuilder{
StackBuilder: CreateStackBuilder(dataStore, fileService, stackDeployer),
},
SecurityContext: securityContext,
}
}
func (b *ComposeStackFileUploadBuilder) SetGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint) FileUploadMethodStackBuildProcess {
b.FileUploadMethodStackBuilder.SetGeneralInfo(payload, endpoint)
return b
}
func (b *ComposeStackFileUploadBuilder) SetUniqueInfo(payload *StackPayload) FileUploadMethodStackBuildProcess {
if b.hasError() {
return b
}
b.stack.Name = payload.Name
b.stack.Type = portainer.DockerComposeStack
b.stack.EntryPoint = filesystem.ComposeFileDefaultName
b.stack.Env = payload.Env
return b
}
func (b *ComposeStackFileUploadBuilder) SetUploadedFile(payload *StackPayload) FileUploadMethodStackBuildProcess {
if b.hasError() {
return b
}
b.FileUploadMethodStackBuilder.SetUploadedFile(payload)
return b
}
func (b *ComposeStackFileUploadBuilder) Deploy(payload *StackPayload, endpoint *portainer.Endpoint) FileUploadMethodStackBuildProcess {
if b.hasError() {
return b
}
composeDeploymentConfig, err := deployments.CreateComposeStackDeploymentConfig(b.SecurityContext, b.stack, endpoint, b.dataStore, b.fileService, b.stackDeployer, false, false)
if err != nil {
b.err = httperror.InternalServerError(err.Error(), err)
return b
}
b.deploymentConfiger = composeDeploymentConfig
b.stack.CreatedBy = b.deploymentConfiger.GetUsername()
return b.FileUploadMethodStackBuilder.Deploy(payload, endpoint)
}

View file

@ -0,0 +1,77 @@
package stackbuilders
import (
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks/deployments"
)
type ComposeStackGitBuilder struct {
GitMethodStackBuilder
SecurityContext *security.RestrictedRequestContext
}
// CreateComposeStackGitBuilder creates a builder for the compose stack (docker standalone) that will be deployed by git repository method
func CreateComposeStackGitBuilder(securityContext *security.RestrictedRequestContext,
dataStore dataservices.DataStore,
fileService portainer.FileService,
gitService portainer.GitService,
scheduler *scheduler.Scheduler,
stackDeployer deployments.StackDeployer) *ComposeStackGitBuilder {
return &ComposeStackGitBuilder{
GitMethodStackBuilder: GitMethodStackBuilder{
StackBuilder: CreateStackBuilder(dataStore, fileService, stackDeployer),
gitService: gitService,
scheduler: scheduler,
},
SecurityContext: securityContext,
}
}
func (b *ComposeStackGitBuilder) SetGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint) GitMethodStackBuildProcess {
b.GitMethodStackBuilder.SetGeneralInfo(payload, endpoint)
return b
}
func (b *ComposeStackGitBuilder) SetUniqueInfo(payload *StackPayload) GitMethodStackBuildProcess {
if b.hasError() {
return b
}
b.stack.Name = payload.Name
b.stack.Type = portainer.DockerComposeStack
b.stack.EntryPoint = payload.ComposeFile
b.stack.FromAppTemplate = payload.FromAppTemplate
b.stack.Env = payload.Env
return b
}
func (b *ComposeStackGitBuilder) SetGitRepository(payload *StackPayload) GitMethodStackBuildProcess {
b.GitMethodStackBuilder.SetGitRepository(payload)
return b
}
func (b *ComposeStackGitBuilder) Deploy(payload *StackPayload, endpoint *portainer.Endpoint) GitMethodStackBuildProcess {
if b.hasError() {
return b
}
composeDeploymentConfig, err := deployments.CreateComposeStackDeploymentConfig(b.SecurityContext, b.stack, endpoint, b.dataStore, b.fileService, b.stackDeployer, false, false)
if err != nil {
b.err = httperror.InternalServerError(err.Error(), err)
return b
}
b.deploymentConfiger = composeDeploymentConfig
b.stack.CreatedBy = b.deploymentConfiger.GetUsername()
return b.GitMethodStackBuilder.Deploy(payload, endpoint)
}
func (b *ComposeStackGitBuilder) SetAutoUpdate(payload *StackPayload) GitMethodStackBuildProcess {
b.GitMethodStackBuilder.SetAutoUpdate(payload)
return b
}

View file

@ -0,0 +1,55 @@
package stackbuilders
import (
"errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
)
type StackBuilderDirector struct {
builder interface{}
}
func NewStackBuilderDirector(b interface{}) *StackBuilderDirector {
return &StackBuilderDirector{
builder: b,
}
}
func (d *StackBuilderDirector) Build(payload *StackPayload, endpoint *portainer.Endpoint) (*portainer.Stack, *httperror.HandlerError) {
switch builder := d.builder.(type) {
case GitMethodStackBuildProcess:
return builder.SetGeneralInfo(payload, endpoint).
SetUniqueInfo(payload).
SetGitRepository(payload).
Deploy(payload, endpoint).
SetAutoUpdate(payload).
SaveStack()
case FileUploadMethodStackBuildProcess:
return builder.SetGeneralInfo(payload, endpoint).
SetUniqueInfo(payload).
SetUploadedFile(payload).
Deploy(payload, endpoint).
SaveStack()
case FileContentMethodStackBuildProcess:
return builder.SetGeneralInfo(payload, endpoint).
SetUniqueInfo(payload).
SetFileContent(payload).
Deploy(payload, endpoint).
SaveStack()
case UrlMethodStackBuildProcess:
return builder.SetGeneralInfo(payload, endpoint).
SetUniqueInfo(payload).
SetURL(payload).
Deploy(payload, endpoint).
SaveStack()
}
return nil, httperror.BadRequest("Invalid value for query parameter: method. Value must be one of: string or repository or url or file", errors.New(request.ErrInvalidQueryParameter))
}

View file

@ -0,0 +1,108 @@
package stackbuilders
import (
"fmt"
"strconv"
"sync"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
k "github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/api/stacks/stackutils"
)
type K8sStackFileContentBuilder struct {
FileContentMethodStackBuilder
stackCreateMut *sync.Mutex
KuberneteDeployer portainer.KubernetesDeployer
User *portainer.User
}
// CreateK8sStackFileContentBuilder creates a builder for the Kubernetes stack that will be deployed by file content method
func CreateK8sStackFileContentBuilder(dataStore dataservices.DataStore,
fileService portainer.FileService,
stackDeployer deployments.StackDeployer,
kuberneteDeployer portainer.KubernetesDeployer,
user *portainer.User) *K8sStackFileContentBuilder {
return &K8sStackFileContentBuilder{
FileContentMethodStackBuilder: FileContentMethodStackBuilder{
StackBuilder: CreateStackBuilder(dataStore, fileService, stackDeployer),
},
stackCreateMut: &sync.Mutex{},
KuberneteDeployer: kuberneteDeployer,
User: user,
}
}
func (b *K8sStackFileContentBuilder) SetGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint) FileContentMethodStackBuildProcess {
b.FileContentMethodStackBuilder.SetGeneralInfo(payload, endpoint)
return b
}
func (b *K8sStackFileContentBuilder) SetUniqueInfo(payload *StackPayload) FileContentMethodStackBuildProcess {
if b.hasError() {
return b
}
b.stack.Name = payload.StackName
b.stack.Type = portainer.KubernetesStack
b.stack.EntryPoint = filesystem.ManifestFileDefaultName
b.stack.Namespace = payload.Namespace
b.stack.CreatedBy = b.User.Username
b.stack.IsComposeFormat = payload.ComposeFormat
return b
}
func (b *K8sStackFileContentBuilder) SetFileContent(payload *StackPayload) FileContentMethodStackBuildProcess {
if b.hasError() {
return b
}
stackFolder := strconv.Itoa(int(b.stack.ID))
projectPath, err := b.fileService.StoreStackFileFromBytes(stackFolder, b.stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
fileType := "Manifest"
if b.stack.IsComposeFormat {
fileType = "Compose"
}
errMsg := fmt.Sprintf("Unable to persist Kubernetes %s file on disk", fileType)
b.err = httperror.InternalServerError(errMsg, err)
return b
}
b.stack.ProjectPath = projectPath
return b
}
func (b *K8sStackFileContentBuilder) Deploy(payload *StackPayload, endpoint *portainer.Endpoint) FileContentMethodStackBuildProcess {
if b.hasError() {
return b
}
b.stackCreateMut.Lock()
defer b.stackCreateMut.Unlock()
k8sAppLabel := k.KubeAppLabels{
StackID: int(b.stack.ID),
StackName: b.stack.Name,
Owner: stackutils.SanitizeLabel(b.stack.CreatedBy),
Kind: "content",
}
k8sDeploymentConfig, err := deployments.CreateKubernetesStackDeploymentConfig(b.stack, b.KuberneteDeployer, k8sAppLabel, b.User, endpoint)
if err != nil {
b.err = httperror.InternalServerError("failed to create temp kub deployment files", err)
return b
}
b.deploymentConfiger = k8sDeploymentConfig
return b.FileContentMethodStackBuilder.Deploy(payload, endpoint)
}
func (b *K8sStackFileContentBuilder) GetResponse() string {
return b.FileContentMethodStackBuilder.deploymentConfiger.GetResponse()
}

View file

@ -0,0 +1,100 @@
package stackbuilders
import (
"sync"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
k "github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/api/stacks/stackutils"
)
type KubernetesStackGitBuilder struct {
GitMethodStackBuilder
stackCreateMut *sync.Mutex
KuberneteDeployer portainer.KubernetesDeployer
user *portainer.User
}
// CreateKuberntesStackGitBuilder creates a builder for the Kubernetes stack that will be deployed by git repository method
func CreateKubernetesStackGitBuilder(dataStore dataservices.DataStore,
fileService portainer.FileService,
gitService portainer.GitService,
scheduler *scheduler.Scheduler,
stackDeployer deployments.StackDeployer,
kuberneteDeployer portainer.KubernetesDeployer,
user *portainer.User) *KubernetesStackGitBuilder {
return &KubernetesStackGitBuilder{
GitMethodStackBuilder: GitMethodStackBuilder{
StackBuilder: CreateStackBuilder(dataStore, fileService, stackDeployer),
gitService: gitService,
scheduler: scheduler,
},
stackCreateMut: &sync.Mutex{},
KuberneteDeployer: kuberneteDeployer,
user: user,
}
}
func (b *KubernetesStackGitBuilder) SetGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint) GitMethodStackBuildProcess {
b.GitMethodStackBuilder.SetGeneralInfo(payload, endpoint)
return b
}
func (b *KubernetesStackGitBuilder) SetUniqueInfo(payload *StackPayload) GitMethodStackBuildProcess {
if b.hasError() {
return b
}
b.stack.Type = portainer.KubernetesStack
b.stack.Namespace = payload.Namespace
b.stack.Name = payload.StackName
b.stack.EntryPoint = payload.ManifestFile
b.stack.CreatedBy = b.user.Username
b.stack.IsComposeFormat = payload.ComposeFormat
return b
}
func (b *KubernetesStackGitBuilder) SetGitRepository(payload *StackPayload) GitMethodStackBuildProcess {
b.GitMethodStackBuilder.SetGitRepository(payload)
return b
}
func (b *KubernetesStackGitBuilder) Deploy(payload *StackPayload, endpoint *portainer.Endpoint) GitMethodStackBuildProcess {
if b.hasError() {
return b
}
b.stackCreateMut.Lock()
defer b.stackCreateMut.Unlock()
k8sAppLabel := k.KubeAppLabels{
StackID: int(b.stack.ID),
StackName: b.stack.Name,
Owner: stackutils.SanitizeLabel(b.stack.CreatedBy),
Kind: "git",
}
k8sDeploymentConfig, err := deployments.CreateKubernetesStackDeploymentConfig(b.stack, b.KuberneteDeployer, k8sAppLabel, b.user, endpoint)
if err != nil {
b.err = httperror.InternalServerError("failed to create temp kub deployment files", err)
return b
}
b.deploymentConfiger = k8sDeploymentConfig
return b.GitMethodStackBuilder.Deploy(payload, endpoint)
}
func (b *KubernetesStackGitBuilder) SetAutoUpdate(payload *StackPayload) GitMethodStackBuildProcess {
b.GitMethodStackBuilder.SetAutoUpdate(payload)
return b
}
func (b *KubernetesStackGitBuilder) GetResponse() string {
return b.GitMethodStackBuilder.deploymentConfiger.GetResponse()
}

View file

@ -0,0 +1,111 @@
package stackbuilders
import (
"strconv"
"sync"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/client"
k "github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/api/stacks/stackutils"
)
type KubernetesStackUrlBuilder struct {
UrlMethodStackBuilder
stackCreateMut *sync.Mutex
KuberneteDeployer portainer.KubernetesDeployer
user *portainer.User
}
// CreateKuberntesStackGitBuilder creates a builder for the Kubernetes stack that will be deployed by git repository method
func CreateKubernetesStackUrlBuilder(dataStore dataservices.DataStore,
fileService portainer.FileService,
stackDeployer deployments.StackDeployer,
kuberneteDeployer portainer.KubernetesDeployer,
user *portainer.User) *KubernetesStackUrlBuilder {
return &KubernetesStackUrlBuilder{
UrlMethodStackBuilder: UrlMethodStackBuilder{
StackBuilder: CreateStackBuilder(dataStore, fileService, stackDeployer),
},
stackCreateMut: &sync.Mutex{},
KuberneteDeployer: kuberneteDeployer,
user: user,
}
}
func (b *KubernetesStackUrlBuilder) SetGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint) UrlMethodStackBuildProcess {
b.UrlMethodStackBuilder.SetGeneralInfo(payload, endpoint)
return b
}
func (b *KubernetesStackUrlBuilder) SetUniqueInfo(payload *StackPayload) UrlMethodStackBuildProcess {
if b.hasError() {
return b
}
b.stack.Type = portainer.KubernetesStack
b.stack.Namespace = payload.Namespace
b.stack.Name = payload.StackName
b.stack.EntryPoint = filesystem.ManifestFileDefaultName
b.stack.CreatedBy = b.user.Username
b.stack.IsComposeFormat = payload.ComposeFormat
return b
}
func (b *KubernetesStackUrlBuilder) SetURL(payload *StackPayload) UrlMethodStackBuildProcess {
if b.hasError() {
return b
}
var manifestContent []byte
manifestContent, err := client.Get(payload.ManifestURL, 30)
if err != nil {
b.err = httperror.InternalServerError("Unable to retrieve manifest from URL", err)
return b
}
stackFolder := strconv.Itoa(int(b.stack.ID))
projectPath, err := b.fileService.StoreStackFileFromBytes(stackFolder, b.stack.EntryPoint, manifestContent)
if err != nil {
b.err = httperror.InternalServerError("Unable to persist Kubernetes manifest file on disk", err)
return b
}
b.stack.ProjectPath = projectPath
return b
}
func (b *KubernetesStackUrlBuilder) Deploy(payload *StackPayload, endpoint *portainer.Endpoint) UrlMethodStackBuildProcess {
if b.hasError() {
return b
}
b.stackCreateMut.Lock()
defer b.stackCreateMut.Unlock()
k8sAppLabel := k.KubeAppLabels{
StackID: int(b.stack.ID),
StackName: b.stack.Name,
Owner: stackutils.SanitizeLabel(b.stack.CreatedBy),
Kind: "url",
}
k8sDeploymentConfig, err := deployments.CreateKubernetesStackDeploymentConfig(b.stack, b.KuberneteDeployer, k8sAppLabel, b.user, endpoint)
if err != nil {
b.err = httperror.InternalServerError("failed to create temp kub deployment files", err)
return b
}
b.deploymentConfiger = k8sDeploymentConfig
return b.UrlMethodStackBuilder.Deploy(payload, endpoint)
}
func (b *KubernetesStackUrlBuilder) GetResponse() string {
return b.UrlMethodStackBuilder.deploymentConfiger.GetResponse()
}

View file

@ -0,0 +1,62 @@
package stackbuilders
import (
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/rs/zerolog/log"
)
type StackBuilder struct {
stack *portainer.Stack
dataStore dataservices.DataStore
fileService portainer.FileService
stackDeployer deployments.StackDeployer
deploymentConfiger deployments.StackDeploymentConfiger
err *httperror.HandlerError
doCleanUp bool
}
func CreateStackBuilder(dataStore dataservices.DataStore, fileService portainer.FileService, deployer deployments.StackDeployer) StackBuilder {
return StackBuilder{
stack: &portainer.Stack{},
dataStore: dataStore,
fileService: fileService,
stackDeployer: deployer,
doCleanUp: true,
}
}
func (b *StackBuilder) SaveStack() (*portainer.Stack, *httperror.HandlerError) {
defer b.cleanUp()
if b.hasError() {
return nil, b.err
}
err := b.dataStore.Stack().Create(b.stack)
if err != nil {
b.err = httperror.InternalServerError("Unable to persist the stack inside the database", err)
return nil, b.err
}
b.doCleanUp = false
return b.stack, b.err
}
func (b *StackBuilder) cleanUp() error {
if !b.doCleanUp {
return nil
}
err := b.fileService.RemoveDirectory(b.stack.ProjectPath)
if err != nil {
log.Error().Err(err).Msg("unable to cleanup stack creation")
}
return nil
}
func (b *StackBuilder) hasError() bool {
return b.err != nil
}

View file

@ -0,0 +1,68 @@
package stackbuilders
import (
"time"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
)
type FileContentMethodStackBuildProcess interface {
// Set general stack information
SetGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint) FileContentMethodStackBuildProcess
// Set unique stack information, e.g. swarm stack has swarmID, kubernetes stack has namespace
SetUniqueInfo(payload *StackPayload) FileContentMethodStackBuildProcess
// Deploy stack based on the configuration
Deploy(payload *StackPayload, endpoint *portainer.Endpoint) FileContentMethodStackBuildProcess
// Save the stack information to database
SaveStack() (*portainer.Stack, *httperror.HandlerError)
// Get reponse from http request. Use if it is needed
GetResponse() string
// Process the file content
SetFileContent(payload *StackPayload) FileContentMethodStackBuildProcess
}
type FileContentMethodStackBuilder struct {
StackBuilder
}
func (b *FileContentMethodStackBuilder) SetGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint) FileContentMethodStackBuildProcess {
stackID := b.dataStore.Stack().GetNextIdentifier()
b.stack.ID = portainer.StackID(stackID)
b.stack.EndpointID = endpoint.ID
b.stack.Status = portainer.StackStatusActive
b.stack.CreationDate = time.Now().Unix()
return b
}
func (b *FileContentMethodStackBuilder) SetUniqueInfo(payload *StackPayload) FileContentMethodStackBuildProcess {
return b
}
func (b *FileContentMethodStackBuilder) SetFileContent(payload *StackPayload) FileContentMethodStackBuildProcess {
if b.hasError() {
return b
}
return b
}
func (b *FileContentMethodStackBuilder) Deploy(payload *StackPayload, endpoint *portainer.Endpoint) FileContentMethodStackBuildProcess {
if b.hasError() {
return b
}
// Deploy the stack
err := b.deploymentConfiger.Deploy()
if err != nil {
b.err = httperror.InternalServerError(err.Error(), err)
return b
}
return b
}
func (b *FileContentMethodStackBuilder) GetResponse() string {
return ""
}

View file

@ -0,0 +1,78 @@
package stackbuilders
import (
"strconv"
"time"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
)
type FileUploadMethodStackBuildProcess interface {
// Set general stack information
SetGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint) FileUploadMethodStackBuildProcess
// Set unique stack information, e.g. swarm stack has swarmID, kubernetes stack has namespace
SetUniqueInfo(payload *StackPayload) FileUploadMethodStackBuildProcess
// Deploy stack based on the configuration
Deploy(payload *StackPayload, endpoint *portainer.Endpoint) FileUploadMethodStackBuildProcess
// Save the stack information to database
SaveStack() (*portainer.Stack, *httperror.HandlerError)
// Get reponse from http request. Use if it is needed
GetResponse() string
// Process the upload file
SetUploadedFile(payload *StackPayload) FileUploadMethodStackBuildProcess
}
type FileUploadMethodStackBuilder struct {
StackBuilder
}
func (b *FileUploadMethodStackBuilder) SetGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint) FileUploadMethodStackBuildProcess {
stackID := b.dataStore.Stack().GetNextIdentifier()
b.stack.ID = portainer.StackID(stackID)
b.stack.EndpointID = endpoint.ID
b.stack.Status = portainer.StackStatusActive
b.stack.CreationDate = time.Now().Unix()
return b
}
func (b *FileUploadMethodStackBuilder) SetUniqueInfo(payload *StackPayload) FileUploadMethodStackBuildProcess {
return b
}
func (b *FileUploadMethodStackBuilder) SetUploadedFile(payload *StackPayload) FileUploadMethodStackBuildProcess {
if b.hasError() {
return b
}
stackFolder := strconv.Itoa(int(b.stack.ID))
projectPath, err := b.fileService.StoreStackFileFromBytes(stackFolder, b.stack.EntryPoint, payload.StackFileContentBytes)
if err != nil {
b.err = httperror.InternalServerError("Unable to persist Compose file on disk", err)
return b
}
b.stack.ProjectPath = projectPath
return b
}
func (b *FileUploadMethodStackBuilder) Deploy(payload *StackPayload, endpoint *portainer.Endpoint) FileUploadMethodStackBuildProcess {
if b.hasError() {
return b
}
// Deploy the stack
err := b.deploymentConfiger.Deploy()
if err != nil {
b.err = httperror.InternalServerError(err.Error(), err)
return b
}
b.doCleanUp = false
return b
}
func (b *FileUploadMethodStackBuilder) GetResponse() string {
return ""
}

View file

@ -0,0 +1,130 @@
package stackbuilders
import (
"strconv"
"time"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/api/stacks/stackutils"
)
type GitMethodStackBuildProcess interface {
// Set general stack information
SetGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint) GitMethodStackBuildProcess
// Set unique stack information, e.g. swarm stack has swarmID, kubernetes stack has namespace
SetUniqueInfo(payload *StackPayload) GitMethodStackBuildProcess
// Deploy stack based on the configuration
Deploy(payload *StackPayload, endpoint *portainer.Endpoint) GitMethodStackBuildProcess
// Save the stack information to database and return the stack object
SaveStack() (*portainer.Stack, *httperror.HandlerError)
// Get reponse from http request. Use if it is needed
GetResponse() string
// Set git repository configuration
SetGitRepository(payload *StackPayload) GitMethodStackBuildProcess
// Set auto update setting
SetAutoUpdate(payload *StackPayload) GitMethodStackBuildProcess
}
type GitMethodStackBuilder struct {
StackBuilder
gitService portainer.GitService
scheduler *scheduler.Scheduler
}
func (b *GitMethodStackBuilder) SetGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint) GitMethodStackBuildProcess {
stackID := b.dataStore.Stack().GetNextIdentifier()
b.stack.ID = portainer.StackID(stackID)
b.stack.EndpointID = endpoint.ID
b.stack.AdditionalFiles = payload.AdditionalFiles
b.stack.Status = portainer.StackStatusActive
b.stack.CreationDate = time.Now().Unix()
b.stack.AutoUpdate = payload.AutoUpdate
return b
}
func (b *GitMethodStackBuilder) SetUniqueInfo(payload *StackPayload) GitMethodStackBuildProcess {
return b
}
func (b *GitMethodStackBuilder) SetGitRepository(payload *StackPayload) GitMethodStackBuildProcess {
if b.hasError() {
return b
}
var repoConfig gittypes.RepoConfig
if payload.Authentication {
repoConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryConfigPayload.Username,
Password: payload.RepositoryConfigPayload.Password,
}
}
repoConfig.URL = payload.URL
repoConfig.ReferenceName = payload.ReferenceName
repoConfig.ConfigFilePath = payload.ComposeFile
if payload.ComposeFile == "" {
repoConfig.ConfigFilePath = filesystem.ComposeFileDefaultName
}
stackFolder := strconv.Itoa(int(b.stack.ID))
// Set the project path on the disk
b.stack.ProjectPath = b.fileService.GetStackProjectPath(stackFolder)
commitHash, err := stackutils.DownloadGitRepository(b.stack.ID, repoConfig, b.gitService, b.fileService)
if err != nil {
b.err = httperror.InternalServerError(err.Error(), err)
return b
}
// Update the latest commit id
repoConfig.ConfigHash = commitHash
b.stack.GitConfig = &repoConfig
return b
}
func (b *GitMethodStackBuilder) Deploy(payload *StackPayload, endpoint *portainer.Endpoint) GitMethodStackBuildProcess {
if b.hasError() {
return b
}
// Deploy the stack
err := b.deploymentConfiger.Deploy()
if err != nil {
b.err = httperror.InternalServerError(err.Error(), err)
return b
}
return b
}
func (b *GitMethodStackBuilder) SetAutoUpdate(payload *StackPayload) GitMethodStackBuildProcess {
if b.hasError() {
return b
}
if payload.AutoUpdate != nil && payload.AutoUpdate.Interval != "" {
jobID, err := deployments.StartAutoupdate(b.stack.ID,
b.stack.AutoUpdate.Interval,
b.scheduler,
b.stackDeployer,
b.dataStore,
b.gitService)
if err != nil {
b.err = err
return b
}
b.stack.AutoUpdate.JobID = jobID
}
return b
}
func (b *GitMethodStackBuilder) GetResponse() string {
return ""
}

View file

@ -0,0 +1,55 @@
package stackbuilders
import (
portainer "github.com/portainer/portainer/api"
)
// StackPayload contains all the fields for creating a stack with all kinds of methods
type StackPayload struct {
// Name of the stack
Name string `example:"myStack" validate:"required"`
// Swarm cluster identifier
SwarmID string `example:"jpofkc0i9uo9wtx1zesuk649w" validate:"required"`
// Stack file data in byte format. Used by file upload method
StackFileContentBytes []byte
// Stack file data in string format. Used by file content method
StackFileContent string
Webhook string
// A list of environment(endpoint) variables used during stack deployment
Env []portainer.Pair
// Optional auto update configuration
AutoUpdate *portainer.StackAutoUpdate
// Whether the stack is from a app template
FromAppTemplate bool `example:"false"`
// Kubernetes stack name
StackName string
// Whether the kubernetes stack config file is compose format
ComposeFormat bool
// Kubernetes stack namespace
Namespace string
// Path to the k8s Stack file. Used by k8s git repository method
ManifestFile string
// URL to the k8s Stack file. Used by k8s git repository method
ManifestURL string
// Path to the Stack file inside the Git repository
ComposeFile string `example:"docker-compose.yml" default:"docker-compose.yml"`
// Applicable when deploying with multiple stack files
AdditionalFiles []string `example:"[nz.compose.yml, uat.compose.yml]"`
// Git repository configuration of a stack
RepositoryConfigPayload
}
type RepositoryConfigPayload struct {
// URL of a Git repository hosting the Stack file
URL string `example:"https://github.com/openfaas/faas" validate:"required"`
// Reference name of a Git repository hosting the Stack file
ReferenceName string `example:"refs/heads/master"`
// Use basic authentication to clone the Git repository
Authentication bool `example:"true"`
// Username used in basic authentication. Required when RepositoryAuthentication is true
// and RepositoryGitCredentialID is 0
Username string `example:"myGitUsername"`
// Password used in basic authentication. Required when RepositoryAuthentication is true
// and RepositoryGitCredentialID is 0
Password string `example:"myGitPassword"`
}

View file

@ -0,0 +1,68 @@
package stackbuilders
import (
"time"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
)
type UrlMethodStackBuildProcess interface {
// Set general stack information
SetGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint) UrlMethodStackBuildProcess
// Set unique stack information, e.g. swarm stack has swarmID, kubernetes stack has namespace
SetUniqueInfo(payload *StackPayload) UrlMethodStackBuildProcess
// Deploy stack based on the configuration
Deploy(payload *StackPayload, endpoint *portainer.Endpoint) UrlMethodStackBuildProcess
// Save the stack information to database
SaveStack() (*portainer.Stack, *httperror.HandlerError)
// Get reponse from http request. Use if it is needed
GetResponse() string
// Set manifest url
SetURL(payload *StackPayload) UrlMethodStackBuildProcess
}
type UrlMethodStackBuilder struct {
StackBuilder
}
func (b *UrlMethodStackBuilder) SetGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint) UrlMethodStackBuildProcess {
stackID := b.dataStore.Stack().GetNextIdentifier()
b.stack.ID = portainer.StackID(stackID)
b.stack.EndpointID = endpoint.ID
b.stack.Status = portainer.StackStatusActive
b.stack.CreationDate = time.Now().Unix()
return b
}
func (b *UrlMethodStackBuilder) SetUniqueInfo(payload *StackPayload) UrlMethodStackBuildProcess {
return b
}
func (b *UrlMethodStackBuilder) SetURL(payload *StackPayload) UrlMethodStackBuildProcess {
if b.hasError() {
return b
}
return b
}
func (b *UrlMethodStackBuilder) Deploy(payload *StackPayload, endpoint *portainer.Endpoint) UrlMethodStackBuildProcess {
if b.hasError() {
return b
}
// Deploy the stack
err := b.deploymentConfiger.Deploy()
if err != nil {
b.err = httperror.InternalServerError(err.Error(), err)
return b
}
return b
}
func (b *UrlMethodStackBuilder) GetResponse() string {
return ""
}

View file

@ -0,0 +1,82 @@
package stackbuilders
import (
"strconv"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/stacks/deployments"
)
type SwarmStackFileContentBuilder struct {
FileContentMethodStackBuilder
SecurityContext *security.RestrictedRequestContext
}
// CreateSwarmStackFileContentBuilder creates a builder for the swarm stack that will be deployed by file content method
func CreateSwarmStackFileContentBuilder(securityContext *security.RestrictedRequestContext,
dataStore dataservices.DataStore,
fileService portainer.FileService,
stackDeployer deployments.StackDeployer) *SwarmStackFileContentBuilder {
return &SwarmStackFileContentBuilder{
FileContentMethodStackBuilder: FileContentMethodStackBuilder{
StackBuilder: CreateStackBuilder(dataStore, fileService, stackDeployer),
},
SecurityContext: securityContext,
}
}
func (b *SwarmStackFileContentBuilder) SetGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint) FileContentMethodStackBuildProcess {
b.FileContentMethodStackBuilder.SetGeneralInfo(payload, endpoint)
return b
}
func (b *SwarmStackFileContentBuilder) SetUniqueInfo(payload *StackPayload) FileContentMethodStackBuildProcess {
if b.hasError() {
return b
}
b.stack.Name = payload.Name
b.stack.Type = portainer.DockerSwarmStack
b.stack.SwarmID = payload.SwarmID
b.stack.EntryPoint = filesystem.ComposeFileDefaultName
b.stack.Env = payload.Env
b.stack.FromAppTemplate = payload.FromAppTemplate
return b
}
func (b *SwarmStackFileContentBuilder) SetFileContent(payload *StackPayload) FileContentMethodStackBuildProcess {
if b.hasError() {
return b
}
stackFolder := strconv.Itoa(int(b.stack.ID))
projectPath, err := b.fileService.StoreStackFileFromBytes(stackFolder, b.stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
b.err = httperror.InternalServerError("Unable to persist Compose file on disk", err)
return b
}
b.stack.ProjectPath = projectPath
return b
}
func (b *SwarmStackFileContentBuilder) Deploy(payload *StackPayload, endpoint *portainer.Endpoint) FileContentMethodStackBuildProcess {
if b.hasError() {
return b
}
swarmDeploymentConfig, err := deployments.CreateSwarmStackDeploymentConfig(b.SecurityContext, b.stack, endpoint, b.dataStore, b.fileService, b.stackDeployer, false, true)
if err != nil {
b.err = httperror.InternalServerError(err.Error(), err)
return b
}
b.deploymentConfiger = swarmDeploymentConfig
b.stack.CreatedBy = b.deploymentConfiger.GetUsername()
return b.FileContentMethodStackBuilder.Deploy(payload, endpoint)
}

View file

@ -0,0 +1,74 @@
package stackbuilders
import (
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/stacks/deployments"
)
type SwarmStackFileUploadBuilder struct {
FileUploadMethodStackBuilder
SecurityContext *security.RestrictedRequestContext
}
// CreateSwarmStackFileUploadBuilder creates a builder for the swarm stack that will be deployed by file upload method
func CreateSwarmStackFileUploadBuilder(securityContext *security.RestrictedRequestContext,
dataStore dataservices.DataStore,
fileService portainer.FileService,
stackDeployer deployments.StackDeployer) *SwarmStackFileUploadBuilder {
return &SwarmStackFileUploadBuilder{
FileUploadMethodStackBuilder: FileUploadMethodStackBuilder{
StackBuilder: CreateStackBuilder(dataStore, fileService, stackDeployer),
},
SecurityContext: securityContext,
}
}
func (b *SwarmStackFileUploadBuilder) SetGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint) FileUploadMethodStackBuildProcess {
b.FileUploadMethodStackBuilder.SetGeneralInfo(payload, endpoint)
return b
}
func (b *SwarmStackFileUploadBuilder) SetUniqueInfo(payload *StackPayload) FileUploadMethodStackBuildProcess {
if b.hasError() {
return b
}
b.stack.Name = payload.Name
b.stack.Type = portainer.DockerSwarmStack
b.stack.SwarmID = payload.SwarmID
b.stack.EntryPoint = filesystem.ComposeFileDefaultName
b.stack.Env = payload.Env
return b
}
func (b *SwarmStackFileUploadBuilder) SetUploadedFile(payload *StackPayload) FileUploadMethodStackBuildProcess {
if b.hasError() {
return b
}
b.FileUploadMethodStackBuilder.SetUploadedFile(payload)
return b
}
func (b *SwarmStackFileUploadBuilder) Deploy(payload *StackPayload, endpoint *portainer.Endpoint) FileUploadMethodStackBuildProcess {
if b.hasError() {
return b
}
swarmDeploymentConfig, err := deployments.CreateSwarmStackDeploymentConfig(b.SecurityContext, b.stack, endpoint, b.dataStore, b.fileService, b.stackDeployer, false, true)
if err != nil {
b.err = httperror.InternalServerError(err.Error(), err)
return b
}
b.deploymentConfiger = swarmDeploymentConfig
b.stack.CreatedBy = b.deploymentConfiger.GetUsername()
return b.FileUploadMethodStackBuilder.Deploy(payload, endpoint)
}

View file

@ -0,0 +1,79 @@
package stackbuilders
import (
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks/deployments"
)
type SwarmStackGitBuilder struct {
GitMethodStackBuilder
SecurityContext *security.RestrictedRequestContext
}
// CreateSwarmStackGitBuilder creates a builder for the swarm stack that will be deployed by git repository method
func CreateSwarmStackGitBuilder(securityContext *security.RestrictedRequestContext,
dataStore dataservices.DataStore,
fileService portainer.FileService,
gitService portainer.GitService,
scheduler *scheduler.Scheduler,
stackDeployer deployments.StackDeployer) *SwarmStackGitBuilder {
return &SwarmStackGitBuilder{
GitMethodStackBuilder: GitMethodStackBuilder{
StackBuilder: CreateStackBuilder(dataStore, fileService, stackDeployer),
gitService: gitService,
scheduler: scheduler,
},
SecurityContext: securityContext,
}
}
func (b *SwarmStackGitBuilder) SetGeneralInfo(payload *StackPayload, endpoint *portainer.Endpoint) GitMethodStackBuildProcess {
b.GitMethodStackBuilder.SetGeneralInfo(payload, endpoint)
return b
}
func (b *SwarmStackGitBuilder) SetUniqueInfo(payload *StackPayload) GitMethodStackBuildProcess {
if b.hasError() {
return b
}
b.stack.Name = payload.Name
b.stack.Type = portainer.DockerSwarmStack
b.stack.SwarmID = payload.SwarmID
b.stack.EntryPoint = payload.ComposeFile
b.stack.FromAppTemplate = payload.FromAppTemplate
b.stack.Env = payload.Env
return b
}
func (b *SwarmStackGitBuilder) SetGitRepository(payload *StackPayload) GitMethodStackBuildProcess {
b.GitMethodStackBuilder.SetGitRepository(payload)
return b
}
// Deploy creates deployment configuration for swarm stack
func (b *SwarmStackGitBuilder) Deploy(payload *StackPayload, endpoint *portainer.Endpoint) GitMethodStackBuildProcess {
if b.hasError() {
return b
}
swarmDeploymentConfig, err := deployments.CreateSwarmStackDeploymentConfig(b.SecurityContext, b.stack, endpoint, b.dataStore, b.fileService, b.stackDeployer, false, true)
if err != nil {
b.err = httperror.InternalServerError(err.Error(), err)
return b
}
b.deploymentConfiger = swarmDeploymentConfig
b.stack.CreatedBy = b.deploymentConfiger.GetUsername()
return b.GitMethodStackBuilder.Deploy(payload, endpoint)
}
func (b *SwarmStackGitBuilder) SetAutoUpdate(payload *StackPayload) GitMethodStackBuildProcess {
b.GitMethodStackBuilder.SetAutoUpdate(payload)
return b
}

View file

@ -0,0 +1,47 @@
package stackutils
import (
"fmt"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
gittypes "github.com/portainer/portainer/api/git/types"
)
var (
ErrStackAlreadyExists = errors.New("A stack already exists with this name")
ErrWebhookIDAlreadyExists = errors.New("A webhook ID already exists")
ErrInvalidGitCredential = errors.New("Invalid git credential")
)
// DownloadGitRepository downloads the target git repository on the disk
// The first return value represents the commit hash of the downloaded git repository
func DownloadGitRepository(stackID portainer.StackID, config gittypes.RepoConfig, gitService portainer.GitService, fileService portainer.FileService) (string, error) {
username := ""
password := ""
if config.Authentication != nil {
username = config.Authentication.Username
password = config.Authentication.Password
}
stackFolder := fmt.Sprintf("%d", stackID)
projectPath := fileService.GetStackProjectPath(stackFolder)
err := gitService.CloneRepository(projectPath, config.URL, config.ReferenceName, username, password)
if err != nil {
if err == gittypes.ErrAuthenticationFailure {
newErr := ErrInvalidGitCredential
return "", newErr
}
newErr := fmt.Errorf("unable to clone git repository: %w", err)
return "", newErr
}
commitID, err := gitService.LatestCommitID(config.URL, config.ReferenceName, username, password)
if err != nil {
newErr := fmt.Errorf("unable to fetch git repository id: %w", err)
return "", newErr
}
return commitID, nil
}

View file

@ -0,0 +1,39 @@
package stackutils
import (
"fmt"
"regexp"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
)
func UserIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.EndpointID) (bool, error) {
isAdmin := user.Role == portainer.AdministratorRole
return isAdmin, nil
}
// GetStackFilePaths returns a list of file paths based on stack project path
func GetStackFilePaths(stack *portainer.Stack, absolute bool) []string {
if !absolute {
return append([]string{stack.EntryPoint}, stack.AdditionalFiles...)
}
var filePaths []string
for _, file := range append([]string{stack.EntryPoint}, stack.AdditionalFiles...) {
filePaths = append(filePaths, filesystem.JoinPaths(stack.ProjectPath, file))
}
return filePaths
}
// ResourceControlID returns the stack resource control id
func ResourceControlID(endpointID portainer.EndpointID, name string) string {
return fmt.Sprintf("%d_%s", endpointID, name)
}
// convert string to valid kubernetes label by replacing invalid characters with periods
func SanitizeLabel(value string) string {
re := regexp.MustCompile(`[^A-Za-z0-9\.\-\_]+`)
return re.ReplaceAllString(value, ".")
}

View file

@ -0,0 +1,26 @@
package stackutils
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func Test_GetStackFilePaths(t *testing.T) {
stack := &portainer.Stack{
ProjectPath: "/tmp/stack/1",
EntryPoint: "file-one.yml",
}
t.Run("stack doesn't have additional files", func(t *testing.T) {
expected := []string{"/tmp/stack/1/file-one.yml"}
assert.ElementsMatch(t, expected, GetStackFilePaths(stack, true))
})
t.Run("stack has additional files", func(t *testing.T) {
stack.AdditionalFiles = []string{"file-two.yml", "file-three.yml"}
expected := []string{"/tmp/stack/1/file-one.yml", "/tmp/stack/1/file-two.yml", "/tmp/stack/1/file-three.yml"}
assert.ElementsMatch(t, expected, GetStackFilePaths(stack, true))
})
}

View file

@ -0,0 +1,98 @@
package stackutils
import (
"time"
"github.com/asaskevich/govalidator"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/types"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
)
func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.EndpointSecuritySettings) error {
composeConfigYAML, err := loader.ParseYAML(stackFileContent)
if err != nil {
return err
}
composeConfigFile := types.ConfigFile{
Config: composeConfigYAML,
}
composeConfigDetails := types.ConfigDetails{
ConfigFiles: []types.ConfigFile{composeConfigFile},
Environment: map[string]string{},
}
composeConfig, err := loader.Load(composeConfigDetails, func(options *loader.Options) {
options.SkipValidation = true
options.SkipInterpolation = true
})
if err != nil {
return err
}
for key := range composeConfig.Services {
service := composeConfig.Services[key]
if !securitySettings.AllowBindMountsForRegularUsers {
for _, volume := range service.Volumes {
if volume.Type == "bind" {
return errors.New("bind-mount disabled for non administrator users")
}
}
}
if !securitySettings.AllowPrivilegedModeForRegularUsers && service.Privileged == true {
return errors.New("privileged mode disabled for non administrator users")
}
if !securitySettings.AllowHostNamespaceForRegularUsers && service.Pid == "host" {
return errors.New("pid host disabled for non administrator users")
}
if !securitySettings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 {
return errors.New("device mapping disabled for non administrator users")
}
if !securitySettings.AllowSysctlSettingForRegularUsers && service.Sysctls != nil && len(service.Sysctls) > 0 {
return errors.New("sysctl setting disabled for non administrator users")
}
if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) {
return errors.New("container capabilities disabled for non administrator users")
}
}
return nil
}
func ValidateStackAutoUpdate(autoUpdate *portainer.StackAutoUpdate) error {
if autoUpdate == nil {
return nil
}
if autoUpdate.Webhook != "" && !govalidator.IsUUID(autoUpdate.Webhook) {
return errors.New("invalid Webhook format")
}
if autoUpdate.Interval != "" {
if _, err := time.ParseDuration(autoUpdate.Interval); err != nil {
return errors.New("invalid Interval format")
}
}
return nil
}
func ValidateStackFiles(stack *portainer.Stack, securitySettings *portainer.EndpointSecuritySettings, fileService portainer.FileService) error {
for _, file := range GetStackFilePaths(stack, false) {
stackContent, err := fileService.GetFileContent(stack.ProjectPath, file)
if err != nil {
return errors.Wrap(err, "failed to get stack file content")
}
err = IsValidStackFile(stackContent, securitySettings)
if err != nil {
return errors.Wrap(err, "stack config file is invalid")
}
}
return nil
}

View file

@ -0,0 +1,42 @@
package stackutils
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func Test_ValidateStackAutoUpdate(t *testing.T) {
tests := []struct {
name string
value *portainer.StackAutoUpdate
wantErr bool
}{
{
name: "webhook is not a valid UUID",
value: &portainer.StackAutoUpdate{Webhook: "fake-webhook"},
wantErr: true,
},
{
name: "incorrect interval value",
value: &portainer.StackAutoUpdate{Interval: "1dd2hh3mm"},
wantErr: true,
},
{
name: "valid auto update",
value: &portainer.StackAutoUpdate{
Webhook: "8dce8c2f-9ca1-482b-ad20-271e86536ada",
Interval: "5h30m40s10ms",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateStackAutoUpdate(tt.value)
assert.Equalf(t, tt.wantErr, err != nil, "received %+v", err)
})
}
}