1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-21 22:39:41 +02:00
portainer/api/http/handler/stacks/create_swarm_stack.go
Anthony Lapenna 19d4db13be
feat(api): rewrite access control management in Docker (#3337)
* feat(api): decorate Docker resource creation response with resource control

* fix(api): fix a potential resource control conflict between stacks/volumes

* feat(api): generate a default private resource control instead of admin only

* fix(api): fix default RC value

* fix(api): update RC authorizations check to support admin only flag

* refactor(api): relocate access control related methods

* fix(api): fix a potential conflict when fetching RC from database

* refactor(api): refactor access control logic

* refactor(api): remove the concept of DecoratedStack

* feat(api): automatically remove RC when removing a Docker resource

* refactor(api): update filter resource methods documentation

* refactor(api): update proxy package structure

* refactor(api): renamed proxy/misc package

* feat(api): re-introduce ResourceControlDelete operation as admin restricted

* refactor(api): relocate default endpoint authorizations

* feat(api): migrate RBAC data

* feat(app): ResourceControl management refactor

* fix(api): fix access control issue on stack deletion and automatically delete RC

* fix(api): fix stack filtering

* fix(api): fix UpdateResourceControl operation checks

* refactor(api): introduce a NewTransport builder method

* refactor(api): inject endpoint in Docker transport

* refactor(api): introduce Docker client into Docker transport

* refactor(api): refactor http/proxy package

* feat(api): inspect a Docker resource labels during access control validation

* fix(api): only apply automatic resource control creation on success response

* fix(api): fix stack access control check

* fix(api): use StatusCreated instead of StatusOK for automatic resource control creation

* fix(app): resource control fixes

* fix(api): fix an issue preventing administrator to inspect a resource with a RC

* refactor(api): remove useless error return

* refactor(api): document DecorateStacks function

* fix(api): fix invalid resource control type for container deletion

* feat(api): support Docker system networks

* feat(api): update Swagger docs

* refactor(api): rename transport variable

* refactor(api): rename transport variable

* feat(networks): add system tag for system networks

* feat(api): add support for resource control labels

* feat(api): upgrade to DBVersion 22

* refactor(api): refactor access control management in Docker proxy

* refactor(api): re-implement docker proxy taskListOperation

* refactor(api): review parameters declaration

* refactor(api): remove extra blank line

* refactor(api): review method comments

* fix(api): fix invalid ServerAddress property and review method visibility

* feat(api): update error message

* feat(api): update restrictedVolumeBrowserOperation method

* refactor(api): refactor method parameters

* refactor(api): minor refactor

* refactor(api): change Azure transport visibility

* refactor(api): update struct documentation

* refactor(api): update struct documentation

* feat(api): review restrictedResourceOperation method

* refactor(api): remove unused authorization methods

* feat(api): apply RBAC when enabled on stack operations

* fix(api): fix invalid data migration procedure for DBVersion = 22

* fix(app): RC duplicate on private resource

* feat(api): change Docker API version logic for libcompose/client factory

* fix(api): update access denied error message to be Docker API compliant

* fix(api): update volume browsing authorizations data migration

* fix(api): fix an issue with access control in multi-node agent Swarm cluster
2019-11-13 12:41:42 +13:00

365 lines
12 KiB
Go

package stacks
import (
"errors"
"net/http"
"path"
"strconv"
"strings"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
)
type swarmStackFromFileContentPayload struct {
Name string
SwarmID string
StackFileContent string
Env []portainer.Pair
}
func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid stack name")
}
if govalidator.IsNull(payload.SwarmID) {
return portainer.Error("Invalid Swarm ID")
}
if govalidator.IsNull(payload.StackFileContent) {
return portainer.Error("Invalid stack file content")
}
return nil
}
func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
var payload swarmStackFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
stacks, err := handler.StackService.Stacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
}
for _, stack := range stacks {
if strings.EqualFold(stack.Name, payload.Name) {
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", portainer.ErrStackAlreadyExists}
}
}
stackID := handler.StackService.GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerSwarmStack,
SwarmID: payload.SwarmID,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
}
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Compose file on disk", err}
}
stack.ProjectPath = projectPath
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
if configErr != nil {
return configErr
}
err = handler.deploySwarmStack(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
err = handler.StackService.CreateStack(stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
}
doCleanUp = false
return handler.decorateStackResponse(w, stack, userID)
}
type swarmStackFromGitRepositoryPayload struct {
Name string
SwarmID string
Env []portainer.Pair
RepositoryURL string
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
ComposeFilePathInRepository string
}
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid stack name")
}
if govalidator.IsNull(payload.SwarmID) {
return portainer.Error("Invalid Swarm ID")
}
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
return portainer.Error("Invalid repository URL. Must correspond to a valid URL format")
}
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
return portainer.Error("Invalid repository credentials. Username and password must be specified when authentication is enabled")
}
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
}
return nil
}
func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
var payload swarmStackFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
stacks, err := handler.StackService.Stacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
}
for _, stack := range stacks {
if strings.EqualFold(stack.Name, payload.Name) {
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", portainer.ErrStackAlreadyExists}
}
}
stackID := handler.StackService.GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerSwarmStack,
SwarmID: payload.SwarmID,
EndpointID: endpoint.ID,
EntryPoint: payload.ComposeFilePathInRepository,
Env: payload.Env,
}
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
stack.ProjectPath = projectPath
gitCloneParams := &cloneRepositoryParameters{
url: payload.RepositoryURL,
referenceName: payload.RepositoryReferenceName,
path: projectPath,
authentication: payload.RepositoryAuthentication,
username: payload.RepositoryUsername,
password: payload.RepositoryPassword,
}
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
err = handler.cloneGitRepository(gitCloneParams)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clone git repository", err}
}
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
if configErr != nil {
return configErr
}
err = handler.deploySwarmStack(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
err = handler.StackService.CreateStack(stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
}
doCleanUp = false
return handler.decorateStackResponse(w, stack, userID)
}
type swarmStackFromFileUploadPayload struct {
Name string
SwarmID string
StackFileContent []byte
Env []portainer.Pair
}
func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error {
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
if err != nil {
return portainer.Error("Invalid stack name")
}
payload.Name = name
swarmID, err := request.RetrieveMultiPartFormValue(r, "SwarmID", false)
if err != nil {
return portainer.Error("Invalid Swarm ID")
}
payload.SwarmID = swarmID
composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file")
if err != nil {
return portainer.Error("Invalid Compose file. Ensure that the Compose file is uploaded correctly")
}
payload.StackFileContent = composeFileContent
var env []portainer.Pair
err = request.RetrieveMultiPartFormJSONValue(r, "Env", &env, true)
if err != nil {
return portainer.Error("Invalid Env parameter")
}
payload.Env = env
return nil
}
func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
payload := &swarmStackFromFileUploadPayload{}
err := payload.Validate(r)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
stacks, err := handler.StackService.Stacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
}
for _, stack := range stacks {
if strings.EqualFold(stack.Name, payload.Name) {
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", portainer.ErrStackAlreadyExists}
}
}
stackID := handler.StackService.GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerSwarmStack,
SwarmID: payload.SwarmID,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
}
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Compose file on disk", err}
}
stack.ProjectPath = projectPath
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
if configErr != nil {
return configErr
}
err = handler.deploySwarmStack(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
err = handler.StackService.CreateStack(stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
}
doCleanUp = false
return handler.decorateStackResponse(w, stack, userID)
}
type swarmStackDeploymentConfig struct {
stack *portainer.Stack
endpoint *portainer.Endpoint
dockerhub *portainer.DockerHub
registries []portainer.Registry
prune bool
isAdmin bool
}
func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
dockerhub, err := handler.DockerHubService.DockerHub()
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err}
}
registries, err := handler.RegistryService.Registries()
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
}
filteredRegistries := security.FilterRegistries(registries, securityContext)
config := &swarmStackDeploymentConfig{
stack: stack,
endpoint: endpoint,
dockerhub: dockerhub,
registries: filteredRegistries,
prune: prune,
isAdmin: securityContext.IsAdmin,
}
return config, nil
}
func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) error {
settings, err := handler.SettingsService.Settings()
if err != nil {
return err
}
if !settings.AllowBindMountsForRegularUsers && !config.isAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
if err != nil {
return err
}
valid, err := handler.isValidStackFile(stackContent)
if err != nil {
return err
}
if !valid {
return errors.New("bind-mount disabled for non administrator users")
}
}
handler.stackCreationMutex.Lock()
defer handler.stackCreationMutex.Unlock()
handler.SwarmStackManager.Login(config.dockerhub, config.registries, config.endpoint)
err = handler.SwarmStackManager.Deploy(config.stack, config.prune, config.endpoint)
if err != nil {
return err
}
err = handler.SwarmStackManager.Logout(config.endpoint)
if err != nil {
return err
}
return nil
}