import { cloneDeep, get } from 'lodash'; import React from 'react'; import type { ILoadBalancerModalProps } from '@spinnaker/core'; import { AccountService, FirewallLabels, LoadBalancerWriter, noop, ReactInjector, ReactModal, TaskMonitor, WizardModal, WizardPage, } from '@spinnaker/core'; import { ALBAdvancedSettings } from './ALBAdvancedSettings'; import { ALBListeners } from './ALBListeners'; import { TargetGroups } from './TargetGroups'; import { AWSProviderSettings } from '../../../aws.settings'; import { LoadBalancerLocation } from '../common/LoadBalancerLocation'; import { SecurityGroups } from '../common/SecurityGroups'; import type { IAmazonApplicationLoadBalancer, IAmazonApplicationLoadBalancerUpsertCommand } from '../../../domain'; import { AwsLoadBalancerTransformer } from '../../loadBalancer.transformer'; import '../common/configure.less'; export interface ICreateApplicationLoadBalancerProps extends ILoadBalancerModalProps { loadBalancer: IAmazonApplicationLoadBalancer; } export interface ICreateApplicationLoadBalancerState { includeSecurityGroups: boolean; isNew: boolean; loadBalancerCommand: IAmazonApplicationLoadBalancerUpsertCommand; taskMonitor: TaskMonitor; } export class CreateApplicationLoadBalancer extends React.Component< ICreateApplicationLoadBalancerProps, ICreateApplicationLoadBalancerState > { public static defaultProps: Partial = { closeModal: noop, dismissModal: noop, }; private _isUnmounted = false; private refreshUnsubscribe: () => void; private certificateTypes = get(AWSProviderSettings, 'loadBalancers.certificateTypes', ['iam', 'acm']); public static show(props: ICreateApplicationLoadBalancerProps): Promise { const modalProps = { dialogClassName: 'wizard-modal modal-lg' }; return ReactModal.show(CreateApplicationLoadBalancer, props, modalProps); } constructor(props: ICreateApplicationLoadBalancerProps) { super(props); const loadBalancerCommand = props.command ? (props.command as IAmazonApplicationLoadBalancerUpsertCommand) // ejecting from a wizard : props.loadBalancer ? AwsLoadBalancerTransformer.convertApplicationLoadBalancerForEditing(props.loadBalancer) : AwsLoadBalancerTransformer.constructNewApplicationLoadBalancerTemplate(props.app); this.state = { includeSecurityGroups: !!loadBalancerCommand.vpcId, isNew: !props.loadBalancer, loadBalancerCommand, taskMonitor: null, }; } protected certificateIdAsARN( accountId: string, certificateId: string, region: string, certificateType: string, ): string { if ( certificateId && (certificateId.indexOf('arn:aws:iam::') !== 0 || certificateId.indexOf('arn:aws:acm:') !== 0) ) { // If they really want to enter the ARN... if (certificateType === 'iam') { return `arn:aws:iam::${accountId}:server-certificate/${certificateId}`; } if (certificateType === 'acm') { return `arn:aws:acm:${region}:${accountId}:certificate/${certificateId}`; } } return certificateId; } private formatListeners(command: IAmazonApplicationLoadBalancerUpsertCommand): PromiseLike { return AccountService.getAccountDetails(command.credentials).then((account) => { command.listeners.forEach((listener) => { if (listener.protocol === 'HTTP') { delete listener.sslPolicy; listener.certificates = []; } listener.certificates.forEach((certificate) => { certificate.certificateArn = this.certificateIdAsARN( account.accountId, certificate.name, command.region, certificate.type || this.certificateTypes[0], ); }); }); }); } private setAvailabilityZones(loadBalancerCommand: IAmazonApplicationLoadBalancerUpsertCommand): void { const availabilityZones: { [region: string]: string[] } = {}; availabilityZones[loadBalancerCommand.region] = loadBalancerCommand.regionZones || []; loadBalancerCommand.availabilityZones = availabilityZones; } private addAppName(name: string): string { return `${this.props.app.name}-${name}`; } private manageTargetGroupNames(command: IAmazonApplicationLoadBalancerUpsertCommand): void { (command.targetGroups || []).forEach((targetGroupDescription) => { targetGroupDescription.name = this.addAppName(targetGroupDescription.name); }); (command.listeners || []).forEach((listenerDescription) => { listenerDescription.defaultActions.forEach((actionDescription) => { if (actionDescription.targetGroupName) { actionDescription.targetGroupName = this.addAppName(actionDescription.targetGroupName); } }); (listenerDescription.rules || []).forEach((ruleDescription) => { ruleDescription.actions.forEach((actionDescription) => { if (actionDescription.targetGroupName) { actionDescription.targetGroupName = this.addAppName(actionDescription.targetGroupName); } }); }); }); } private manageRules(command: IAmazonApplicationLoadBalancerUpsertCommand): void { command.listeners.forEach((listener) => { listener.rules.forEach((rule, index) => { // Set the priority in array order, starting with 1 rule.priority = index + 1; // Remove conditions that have no value rule.conditions = rule.conditions.filter((condition) => { if (condition.field !== 'http-request-method') { return condition.values[0].length > 0; } return condition.values.length > 0; }); }); }); } private setIpAddressType(command: IAmazonApplicationLoadBalancerUpsertCommand): void { command.ipAddressType = command.dualstack ? 'dualstack' : 'ipv4'; delete command.dualstack; } private formatCommand(command: IAmazonApplicationLoadBalancerUpsertCommand): void { this.setAvailabilityZones(command); this.manageTargetGroupNames(command); this.manageRules(command); this.setIpAddressType(command); } protected onApplicationRefresh(values: IAmazonApplicationLoadBalancerUpsertCommand): void { if (this._isUnmounted) { return; } this.refreshUnsubscribe = undefined; this.props.dismissModal(); this.setState({ taskMonitor: undefined }); const newStateParams = { name: values.name, accountId: values.credentials, region: values.region, vpcId: values.vpcId, provider: 'aws', }; if (!ReactInjector.$state.includes('**.loadBalancerDetails')) { ReactInjector.$state.go('.loadBalancerDetails', newStateParams); } else { ReactInjector.$state.go('^.loadBalancerDetails', newStateParams); } } public componentWillUnmount(): void { this._isUnmounted = true; if (this.refreshUnsubscribe) { this.refreshUnsubscribe(); } } private onTaskComplete(values: IAmazonApplicationLoadBalancerUpsertCommand): void { this.props.app.loadBalancers.refresh(); this.refreshUnsubscribe = this.props.app.loadBalancers.onNextRefresh(null, () => this.onApplicationRefresh(values)); } private submit = (values: IAmazonApplicationLoadBalancerUpsertCommand): void => { const { app, forPipelineConfig, closeModal } = this.props; const { isNew } = this.state; const descriptor = isNew ? 'Create' : 'Update'; const loadBalancerCommandFormatted = cloneDeep(values); // replace all authenticateOidcConfig with authenticateOidcActionConfig because aws loadBalancerCommandFormatted.listeners.forEach((listener) => { listener.defaultActions.forEach((a: any) => { if (a.authenticateOidcConfig) { a.authenticateOidcActionConfig = a.authenticateOidcConfig; delete a.authenticateOidcConfig; } }); listener.rules.forEach((r) => r.actions.forEach((a: any) => { if (a.authenticateOidcConfig) { a.authenticateOidcActionConfig = a.authenticateOidcConfig; delete a.authenticateOidcConfig; } }), ); }); if (forPipelineConfig) { // don't submit to backend for creation. Just return the loadBalancerCommand object this.formatListeners(loadBalancerCommandFormatted).then(() => { this.setIpAddressType(loadBalancerCommandFormatted); closeModal && closeModal(loadBalancerCommandFormatted); }); } else { const taskMonitor = new TaskMonitor({ application: app, title: `${isNew ? 'Creating' : 'Updating'} your load balancer`, modalInstance: TaskMonitor.modalInstanceEmulation(() => this.props.dismissModal()), onTaskComplete: () => this.onTaskComplete(loadBalancerCommandFormatted), }); taskMonitor.submit(() => { return this.formatListeners(loadBalancerCommandFormatted).then(() => { this.formatCommand(loadBalancerCommandFormatted); return LoadBalancerWriter.upsertLoadBalancer(loadBalancerCommandFormatted, app, descriptor); }); }); this.setState({ taskMonitor }); } }; public render() { const { app, dismissModal, forPipelineConfig, loadBalancer } = this.props; const { isNew, loadBalancerCommand, taskMonitor } = this.state; let heading = forPipelineConfig ? 'Configure Application Load Balancer' : 'Create New Application Load Balancer'; if (!isNew) { heading = `Edit ${loadBalancerCommand.name}: ${loadBalancerCommand.region}: ${loadBalancerCommand.credentials}`; } return ( heading={heading} initialValues={loadBalancerCommand} taskMonitor={taskMonitor} dismissModal={dismissModal} closeModal={this.submit} submitButtonLabel={forPipelineConfig ? (isNew ? 'Add' : 'Done') : isNew ? 'Create' : 'Update'} render={({ formik, nextIdx, wizard }) => { const showLocationSection = isNew || forPipelineConfig; const showSecurityGroups = !!formik.values.vpcId; return ( <> {showLocationSection && ( ( )} /> )} {showSecurityGroups && ( ( )} /> )} ( )} /> } /> } /> ); }} /> ); } }