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 ) => field && field . includes ( '${' ) ; const isStackPattern = ( stack ) => ! isExpressionLanguage ( stack ) ? /^([a-zA-Z_0-9._${}]*(\${.+})*)*$/ . test ( stack ) : true ; const isDetailPattern = ( detail ) => ! isExpressionLanguage ( detail ) ? /^([a-zA-Z_0-9._${}-]*(\${.+})*)*$/ . test ( detail ) : true ; export interface IServerGroupBasicSettingsProps { app : Application ; formik : FormikProps < IAmazonServerGroupCommand > ; } export interface IServerGroupBasicSettingsState { selectedImage : IAmazonImage ; namePreview : string ; createsNewCluster : boolean ; latestServerGroup : IServerGroup ; } export class ServerGroupBasicSettings extends React . Component < IServerGroupBasicSettingsProps , IServerGroupBasicSettingsState > implements IWizardPageComponent < IAmazonServerGroupCommand > { constructor ( props ) { 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 ) { 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 ) => { const { setFieldValue , values } = this . props . formik ; this . setState ( { selectedImage : image } ) ; const virtualizationType = image && image . attributes . virtualizationType ; const imageName = image && image . imageName ; values . virtualizationType = virtualizationType ; values . amiName = imageName ; 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 ) ; } } } ; private accountUpdated = ( account ) : 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 ) : 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 ) { const errors = { } ; 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.' ; } 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 ) => { const { setFieldValue , values } = this . props . formik ; values . stack = stack ; setFieldValue ( 'stack' , stack ) ; values . clusterChanged ( values ) ; } ; public componentWillReceiveProps ( nextProps ) { this . setState ( this . getStateFromProps ( nextProps ) ) ; } private handleReasonChanged = ( reason ) => { this . props . formik . setFieldValue ( 'reason' , reason ) ; } ; private strategyChanged = ( values , strategy ) => { values . onStrategyChange ( values , strategy ) ; this . props . formik . setFieldValue ( 'strategy' , strategy . key ) ; } ; private onStrategyFieldChange = ( key , value ) => { 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 ( < div className = "container-fluid form-horizontal" > { values . regionIsDeprecated ( values ) && ( < div className = "form-group row" > < div className = "col-md-12 error-message" > < div className = "alert alert-danger" > You are deploying into a deprecated region within the { values . credentials } account ! < /div> < /div> < /div>)} < DeployingIntoManagedClusterWarning app = { app } formik = { formik } / > < div className = "form-group" > < div className = "col-md-3 sm-label-right" > Account < /div> < div className = "col-md-7" > < AccountSelectInput value = { values . credentials } onChange = { ( evt ) => this . accountUpdated ( evt . target . value ) } readOnly = { readOnlyFields . credentials } accounts = { accounts } provider = "aws" / > < /div> < /div> < RegionSelectField readOnly = { readOnlyFields . region } labelColumns = { 3 } component = { values } field = "region" account = { values . credentials } regions = { values . backingData . filtered . regions } onChange = { this . regionUpdated } / > < SubnetSelectField readOnly = { readOnlyFields . subnet } labelColumns = { 3 } helpKey = "aws.serverGroup.subnet" component = { values } field = "subnetType" region = { values . region } application = { app } subnets = { values . backingData . filtered . subnetPurposes } onChange = { this . subnetUpdated } showSubnetWarning = { true } / > < div className = "form-group" > < div className = "col-md-3 sm-label-right" > Stack < HelpField id = "aws.serverGroup.stack" / > < /div> < div className = "col-md-7" > < input type = "text" className = "form-control input-sm no-spel" value = { values . stack } onChange = { ( e ) => this . stackChanged ( e . target . value ) } /> < /div> < /div> { errors . stack && ( < div className = "form-group row slide-in" > < div className = "col-sm-9 col-sm-offset-2 error-message" > < span > { errors . stack } < /span> < /div> < /div>)} < ServerGroupDetailsField app = { app } formik = { formik } / > { values . viewState . imageSourceText && ( < div className = "form-group" > < div className = "col-md-3 sm-label-right" > Image Source < / div > < div className = "col-md-7" style = { { marginTop : '5px' } } > < Markdown tag = "span" message = { values . viewState . imageSourceText } / > < /div> < /div>)} { ! values . viewState . disableImageSelection && ( < div className = "form-group" > < div className = "col-md-3 sm-label-right" > Image < HelpField id = "aws.serverGroup.imageName" / > < /div> { isExpressionLanguage ( values . amiName ) ? ( < Field name = "amiName" / > ) : ( < AmazonImageSelectInput onChange = { ( image ) => this . imageChanged ( image ) } value = { this . state . selectedImage } application = { app } credentials = { values . credentials } region = { values . region } / > ) } < /div>)} < div className = "form-group" > < div className = "col-md-3 sm-label-right" > Traffic < HelpField id = "aws.serverGroup.traffic" / > < /div> < div className = "col-md-9 checkbox" > < label > < input type = "checkbox" onChange = { this . clientRequestsChanged } checked = { ! values . processIsSuspended ( values , 'AddToLoadBalancer' ) } disabled = { values . strategy !== '' && values . strategy !== 'custom' } / > Send client requests to new instances < /label> < /div> < /div> { ! values . viewState . disableStrategySelection && values . selectedProvider && ( < DeploymentStrategySelector command = { values } onFieldChange = { this . onStrategyFieldChange } onStrategyChange = { this . strategyChanged } / > ) } { ! values . viewState . hideClusterNamePreview && ( < ServerGroupNamePreview createsNewCluster = { createsNewCluster } latestServerGroupName = { latestServerGroup ?. name } mode = { values . viewState . mode } namePreview = { namePreview } navigateToLatestServerGroup = { this . navigateToLatestServerGroup } / > ) } < TaskReason reason = { values . reason } onChange = { this . handleReasonChanged } / > < /div>); } }