mirror of https://github.com/portainer/portainer
				
				
				
			refactor(app): migrate-autoscaling [EE-6387] (#10709)
* refactor(app): migrate-autoscaling [EE-6387]pull/10818/head
							parent
							
								
									6da71661d5
								
							
						
					
					
						commit
						2d77e71085
					
				|  | @ -38,6 +38,10 @@ import { | |||
|   ReplicationFormSection, | ||||
|   replicationValidation, | ||||
| } from '@/react/kubernetes/applications/components/ReplicationFormSection'; | ||||
| import { | ||||
|   AutoScalingFormSection, | ||||
|   autoScalingValidation, | ||||
| } from '@/react/kubernetes/applications/components/AutoScalingFormSection'; | ||||
| 
 | ||||
| import { EnvironmentVariablesFieldset } from '@@/form-components/EnvironmentVariablesFieldset'; | ||||
| 
 | ||||
|  | @ -255,3 +259,11 @@ withFormValidation( | |||
|   ], | ||||
|   replicationValidation | ||||
| ); | ||||
| 
 | ||||
| withFormValidation( | ||||
|   ngModule, | ||||
|   withUIRouter(withCurrentUser(withReactQuery(AutoScalingFormSection))), | ||||
|   'autoScalingFormSection', | ||||
|   ['isMetricsEnabled'], | ||||
|   autoScalingValidation | ||||
| ); | ||||
|  |  | |||
|  | @ -466,128 +466,13 @@ | |||
|                   <!-- #endregion --> | ||||
| 
 | ||||
|                   <!-- #region AUTO SCALING --> | ||||
| 
 | ||||
|                   <div class="form-group !mb-0" ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL && !ctrl.state.useServerMetrics"> | ||||
|                     <div class="col-sm-12 small text-muted"> | ||||
|                       <p ng-if="!ctrl.isAdmin"> This feature is currently disabled and must be enabled by an administrator user. </p> | ||||
|                       <p ng-if="ctrl.isAdmin"> | ||||
|                         Server metrics features must be enabled in the | ||||
|                         <a ui-sref="kubernetes.cluster.setup" class="ctrl.isAdmin">environment configuration view</a>. | ||||
|                       </p> | ||||
|                     </div> | ||||
|                   </div> | ||||
| 
 | ||||
|                   <div class="form-group"> | ||||
|                     <div class="col-sm-12"> | ||||
|                       <div class="col-sm-3 col-lg-2 pl-0 pt-0"> | ||||
|                         <label for="enable_auto_scaling" class="control-label text-left"> Enable auto scaling for this application </label> | ||||
|                       </div> | ||||
|                       <label class="switch ml-4 mt-1"> | ||||
|                         <input | ||||
|                           type="checkbox" | ||||
|                           class="form-control" | ||||
|                           name="enable_auto_scaling" | ||||
|                           ng-model="ctrl.formValues.AutoScaler.IsUsed" | ||||
|                           data-cy="k8sAppCreate-autoScaleCheckbox" | ||||
|                           ng-disabled="!(ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL && ctrl.state.useServerMetrics)" | ||||
|                         /> | ||||
|                         <span class="slider round"></span> | ||||
|                       </label> | ||||
|                     </div> | ||||
|                   </div> | ||||
| 
 | ||||
|                   <div class="form-inline" ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL && ctrl.formValues.AutoScaler.IsUsed"> | ||||
|                     <div class="row"> | ||||
|                       <div class="col-sm-4 pl-0"> | ||||
|                         <label class="control-label pb-2 text-left" for="auto_scaler_min">Minimum instances</label> | ||||
|                         <div class="input-group input-group-sm" style="width: 100%"> | ||||
|                           <input | ||||
|                             type="number" | ||||
|                             class="form-control" | ||||
|                             name="auto_scaler_min" | ||||
|                             min="0" | ||||
|                             ng-max="ctrl.formValues.AutoScaler.MaxReplicas" | ||||
|                             ng-model="ctrl.formValues.AutoScaler.MinReplicas" | ||||
|                             data-cy="k8sAppCreate-autoScaleMin" | ||||
|                             required | ||||
|                           /> | ||||
|                         </div> | ||||
|                         <span ng-show="kubernetesApplicationCreationForm['auto_scaler_min'].$invalid"> | ||||
|                           <div class="small text-warning" style="margin-top: 5px"> | ||||
|                             <ng-messages for="kubernetesApplicationCreationForm['auto_scaler_min'].$error"> | ||||
|                               <p ng-message="required" class="vertical-center"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Minimum instances is required. </p> | ||||
|                               <p ng-message="min" class="vertical-center"> | ||||
|                                 <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Minimum instances must be greater than 0. | ||||
|                               </p> | ||||
|                               <p ng-message="max" class="vertical-center"> | ||||
|                                 <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Minimum instances must be smaller than maximum instances. | ||||
|                               </p> | ||||
|                             </ng-messages> | ||||
|                           </div> | ||||
|                         </span> | ||||
|                       </div> | ||||
|                       <div class="col-sm-4 pl-0"> | ||||
|                         <label class="control-label pb-2 text-left" for="auto_scaler_max">Maximum instances</label> | ||||
|                         <div class="input-group input-group-sm" style="width: 100%"> | ||||
|                           <input | ||||
|                             type="number" | ||||
|                             class="form-control" | ||||
|                             name="auto_scaler_max" | ||||
|                             ng-min="ctrl.formValues.AutoScaler.MinReplicas" | ||||
|                             ng-model="ctrl.formValues.AutoScaler.MaxReplicas" | ||||
|                           /> | ||||
|                         </div> | ||||
|                         <span ng-show="kubernetesApplicationCreationForm['auto_scaler_max'].$invalid || ctrl.autoScalerOverflow()"> | ||||
|                           <div class="small text-warning" style="margin-top: 5px"> | ||||
|                             <ng-messages for="kubernetesApplicationCreationForm['auto_scaler_max'].$error"> | ||||
|                               <p ng-message="required" class="vertical-center"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Maximum instances is required. </p> | ||||
|                               <p ng-message="min" class="vertical-center"> | ||||
|                                 <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Maximum instances must be greater than minimum instances. | ||||
|                               </p> | ||||
|                             </ng-messages> | ||||
|                           </div> | ||||
|                         </span> | ||||
|                       </div> | ||||
|                       <div class="col-sm-4 pl-0"> | ||||
|                         <label class="control-label pb-2 text-left" for="auto_scaler_cpu"> | ||||
|                           Target CPU usage (<b>%</b>) | ||||
|                           <portainer-tooltip message="'The autoscaler will ensure enough instances are running to maintain an average CPU usage across all instances.'"> | ||||
|                           </portainer-tooltip> | ||||
|                         </label> | ||||
|                         <div class="input-group input-group-sm" style="width: 100%"> | ||||
|                           <input | ||||
|                             type="number" | ||||
|                             class="form-control" | ||||
|                             name="auto_scaler_cpu" | ||||
|                             ng-model="ctrl.formValues.AutoScaler.TargetCPUUtilization" | ||||
|                             min="1" | ||||
|                             max="100" | ||||
|                             required | ||||
|                             data-cy="k8sAppCreate-targetCPUInput" | ||||
|                           /> | ||||
|                         </div> | ||||
|                         <span ng-show="kubernetesApplicationCreationForm['auto_scaler_cpu'].$invalid"> | ||||
|                           <div class="small text-warning" style="margin-top: 5px"> | ||||
|                             <ng-messages for="kubernetesApplicationCreationForm['auto_scaler_cpu'].$error"> | ||||
|                               <p ng-message="required" class="vertical-center"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Target CPU usage is required. </p> | ||||
|                               <p ng-message="min" class="vertical-center"> | ||||
|                                 <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Target CPU usage must be greater than 0. | ||||
|                               </p> | ||||
|                               <p ng-message="max" class="vertical-center"> | ||||
|                                 <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Target CPU usage must be smaller than 100. | ||||
|                               </p> | ||||
|                             </ng-messages> | ||||
|                           </div> | ||||
|                         </span> | ||||
|                       </div> | ||||
|                     </div> | ||||
| 
 | ||||
|                     <div class="form-group" ng-if="ctrl.autoScalerOverflow()" style="margin-bottom: 10px"> | ||||
|                       <div class="col-sm-12 small text-danger"> | ||||
|                         <pr-icon icon="'alert-circle'" mode="'danger'"></pr-icon> | ||||
|                         This application would exceed available resources. Please review resource reservations or the maximum instance count of the auto-scaling policy. | ||||
|                       </div> | ||||
|                     </div> | ||||
|                   <div ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL"> | ||||
|                     <auto-scaling-form-section | ||||
|                       values="{isUsed: ctrl.formValues.AutoScaler.IsUsed, minReplicas: ctrl.formValues.AutoScaler.MinReplicas, maxReplicas: ctrl.formValues.AutoScaler.MaxReplicas, targetCpuUtilizationPercentage: ctrl.formValues.AutoScaler.TargetCPUUtilization}" | ||||
|                       on-change="(ctrl.onAutoScaleChange)" | ||||
|                       validation-data="{autoScalerOverflow: ctrl.autoScalerOverflow()}" | ||||
|                       is-metrics-enabled="ctrl.state.useServerMetrics" | ||||
|                     ></auto-scaling-form-section> | ||||
|                   </div> | ||||
|                   <!-- #endregion --> | ||||
| 
 | ||||
|  |  | |||
|  | @ -156,6 +156,7 @@ class KubernetesCreateApplicationController { | |||
|     this.onChangePersistedFolder = this.onChangePersistedFolder.bind(this); | ||||
|     this.onChangeResourceReservation = this.onChangeResourceReservation.bind(this); | ||||
|     this.onChangeReplicaCount = this.onChangeReplicaCount.bind(this); | ||||
|     this.onAutoScaleChange = this.onAutoScaleChange.bind(this); | ||||
|   } | ||||
|   /* #endregion */ | ||||
| 
 | ||||
|  | @ -238,6 +239,26 @@ class KubernetesCreateApplicationController { | |||
|       this.formValues.AutoScaler.IsUsed = false; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onAutoScaleChange(values) { | ||||
|     return this.$async(async () => { | ||||
|       if (!this.formValues.AutoScaler.IsUsed && values.isUsed) { | ||||
|         this.formValues.AutoScaler = { | ||||
|           IsUsed: values.isUsed, | ||||
|           MinReplicas: 1, | ||||
|           MaxReplicas: 3, | ||||
|           TargetCPUUtilization: 50, | ||||
|         }; | ||||
|         return; | ||||
|       } | ||||
|       this.formValues.AutoScaler = { | ||||
|         IsUsed: values.isUsed, | ||||
|         MinReplicas: values.minReplicas, | ||||
|         MaxReplicas: values.maxReplicas, | ||||
|         TargetCPUUtilization: values.targetCpuUtilizationPercentage, | ||||
|       }; | ||||
|     }); | ||||
|   } | ||||
|   /* #endregion */ | ||||
| 
 | ||||
|   /* #region CONFIGMAP UI MANAGEMENT */ | ||||
|  |  | |||
|  | @ -0,0 +1,132 @@ | |||
| import { FormikErrors } from 'formik'; | ||||
| 
 | ||||
| import { useCurrentUser } from '@/react/hooks/useUser'; | ||||
| 
 | ||||
| import { SwitchField } from '@@/form-components/SwitchField'; | ||||
| import { Link } from '@@/Link'; | ||||
| import { TextTip } from '@@/Tip/TextTip'; | ||||
| import { Input } from '@@/form-components/Input'; | ||||
| import { FormError } from '@@/form-components/FormError'; | ||||
| import { Tooltip } from '@@/Tip/Tooltip'; | ||||
| 
 | ||||
| import { AutoScalingFormValues } from './types'; | ||||
| 
 | ||||
| type Props = { | ||||
|   values: AutoScalingFormValues; | ||||
|   onChange: (values: AutoScalingFormValues) => void; | ||||
|   errors: FormikErrors<AutoScalingFormValues>; | ||||
|   isMetricsEnabled: boolean; | ||||
| }; | ||||
| 
 | ||||
| export function AutoScalingFormSection({ | ||||
|   values, | ||||
|   onChange, | ||||
|   errors, | ||||
|   isMetricsEnabled, | ||||
| }: Props) { | ||||
|   return ( | ||||
|     <> | ||||
|       {!isMetricsEnabled && <NoMetricsServerWarning />} | ||||
|       <SwitchField | ||||
|         disabled={!isMetricsEnabled} | ||||
|         label="Enable auto scaling for this application" | ||||
|         labelClass="col-sm-3 col-lg-2" | ||||
|         checked={values.isUsed} | ||||
|         onChange={(value: boolean) => | ||||
|           onChange({ | ||||
|             ...values, | ||||
|             isUsed: value, | ||||
|           }) | ||||
|         } | ||||
|       /> | ||||
|       {values.isUsed && ( | ||||
|         <div className="grid grid-cols-1 md:grid-cols-3 w-full gap-x-4 gap-y-2 my-3"> | ||||
|           <div className="flex flex-col min-w-fit"> | ||||
|             <label htmlFor="min-instances" className="font-normal text-xs"> | ||||
|               Minimum instances | ||||
|             </label> | ||||
|             <Input | ||||
|               id="min-instances" | ||||
|               type="number" | ||||
|               min="0" | ||||
|               value={values.minReplicas} | ||||
|               max={values.maxReplicas || 1} | ||||
|               onChange={(e) => | ||||
|                 onChange({ | ||||
|                   ...values, | ||||
|                   minReplicas: Number(e.target.value) || 0, | ||||
|                 }) | ||||
|               } | ||||
|               data-cy="k8sAppCreate-autoScaleMin" | ||||
|             /> | ||||
|             {errors?.minReplicas && <FormError>{errors.minReplicas}</FormError>} | ||||
|           </div> | ||||
|           <div className="flex flex-col min-w-fit"> | ||||
|             <label htmlFor="max-instances" className="font-normal text-xs"> | ||||
|               Maximum instances | ||||
|             </label> | ||||
|             <Input | ||||
|               id="max-instances" | ||||
|               type="number" | ||||
|               value={values.maxReplicas} | ||||
|               min={values.minReplicas || 1} | ||||
|               onChange={(e) => | ||||
|                 onChange({ | ||||
|                   ...values, | ||||
|                   maxReplicas: Number(e.target.value) || 1, | ||||
|                 }) | ||||
|               } | ||||
|               data-cy="k8sAppCreate-autoScaleMax" | ||||
|             /> | ||||
|             {errors?.maxReplicas && <FormError>{errors.maxReplicas}</FormError>} | ||||
|           </div> | ||||
|           <div className="flex flex-col min-w-fit"> | ||||
|             <label | ||||
|               htmlFor="cpu-threshold" | ||||
|               className="font-normal text-xs flex items-center" | ||||
|             > | ||||
|               Target CPU usage (<b>%</b>) | ||||
|               <Tooltip message="The autoscaler will ensure enough instances are running to maintain an average CPU usage across all instances." /> | ||||
|             </label> | ||||
|             <Input | ||||
|               id="cpu-threshold" | ||||
|               type="number" | ||||
|               value={values.targetCpuUtilizationPercentage} | ||||
|               min="1" | ||||
|               max="100" | ||||
|               onChange={(e) => | ||||
|                 onChange({ | ||||
|                   ...values, | ||||
|                   targetCpuUtilizationPercentage: Number(e.target.value) || 1, | ||||
|                 }) | ||||
|               } | ||||
|               data-cy="k8sAppCreate-targetCPUInput" | ||||
|             /> | ||||
|             {errors?.targetCpuUtilizationPercentage && ( | ||||
|               <FormError>{errors.targetCpuUtilizationPercentage}</FormError> | ||||
|             )} | ||||
|           </div> | ||||
|         </div> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| function NoMetricsServerWarning() { | ||||
|   const { isAdmin } = useCurrentUser(); | ||||
|   return ( | ||||
|     <TextTip color="orange"> | ||||
|       {isAdmin && ( | ||||
|         <> | ||||
|           Server metrics features must be enabled in the{' '} | ||||
|           <Link to="kubernetes.cluster.setup"> | ||||
|             environment configuration view | ||||
|           </Link> | ||||
|           . | ||||
|         </> | ||||
|       )} | ||||
|       {!isAdmin && | ||||
|         'This feature is currently disabled and must be enabled by an administrator user.'} | ||||
|     </TextTip> | ||||
|   ); | ||||
| } | ||||
|  | @ -0,0 +1,69 @@ | |||
| import { SchemaOf, boolean, number, object } from 'yup'; | ||||
| 
 | ||||
| import { AutoScalingFormValues } from './types'; | ||||
| 
 | ||||
| type ValidationData = { | ||||
|   autoScalerOverflow: boolean; | ||||
| }; | ||||
| 
 | ||||
| export function autoScalingValidation( | ||||
|   validationData?: ValidationData | ||||
| ): SchemaOf<AutoScalingFormValues> { | ||||
|   const { autoScalerOverflow } = validationData || {}; | ||||
|   return object({ | ||||
|     isUsed: boolean().required(), | ||||
|     minReplicas: number() | ||||
|       .min(0, 'Minimum instances must be greater than 0.') | ||||
|       .when('isUsed', (isUsed: boolean) => | ||||
|         isUsed | ||||
|           ? number() | ||||
|               .required('Minimum instances is required.') | ||||
|               .test( | ||||
|                 'maxReplicas', | ||||
|                 'Minimum instances must be less than maximum instances.', | ||||
|                 // eslint-disable-next-line func-names
 | ||||
|                 function (this, value?: number): boolean { | ||||
|                   if (!value) { | ||||
|                     return false; | ||||
|                   } | ||||
|                   const { maxReplicas } = this.parent as AutoScalingFormValues; | ||||
|                   return !maxReplicas || value < maxReplicas; | ||||
|                 } | ||||
|               ) | ||||
|           : number() | ||||
|       ), | ||||
|     maxReplicas: number().when('isUsed', (isUsed: boolean) => | ||||
|       isUsed | ||||
|         ? number() | ||||
|             .required('Maximum instances is required.') | ||||
|             .test( | ||||
|               'minReplicas', | ||||
|               'Maximum instances must be greater than minimum instances.', | ||||
|               // eslint-disable-next-line func-names
 | ||||
|               function (this, value?: number): boolean { | ||||
|                 if (!value) { | ||||
|                   return false; | ||||
|                 } | ||||
|                 const { minReplicas } = this.parent as AutoScalingFormValues; | ||||
|                 return !minReplicas || value > minReplicas; | ||||
|               } | ||||
|             ) | ||||
|             .test( | ||||
|               'overflow', | ||||
|               'This application would exceed available resources. Please reduce the maximum instances or the resource reservations.', | ||||
|               () => !autoScalerOverflow | ||||
|             ) | ||||
|         : number() | ||||
|     ), | ||||
|     targetCpuUtilizationPercentage: number().when( | ||||
|       'isUsed', | ||||
|       (isUsed: boolean) => | ||||
|         isUsed | ||||
|           ? number() | ||||
|               .min(0, 'Target CPU usage must be greater than 0.') | ||||
|               .max(100, 'Target CPU usage must be smaller than 100.') | ||||
|               .required('Target CPU utilization percentage is required.') | ||||
|           : number() | ||||
|     ), | ||||
|   }); | ||||
| } | ||||
|  | @ -0,0 +1,2 @@ | |||
| export { AutoScalingFormSection } from './AutoScalingFormSection'; | ||||
| export { autoScalingValidation } from './autoScalingValidation'; | ||||
|  | @ -0,0 +1,6 @@ | |||
| export type AutoScalingFormValues = { | ||||
|   isUsed: boolean; | ||||
|   minReplicas?: number; | ||||
|   maxReplicas?: number; | ||||
|   targetCpuUtilizationPercentage?: number; | ||||
| }; | ||||
		Loading…
	
		Reference in New Issue
	
	 Ali
						Ali