import type { Formik, FormikErrors, FormikProps } from 'formik'; import { Form } from 'formik'; import { pickBy } from 'lodash'; import React from 'react'; import { Modal } from 'react-bootstrap'; import type { Application, ICapacity, IModalComponentProps, IServerGroupJob } from '@spinnaker/core'; import { CheckboxInput, confirmNotManaged, FormikFormField, HelpField, MinMaxDesiredChanges, ModalClose, noop, NumberInput, PlatformHealthOverride, ReactInjector, ReactModal, SpinFormik, TaskMonitor, TaskMonitorWrapper, TaskReason, ValidationMessage, } from '@spinnaker/core'; import { AwsModalFooter } from '../../../common'; import type { IAmazonServerGroup } from '../../../domain'; export interface IAmazonResizeServerGroupModalProps extends IModalComponentProps { application: Application; serverGroup: IAmazonServerGroup; } export interface IAmazonResizeServerGroupModalState { advancedMode: boolean; interestingHealthProviderNames: string[]; // managed by a separate component initialValues: IAmazonResizeServerGroupValues; taskMonitor: TaskMonitor; platformHealthOnlyShowOverride?: boolean; } export interface IAmazonResizeServerGroupValues { min: number; max: number; desired: number; enforceCapacityConstraints: boolean; reason?: string; } export interface IResizeJob extends IServerGroupJob { constraints?: { capacity: ICapacity }; reason?: string; interestingHealthProviderNames: string[]; } export class AmazonResizeServerGroupModal extends React.Component< IAmazonResizeServerGroupModalProps, IAmazonResizeServerGroupModalState > { public static defaultProps: Partial = { closeModal: noop, dismissModal: noop, }; private formikRef = React.createRef>(); public static show(props: IAmazonResizeServerGroupModalProps) { const modalProps = {}; const { serverGroup, application } = props; return confirmNotManaged(serverGroup, application).then((notManaged) => { notManaged && ReactModal.show(AmazonResizeServerGroupModal, props, modalProps); }); } constructor(props: IAmazonResizeServerGroupModalProps) { super(props); const { minSize, maxSize, desiredCapacity } = props.serverGroup.asg; const { attributes } = props.application; this.state = { advancedMode: minSize !== maxSize, initialValues: { min: minSize, max: maxSize, desired: desiredCapacity, enforceCapacityConstraints: false, }, taskMonitor: new TaskMonitor({ application: props.application, title: 'Resizing your server group', modalInstance: TaskMonitor.modalInstanceEmulation(() => this.props.dismissModal()), onTaskComplete: () => this.props.application.serverGroups.refresh(), }), platformHealthOnlyShowOverride: attributes.platformHealthOnlyShowOverride, interestingHealthProviderNames: attributes.platformHealthOnlyShowOverride && attributes.platformHealthOnly ? ['Amazon'] : null, }; } private validate = ( values: IAmazonResizeServerGroupValues, ): Partial> => { const { min, max, desired } = values; const errors: Partial> = {}; if (!this.state.advancedMode) { return errors; } // try to only show one error message at a time if (min > max && min > desired) { errors.min = 'Min cannot be larger than Max/Desired'; } else if (max < min && max < desired) { errors.max = 'Max cannot be smaller than Min/Desired'; } else { if (min > max) { errors.min = 'Min cannot be larger than Max'; } if (!this.isDesiredControlledByAutoscaling()) { if (desired < min) { errors.desired = 'Desired cannot be smaller than Min'; } if (desired > max) { errors.desired = 'Desired cannot be larger than Max'; } } } return errors; }; private toggleAdvancedMode = (): void => { const { desired } = this.formikRef.current.getFormikContext().values; this.formikRef.current.setFieldValue('min', desired); this.formikRef.current.setFieldValue('max', desired); this.setState({ advancedMode: !this.state.advancedMode }); }; private close = (args?: any): void => { this.props.dismissModal.apply(null, args); }; private autoIncrementDesiredIfNeeded(): void { if (!this.isDesiredControlledByAutoscaling()) { return; } const formik = this.formikRef.current.getFormikContext(); const { asg } = this.props.serverGroup; const { min, max, desired } = formik.values; const newDesired = Math.min(max, Math.max(min, asg.desiredCapacity)); if (desired !== newDesired) { formik.setFieldValue('desired', newDesired); } } private platformHealthOverrideChanged = (interestingHealthProviderNames: string[]) => { this.setState({ interestingHealthProviderNames }); }; private isDesiredControlledByAutoscaling = (): boolean => { const { serverGroup } = this.props; const { suspendedProcesses } = serverGroup.asg; const { advancedMode } = this.state; const scalingPolicies = serverGroup.scalingPolicies || []; return ( scalingPolicies.length && advancedMode && suspendedProcesses.every((p) => p.processName !== 'AlarmNotification') ); }; private submit = (values: IAmazonResizeServerGroupValues): void => { const { min, max, desired, enforceCapacityConstraints, reason } = values; const { interestingHealthProviderNames } = this.state; const { serverGroup, application } = this.props; const { asg } = serverGroup; const capacity = pickBy( { min: min !== asg.minSize ? min : undefined, max: max !== asg.maxSize ? max : undefined, desired: desired !== asg.desiredCapacity ? desired : undefined, }, (x) => x !== undefined, ); const command: IResizeJob = { capacity, reason, interestingHealthProviderNames, }; if (enforceCapacityConstraints) { command.constraints = { capacity: { min: asg.minSize, max: asg.maxSize, desired: asg.desiredCapacity, }, }; } this.state.taskMonitor.submit(() => { return ReactInjector.serverGroupWriter.resizeServerGroup(serverGroup, application, command); }); }; private renderSimpleMode(formik: FormikProps): JSX.Element { const { serverGroup } = this.props; const { asg } = serverGroup; return (

Sets min, max, and desired instance counts to the same value.

To allow autoscaling, use the{' '} this.toggleAdvancedMode()}> Advanced Mode .

Current size
instances
Resize to
} touched={true} onChange={(value) => { formik.setFieldValue('min', value); formik.setFieldValue('max', value); }} />
instances
); } private renderAdvancedMode(formik: FormikProps): JSX.Element { const { serverGroup } = this.props; const { errors } = formik; const { asg } = serverGroup; const surfacedErrorMessage: string = errors.min || errors.max || errors.desired; return (

Sets up autoscaling for this server group.

To disable autoscaling, use the{' '} this.toggleAdvancedMode()}> Simple Mode .

Min
Max
Desired
Current
Resize to
} onChange={() => this.autoIncrementDesiredIfNeeded()} layout={({ input }) => <>{input}} touched={true} />
} onChange={() => this.autoIncrementDesiredIfNeeded()} layout={({ input }) => <>{input}} touched={true} />
} layout={({ input }) => <>{input}} touched={true} />
{!!surfacedErrorMessage && (
)}
); } private renderCapacityConstraintSelector(): JSX.Element { return (
( <> )} />
); } private renderScalingPolicyWarning(formik: FormikProps): JSX.Element { const { serverGroup } = this.props; const { min, max } = formik.values; const { advancedMode } = this.state; const scalingPolicies = serverGroup.scalingPolicies || []; if (scalingPolicies.length && min === max) { return (
Warning: this server group has {scalingPolicies.length === 1 && a scaling policy. } {scalingPolicies.length > 1 && scaling policies. } {!advancedMode && ( Scaling policies will not take effect in Simple Mode. Switch to{' '} this.toggleAdvancedMode()}> Advanced Mode . )} {advancedMode && ( Scaling policies will not take effect when Min is the same as Max. )}
); } if (this.isDesiredControlledByAutoscaling()) { return (

Desired capacity is managed by Autoscaling Policies.

If you need to scale down this server group, set Max to the new desired size.

If you need to scale up this server group, set Min to the new desired size.

); } return null; } public render() { const { serverGroup } = this.props; const { advancedMode, initialValues, platformHealthOnlyShowOverride } = this.state; return ( <> ref={this.formikRef} initialValues={initialValues} validate={this.validate} onSubmit={this.submit} render={(formik) => { const { asg } = serverGroup; const currentCapacity = { min: asg.minSize, max: asg.maxSize, desired: asg.desiredCapacity }; const nextCapacity = formik.values; return ( <> Resize {serverGroup.name}
{advancedMode && this.renderAdvancedMode(formik)} {!advancedMode && this.renderSimpleMode(formik)} {this.renderScalingPolicyWarning(formik)} {this.renderCapacityConstraintSelector()} {platformHealthOnlyShowOverride && (
)} formik.setFieldValue('reason', val)} />
Changes
this.submit(formik.values)} onCancel={this.close} isValid={formik.isValid} account={serverGroup.account} /> ); }} /> ); } }