2020-07-05 23:21:03 +00:00
package stacks
import (
2021-09-07 00:37:26 +00:00
"fmt"
2020-07-05 23:21:03 +00:00
"net/http"
2021-09-07 00:37:26 +00:00
"github.com/asaskevich/govalidator"
2022-09-28 17:56:32 +00:00
"github.com/pkg/errors"
2021-09-07 00:37:26 +00:00
2020-07-05 23:21:03 +00:00
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
2023-03-02 15:07:50 +00:00
"github.com/portainer/portainer/api/git/update"
2023-04-27 04:03:55 +00:00
"github.com/portainer/portainer/api/internal/endpointutils"
2023-08-28 22:32:41 +00:00
"github.com/portainer/portainer/api/internal/registryutils"
2021-09-07 00:37:26 +00:00
k "github.com/portainer/portainer/api/kubernetes"
2022-10-05 09:33:59 +00:00
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/api/stacks/stackbuilders"
"github.com/portainer/portainer/api/stacks/stackutils"
2020-07-05 23:21:03 +00:00
)
2021-06-16 21:47:32 +00:00
type kubernetesStringDeploymentPayload struct {
2021-09-29 23:58:10 +00:00
StackName string
2020-07-05 23:21:03 +00:00
ComposeFormat bool
Namespace string
StackFileContent string
2023-04-04 00:44:42 +00:00
// Whether the stack is from a app template
FromAppTemplate bool ` example:"false" `
2020-07-05 23:21:03 +00:00
}
2023-04-04 00:44:42 +00:00
func createStackPayloadFromK8sFileContentPayload ( name , namespace , fileContent string , composeFormat , fromAppTemplate bool ) stackbuilders . StackPayload {
2022-10-05 09:33:59 +00:00
return stackbuilders . StackPayload {
StackName : name ,
Namespace : namespace ,
StackFileContent : fileContent ,
ComposeFormat : composeFormat ,
2023-04-04 00:44:42 +00:00
FromAppTemplate : fromAppTemplate ,
2022-10-05 09:33:59 +00:00
}
}
2021-06-16 21:47:32 +00:00
type kubernetesGitDeploymentPayload struct {
2021-09-29 23:58:10 +00:00
StackName string
2021-06-16 21:47:32 +00:00
ComposeFormat bool
Namespace string
RepositoryURL string
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
2021-09-29 23:58:10 +00:00
ManifestFile string
AdditionalFiles [ ] string
2023-03-02 15:07:50 +00:00
AutoUpdate * portainer . AutoUpdateSettings
2023-04-03 06:19:17 +00:00
// TLSSkipVerify skips SSL verification when cloning the Git repository
TLSSkipVerify bool ` example:"false" `
2021-06-16 21:47:32 +00:00
}
2023-04-03 06:19:17 +00:00
func createStackPayloadFromK8sGitPayload ( name , repoUrl , repoReference , repoUsername , repoPassword string , repoAuthentication , composeFormat bool , namespace , manifest string , additionalFiles [ ] string , autoUpdate * portainer . AutoUpdateSettings , repoSkipSSLVerify bool ) stackbuilders . StackPayload {
2022-10-05 09:33:59 +00:00
return stackbuilders . StackPayload {
StackName : name ,
RepositoryConfigPayload : stackbuilders . RepositoryConfigPayload {
URL : repoUrl ,
ReferenceName : repoReference ,
Authentication : repoAuthentication ,
Username : repoUsername ,
Password : repoPassword ,
2023-04-03 06:19:17 +00:00
TLSSkipVerify : repoSkipSSLVerify ,
2022-10-05 09:33:59 +00:00
} ,
Namespace : namespace ,
ComposeFormat : composeFormat ,
ManifestFile : manifest ,
AdditionalFiles : additionalFiles ,
AutoUpdate : autoUpdate ,
}
}
2021-09-03 05:37:34 +00:00
type kubernetesManifestURLDeploymentPayload struct {
2021-09-29 23:58:10 +00:00
StackName string
2021-09-07 00:37:26 +00:00
Namespace string
ComposeFormat bool
ManifestURL string
2021-09-03 05:37:34 +00:00
}
2022-10-05 09:33:59 +00:00
func createStackPayloadFromK8sUrlPayload ( name , namespace , manifestUrl string , composeFormat bool ) stackbuilders . StackPayload {
return stackbuilders . StackPayload {
StackName : name ,
Namespace : namespace ,
ManifestURL : manifestUrl ,
ComposeFormat : composeFormat ,
}
}
2021-06-16 21:47:32 +00:00
func ( payload * kubernetesStringDeploymentPayload ) Validate ( r * http . Request ) error {
2020-07-05 23:21:03 +00:00
if govalidator . IsNull ( payload . StackFileContent ) {
2020-07-07 21:57:52 +00:00
return errors . New ( "Invalid stack file content" )
2020-07-05 23:21:03 +00:00
}
2021-09-29 23:58:10 +00:00
if govalidator . IsNull ( payload . StackName ) {
return errors . New ( "Invalid stack name" )
}
2020-07-05 23:21:03 +00:00
return nil
}
2021-06-16 21:47:32 +00:00
func ( payload * kubernetesGitDeploymentPayload ) Validate ( r * http . Request ) error {
if govalidator . IsNull ( payload . RepositoryURL ) || ! govalidator . IsURL ( payload . RepositoryURL ) {
return errors . New ( "Invalid repository URL. Must correspond to a valid URL format" )
}
if payload . RepositoryAuthentication && govalidator . IsNull ( payload . RepositoryPassword ) {
return errors . New ( "Invalid repository credentials. Password must be specified when authentication is enabled" )
}
2021-09-29 23:58:10 +00:00
if govalidator . IsNull ( payload . ManifestFile ) {
return errors . New ( "Invalid manifest file in repository" )
2021-06-16 21:47:32 +00:00
}
2023-03-02 15:07:50 +00:00
if err := update . ValidateAutoUpdateSettings ( payload . AutoUpdate ) ; err != nil {
2021-09-29 23:58:10 +00:00
return err
}
if govalidator . IsNull ( payload . StackName ) {
return errors . New ( "Invalid stack name" )
}
2021-06-16 21:47:32 +00:00
return nil
}
2021-09-03 05:37:34 +00:00
func ( payload * kubernetesManifestURLDeploymentPayload ) Validate ( r * http . Request ) error {
if govalidator . IsNull ( payload . ManifestURL ) || ! govalidator . IsURL ( payload . ManifestURL ) {
return errors . New ( "Invalid manifest URL" )
}
2021-09-29 23:58:10 +00:00
if govalidator . IsNull ( payload . StackName ) {
return errors . New ( "Invalid stack name" )
}
2021-09-03 05:37:34 +00:00
return nil
}
2020-07-05 23:21:03 +00:00
type createKubernetesStackResponse struct {
Output string ` json:"Output" `
}
2023-04-27 04:03:55 +00:00
// @id StackCreateKubernetesFile
// @summary Deploy a new kubernetes stack from a file
// @description Deploy a new stack into a Docker environment specified via the environment identifier.
// @description **Access policy**: authenticated
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param body body kubernetesStringDeploymentPayload true "stack config"
// @param endpointId query int true "Identifier of the environment that will be used to deploy the stack"
// @success 200 {object} portainer.Stack
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /stacks/create/kubernetes/string [post]
2021-09-07 00:37:26 +00:00
func ( handler * Handler ) createKubernetesStackFromFileContent ( w http . ResponseWriter , r * http . Request , endpoint * portainer . Endpoint , userID portainer . UserID ) * httperror . HandlerError {
2023-04-27 04:03:55 +00:00
if ! endpointutils . IsKubernetesEndpoint ( endpoint ) {
return httperror . BadRequest ( "Environment type does not match" , errors . New ( "Environment type does not match" ) )
}
2021-06-16 21:47:32 +00:00
var payload kubernetesStringDeploymentPayload
if err := request . DecodeAndValidateJSONPayload ( r , & payload ) ; err != nil {
2022-09-14 23:42:39 +00:00
return httperror . BadRequest ( "Invalid request payload" , err )
2021-06-16 21:47:32 +00:00
}
2023-06-22 21:28:07 +00:00
user , err := handler . DataStore . User ( ) . Read ( userID )
2021-09-07 00:37:26 +00:00
if err != nil {
2022-09-14 23:42:39 +00:00
return httperror . InternalServerError ( "Unable to load user information from the database" , err )
2021-09-07 00:37:26 +00:00
}
2022-03-16 00:32:12 +00:00
isUnique , err := handler . checkUniqueStackNameInKubernetes ( endpoint , payload . StackName , 0 , payload . Namespace )
2021-09-29 23:58:10 +00:00
if err != nil {
2022-09-14 23:42:39 +00:00
return httperror . InternalServerError ( "Unable to check for name collision" , err )
2021-09-29 23:58:10 +00:00
}
if ! isUnique {
2022-10-05 09:33:59 +00:00
return & httperror . HandlerError { StatusCode : http . StatusConflict , Message : fmt . Sprintf ( "A stack with the name '%s' already exists" , payload . StackName ) , Err : stackutils . ErrStackAlreadyExists }
2020-07-05 23:21:03 +00:00
}
2023-04-04 00:44:42 +00:00
stackPayload := createStackPayloadFromK8sFileContentPayload ( payload . StackName , payload . Namespace , payload . StackFileContent , payload . ComposeFormat , payload . FromAppTemplate )
2021-09-07 00:37:26 +00:00
2022-10-05 09:33:59 +00:00
k8sStackBuilder := stackbuilders . CreateK8sStackFileContentBuilder ( handler . DataStore ,
handler . FileService ,
handler . StackDeployer ,
handler . KubernetesDeployer ,
user )
2021-06-16 21:47:32 +00:00
2023-08-28 22:32:41 +00:00
// Refresh ECR registry secret if needed
// RefreshEcrSecret method checks if the namespace has any ECR registry
// otherwise return nil
cli , err := handler . KubernetesClientFactory . GetKubeClient ( endpoint )
if err == nil {
registryutils . RefreshEcrSecret ( cli , endpoint , handler . DataStore , payload . Namespace )
}
2022-10-05 09:33:59 +00:00
stackBuilderDirector := stackbuilders . NewStackBuilderDirector ( k8sStackBuilder )
_ , httpErr := stackBuilderDirector . Build ( & stackPayload , endpoint )
if httpErr != nil {
return httpErr
2020-07-05 23:21:03 +00:00
}
resp := & createKubernetesStackResponse {
2022-10-05 09:33:59 +00:00
Output : k8sStackBuilder . GetResponse ( ) ,
2020-07-05 23:21:03 +00:00
}
return response . JSON ( w , resp )
}
2023-04-27 04:03:55 +00:00
// @id StackCreateKubernetesGit
// @summary Deploy a new kubernetes stack from a git repository
// @description Deploy a new stack into a Docker environment specified via the environment identifier.
// @description **Access policy**: authenticated
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param body body kubernetesGitDeploymentPayload true "stack config"
// @param endpointId query int true "Identifier of the environment that will be used to deploy the stack"
// @success 200 {object} portainer.Stack
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /stacks/create/kubernetes/repository [post]
2021-09-07 00:37:26 +00:00
func ( handler * Handler ) createKubernetesStackFromGitRepository ( w http . ResponseWriter , r * http . Request , endpoint * portainer . Endpoint , userID portainer . UserID ) * httperror . HandlerError {
2023-04-27 04:03:55 +00:00
if ! endpointutils . IsKubernetesEndpoint ( endpoint ) {
return httperror . BadRequest ( "Environment type does not match" , errors . New ( "Environment type does not match" ) )
}
2021-06-16 21:47:32 +00:00
var payload kubernetesGitDeploymentPayload
if err := request . DecodeAndValidateJSONPayload ( r , & payload ) ; err != nil {
2022-09-14 23:42:39 +00:00
return httperror . BadRequest ( "Invalid request payload" , err )
2021-06-16 21:47:32 +00:00
}
2023-06-22 21:28:07 +00:00
user , err := handler . DataStore . User ( ) . Read ( userID )
2021-09-07 00:37:26 +00:00
if err != nil {
2022-09-14 23:42:39 +00:00
return httperror . InternalServerError ( "Unable to load user information from the database" , err )
2021-09-07 00:37:26 +00:00
}
2022-03-16 00:32:12 +00:00
isUnique , err := handler . checkUniqueStackNameInKubernetes ( endpoint , payload . StackName , 0 , payload . Namespace )
2021-09-29 23:58:10 +00:00
if err != nil {
2022-09-14 23:42:39 +00:00
return httperror . InternalServerError ( "Unable to check for name collision" , err )
2021-09-29 23:58:10 +00:00
}
if ! isUnique {
2022-10-05 09:33:59 +00:00
return & httperror . HandlerError { StatusCode : http . StatusConflict , Message : fmt . Sprintf ( "A stack with the name '%s' already exists" , payload . StackName ) , Err : stackutils . ErrStackAlreadyExists }
2021-09-29 23:58:10 +00:00
}
//make sure the webhook ID is unique
if payload . AutoUpdate != nil && payload . AutoUpdate . Webhook != "" {
isUnique , err := handler . checkUniqueWebhookID ( payload . AutoUpdate . Webhook )
if err != nil {
2022-09-14 23:42:39 +00:00
return httperror . InternalServerError ( "Unable to check for webhook ID collision" , err )
2021-09-29 23:58:10 +00:00
}
if ! isUnique {
2022-10-05 09:33:59 +00:00
return & httperror . HandlerError { StatusCode : http . StatusConflict , Message : fmt . Sprintf ( "Webhook ID: %s already exists" , payload . AutoUpdate . Webhook ) , Err : stackutils . ErrWebhookIDAlreadyExists }
2021-09-07 00:37:26 +00:00
}
2021-06-16 21:47:32 +00:00
}
2022-10-05 09:33:59 +00:00
stackPayload := createStackPayloadFromK8sGitPayload ( payload . StackName ,
payload . RepositoryURL ,
payload . RepositoryReferenceName ,
payload . RepositoryUsername ,
payload . RepositoryPassword ,
payload . RepositoryAuthentication ,
payload . ComposeFormat ,
payload . Namespace ,
payload . ManifestFile ,
payload . AdditionalFiles ,
2023-04-03 06:19:17 +00:00
payload . AutoUpdate ,
payload . TLSSkipVerify ,
)
2022-10-05 09:33:59 +00:00
k8sStackBuilder := stackbuilders . CreateKubernetesStackGitBuilder ( handler . DataStore ,
handler . FileService ,
handler . GitService ,
handler . Scheduler ,
handler . StackDeployer ,
handler . KubernetesDeployer ,
user )
stackBuilderDirector := stackbuilders . NewStackBuilderDirector ( k8sStackBuilder )
_ , httpErr := stackBuilderDirector . Build ( & stackPayload , endpoint )
if httpErr != nil {
return httpErr
2021-06-16 21:47:32 +00:00
}
resp := & createKubernetesStackResponse {
2022-10-05 09:33:59 +00:00
Output : k8sStackBuilder . GetResponse ( ) ,
2021-06-16 21:47:32 +00:00
}
2021-09-02 05:28:51 +00:00
2021-06-16 21:47:32 +00:00
return response . JSON ( w , resp )
}
2023-04-27 04:03:55 +00:00
// @id StackCreateKubernetesUrl
// @summary Deploy a new kubernetes stack from a url
// @description Deploy a new stack into a Docker environment specified via the environment identifier.
// @description **Access policy**: authenticated
// @tags stacks
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param body body kubernetesManifestURLDeploymentPayload true "stack config"
// @param endpointId query int true "Identifier of the environment that will be used to deploy the stack"
// @success 200 {object} portainer.Stack
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /stacks/create/kubernetes/url [post]
2021-09-07 00:37:26 +00:00
func ( handler * Handler ) createKubernetesStackFromManifestURL ( w http . ResponseWriter , r * http . Request , endpoint * portainer . Endpoint , userID portainer . UserID ) * httperror . HandlerError {
2021-09-03 05:37:34 +00:00
var payload kubernetesManifestURLDeploymentPayload
if err := request . DecodeAndValidateJSONPayload ( r , & payload ) ; err != nil {
2022-09-14 23:42:39 +00:00
return httperror . BadRequest ( "Invalid request payload" , err )
2021-09-03 05:37:34 +00:00
}
2023-06-22 21:28:07 +00:00
user , err := handler . DataStore . User ( ) . Read ( userID )
2021-09-07 00:37:26 +00:00
if err != nil {
2022-09-14 23:42:39 +00:00
return httperror . InternalServerError ( "Unable to load user information from the database" , err )
2021-09-07 00:37:26 +00:00
}
2022-03-16 00:32:12 +00:00
isUnique , err := handler . checkUniqueStackNameInKubernetes ( endpoint , payload . StackName , 0 , payload . Namespace )
2021-09-29 23:58:10 +00:00
if err != nil {
2022-09-14 23:42:39 +00:00
return httperror . InternalServerError ( "Unable to check for name collision" , err )
2021-09-29 23:58:10 +00:00
}
if ! isUnique {
2022-10-05 09:33:59 +00:00
return & httperror . HandlerError { StatusCode : http . StatusConflict , Message : fmt . Sprintf ( "A stack with the name '%s' already exists" , payload . StackName ) , Err : stackutils . ErrStackAlreadyExists }
2021-09-29 23:58:10 +00:00
}
2021-09-07 00:37:26 +00:00
2022-10-05 09:33:59 +00:00
stackPayload := createStackPayloadFromK8sUrlPayload ( payload . StackName ,
payload . Namespace ,
payload . ManifestURL ,
payload . ComposeFormat )
2021-09-03 05:37:34 +00:00
2022-10-05 09:33:59 +00:00
k8sStackBuilder := stackbuilders . CreateKubernetesStackUrlBuilder ( handler . DataStore ,
handler . FileService ,
handler . StackDeployer ,
handler . KubernetesDeployer ,
user )
2021-09-03 05:37:34 +00:00
2022-10-05 09:33:59 +00:00
stackBuilderDirector := stackbuilders . NewStackBuilderDirector ( k8sStackBuilder )
_ , httpErr := stackBuilderDirector . Build ( & stackPayload , endpoint )
if httpErr != nil {
return httpErr
2021-09-03 05:37:34 +00:00
}
resp := & createKubernetesStackResponse {
2022-10-05 09:33:59 +00:00
Output : k8sStackBuilder . GetResponse ( ) ,
2021-09-03 05:37:34 +00:00
}
return response . JSON ( w , resp )
}
2021-09-29 23:58:10 +00:00
func ( handler * Handler ) deployKubernetesStack ( userID portainer . UserID , endpoint * portainer . Endpoint , stack * portainer . Stack , appLabels k . KubeAppLabels ) ( string , error ) {
2020-07-05 23:21:03 +00:00
handler . stackCreationMutex . Lock ( )
defer handler . stackCreationMutex . Unlock ( )
2022-10-05 09:33:59 +00:00
user := & portainer . User {
ID : userID ,
}
k8sDeploymentConfig , err := deployments . CreateKubernetesStackDeploymentConfig ( stack , handler . KubernetesDeployer , appLabels , user , endpoint )
2021-06-16 21:47:32 +00:00
if err != nil {
2021-09-29 23:58:10 +00:00
return "" , errors . Wrap ( err , "failed to create temp kub deployment files" )
2021-06-16 21:47:32 +00:00
}
2022-10-05 09:33:59 +00:00
err = k8sDeploymentConfig . Deploy ( )
if err != nil {
return "" , err
}
return k8sDeploymentConfig . GetResponse ( ) , nil
2021-06-16 21:47:32 +00:00
}