portainer/portainer

View on GitHub
api/http/handler/edgestacks/edgestack_create_git.go

Summary

Maintainability
A
2 hrs
Test Coverage
package edgestacks

import (
    "fmt"
    "net/http"

    portainer "github.com/portainer/portainer/api"
    "github.com/portainer/portainer/api/dataservices"
    "github.com/portainer/portainer/api/filesystem"
    gittypes "github.com/portainer/portainer/api/git/types"
    httperrors "github.com/portainer/portainer/api/http/errors"
    "github.com/portainer/portainer/pkg/libhttp/request"

    "github.com/asaskevich/govalidator"
    "github.com/pkg/errors"
)

type edgeStackFromGitRepositoryPayload struct {
    // Name of the stack
    Name string `example:"myStack" validate:"required"`
    // URL of a Git repository hosting the Stack file
    RepositoryURL string `example:"https://github.com/openfaas/faas" validate:"required"`
    // Reference name of a Git repository hosting the Stack file
    RepositoryReferenceName string `example:"refs/heads/master"`
    // Use basic authentication to clone the Git repository
    RepositoryAuthentication bool `example:"true"`
    // Username used in basic authentication. Required when RepositoryAuthentication is true.
    RepositoryUsername string `example:"myGitUsername"`
    // Password used in basic authentication. Required when RepositoryAuthentication is true.
    RepositoryPassword string `example:"myGitPassword"`
    // Path to the Stack file inside the Git repository
    FilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
    // List of identifiers of EdgeGroups
    EdgeGroups []portainer.EdgeGroupID `example:"1" validate:"required"`
    // Deployment type to deploy this stack
    // Valid values are: 0 - 'compose', 1 - 'kubernetes'
    // compose is enabled only for docker environments
    // kubernetes is enabled only for kubernetes environments
    DeploymentType portainer.EdgeStackDeploymentType `example:"0" enums:"0,1,2"`
    // List of Registries to use for this stack
    Registries []portainer.RegistryID
    // Uses the manifest's namespaces instead of the default one
    UseManifestNamespaces bool
    // TLSSkipVerify skips SSL verification when cloning the Git repository
    TLSSkipVerify bool `example:"false"`
}

func (payload *edgeStackFromGitRepositoryPayload) Validate(r *http.Request) error {
    if govalidator.IsNull(payload.Name) {
        return httperrors.NewInvalidPayloadError("Invalid stack name")
    }

    if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
        return httperrors.NewInvalidPayloadError("Invalid repository URL. Must correspond to a valid URL format")
    }

    if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
        return httperrors.NewInvalidPayloadError("Invalid repository credentials. Password must be specified when authentication is enabled")
    }

    if payload.DeploymentType != portainer.EdgeStackDeploymentCompose && payload.DeploymentType != portainer.EdgeStackDeploymentKubernetes {
        return httperrors.NewInvalidPayloadError("Invalid deployment type")
    }

    if govalidator.IsNull(payload.FilePathInRepository) {
        switch payload.DeploymentType {
        case portainer.EdgeStackDeploymentCompose:
            payload.FilePathInRepository = filesystem.ComposeFileDefaultName
        case portainer.EdgeStackDeploymentKubernetes:
            payload.FilePathInRepository = filesystem.ManifestFileDefaultName
        }
    }

    if len(payload.EdgeGroups) == 0 {
        return httperrors.NewInvalidPayloadError("Invalid edge groups. At least one edge group must be specified")
    }

    return nil
}

// @id EdgeStackCreateRepository
// @summary Create an EdgeStack from a git repository
// @description **Access policy**: administrator
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param body body edgeStackFromGitRepositoryPayload true "stack config"
// @param dryrun query string false "if true, will not create an edge stack, but just will check the settings and return a non-persisted edge stack object"
// @success 200 {object} portainer.EdgeStack
// @failure 400 "Bad request"
// @failure 500 "Internal server error"
// @failure 503 "Edge compute features are disabled"
// @router /edge_stacks/create/repository [post]
func (handler *Handler) createEdgeStackFromGitRepository(r *http.Request, tx dataservices.DataStoreTx, dryrun bool, userID portainer.UserID) (*portainer.EdgeStack, error) {
    var payload edgeStackFromGitRepositoryPayload
    err := request.DecodeAndValidateJSONPayload(r, &payload)
    if err != nil {
        return nil, err
    }

    stack, err := handler.edgeStacksService.BuildEdgeStack(tx, payload.Name, payload.DeploymentType, payload.EdgeGroups, payload.Registries, payload.UseManifestNamespaces)
    if err != nil {
        return nil, errors.Wrap(err, "failed to create edge stack object")
    }

    if dryrun {
        return stack, nil
    }

    repoConfig := gittypes.RepoConfig{
        URL:            payload.RepositoryURL,
        ReferenceName:  payload.RepositoryReferenceName,
        ConfigFilePath: payload.FilePathInRepository,
        TLSSkipVerify:  payload.TLSSkipVerify,
    }

    if payload.RepositoryAuthentication {
        repoConfig.Authentication = &gittypes.GitAuthentication{
            Username: payload.RepositoryUsername,
            Password: payload.RepositoryPassword,
        }
    }

    return handler.edgeStacksService.PersistEdgeStack(tx, stack, func(stackFolder string, relatedEndpointIds []portainer.EndpointID) (composePath string, manifestPath string, projectPath string, err error) {
        return handler.storeManifestFromGitRepository(tx, stackFolder, relatedEndpointIds, payload.DeploymentType, userID, repoConfig)
    })
}

func (handler *Handler) storeManifestFromGitRepository(tx dataservices.DataStoreTx, stackFolder string, relatedEndpointIds []portainer.EndpointID, deploymentType portainer.EdgeStackDeploymentType, currentUserID portainer.UserID, repositoryConfig gittypes.RepoConfig) (composePath, manifestPath, projectPath string, err error) {
    hasWrongType, err := hasWrongEnvironmentType(tx.Endpoint(), relatedEndpointIds, deploymentType)
    if err != nil {
        return "", "", "", fmt.Errorf("unable to check for existence of non fitting environments: %w", err)
    }
    if hasWrongType {
        return "", "", "", fmt.Errorf("edge stack with config do not match the environment type")
    }

    projectPath = handler.FileService.GetEdgeStackProjectPath(stackFolder)
    repositoryUsername := ""
    repositoryPassword := ""
    if repositoryConfig.Authentication != nil && repositoryConfig.Authentication.Password != "" {
        repositoryUsername = repositoryConfig.Authentication.Username
        repositoryPassword = repositoryConfig.Authentication.Password
    }

    err = handler.GitService.CloneRepository(projectPath, repositoryConfig.URL, repositoryConfig.ReferenceName, repositoryUsername, repositoryPassword, repositoryConfig.TLSSkipVerify)
    if err != nil {
        return "", "", "", err
    }

    if deploymentType == portainer.EdgeStackDeploymentCompose {
        return repositoryConfig.ConfigFilePath, "", projectPath, nil
    }

    if deploymentType == portainer.EdgeStackDeploymentKubernetes {
        return "", repositoryConfig.ConfigFilePath, projectPath, nil
    }

    errMessage := fmt.Sprintf("unknown deployment type: %d", deploymentType)
    return "", "", "", httperrors.NewInvalidPayloadError(errMessage)
}