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

@ -3,16 +3,15 @@ package stacks
import (
"fmt"
"net/http"
"strconv"
"time"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
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/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/api/stacks/stackbuilders"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
@ -40,6 +39,16 @@ func (payload *composeStackFromFileContentPayload) Validate(r *http.Request) err
}
return nil
}
func createStackPayloadFromComposeFileContentPayload(name string, fileContent string, env []portainer.Pair, fromAppTemplate bool) stackbuilders.StackPayload {
return stackbuilders.StackPayload{
Name: name,
StackFileContent: fileContent,
Env: env,
FromAppTemplate: fromAppTemplate,
}
}
func (handler *Handler) checkAndCleanStackDupFromSwarm(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID, stack *portainer.Stack) error {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
@ -48,7 +57,7 @@ func (handler *Handler) checkAndCleanStackDupFromSwarm(w http.ResponseWriter, r
// stop scheduler updates of the stack before removal
if stack.AutoUpdate != nil {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
deployments.StopAutoupdate(stack.ID, stack.AutoUpdate.JobID, handler.Scheduler)
}
err = handler.DataStore.Stack().DeleteStack(stack.ID)
@ -108,47 +117,24 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerComposeStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
FromAppTemplate: payload.FromAppTemplate,
}
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return httperror.InternalServerError("Unable to persist Compose file on disk", err)
}
stack.ProjectPath = projectPath
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint, false)
if configErr != nil {
return configErr
return httperror.InternalServerError("Unable to retrieve info from request context", err)
}
err = handler.deployComposeStack(config, false)
if err != nil {
return httperror.InternalServerError(err.Error(), err)
stackPayload := createStackPayloadFromComposeFileContentPayload(payload.Name, payload.StackFileContent, payload.Env, payload.FromAppTemplate)
composeStackBuilder := stackbuilders.CreateComposeStackFileContentBuilder(securityContext,
handler.DataStore,
handler.FileService,
handler.StackDeployer)
stackBuilderDirector := stackbuilders.NewStackBuilderDirector(composeStackBuilder)
stack, httpErr := stackBuilderDirector.Build(&stackPayload, endpoint)
if httpErr != nil {
return httpErr
}
stack.CreatedBy = config.user.Username
err = handler.DataStore.Stack().Create(stack)
if err != nil {
return httperror.InternalServerError("Unable to persist the stack inside the database", err)
}
doCleanUp = false
return handler.decorateStackResponse(w, stack, userID)
}
@ -177,6 +163,24 @@ type composeStackFromGitRepositoryPayload struct {
FromAppTemplate bool `example:"false"`
}
func createStackPayloadFromComposeGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication bool, composeFile string, additionalFiles []string, autoUpdate *portainer.StackAutoUpdate, env []portainer.Pair, fromAppTemplate bool) stackbuilders.StackPayload {
return stackbuilders.StackPayload{
Name: name,
RepositoryConfigPayload: stackbuilders.RepositoryConfigPayload{
URL: repoUrl,
ReferenceName: repoReference,
Authentication: repoAuthentication,
Username: repoUsername,
Password: repoPassword,
},
ComposeFile: composeFile,
AdditionalFiles: additionalFiles,
AutoUpdate: autoUpdate,
Env: env,
FromAppTemplate: fromAppTemplate,
}
}
func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return errors.New("Invalid stack name")
@ -187,7 +191,7 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
}
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
if err := stackutils.ValidateStackAutoUpdate(payload.AutoUpdate); err != nil {
return err
}
return nil
@ -234,81 +238,40 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
return httperror.InternalServerError("Unable to check for webhook ID collision", err)
}
if !isUnique {
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("Webhook ID: %s already exists", payload.AutoUpdate.Webhook), Err: errWebhookIDAlreadyExists}
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("Webhook ID: %s already exists", payload.AutoUpdate.Webhook), Err: stackutils.ErrWebhookIDAlreadyExists}
}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerComposeStack,
EndpointID: endpoint.ID,
EntryPoint: payload.ComposeFile,
AdditionalFiles: payload.AdditionalFiles,
AutoUpdate: payload.AutoUpdate,
Env: payload.Env,
FromAppTemplate: payload.FromAppTemplate,
GitConfig: &gittypes.RepoConfig{
URL: payload.RepositoryURL,
ReferenceName: payload.RepositoryReferenceName,
ConfigFilePath: payload.ComposeFile,
},
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
}
if payload.RepositoryAuthentication {
stack.GitConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername,
Password: payload.RepositoryPassword,
}
}
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
stack.ProjectPath = projectPath
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
err = handler.clone(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return httperror.InternalServerError("Unable to clone git repository", err)
return httperror.InternalServerError("Unable to retrieve info from request context", err)
}
commitID, err := handler.latestCommitID(payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
if err != nil {
return httperror.InternalServerError("Unable to fetch git repository id", err)
}
stack.GitConfig.ConfigHash = commitID
stackPayload := createStackPayloadFromComposeGitPayload(payload.Name,
payload.RepositoryURL,
payload.RepositoryReferenceName,
payload.RepositoryUsername,
payload.RepositoryPassword,
payload.RepositoryAuthentication,
payload.ComposeFile,
payload.AdditionalFiles,
payload.AutoUpdate,
payload.Env,
payload.FromAppTemplate)
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint, false)
if configErr != nil {
return configErr
composeStackBuilder := stackbuilders.CreateComposeStackGitBuilder(securityContext,
handler.DataStore,
handler.FileService,
handler.GitService,
handler.Scheduler,
handler.StackDeployer)
stackBuilderDirector := stackbuilders.NewStackBuilderDirector(composeStackBuilder)
stack, httpErr := stackBuilderDirector.Build(&stackPayload, endpoint)
if httpErr != nil {
return httpErr
}
err = handler.deployComposeStack(config, false)
if err != nil {
return httperror.InternalServerError(err.Error(), err)
}
if payload.AutoUpdate != nil && payload.AutoUpdate.Interval != "" {
jobID, e := startAutoupdate(stack.ID, stack.AutoUpdate.Interval, handler.Scheduler, handler.StackDeployer, handler.DataStore, handler.GitService)
if e != nil {
return e
}
stack.AutoUpdate.JobID = jobID
}
stack.CreatedBy = config.user.Username
err = handler.DataStore.Stack().Create(stack)
if err != nil {
return httperror.InternalServerError("Unable to persist the stack inside the database", err)
}
doCleanUp = false
return handler.decorateStackResponse(w, stack, userID)
}
@ -318,6 +281,14 @@ type composeStackFromFileUploadPayload struct {
Env []portainer.Pair
}
func createStackPayloadFromComposeFileUploadPayload(name string, fileContentBytes []byte, env []portainer.Pair) stackbuilders.StackPayload {
return stackbuilders.StackPayload{
Name: name,
StackFileContentBytes: fileContentBytes,
Env: env,
}
}
func decodeRequestForm(r *http.Request) (*composeStackFromFileUploadPayload, error) {
payload := &composeStackFromFileUploadPayload{}
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
@ -371,121 +342,23 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerComposeStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
}
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, payload.StackFileContent)
if err != nil {
return httperror.InternalServerError("Unable to persist Compose file on disk", err)
}
stack.ProjectPath = projectPath
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint, false)
if configErr != nil {
return configErr
}
err = handler.deployComposeStack(config, false)
if err != nil {
return httperror.InternalServerError(err.Error(), err)
}
stack.CreatedBy = config.user.Username
err = handler.DataStore.Stack().Create(stack)
if err != nil {
return httperror.InternalServerError("Unable to persist the stack inside the database", err)
}
doCleanUp = false
return handler.decorateStackResponse(w, stack, userID)
}
type composeStackDeploymentConfig struct {
stack *portainer.Stack
endpoint *portainer.Endpoint
registries []portainer.Registry
isAdmin bool
user *portainer.User
forcePullImage bool
}
func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, forcePullImage bool) (*composeStackDeploymentConfig, *httperror.HandlerError) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return nil, httperror.InternalServerError("Unable to retrieve info from request context", err)
return httperror.InternalServerError("Unable to retrieve info from request context", err)
}
user, err := handler.DataStore.User().User(securityContext.UserID)
if err != nil {
return nil, httperror.InternalServerError("Unable to load user information from the database", err)
stackPayload := createStackPayloadFromComposeFileUploadPayload(payload.Name, payload.StackFileContent, payload.Env)
composeStackBuilder := stackbuilders.CreateComposeStackFileUploadBuilder(securityContext,
handler.DataStore,
handler.FileService,
handler.StackDeployer)
stackBuilderDirector := stackbuilders.NewStackBuilderDirector(composeStackBuilder)
stack, httpErr := stackBuilderDirector.Build(&stackPayload, endpoint)
if httpErr != nil {
return httpErr
}
registries, err := handler.DataStore.Registry().Registries()
if err != nil {
return nil, httperror.InternalServerError("Unable to retrieve registries from the database", 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,
}
return config, nil
}
// TODO: libcompose uses credentials store into a config.json file to pull images from
// private registries. Right now the only solution is to re-use the embedded Docker binary
// to login/logout, which will generate the required data in the config.json file and then
// clean it. Hence the use of the mutex.
// We should contribute to libcompose to support authentication without using the config.json file.
func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig, forceCreate bool) error {
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil {
return errors.Wrap(err, "failed to check user priviliges deploying a stack")
}
securitySettings := &config.endpoint.SecuritySettings
if (!securitySettings.AllowBindMountsForRegularUsers ||
!securitySettings.AllowPrivilegedModeForRegularUsers ||
!securitySettings.AllowHostNamespaceForRegularUsers ||
!securitySettings.AllowDeviceMappingForRegularUsers ||
!securitySettings.AllowSysctlSettingForRegularUsers ||
!securitySettings.AllowContainerCapabilitiesForRegularUsers) &&
!isAdminOrEndpointAdmin {
for _, file := range append([]string{config.stack.EntryPoint}, config.stack.AdditionalFiles...) {
stackContent, err := handler.FileService.GetFileContent(config.stack.ProjectPath, file)
if err != nil {
return errors.Wrapf(err, "failed to get stack file content `%q`", file)
}
err = handler.isValidStackFile(stackContent, securitySettings)
if err != nil {
return errors.Wrap(err, "compose file is invalid")
}
}
}
return handler.StackDeployer.DeployComposeStack(config.stack, config.endpoint, config.registries, config.forcePullImage, forceCreate)
return handler.decorateStackResponse(w, stack, userID)
}