2021-02-26 15:50:33 +00:00
import _ from 'lodash-es' ;
2020-07-05 23:21:03 +00:00
import angular from 'angular' ;
2020-08-20 00:51:14 +00:00
import { KubernetesStorageClass , KubernetesStorageClassAccessPolicies } from 'Kubernetes/models/storage-class/models' ;
2021-02-26 15:50:33 +00:00
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues' ;
2020-08-20 00:51:14 +00:00
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants' ;
2021-08-26 14:00:59 +00:00
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper' ;
2022-11-13 08:10:18 +00:00
import { FeatureId } from '@/react/portainer/feature-flags/enums' ;
2021-12-14 19:14:53 +00:00
2022-09-26 19:43:24 +00:00
import { getIngressControllerClassMap , updateIngressControllerClassMap } from '@/react/kubernetes/cluster/ingressClass/utils' ;
2023-02-14 08:19:41 +00:00
import { buildConfirmButton } from '@@/modals/utils' ;
import { confirm } from '@@/modals/confirm' ;
2023-02-28 15:32:29 +00:00
import { getIsRBACEnabled } from '@/react/kubernetes/cluster/getIsRBACEnabled' ;
2022-09-26 19:43:24 +00:00
2020-07-05 23:21:03 +00:00
class KubernetesConfigureController {
2020-08-20 00:51:14 +00:00
/* #region CONSTRUCTOR */
2021-03-22 19:58:11 +00:00
2020-07-05 23:21:03 +00:00
/* @ngInject */
2021-02-18 01:52:59 +00:00
constructor (
$async ,
$state ,
2021-12-14 19:14:53 +00:00
$scope ,
2021-02-18 01:52:59 +00:00
Notifications ,
KubernetesStorageService ,
EndpointService ,
EndpointProvider ,
KubernetesResourcePoolService ,
2021-04-29 01:10:14 +00:00
KubernetesIngressService ,
KubernetesMetricsService
2021-02-18 01:52:59 +00:00
) {
2020-07-05 23:21:03 +00:00
this . $async = $async ;
this . $state = $state ;
2021-12-14 19:14:53 +00:00
this . $scope = $scope ;
2020-07-05 23:21:03 +00:00
this . Notifications = Notifications ;
this . KubernetesStorageService = KubernetesStorageService ;
this . EndpointService = EndpointService ;
this . EndpointProvider = EndpointProvider ;
2021-02-18 01:52:59 +00:00
this . KubernetesResourcePoolService = KubernetesResourcePoolService ;
this . KubernetesIngressService = KubernetesIngressService ;
2021-04-29 01:10:14 +00:00
this . KubernetesMetricsService = KubernetesMetricsService ;
2020-08-20 00:51:14 +00:00
this . IngressClassTypes = KubernetesIngressClassTypes ;
2020-07-05 23:21:03 +00:00
this . onInit = this . onInit . bind ( this ) ;
this . configureAsync = this . configureAsync . bind ( this ) ;
2022-09-26 19:43:24 +00:00
this . areControllersChanged = this . areControllersChanged . bind ( this ) ;
this . areFormValuesChanged = this . areFormValuesChanged . bind ( this ) ;
2022-12-05 19:49:26 +00:00
this . areStorageClassesChanged = this . areStorageClassesChanged . bind ( this ) ;
2022-09-26 19:43:24 +00:00
this . onBeforeOnload = this . onBeforeOnload . bind ( this ) ;
2021-12-14 19:14:53 +00:00
this . limitedFeature = FeatureId . K8S _SETUP _DEFAULT ;
this . limitedFeatureAutoWindow = FeatureId . HIDE _AUTO _UPDATE _WINDOW ;
2022-11-15 07:57:01 +00:00
this . limitedFeatureIngressDeploy = FeatureId . K8S _ADM _ONLY _USR _INGRESS _DEPLY ;
2021-12-14 19:14:53 +00:00
this . onToggleAutoUpdate = this . onToggleAutoUpdate . bind ( this ) ;
2022-10-24 20:41:30 +00:00
this . onChangeControllers = this . onChangeControllers . bind ( this ) ;
2021-12-14 19:14:53 +00:00
this . onChangeEnableResourceOverCommit = this . onChangeEnableResourceOverCommit . bind ( this ) ;
2022-09-26 19:43:24 +00:00
this . onToggleIngressAvailabilityPerNamespace = this . onToggleIngressAvailabilityPerNamespace . bind ( this ) ;
2022-10-24 20:41:30 +00:00
this . onToggleAllowNoneIngressClass = this . onToggleAllowNoneIngressClass . bind ( this ) ;
2022-09-21 07:10:58 +00:00
this . onChangeStorageClassAccessMode = this . onChangeStorageClassAccessMode . bind ( this ) ;
2022-12-07 02:53:06 +00:00
this . onToggleRestrictNs = this . onToggleRestrictNs . bind ( this ) ;
2020-07-05 23:21:03 +00:00
}
2020-08-20 00:51:14 +00:00
/* #endregion */
2020-07-05 23:21:03 +00:00
2020-08-20 00:51:14 +00:00
/* #region STORAGE CLASSES UI MANAGEMENT */
2020-07-05 23:21:03 +00:00
storageClassAvailable ( ) {
return this . StorageClasses && this . StorageClasses . length > 0 ;
}
hasValidStorageConfiguration ( ) {
let valid = true ;
_ . forEach ( this . StorageClasses , ( item ) => {
if ( item . selected && item . AccessModes . length === 0 ) {
valid = false ;
}
} ) ;
return valid ;
}
2020-08-20 00:51:14 +00:00
/* #endregion */
/* #region INGRESS CLASSES UI MANAGEMENT */
2022-10-24 20:41:30 +00:00
onChangeControllers ( controllerClassMap ) {
2022-09-26 19:43:24 +00:00
this . ingressControllers = controllerClassMap ;
2020-08-20 00:51:14 +00:00
}
2020-08-20 09:24:12 +00:00
hasTraefikIngress ( ) {
return _ . find ( this . formValues . IngressClasses , { Type : this . IngressClassTypes . TRAEFIK } ) ;
}
2022-09-26 19:43:24 +00:00
2022-10-24 20:41:30 +00:00
toggleAdvancedIngSettings ( ) {
this . $scope . $evalAsync ( ( ) => {
this . state . isIngToggleSectionExpanded = ! this . state . isIngToggleSectionExpanded ;
} ) ;
}
onToggleAllowNoneIngressClass ( ) {
this . $scope . $evalAsync ( ( ) => {
this . formValues . AllowNoneIngressClass = ! this . formValues . AllowNoneIngressClass ;
} ) ;
}
2022-09-26 19:43:24 +00:00
onToggleIngressAvailabilityPerNamespace ( ) {
this . $scope . $evalAsync ( ( ) => {
this . formValues . IngressAvailabilityPerNamespace = ! this . formValues . IngressAvailabilityPerNamespace ;
} ) ;
}
2020-08-20 00:51:14 +00:00
/* #endregion */
2022-09-26 19:43:24 +00:00
/* #region RESOURCES AND METRICS */
2021-12-14 19:14:53 +00:00
onChangeEnableResourceOverCommit ( enabled ) {
this . $scope . $evalAsync ( ( ) => {
this . formValues . EnableResourceOverCommit = enabled ;
if ( enabled ) {
this . formValues . ResourceOverCommitPercentage = 20 ;
}
} ) ;
}
2022-09-26 19:43:24 +00:00
/* #endregion */
2020-08-20 00:51:14 +00:00
/* #region CONFIGURE */
assignFormValuesToEndpoint ( endpoint , storageClasses , ingressClasses ) {
endpoint . Kubernetes . Configuration . StorageClasses = storageClasses ;
endpoint . Kubernetes . Configuration . UseLoadBalancer = this . formValues . UseLoadBalancer ;
endpoint . Kubernetes . Configuration . UseServerMetrics = this . formValues . UseServerMetrics ;
2022-09-26 19:43:24 +00:00
endpoint . Kubernetes . Configuration . EnableResourceOverCommit = this . formValues . EnableResourceOverCommit ;
endpoint . Kubernetes . Configuration . ResourceOverCommitPercentage = this . formValues . ResourceOverCommitPercentage ;
2020-08-20 00:51:14 +00:00
endpoint . Kubernetes . Configuration . IngressClasses = ingressClasses ;
2021-07-23 05:10:46 +00:00
endpoint . Kubernetes . Configuration . RestrictDefaultNamespace = this . formValues . RestrictDefaultNamespace ;
2022-09-26 19:43:24 +00:00
endpoint . Kubernetes . Configuration . IngressAvailabilityPerNamespace = this . formValues . IngressAvailabilityPerNamespace ;
2022-10-24 20:41:30 +00:00
endpoint . Kubernetes . Configuration . AllowNoneIngressClass = this . formValues . AllowNoneIngressClass ;
2022-09-26 19:43:24 +00:00
endpoint . ChangeWindow = this . state . autoUpdateSettings ;
2020-08-20 00:51:14 +00:00
}
transformFormValues ( ) {
const storageClasses = _ . map ( this . StorageClasses , ( item ) => {
if ( item . selected ) {
const res = new KubernetesStorageClass ( ) ;
res . Name = item . Name ;
res . AccessModes = _ . map ( item . AccessModes , 'Name' ) ;
res . Provisioner = item . Provisioner ;
res . AllowVolumeExpansion = item . AllowVolumeExpansion ;
return res ;
}
} ) ;
_ . pull ( storageClasses , undefined ) ;
const ingressClasses = _ . without (
_ . map ( this . formValues . IngressClasses , ( ic ) => ( ic . NeedsDeletion ? undefined : ic ) ) ,
undefined
) ;
_ . pull ( ingressClasses , undefined ) ;
return [ storageClasses , ingressClasses ] ;
}
2020-07-05 23:21:03 +00:00
2021-02-18 01:52:59 +00:00
async removeIngressesAcrossNamespaces ( ) {
const ingressesToDel = _ . filter ( this . formValues . IngressClasses , { NeedsDeletion : true } ) ;
2021-06-09 19:54:36 +00:00
if ( ! ingressesToDel . length ) {
return ;
}
const promises = [ ] ;
2022-11-02 11:29:26 +00:00
const oldEndpoint = this . EndpointProvider . currentEndpoint ( ) ;
this . EndpointProvider . setCurrentEndpoint ( this . endpoint ) ;
2021-06-09 19:54:36 +00:00
try {
const allResourcePools = await this . KubernetesResourcePoolService . get ( ) ;
const resourcePools = _ . filter (
allResourcePools ,
2023-03-15 21:10:37 +00:00
( resourcePool ) =>
! KubernetesNamespaceHelper . isSystemNamespace ( resourcePool . Namespace . Name ) &&
! KubernetesNamespaceHelper . isDefaultNamespace ( resourcePool . Namespace . Name ) &&
resourcePool . Namespace . Status === 'Active'
2021-06-09 19:54:36 +00:00
) ;
ingressesToDel . forEach ( ( ingress ) => {
resourcePools . forEach ( ( resourcePool ) => {
promises . push ( this . KubernetesIngressService . delete ( resourcePool . Namespace . Name , ingress . Name ) ) ;
} ) ;
2021-02-18 01:52:59 +00:00
} ) ;
2021-06-09 19:54:36 +00:00
} finally {
2022-11-02 11:29:26 +00:00
this . EndpointProvider . setCurrentEndpoint ( oldEndpoint ) ;
2021-06-09 19:54:36 +00:00
}
2021-02-18 01:52:59 +00:00
const responses = await Promise . allSettled ( promises ) ;
responses . forEach ( ( respons ) => {
if ( respons . status == 'rejected' && respons . reason . err . status != 404 ) {
throw respons . reason ;
}
} ) ;
}
2021-06-16 21:47:32 +00:00
2021-04-29 01:10:14 +00:00
enableMetricsServer ( ) {
if ( this . formValues . UseServerMetrics ) {
this . state . metrics . userClick = true ;
this . state . metrics . pending = true ;
this . KubernetesMetricsService . capabilities ( this . endpoint . Id )
. then ( ( ) => {
this . state . metrics . isServerRunning = true ;
this . state . metrics . pending = false ;
this . formValues . UseServerMetrics = true ;
} )
. catch ( ( ) => {
this . state . metrics . isServerRunning = false ;
this . state . metrics . pending = false ;
this . formValues . UseServerMetrics = false ;
} ) ;
} else {
this . state . metrics . userClick = false ;
this . formValues . UseServerMetrics = false ;
}
}
2021-02-18 01:52:59 +00:00
2020-07-05 23:21:03 +00:00
async configureAsync ( ) {
try {
this . state . actionInProgress = true ;
2020-08-20 00:51:14 +00:00
const [ storageClasses , ingressClasses ] = this . transformFormValues ( ) ;
2020-07-05 23:21:03 +00:00
2021-02-18 01:52:59 +00:00
await this . removeIngressesAcrossNamespaces ( ) ;
2020-08-20 00:51:14 +00:00
this . assignFormValuesToEndpoint ( this . endpoint , storageClasses , ingressClasses ) ;
2020-07-05 23:21:03 +00:00
await this . EndpointService . updateEndpoint ( this . endpoint . Id , this . endpoint ) ;
2022-09-26 19:43:24 +00:00
// updateIngressControllerClassMap must be done after updateEndpoint, as a hacky workaround. A better solution: saving ingresscontrollers somewhere else, is being discussed
2022-10-03 23:13:56 +00:00
await updateIngressControllerClassMap ( this . state . endpointId , this . ingressControllers || [ ] ) ;
2022-09-26 19:43:24 +00:00
this . state . isSaving = true ;
2020-08-20 00:51:14 +00:00
const storagePromises = _ . map ( storageClasses , ( storageClass ) => {
2020-08-07 04:40:24 +00:00
const oldStorageClass = _ . find ( this . oldStorageClasses , { Name : storageClass . Name } ) ;
if ( oldStorageClass ) {
2020-08-07 22:43:34 +00:00
return this . KubernetesStorageService . patch ( this . state . endpointId , oldStorageClass , storageClass ) ;
2020-08-07 04:40:24 +00:00
}
} ) ;
await Promise . all ( storagePromises ) ;
2022-12-16 03:03:40 +00:00
this . $state . reload ( ) ;
2022-08-10 05:07:35 +00:00
this . Notifications . success ( 'Success' , 'Configuration successfully applied' ) ;
2020-07-05 23:21:03 +00:00
} catch ( err ) {
this . Notifications . error ( 'Failure' , err , 'Unable to apply configuration' ) ;
} finally {
this . state . actionInProgress = false ;
}
}
configure ( ) {
2022-10-03 23:13:56 +00:00
return this . $async ( this . configureAsync ) ;
2020-07-05 23:21:03 +00:00
}
2020-08-20 00:51:14 +00:00
/* #endregion */
2020-07-05 23:21:03 +00:00
2021-09-05 10:03:48 +00:00
restrictDefaultToggledOn ( ) {
return this . formValues . RestrictDefaultNamespace && ! this . oldFormValues . RestrictDefaultNamespace ;
}
2021-12-14 19:14:53 +00:00
onToggleAutoUpdate ( value ) {
return this . $scope . $evalAsync ( ( ) => {
this . state . autoUpdateSettings . Enabled = value ;
} ) ;
}
2022-09-21 07:10:58 +00:00
onChangeStorageClassAccessMode ( storageClassName , accessModes ) {
return this . $scope . $evalAsync ( ( ) => {
const storageClass = this . StorageClasses . find ( ( item ) => item . Name === storageClassName ) ;
if ( ! storageClass ) {
throw new Error ( 'Storage class not found' ) ;
}
storageClass . AccessModes = accessModes ;
} ) ;
}
2022-12-07 02:53:06 +00:00
onToggleRestrictNs ( ) {
this . $scope . $evalAsync ( ( ) => {
this . formValues . RestrictDefaultNamespace = ! this . formValues . RestrictDefaultNamespace ;
} ) ;
}
2020-08-20 00:51:14 +00:00
/* #region ON INIT */
2020-07-05 23:21:03 +00:00
async onInit ( ) {
this . state = {
actionInProgress : false ,
displayConfigureClassPanel : { } ,
viewReady : false ,
2022-10-24 20:41:30 +00:00
isIngToggleSectionExpanded : false ,
2022-07-22 02:14:31 +00:00
endpointId : this . $state . params . endpointId ,
2020-08-20 00:51:14 +00:00
duplicates : {
2021-02-26 15:50:33 +00:00
ingressClasses : new KubernetesFormValidationReferences ( ) ,
2020-08-20 00:51:14 +00:00
} ,
2021-04-29 01:10:14 +00:00
metrics : {
pending : false ,
isServerRunning : false ,
userClick : false ,
} ,
2022-09-26 19:43:24 +00:00
timeZone : '' ,
isSaving : false ,
2020-07-05 23:21:03 +00:00
} ;
this . formValues = {
UseLoadBalancer : false ,
2020-08-04 22:08:11 +00:00
UseServerMetrics : false ,
2022-09-26 19:43:24 +00:00
EnableResourceOverCommit : true ,
ResourceOverCommitPercentage : 20 ,
2020-08-20 00:51:14 +00:00
IngressClasses : [ ] ,
2021-07-23 05:10:46 +00:00
RestrictDefaultNamespace : false ,
2022-09-26 19:43:24 +00:00
enableAutoUpdateTimeWindow : false ,
IngressAvailabilityPerNamespace : false ,
2020-07-05 23:21:03 +00:00
} ;
2022-12-07 02:53:06 +00:00
// default to true if error is thrown
this . isRBACEnabled = true ;
2022-10-03 23:13:56 +00:00
this . isIngressControllersLoading = true ;
2020-07-05 23:21:03 +00:00
try {
2022-09-21 07:10:58 +00:00
this . availableAccessModes = new KubernetesStorageClassAccessPolicies ( ) ;
2022-12-07 02:53:06 +00:00
[ this . StorageClasses , this . endpoint , this . isRBACEnabled ] = await Promise . all ( [
this . KubernetesStorageService . get ( this . state . endpointId ) ,
this . EndpointService . endpoint ( this . state . endpointId ) ,
getIsRBACEnabled ( this . state . endpointId ) ,
] ) ;
2022-09-26 19:43:24 +00:00
this . ingressControllers = await getIngressControllerClassMap ( { environmentId : this . state . endpointId } ) ;
2022-12-05 19:49:26 +00:00
this . originalIngressControllers = structuredClone ( this . ingressControllers ) || [ ] ;
2022-09-26 19:43:24 +00:00
this . state . autoUpdateSettings = this . endpoint . ChangeWindow ;
2020-07-05 23:21:03 +00:00
_ . forEach ( this . StorageClasses , ( item ) => {
const storage = _ . find ( this . endpoint . Kubernetes . Configuration . StorageClasses , ( sc ) => sc . Name === item . Name ) ;
if ( storage ) {
item . selected = true ;
2022-09-21 07:10:58 +00:00
item . AccessModes = storage . AccessModes . map ( ( name ) => this . availableAccessModes . find ( ( accessMode ) => accessMode . Name === name ) ) ;
2022-09-28 21:26:25 +00:00
} else if ( this . availableAccessModes . length ) {
// set a default access mode if the storage class is not enabled and there are available access modes
item . AccessModes = [ this . availableAccessModes [ 0 ] ] ;
2020-07-05 23:21:03 +00:00
}
} ) ;
this . formValues . UseLoadBalancer = this . endpoint . Kubernetes . Configuration . UseLoadBalancer ;
2020-08-04 22:08:11 +00:00
this . formValues . UseServerMetrics = this . endpoint . Kubernetes . Configuration . UseServerMetrics ;
2022-09-26 19:43:24 +00:00
this . formValues . EnableResourceOverCommit = this . endpoint . Kubernetes . Configuration . EnableResourceOverCommit ;
this . formValues . ResourceOverCommitPercentage = this . endpoint . Kubernetes . Configuration . ResourceOverCommitPercentage ;
2021-07-23 05:10:46 +00:00
this . formValues . RestrictDefaultNamespace = this . endpoint . Kubernetes . Configuration . RestrictDefaultNamespace ;
2020-08-20 00:51:14 +00:00
this . formValues . IngressClasses = _ . map ( this . endpoint . Kubernetes . Configuration . IngressClasses , ( ic ) => {
ic . IsNew = false ;
ic . NeedsDeletion = false ;
return ic ;
} ) ;
2022-09-26 19:43:24 +00:00
this . formValues . IngressAvailabilityPerNamespace = this . endpoint . Kubernetes . Configuration . IngressAvailabilityPerNamespace ;
2022-10-24 20:41:30 +00:00
this . formValues . AllowNoneIngressClass = this . endpoint . Kubernetes . Configuration . AllowNoneIngressClass ;
2021-09-05 10:03:48 +00:00
2022-12-05 19:49:26 +00:00
this . oldStorageClasses = angular . copy ( this . StorageClasses ) ;
this . oldFormValues = angular . copy ( this . formValues ) ;
2020-07-05 23:21:03 +00:00
} catch ( err ) {
2021-09-08 08:42:17 +00:00
this . Notifications . error ( 'Failure' , err , 'Unable to retrieve environment configuration' ) ;
2020-07-05 23:21:03 +00:00
} finally {
this . state . viewReady = true ;
2022-10-03 23:13:56 +00:00
this . isIngressControllersLoading = false ;
2020-07-05 23:21:03 +00:00
}
2022-09-26 19:43:24 +00:00
window . addEventListener ( 'beforeunload' , this . onBeforeOnload ) ;
2020-07-05 23:21:03 +00:00
}
$onInit ( ) {
return this . $async ( this . onInit ) ;
}
2020-08-20 00:51:14 +00:00
/* #endregion */
2022-09-26 19:43:24 +00:00
$onDestroy ( ) {
window . removeEventListener ( 'beforeunload' , this . onBeforeOnload ) ;
}
areControllersChanged ( ) {
return ! _ . isEqual ( this . ingressControllers , this . originalIngressControllers ) ;
}
areFormValuesChanged ( ) {
return ! _ . isEqual ( this . formValues , this . oldFormValues ) ;
}
2022-12-05 19:49:26 +00:00
areStorageClassesChanged ( ) {
// angular is pesky and modifies this.StorageClasses (adds $$hashkey to each item)
// angular.toJson removes this to make the comparison work
const storageClassesWithoutHashKey = angular . toJson ( this . StorageClasses ) ;
const oldStorageClassesWithoutHashKey = angular . toJson ( this . oldStorageClasses ) ;
return ! _ . isEqual ( storageClassesWithoutHashKey , oldStorageClassesWithoutHashKey ) ;
}
2022-09-26 19:43:24 +00:00
onBeforeOnload ( event ) {
2022-12-05 19:49:26 +00:00
if ( ! this . state . isSaving && ( this . areControllersChanged ( ) || this . areFormValuesChanged ( ) || this . areStorageClassesChanged ( ) ) ) {
2022-09-26 19:43:24 +00:00
event . preventDefault ( ) ;
event . returnValue = '' ;
}
}
uiCanExit ( ) {
2022-12-05 19:49:26 +00:00
if ( ! this . state . isSaving && ( this . areControllersChanged ( ) || this . areFormValuesChanged ( ) || this . areStorageClassesChanged ( ) ) && ! this . isIngressControllersLoading ) {
2023-02-14 08:19:41 +00:00
return confirm ( {
2022-09-26 19:43:24 +00:00
title : 'Are you sure?' ,
message : 'You currently have unsaved changes in the cluster setup view. Are you sure you want to leave?' ,
2023-02-14 08:19:41 +00:00
confirmButton : buildConfirmButton ( 'Yes' , 'danger' ) ,
2022-09-26 19:43:24 +00:00
} ) ;
}
}
2020-07-05 23:21:03 +00:00
}
export default KubernetesConfigureController ;
angular . module ( 'portainer.kubernetes' ) . controller ( 'KubernetesConfigureController' , KubernetesConfigureController ) ;