import type { FormikErrors, FormikProps } from 'formik'; import { Field } from 'formik'; import React from 'react'; import type { Application, IServerGroup, IWizardPageComponent } from '@spinnaker/core'; import { AccountSelectInput, DeployingIntoManagedClusterWarning, DeploymentStrategySelector, HelpField, Markdown, NameUtils, ReactInjector, RegionSelectField, ServerGroupDetailsField, ServerGroupNamePreview, SETTINGS, TaskReason, } from '@spinnaker/core'; import { AmazonImageSelectInput } from '../../AmazonImageSelectInput'; import { AWSProviderSettings } from '../../../../aws.settings'; import type { IAmazonImage } from '../../../../image'; import type { IAmazonServerGroupCommand } from '../../serverGroupConfiguration.service'; import { SubnetSelectField } from '../../../../subnet'; const isExpressionLanguage = (field: string) => field && field.includes('${'); const isStackPattern = (stack: string) => !isExpressionLanguage(stack) ? /^([a-zA-Z_0-9._${}]*(\${.+})*)*$/.test(stack) : true; const isDetailPattern = (detail: string) => !isExpressionLanguage(detail) ? /^([a-zA-Z_0-9._${}-]*(\${.+})*)*$/.test(detail) : true; export interface IServerGroupBasicSettingsProps { app: Application; formik: FormikProps; } export interface IServerGroupBasicSettingsState { selectedImage: IAmazonImage; namePreview: string; createsNewCluster: boolean; latestServerGroup: IServerGroup; } export class ServerGroupBasicSettings extends React.Component implements IWizardPageComponent { constructor(props: IServerGroupBasicSettingsProps) { super(props); const { amiName, region, viewState: { imageId }, } = props.formik.values; const selectedImage = AmazonImageSelectInput.makeFakeImage(amiName, imageId, region); this.state = { ...this.getStateFromProps(props), selectedImage }; } private getStateFromProps(props: IServerGroupBasicSettingsProps) { const { app } = props; const { values } = props.formik; const namePreview = NameUtils.getClusterName(app.name, values.stack, values.freeFormDetails); const createsNewCluster = !app.clusters.find((c) => c.name === namePreview); const inCluster = (app.serverGroups.data as IServerGroup[]) .filter((serverGroup) => { return ( serverGroup.cluster === namePreview && serverGroup.account === values.credentials && serverGroup.region === values.region ); }) .sort((a, b) => a.createdTime - b.createdTime); const latestServerGroup = inCluster.length ? inCluster.pop() : null; return { namePreview, createsNewCluster, latestServerGroup }; } private imageChanged = (image: IAmazonImage) => { const { setFieldValue, values } = this.props.formik; this.setState({ selectedImage: image }); const virtualizationType = image && image.attributes.virtualizationType; const amiArchitecture = image && image.attributes.architecture; const imageName = image && image.imageName; values.virtualizationType = virtualizationType; values.amiArchitecture = amiArchitecture; values.amiName = imageName; setFieldValue('amiArchitecture', amiArchitecture); setFieldValue('virtualizationType', virtualizationType); setFieldValue('amiName', imageName); values.imageChanged(values); if (image && SETTINGS.disabledImages?.length && AWSProviderSettings.serverGroups?.enableIPv6) { const isImageDisabled = SETTINGS.disabledImages.some((i) => image.imageName.includes(i)); if (isImageDisabled) { setFieldValue('associateIPv6Address', false); } } // an image change might clear the single or multiple instance types previously selected with the image, validate the form to surface related errors. this.props.formik.validateForm(); }; private accountUpdated = (account: string): void => { const { setFieldValue, values } = this.props.formik; values.credentials = account; values.credentialsChanged(values); values.subnetChanged(values); setFieldValue('credentials', account); const accountDetails = values.backingData.credentialsKeyedByAccount[account]; const enableIPv6InTest = AWSProviderSettings?.serverGroups?.enableIPv6 && AWSProviderSettings?.serverGroups?.setIPv6InTest && accountDetails.environment === 'test'; setFieldValue('associateIPv6Address', enableIPv6InTest); if (AWSProviderSettings.serverGroups?.enableIMDSv2) { const isIMDSv2AllowedOnAccount = !AWSProviderSettings?.serverGroups?.accountDenyListIMDSv2?.includes(account); const appAgeRequirement = AWSProviderSettings.serverGroups?.defaultIMDSv2AppAgeLimit; const creationDate = this.props.app?.attributes?.createTs; const setIMDSv2 = isIMDSv2AllowedOnAccount && appAgeRequirement && creationDate && Number(creationDate) > appAgeRequirement; setFieldValue('requireIMDSv2', setIMDSv2); } }; private regionUpdated = (region: string): void => { const { values, setFieldValue } = this.props.formik; values.region = region; values.regionChanged(values); setFieldValue('region', region); }; private subnetUpdated = (): void => { const { setFieldValue, values } = this.props.formik; values.subnetChanged(values); setFieldValue('subnetType', values.subnetType); }; public validate(values: IAmazonServerGroupCommand): FormikErrors { const errors: FormikErrors = {}; if (!isStackPattern(values.stack)) { errors.stack = 'Only dot(.) and underscore(_) special characters are allowed in the Stack field.'; } if (!isDetailPattern(values.freeFormDetails)) { errors.freeFormDetails = 'Only dot(.), underscore(_), and dash(-) special characters are allowed in the Detail field.'; } if (!values.viewState.disableImageSelection && !values.amiName) { errors.amiName = 'Image required.'; } // this error is added exclusively to disable the "create/clone" button - it is not visible aside from the warning // rendered by the DeployingIntoManagedClusterWarning component if (values.resourceSummary) { errors.resourceSummary = { id: 'Cluster is managed' }; } return errors; } private clientRequestsChanged = () => { const { values, setFieldValue } = this.props.formik; values.toggleSuspendedProcess(values, 'AddToLoadBalancer'); setFieldValue('suspendedProcesses', values.suspendedProcesses); this.setState({}); }; private navigateToLatestServerGroup = () => { const { values } = this.props.formik; const { latestServerGroup } = this.state; const params = { provider: values.selectedProvider, accountId: latestServerGroup.account, region: latestServerGroup.region, serverGroup: latestServerGroup.name, }; const { $state } = ReactInjector; if ($state.is('home.applications.application.insight.clusters')) { $state.go('.serverGroup', params); } else { $state.go('^.serverGroup', params); } }; private stackChanged = (stack: string) => { const { setFieldValue, values } = this.props.formik; values.stack = stack; // have to do it here to make sure it's done before calling values.clusterChanged setFieldValue('stack', stack); values.clusterChanged(values); }; public componentWillReceiveProps(nextProps: IServerGroupBasicSettingsProps) { this.setState(this.getStateFromProps(nextProps)); } private handleReasonChanged = (reason: string) => { this.props.formik.setFieldValue('reason', reason); }; private strategyChanged = (values: IAmazonServerGroupCommand, strategy: any) => { values.onStrategyChange(values, strategy); this.props.formik.setFieldValue('strategy', strategy.key); }; private onStrategyFieldChange = (key: string, value: any) => { this.props.formik.setFieldValue(key, value); }; public render() { const { app, formik } = this.props; const { errors, values } = formik; const { createsNewCluster, latestServerGroup, namePreview } = this.state; const accounts = values.backingData.accounts; const readOnlyFields = values.viewState.readOnlyFields || {}; return (
{values.regionIsDeprecated(values) && (
You are deploying into a deprecated region within the {values.credentials} account!
)}
Account
this.accountUpdated(evt.target.value)} readOnly={readOnlyFields.credentials} accounts={accounts} provider="aws" />
Stack
this.stackChanged(e.target.value)} />
{errors.stack && (
{errors.stack}
)} {values.viewState.imageSourceText && (
Image Source
)} {!values.viewState.disableImageSelection && (
Image
{isExpressionLanguage(values.amiName) ? ( ) : ( this.imageChanged(image)} value={this.state.selectedImage} application={app} credentials={values.credentials} region={values.region} /> )}
)}
Traffic
{!values.viewState.disableStrategySelection && values.selectedProvider && ( )} {!values.viewState.hideClusterNamePreview && ( )}
); } }