1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-23 07:19: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,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
}