import type { FormikErrors, FormikProps } from 'formik'; import { filter, flatten, groupBy, set, uniq } from 'lodash'; import React from 'react'; import { from as observableFrom, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import type { Application, IWizardPageComponent } from '@spinnaker/core'; import { FormValidator, HelpField, spelNumberCheck, SpelNumberInput, SpInput, ValidationMessage, Validators, } from '@spinnaker/core'; import { isNameInUse, isNameLong, isValidHealthCheckInterval, isValidTimeout } from '../common/targetGroupValidators'; import type { IAmazonApplicationLoadBalancer, IAmazonApplicationLoadBalancerUpsertCommand } from '../../../domain'; export interface ITargetGroupsProps { app: Application; formik: FormikProps; isNew: boolean; loadBalancer: IAmazonApplicationLoadBalancer; } export interface ITargetGroupsState { existingTargetGroupNames: { [account: string]: { [region: string]: string[] } }; oldTargetGroupCount: number; } export class TargetGroups extends React.Component implements IWizardPageComponent { public protocols = ['HTTP', 'HTTPS']; public targetTypes = ['instance', 'ip', 'lambda']; private destroy$ = new Subject(); constructor(props: ITargetGroupsProps) { super(props); const oldTargetGroupCount = !props.isNew ? props.formik.initialValues.targetGroups.length : 0; this.state = { existingTargetGroupNames: {}, oldTargetGroupCount, }; } public validate( values: IAmazonApplicationLoadBalancerUpsertCommand, ): FormikErrors { const duplicateTargetGroups = uniq( flatten(filter(groupBy(values.targetGroups, 'name'), (count) => count.length > 1)).map((tg) => tg.name), ); const formValidator = new FormValidator(values); const { arrayForEach } = formValidator; formValidator.field('targetGroups').withValidators( arrayForEach((builder, item) => { builder .field('name', 'Name') .withValidators( isNameInUse(this.state.existingTargetGroupNames, values.credentials, values.region), isNameLong(this.props.app.name.length), Validators.valueUnique( duplicateTargetGroups, 'There is already a target group in this load balancer with the same name.', ), ); builder .field('healthCheckInterval', 'Health Check Interval') .withValidators(isValidHealthCheckInterval(item), Validators.checkBetween('healthCheckInterval', 5, 300)); builder .field('healthyThreshold', 'Healthy Threshold') .withValidators(Validators.checkBetween('healthyThreshold', 2, 10)); builder .field('unhealthyThreshold', 'Unhealthy Threshold') .spelAware() .withValidators(Validators.checkBetween('unhealthyThreshold', 2, 10)); builder .field('healthCheckTimeout', 'Timeout') .withValidators(isValidTimeout(item), Validators.checkBetween('healthCheckTimeout', 2, 120)); if (item.targetType !== 'lambda') { builder.field('protocol', 'Protocol').required(); builder.field('healthCheckPath', 'Health Check Path').required(); builder.field('healthCheckProtocol', 'Health Check Protocol').required(); builder.field('name', 'Name').required(); builder .field('healthyThreshold', 'Healthy Threshold') .required() .spelAware() .withValidators((value) => spelNumberCheck(value)); builder .field('unhealthyThreshold', 'Unhealthy Threshold') .required() .spelAware() .withValidators((value) => spelNumberCheck(value)); builder .field('healthCheckInterval', 'Health Check Interval') .required() .spelAware() .withValidators((value) => spelNumberCheck(value)); builder .field('healthCheckPort', 'Health Check Port') .required() .spelAware() .withValidators((value) => (value === 'traffic-port' ? null : spelNumberCheck(value))); builder .field('port', 'Port') .required() .spelAware() .withValidators((value) => spelNumberCheck(value)); builder .field('healthyThreshold', 'Healthy Threshold') .required() .spelAware() .withValidators((value) => spelNumberCheck(value), Validators.checkBetween('healthyThreshold', 2, 10)); builder .field('unhealthyThreshold', 'Unhealthy Threshold') .required() .spelAware() .withValidators((value) => spelNumberCheck(value), Validators.checkBetween('unhealthyThreshold', 2, 10)); } }), ); return formValidator.validateForm(); } private removeAppName(name: string): string { return name.replace(`${this.props.app.name}-`, ''); } protected updateLoadBalancerNames(props: ITargetGroupsProps): void { const { app, loadBalancer } = props; const targetGroupsByAccountAndRegion: { [account: string]: { [region: string]: string[] } } = {}; observableFrom(app.getDataSource('loadBalancers').refresh(true)) .pipe(takeUntil(this.destroy$)) .subscribe(() => { app.getDataSource('loadBalancers').data.forEach((lb: IAmazonApplicationLoadBalancer) => { if (lb.loadBalancerType !== 'classic') { if (!loadBalancer || lb.name !== loadBalancer.name) { lb.targetGroups.forEach((targetGroup) => { targetGroupsByAccountAndRegion[lb.account] = targetGroupsByAccountAndRegion[lb.account] || {}; targetGroupsByAccountAndRegion[lb.account][lb.region] = targetGroupsByAccountAndRegion[lb.account][lb.region] || []; targetGroupsByAccountAndRegion[lb.account][lb.region].push(this.removeAppName(targetGroup.name)); }); } } }); this.setState({ existingTargetGroupNames: targetGroupsByAccountAndRegion }, () => this.props.formik.validateForm(), ); }); } private targetGroupFieldChanged(index: number, field: string, value: string | boolean | number): void { const { setFieldValue, values } = this.props.formik; const targetGroup = values.targetGroups[index]; if (field === 'targetType' && value === 'lambda') { delete targetGroup.port; } set(targetGroup, field, value); setFieldValue('targetGroups', values.targetGroups); } private addTargetGroup = (): void => { const { setFieldValue, values } = this.props.formik; const tgLength = values.targetGroups.length; values.targetGroups.push({ name: `targetgroup${tgLength ? `${tgLength}` : ''}`, protocol: 'HTTP', port: 7001, targetType: 'instance', healthCheckProtocol: 'HTTP', healthCheckPort: '7001', healthCheckPath: '/healthcheck', healthCheckTimeout: 5, healthCheckInterval: 10, healthyThreshold: 10, unhealthyThreshold: 2, attributes: { deregistrationDelay: 300, stickinessEnabled: false, stickinessType: 'lb_cookie', stickinessDuration: 8400, }, }); setFieldValue('targetGroups', values.targetGroups); }; private removeTargetGroup(index: number): void { const { setFieldValue, values } = this.props.formik; const { oldTargetGroupCount } = this.state; values.targetGroups.splice(index, 1); if (index < oldTargetGroupCount) { this.setState({ oldTargetGroupCount: oldTargetGroupCount - 1 }); } setFieldValue('targetGroups', values.targetGroups); } public componentDidMount(): void { this.updateLoadBalancerNames(this.props); } public componentWillUnmount(): void { this.destroy$.next(); this.destroy$.complete(); } public render() { const { app } = this.props; const { errors, values } = this.props.formik; const { oldTargetGroupCount } = this.state; const ProtocolOptions = this.protocols.map((p) => ); const TargetTypeOptions = this.targetTypes.map((p) => ); return (
{values.targetGroups.map((targetGroup, index) => { const tgErrors = (errors.targetGroups && errors.targetGroups[index]) || {}; const has6sTimeout = (targetGroup.protocol === 'TCP' || targetGroup.protocol === 'TLS') && targetGroup.healthCheckProtocol === 'HTTP'; const has10sTimeout = (targetGroup.protocol === 'TCP' || targetGroup.protocol === 'TLS') && targetGroup.healthCheckProtocol === 'HTTPS'; return (
Group Name
{app.name}- this.targetGroupFieldChanged(index, 'name', event.target.value)} required={true} disabled={index < oldTargetGroupCount} /> this.removeTargetGroup(index)}>
{tgErrors.name && (
)}
Target Type 
{targetGroup.targetType !== 'lambda' && (
Backend Connection
{' '} {' '} this.targetGroupFieldChanged(index, 'port', event.target.value)} type="text" required={true} disabled={index < oldTargetGroupCount} />
)}
Healthcheck
{targetGroup.targetType !== 'lambda' && ( {targetGroup.healthCheckProtocol === 'TCP' && ( )}{' '} )} {targetGroup.targetType !== 'lambda' && ( {' '} {' '} this.targetGroupFieldChanged(index, 'healthCheckPort', event.target.value) } /> )} this.targetGroupFieldChanged(index, 'healthCheckPath', event.target.value) } /> {(has6sTimeout || has10sTimeout) && } this.targetGroupFieldChanged(index, 'healthCheckTimeout', value) } /> this.targetGroupFieldChanged(index, 'healthCheckInterval', value) } />
Healthcheck Threshold
this.targetGroupFieldChanged(index, 'healthyThreshold', value) } /> this.targetGroupFieldChanged(index, 'unhealthyThreshold', value) } />
Attributes
{targetGroup.targetType !== 'lambda' ? (
{' '} this.targetGroupFieldChanged( index, 'attributes.deregistrationDelay', event.target.value, ) } /> {targetGroup.attributes.stickinessEnabled && ( {' '} this.targetGroupFieldChanged( index, 'attributes.stickinessDuration', event.target.value, ) } type="text" /> )}
) : ( )}
); })}
); } }