2020-07-06 11:21:03 +12:00
import angular from 'angular' ;
import _ from 'lodash-es' ;
import stripAnsi from 'strip-ansi' ;
2022-05-31 13:00:47 +03:00
import PortainerError from '@/portainer/error' ;
2021-09-30 12:58:10 +13:00
import { KubernetesDeployManifestTypes , KubernetesDeployBuildMethods , KubernetesDeployRequestMethods , RepositoryMechanismTypes } from 'Kubernetes/models/deploy' ;
2023-11-15 10:45:07 +02:00
import { isTemplateVariablesEnabled , renderTemplate } from '@/react/portainer/custom-templates/components/utils' ;
2023-10-16 14:08:06 +13:00
import { getDeploymentOptions } from '@/react/portainer/environments/environment.service' ;
2023-02-23 01:43:33 +05:30
import { parseAutoUpdateResponse , transformAutoUpdateViewModel } from '@/react/portainer/gitops/AutoUpdateFieldset/utils' ;
2023-03-02 17:07:50 +02:00
import { baseStackWebhookUrl , createWebhookId } from '@/portainer/helpers/webhookHelper' ;
2023-11-15 10:45:07 +02:00
import { getVariablesFieldDefaultValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField' ;
2024-06-17 09:24:54 +12:00
import { KUBE _STACK _NAME _VALIDATION _REGEX } from '@/react/kubernetes/DeployView/StackName/constants' ;
2025-03-14 07:57:06 +13:00
import { confirmWebEditorDiscard } from '@@/modals/confirm' ;
import { editor , git , customTemplate , url , helm } from '@@/BoxSelector/common-options/build-methods' ;
import { kubernetes } from '@@/BoxSelector/common-options/deployment-methods' ;
2021-12-14 21:14:53 +02:00
2020-07-06 11:21:03 +12:00
class KubernetesDeployController {
/* @ngInject */
2025-04-08 12:51:36 +12:00
constructor ( $scope , $async , $state , $window , Authentication , Notifications , KubernetesResourcePoolService , StackService , CustomTemplateService , KubernetesApplicationService ) {
this . $scope = $scope ;
2020-07-06 11:21:03 +12:00
this . $async = $async ;
this . $state = $state ;
2021-03-20 22:13:27 +01:00
this . $window = $window ;
2021-09-05 13:03:48 +03:00
this . Authentication = Authentication ;
2020-07-06 11:21:03 +12:00
this . Notifications = Notifications ;
this . KubernetesResourcePoolService = KubernetesResourcePoolService ;
this . StackService = StackService ;
2021-09-30 12:58:10 +13:00
this . CustomTemplateService = CustomTemplateService ;
2024-02-12 10:54:29 +13:00
this . KubernetesApplicationService = KubernetesApplicationService ;
2020-07-06 11:21:03 +12:00
2023-11-15 10:45:07 +02:00
this . isTemplateVariablesEnabled = isTemplateVariablesEnabled ;
2022-06-16 08:32:41 +03:00
2022-12-01 13:46:23 +13:00
this . deployOptions = [ { ... kubernetes , value : KubernetesDeployManifestTypes . KUBERNETES } ] ;
2021-08-18 14:56:13 +03:00
this . methodOptions = [
2022-08-22 11:55:48 +03:00
{ ... git , value : KubernetesDeployBuildMethods . GIT } ,
{ ... editor , value : KubernetesDeployBuildMethods . WEB _EDITOR } ,
{ ... url , value : KubernetesDeployBuildMethods . URL } ,
2024-02-16 12:34:06 +13:00
{ ... customTemplate , value : KubernetesDeployBuildMethods . CUSTOM _TEMPLATE } ,
2023-10-09 11:20:44 +13:00
{ ... helm , value : KubernetesDeployBuildMethods . HELM } ,
2021-08-18 14:56:13 +03:00
] ;
2023-10-09 11:20:44 +13:00
let buildMethod = Number ( this . $state . params . buildMethod ) || KubernetesDeployBuildMethods . GIT ;
if ( buildMethod > Object . keys ( KubernetesDeployBuildMethods ) . length ) {
buildMethod = KubernetesDeployBuildMethods . GIT ;
}
2021-08-18 14:56:13 +03:00
this . state = {
2023-10-09 11:20:44 +13:00
DeployType : buildMethod ,
2021-08-18 14:56:13 +03:00
BuildMethod : KubernetesDeployBuildMethods . GIT ,
tabLogsDisabled : true ,
activeTab : 0 ,
viewReady : false ,
isEditorDirty : false ,
2021-09-02 08:28:51 +03:00
templateId : null ,
2022-05-31 13:00:47 +03:00
template : null ,
2023-02-23 01:43:33 +05:30
baseWebhookUrl : baseStackWebhookUrl ( ) ,
2023-03-02 17:07:50 +02:00
webhookId : createWebhookId ( ) ,
2023-04-04 12:44:42 +12:00
templateLoadFailed : false ,
isEditorReadOnly : false ,
2023-10-12 11:02:09 +13:00
selectedHelmChart : '' ,
2024-06-17 09:24:54 +12:00
stackNameError : '' ,
2023-04-04 12:44:42 +12:00
} ;
this . currentUser = {
isAdmin : false ,
id : null ,
2021-08-18 14:56:13 +03:00
} ;
2021-09-30 12:58:10 +13:00
this . formValues = {
StackName : '' ,
RepositoryURL : '' ,
RepositoryReferenceName : '' ,
2022-01-16 00:44:20 +08:00
RepositoryAuthentication : false ,
2021-09-30 12:58:10 +13:00
RepositoryUsername : '' ,
RepositoryPassword : '' ,
AdditionalFiles : [ ] ,
2021-10-06 10:12:36 -03:00
ComposeFilePathInRepository : '' ,
2023-11-15 10:45:07 +02:00
Variables : [ ] ,
2023-02-23 01:43:33 +05:30
AutoUpdate : parseAutoUpdateResponse ( ) ,
2023-04-03 09:19:17 +03:00
TLSSkipVerify : false ,
2023-10-18 11:37:03 +13:00
Name : '' ,
2021-09-30 12:58:10 +13:00
} ;
2021-11-23 09:51:02 +13:00
2024-02-12 10:54:29 +13:00
this . stacks = [ ] ;
2021-08-18 14:56:13 +03:00
this . ManifestDeployTypes = KubernetesDeployManifestTypes ;
this . BuildMethods = KubernetesDeployBuildMethods ;
2023-10-12 11:02:09 +13:00
this . onSelectHelmChart = this . onSelectHelmChart . bind ( this ) ;
2021-09-02 08:28:51 +03:00
this . onChangeTemplateId = this . onChangeTemplateId . bind ( this ) ;
2020-07-06 11:21:03 +12:00
this . deployAsync = this . deployAsync . bind ( this ) ;
2021-08-18 14:56:13 +03:00
this . onChangeFileContent = this . onChangeFileContent . bind ( this ) ;
2020-07-06 11:21:03 +12:00
this . getNamespacesAsync = this . getNamespacesAsync . bind ( this ) ;
2021-08-18 14:56:13 +03:00
this . onChangeFormValues = this . onChangeFormValues . bind ( this ) ;
2021-09-05 13:03:48 +03:00
this . buildAnalyticsProperties = this . buildAnalyticsProperties . bind ( this ) ;
2021-12-14 21:14:53 +02:00
this . onChangeMethod = this . onChangeMethod . bind ( this ) ;
this . onChangeDeployType = this . onChangeDeployType . bind ( this ) ;
2022-05-31 13:00:47 +03:00
this . onChangeTemplateVariables = this . onChangeTemplateVariables . bind ( this ) ;
2023-10-16 14:08:06 +13:00
this . setStackName = this . setStackName . bind ( this ) ;
2024-02-12 10:54:29 +13:00
this . onChangeNamespace = this . onChangeNamespace . bind ( this ) ;
}
2025-07-13 10:37:43 +12:00
onChangeNamespace ( namespaceName ) {
2024-02-12 10:54:29 +13:00
return this . $async ( async ( ) => {
2025-07-13 10:37:43 +12:00
this . formValues . Namespace = namespaceName ;
const applications = await this . KubernetesApplicationService . get ( namespaceName ) ;
2024-02-12 10:54:29 +13:00
const stacks = _ . map ( applications , ( item ) => item . StackName ) . filter ( ( item ) => item !== '' ) ;
this . stacks = _ . uniq ( stacks ) ;
} ) ;
2022-05-31 13:00:47 +03:00
}
2023-10-12 11:02:09 +13:00
onSelectHelmChart ( chart ) {
this . state . selectedHelmChart = chart ;
2025-04-08 12:51:36 +12:00
// Force a digest cycle to ensure the change is reflected in the UI
this . $scope . $apply ( ) ;
2023-10-12 11:02:09 +13:00
}
2022-05-31 13:00:47 +03:00
onChangeTemplateVariables ( value ) {
this . onChangeFormValues ( { Variables : value } ) ;
this . renderTemplate ( ) ;
}
2023-10-16 14:08:06 +13:00
setStackName ( name ) {
2024-06-17 09:24:54 +12:00
return this . $async ( async ( ) => {
if ( KUBE _STACK _NAME _VALIDATION _REGEX . test ( name ) || name === '' ) {
this . state . stackNameError = '' ;
} else {
this . state . stackNameError =
"Stack must consist of alphanumeric characters, '-', '_' or '.', must start and end with an alphanumeric character and must be 63 characters or less (e.g. 'my-name', or 'abc-123')." ;
}
this . formValues . StackName = name ;
} ) ;
2023-10-16 14:08:06 +13:00
}
2022-05-31 13:00:47 +03:00
renderTemplate ( ) {
2022-06-16 08:32:41 +03:00
if ( ! this . isTemplateVariablesEnabled ) {
return ;
}
2022-05-31 13:00:47 +03:00
const rendered = renderTemplate ( this . state . templateContent , this . formValues . Variables , this . state . template . Variables ) ;
this . onChangeFormValues ( { EditorContent : rendered } ) ;
2021-09-05 13:03:48 +03:00
}
buildAnalyticsProperties ( ) {
const metadata = {
type : buildLabel ( this . state . BuildMethod ) ,
format : formatLabel ( this . state . DeployType ) ,
2023-04-04 12:44:42 +12:00
role : roleLabel ( this . currentUser . isAdmin ) ,
2021-09-30 12:58:10 +13:00
'automatic-updates' : automaticUpdatesLabel ( this . formValues . RepositoryAutomaticUpdates , this . formValues . RepositoryMechanism ) ,
2021-09-05 13:03:48 +03:00
} ;
if ( this . state . BuildMethod === KubernetesDeployBuildMethods . GIT ) {
metadata . auth = this . formValues . RepositoryAuthentication ;
}
return { metadata } ;
2021-09-30 12:58:10 +13:00
function automaticUpdatesLabel ( repositoryAutomaticUpdates , repositoryMechanism ) {
switch ( repositoryAutomaticUpdates && repositoryMechanism ) {
case RepositoryMechanismTypes . INTERVAL :
return 'polling' ;
case RepositoryMechanismTypes . WEBHOOK :
return 'webhook' ;
default :
return 'off' ;
}
}
2021-09-05 13:03:48 +03:00
function roleLabel ( isAdmin ) {
if ( isAdmin ) {
return 'admin' ;
}
return 'standard' ;
}
function buildLabel ( buildMethod ) {
switch ( buildMethod ) {
case KubernetesDeployBuildMethods . GIT :
return 'git' ;
case KubernetesDeployBuildMethods . WEB _EDITOR :
return 'web-editor' ;
}
}
function formatLabel ( format ) {
switch ( format ) {
case KubernetesDeployManifestTypes . COMPOSE :
return 'compose' ;
case KubernetesDeployManifestTypes . KUBERNETES :
return 'manifest' ;
}
}
2020-07-06 11:21:03 +12:00
}
2021-12-14 21:14:53 +02:00
onChangeMethod ( method ) {
2023-02-23 01:43:33 +05:30
return this . $async ( async ( ) => {
this . state . BuildMethod = method ;
} ) ;
2021-12-14 21:14:53 +02:00
}
onChangeDeployType ( type ) {
2023-02-23 01:43:33 +05:30
return this . $async ( async ( ) => {
this . state . DeployType = type ;
} ) ;
2021-12-14 21:14:53 +02:00
}
2020-07-06 11:21:03 +12:00
disableDeploy ( ) {
2023-11-02 08:50:12 +13:00
const isWebEditorInvalid = this . state . BuildMethod === KubernetesDeployBuildMethods . WEB _EDITOR && _ . isEmpty ( this . formValues . EditorContent ) ;
2023-11-09 13:26:42 +13:00
const isURLFormInvalid = this . state . BuildMethod === KubernetesDeployBuildMethods . URL && _ . isEmpty ( this . formValues . ManifestURL ) ;
2023-11-03 11:39:44 +13:00
const isCustomTemplateInvalid = this . state . BuildMethod === KubernetesDeployBuildMethods . CUSTOM _TEMPLATE && _ . isEmpty ( this . formValues . EditorContent ) ;
2021-09-22 16:01:28 +12:00
const isNamespaceInvalid = _ . isEmpty ( this . formValues . Namespace ) ;
2024-06-17 09:24:54 +12:00
const isStackNameInvalid = this . state . stackNameError !== '' ;
return isWebEditorInvalid || isURLFormInvalid || isCustomTemplateInvalid || this . state . actionInProgress || isNamespaceInvalid || isStackNameInvalid ;
2020-07-06 11:21:03 +12:00
}
2023-02-23 01:43:33 +05:30
onChangeFormValues ( newValues ) {
return this . $async ( async ( ) => {
this . formValues = {
... this . formValues ,
... newValues ,
} ;
} ) ;
2020-07-06 11:21:03 +12:00
}
2022-05-31 13:00:47 +03:00
onChangeTemplateId ( templateId , template ) {
2021-09-02 08:28:51 +03:00
return this . $async ( async ( ) => {
2022-05-31 13:00:47 +03:00
if ( ! template || ( this . state . templateId === templateId && this . state . template === template ) ) {
2021-09-02 08:28:51 +03:00
return ;
}
this . state . templateId = templateId ;
2022-05-31 13:00:47 +03:00
this . state . template = template ;
2021-09-02 08:28:51 +03:00
try {
2023-04-04 12:44:42 +12:00
try {
this . state . templateContent = await this . CustomTemplateService . customTemplateFile ( templateId , template . GitConfig !== null ) ;
this . onChangeFileContent ( this . state . templateContent ) ;
2023-12-07 09:42:18 -03:00
this . state . isEditorReadOnly = false ;
2023-04-04 12:44:42 +12:00
} catch ( err ) {
this . state . templateLoadFailed = true ;
throw err ;
}
2022-05-31 13:00:47 +03:00
if ( template . Variables && template . Variables . length > 0 ) {
2023-11-15 10:45:07 +02:00
const variables = getVariablesFieldDefaultValues ( template . Variables ) ;
2022-05-31 13:00:47 +03:00
this . onChangeTemplateVariables ( variables ) ;
}
2021-09-02 08:28:51 +03:00
} catch ( err ) {
this . Notifications . error ( 'Failure' , err , 'Unable to load template file' ) ;
}
} ) ;
}
2021-08-18 14:56:13 +03:00
onChangeFileContent ( value ) {
this . formValues . EditorContent = value ;
this . state . isEditorDirty = true ;
2020-07-06 11:21:03 +12:00
}
displayErrorLog ( log ) {
this . errorLog = stripAnsi ( log ) ;
this . state . tabLogsDisabled = false ;
this . state . activeTab = 1 ;
}
async deployAsync ( ) {
this . errorLog = '' ;
this . state . actionInProgress = true ;
try {
2021-09-03 17:37:34 +12:00
let method ;
2021-09-02 08:28:51 +03:00
let composeFormat = this . state . DeployType === this . ManifestDeployTypes . COMPOSE ;
switch ( this . state . BuildMethod ) {
2021-09-03 17:37:34 +12:00
case this . BuildMethods . GIT :
2021-09-02 08:28:51 +03:00
method = KubernetesDeployRequestMethods . REPOSITORY ;
break ;
2021-09-03 17:37:34 +12:00
case this . BuildMethods . WEB _EDITOR :
method = KubernetesDeployRequestMethods . STRING ;
break ;
2021-09-02 08:28:51 +03:00
case KubernetesDeployBuildMethods . CUSTOM _TEMPLATE :
2021-09-03 17:37:34 +12:00
method = KubernetesDeployRequestMethods . STRING ;
2021-09-02 08:28:51 +03:00
composeFormat = false ;
2021-09-05 13:03:48 +03:00
break ;
2021-09-03 17:37:34 +12:00
case this . BuildMethods . URL :
method = KubernetesDeployRequestMethods . URL ;
2021-09-02 08:28:51 +03:00
break ;
2021-09-03 17:37:34 +12:00
default :
throw new PortainerError ( 'Unable to determine build method' ) ;
2021-09-02 08:28:51 +03:00
}
2021-06-17 09:47:32 +12:00
2021-11-23 09:51:02 +13:00
let deployNamespace = '' ;
2022-03-24 21:28:53 +13:00
if ( this . formValues . namespace _toggle ) {
deployNamespace = '' ;
} else {
2021-11-23 09:51:02 +13:00
deployNamespace = this . formValues . Namespace ;
}
2021-06-17 09:47:32 +12:00
const payload = {
2021-09-02 08:28:51 +03:00
ComposeFormat : composeFormat ,
2021-11-23 09:51:02 +13:00
Namespace : deployNamespace ,
2021-09-30 12:58:10 +13:00
StackName : this . formValues . StackName ,
2021-06-17 09:47:32 +12:00
} ;
if ( method === KubernetesDeployRequestMethods . REPOSITORY ) {
2023-04-03 09:19:17 +03:00
payload . TLSSkipVerify = this . formValues . TLSSkipVerify ;
2021-06-17 09:47:32 +12:00
payload . RepositoryURL = this . formValues . RepositoryURL ;
payload . RepositoryReferenceName = this . formValues . RepositoryReferenceName ;
payload . RepositoryAuthentication = this . formValues . RepositoryAuthentication ? true : false ;
if ( payload . RepositoryAuthentication ) {
payload . RepositoryUsername = this . formValues . RepositoryUsername ;
payload . RepositoryPassword = this . formValues . RepositoryPassword ;
}
2021-09-30 12:58:10 +13:00
payload . ManifestFile = this . formValues . ComposeFilePathInRepository ;
payload . AdditionalFiles = this . formValues . AdditionalFiles ;
2023-03-02 17:07:50 +02:00
payload . AutoUpdate = transformAutoUpdateViewModel ( this . formValues . AutoUpdate , this . state . webhookId ) ;
2021-09-03 17:37:34 +12:00
} else if ( method === KubernetesDeployRequestMethods . STRING ) {
2021-06-17 09:47:32 +12:00
payload . StackFileContent = this . formValues . EditorContent ;
2021-09-03 17:37:34 +12:00
} else {
payload . ManifestURL = this . formValues . ManifestURL ;
2021-06-17 09:47:32 +12:00
}
2021-12-14 09:34:54 +02:00
await this . StackService . kubernetesDeploy ( this . endpoint . Id , method , payload ) ;
2021-06-17 09:47:32 +12:00
2023-05-01 09:14:30 +12:00
this . Notifications . success ( 'Success' , 'Request to deploy manifest successfully submitted' ) ;
2021-03-20 22:13:27 +01:00
this . state . isEditorDirty = false ;
2023-04-26 11:23:15 +12:00
2023-06-12 09:46:48 +12:00
if ( this . $state . params . referrer && this . $state . params . tab ) {
this . $state . go ( this . $state . params . referrer , { tab : this . $state . params . tab } ) ;
return ;
}
2023-04-26 11:23:15 +12:00
if ( this . $state . params . referrer ) {
this . $state . go ( this . $state . params . referrer ) ;
return ;
}
2020-07-06 11:21:03 +12:00
this . $state . go ( 'kubernetes.applications' ) ;
} catch ( err ) {
this . Notifications . error ( 'Unable to deploy manifest' , err , 'Unable to deploy resources' ) ;
this . displayErrorLog ( err . err . data . details ) ;
} finally {
this . state . actionInProgress = false ;
}
}
deploy ( ) {
return this . $async ( this . deployAsync ) ;
}
async getNamespacesAsync ( ) {
try {
const pools = await this . KubernetesResourcePoolService . get ( ) ;
2023-03-16 10:10:37 +13:00
let namespaces = pools . filter ( ( pool ) => pool . Namespace . Status === 'Active' ) ;
namespaces = _ . map ( namespaces , 'Namespace' ) . sort ( ( a , b ) => {
2021-03-23 00:35:17 +02:00
if ( a . Name === 'default' ) {
return - 1 ;
}
if ( b . Name === 'default' ) {
return 1 ;
}
return 0 ;
} ) ;
this . namespaces = namespaces ;
2021-09-22 16:01:28 +12:00
if ( this . namespaces . length > 0 ) {
this . formValues . Namespace = this . namespaces [ 0 ] . Name ;
}
2025-07-13 10:37:43 +12:00
this . namespaceOptions = _ . map ( namespaces , ( namespace ) => ( {
label : namespace . Name ,
value : namespace . Name ,
} ) ) ;
2020-07-06 11:21:03 +12:00
} catch ( err ) {
2021-04-27 11:12:34 +03:00
this . Notifications . error ( 'Failure' , err , 'Unable to load namespaces data' ) ;
2020-07-06 11:21:03 +12:00
}
}
getNamespaces ( ) {
return this . $async ( this . getNamespacesAsync ) ;
}
2021-03-20 22:13:27 +01:00
async uiCanExit ( ) {
if ( this . formValues . EditorContent && this . state . isEditorDirty ) {
2023-02-14 13:49:41 +05:30
return confirmWebEditorDiscard ( ) ;
2021-03-20 22:13:27 +01:00
}
}
2021-09-02 08:28:51 +03:00
$onInit ( ) {
return this . $async ( async ( ) => {
2023-04-04 12:44:42 +12:00
this . currentUser . isAdmin = this . Authentication . isAdmin ( ) ;
this . currentUser . id = this . Authentication . getUserDetails ( ) . ID ;
2022-03-24 21:28:53 +13:00
this . formValues . namespace _toggle = false ;
2021-09-02 08:28:51 +03:00
await this . getNamespaces ( ) ;
2023-10-16 14:08:06 +13:00
this . deploymentOptions = await getDeploymentOptions ( this . endpoint . Id ) ;
2021-09-02 08:28:51 +03:00
if ( this . $state . params . templateId ) {
const templateId = parseInt ( this . $state . params . templateId , 10 ) ;
if ( templateId && ! Number . isNaN ( templateId ) ) {
this . state . BuildMethod = KubernetesDeployBuildMethods . CUSTOM _TEMPLATE ;
2022-05-31 13:00:47 +03:00
this . state . templateId = templateId ;
2021-09-02 08:28:51 +03:00
}
2021-03-20 22:13:27 +01:00
}
2020-07-06 11:21:03 +12:00
2025-07-13 10:37:43 +12:00
this . onChangeNamespace ( this . formValues . Namespace ) ;
2021-09-02 08:28:51 +03:00
this . state . viewReady = true ;
this . $window . onbeforeunload = ( ) => {
if ( this . formValues . EditorContent && this . state . isEditorDirty ) {
return '' ;
}
} ;
} ) ;
2020-07-06 11:21:03 +12:00
}
2021-08-10 16:44:33 +12:00
$onDestroy ( ) {
this . state . isEditorDirty = false ;
}
2020-07-06 11:21:03 +12:00
}
export default KubernetesDeployController ;
angular . module ( 'portainer.kubernetes' ) . controller ( 'KubernetesDeployController' , KubernetesDeployController ) ;