Skip to content

Commit

Permalink
feat(provider/ecs): add support for services with multiple target gro…
Browse files Browse the repository at this point in the history
…ups (#7692)

* feat(provider/ecs): Implement multiple target group support in spinnaker

* Fix rebase issues

* feat(provider/ecs): Implement multiple target group support in spinnaker

* Fix rebase issues

* Add alert if no target groups are available

* remove duplicate

* Update add new target mappings
  • Loading branch information
piradeepk authored and mergify[bot] committed Dec 19, 2019
1 parent 82efc2f commit 5c9ba2c
Show file tree
Hide file tree
Showing 9 changed files with 448 additions and 184 deletions.
2 changes: 2 additions & 0 deletions app/scripts/modules/ecs/src/ecs.help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ const helpContents: { [key: string]: string } = {
'ecs.containerMappingName':
'<p>The name of the container. Name should match the <a href="https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ContainerDefinition.html#ECS-Type-ContainerDefinition-name"><b>containerDefinition.name</b></a> field as it appears in the Task Definition.</p>',
'ecs.containerMappingImage': '<p>The container image the named container should run.</p>',
'ecs.targetGroupMappings':
'<p>The list of target groups through which the ECS service will receive load balancer traffic. Each target group is mapped to a container name and port within the Task Definition to specify which container should be registered to the target group.</p>',
'ecs.loadBalancedContainer':
'<p>The container in the Task Definition that should receive traffic from the load balancer. Required if a load balancer target group has been specified.</p>',
'ecs.tags': '<p>The tags to apply to the task definition and the service',
Expand Down
4 changes: 2 additions & 2 deletions app/scripts/modules/ecs/src/ecs.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import './ecs.help';
import { COMMON_MODULE } from './common/common.module';
import { ECS_SERVERGROUP_MODULE } from './serverGroup/serverGroup.module';
import { ECS_SERVER_GROUP_LOGGING } from './serverGroup/configure/wizard/logging/logging.component';
import { CONTAINER_REACT } from './serverGroup/configure/wizard/container/Container';
import { TASK_DEFINITION_REACT } from './serverGroup/configure/wizard/taskDefinition/TaskDefinition';
import { ECS_SECURITY_GROUP_MODULE } from './securityGroup/securityGroup.module';

import './logo/ecs.logo.less';
import { ECS_SERVERGROUP_CONFIGURE_WIZARD_CLONESERVERGROUP_ECS_CONTROLLER } from './serverGroup/configure/wizard/CloneServerGroup.ecs.controller';
import { ECS_SERVERGROUP_CONFIGURE_WIZARD_ADVANCEDSETTINGS_ADVANCEDSETTINGS_COMPONENT } from './serverGroup/configure/wizard/advancedSettings/advancedSettings.component';
import { ECS_SERVERGROUP_CONFIGURE_WIZARD_CONTAINER_CONTAINER_COMPONENT } from './serverGroup/configure/wizard/container/container.component';
import { ECS_SERVERGROUP_CONFIGURE_WIZARD_HORIZONTALSCALING_HORIZONTALSCALING_COMPONENT } from './serverGroup/configure/wizard/horizontalScaling/horizontalScaling.component';
import { ECS_SERVERGROUP_CONFIGURE_WIZARD_SERVICEDISCOVERY_SERVICEDISCOVERY_COMPONENT } from './serverGroup/configure/wizard/serviceDiscovery/serviceDiscovery.component';
import { ECS_SERVERGROUP_CONFIGURE_WIZARD_LOCATION_SERVERGROUPBASICSETTINGS_CONTROLLER } from './serverGroup/configure/wizard/location/ServerGroupBasicSettings.controller';
Expand Down Expand Up @@ -53,9 +53,9 @@ module(ECS_MODULE, [
ECS_SERVER_GROUP_TRANSFORMER,
// require('./pipeline/stages/cloneServerGroup/ecsCloneServerGroupStage').name, // TODO(Bruno Carrier): We should enable this on Clouddriver before revealing this stage
ECS_SERVERGROUP_CONFIGURE_WIZARD_ADVANCEDSETTINGS_ADVANCEDSETTINGS_COMPONENT,
ECS_SERVERGROUP_CONFIGURE_WIZARD_CONTAINER_CONTAINER_COMPONENT,
ECS_SERVERGROUP_CONFIGURE_WIZARD_HORIZONTALSCALING_HORIZONTALSCALING_COMPONENT,
TASK_DEFINITION_REACT,
CONTAINER_REACT,
ECS_SERVER_GROUP_LOGGING,
ECS_NETWORKING_SECTION,
ECS_CLUSTER_READ_SERVICE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,19 @@ export interface IEcsContainerMapping {
imageDescription: IEcsDockerImage;
}

export interface IEcsTargetGroupMapping {
containerName: string;
containerPort: number;
targetGroup: string;
}

export interface IEcsServerGroupCommand extends IServerGroupCommand {
backingData: IEcsServerGroupCommandBackingData;
computeUnits: number;
reservedMemory: number;
targetHealthyDeployPercentage: number;
targetGroup: string;
containerPort: number;
placementStrategyName: string;
placementStrategySequence: IPlacementStrategy[];
imageDescription: IEcsDockerImage;
Expand All @@ -114,6 +123,7 @@ export interface IEcsServerGroupCommand extends IServerGroupCommand {
taskDefinitionArtifactAccount: string;
containerMappings: IEcsContainerMapping[];
loadBalancedContainer: string;
targetGroupMappings: IEcsTargetGroupMapping[];

subnetTypeChanged: (command: IEcsServerGroupCommand) => IServerGroupCommandResult;
placementStrategyNameChanged: (command: IEcsServerGroupCommand) => IServerGroupCommandResult;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
import * as React from 'react';
import { module, IPromise } from 'angular';
import { uniqWith, isEqual } from 'lodash';
import { react2angular } from 'react2angular';
import {
IEcsDockerImage,
IEcsServerGroupCommand,
IEcsTargetGroupMapping,
} from '../../serverGroupConfiguration.service';
import { HelpField } from '@spinnaker/core';
import { Alert } from 'react-bootstrap';

export interface IContainerProps {
command: IEcsServerGroupCommand;
notifyAngular: (key: string, value: any) => void;
configureCommand: (query: string) => IPromise<void>;
}

interface IContainerState {
imageDescription: IEcsDockerImage;
computeUnits: number;
reservedMemory: number;
dockerImages: IEcsDockerImage[];
targetGroupsAvailable: string[];
targetGroupMappings: IEcsTargetGroupMapping[];
}

export class Container extends React.Component<IContainerProps, IContainerState> {
constructor(props: IContainerProps) {
super(props);
const cmd = this.props.command;
let defaultContainer = '';
if (cmd.containerMappings && cmd.containerMappings.length > 0) {
defaultContainer = cmd.containerMappings[0].containerName;
}

let defaultTargetGroup: IEcsTargetGroupMapping[] = [];
if (cmd.targetGroupMappings && cmd.targetGroupMappings.length > 0) {
defaultTargetGroup = cmd.targetGroupMappings;
}

if (cmd.targetGroup && cmd.targetGroup.length > 0) {
defaultTargetGroup.push({
containerName: cmd.loadBalancedContainer || defaultContainer,
targetGroup: cmd.targetGroup,
containerPort: cmd.containerPort,
});
cmd.targetGroup = '';
}

cmd.targetGroupMappings = uniqWith(defaultTargetGroup, isEqual);

this.state = {
imageDescription: cmd.imageDescription ? cmd.imageDescription : this.getEmptyImageDescription(),
computeUnits: cmd.computeUnits,
reservedMemory: cmd.reservedMemory,
dockerImages: cmd.backingData && cmd.backingData.filtered ? cmd.backingData.filtered.images : [],
targetGroupMappings: cmd.targetGroupMappings,
targetGroupsAvailable: cmd.backingData && cmd.backingData.filtered ? cmd.backingData.filtered.targetGroups : [],
};
}

public componentDidMount() {
this.props.configureCommand('1').then(() => {
this.setState({
dockerImages: this.props.command.backingData.filtered.images,
targetGroupsAvailable: this.props.command.backingData.filtered.targetGroups,
});
});
}

// TODO: Separate docker image component used by both TaskDefinition and Container

private getIdToImageMap = (): Map<string, IEcsDockerImage> => {
const imageIdToDescription = new Map<string, IEcsDockerImage>();
this.props.command.backingData.filtered.images.forEach(e => {
imageIdToDescription.set(e.imageId, e);
});

return imageIdToDescription;
};

private getEmptyImageDescription = (): IEcsDockerImage => {
return {
imageId: '',
message: '',
fromTrigger: false,
fromContext: false,
stageId: '',
imageLabelOrSha: '',
account: '',
registry: '',
repository: '',
tag: '',
};
};

private pushTargetGroupMapping = () => {
const targetMaps = this.state.targetGroupMappings;
targetMaps.push({ containerName: '', targetGroup: '', containerPort: 80 });
this.setState({ targetGroupMappings: targetMaps });
};

private updateContainerMappingImage = (newImage: string) => {
const imageMap = this.getIdToImageMap();
let newImageDescription = imageMap.get(newImage);
if (!newImageDescription) {
newImageDescription = this.getEmptyImageDescription();
}

this.props.notifyAngular('imageDescription', newImageDescription);
this.setState({ imageDescription: newImageDescription });
};

private updateTargetGroupMappingTargetGroup = (index: number, newTargetGroup: string) => {
const currentMappings = this.state.targetGroupMappings;
const targetMapping = currentMappings[index];
targetMapping.targetGroup = newTargetGroup;
this.props.notifyAngular('targetGroupMappings', currentMappings);
this.setState({ targetGroupMappings: currentMappings });
};

private updateTargetGroupMappingPort = (index: number, targetPort: number) => {
const currentMappings = this.state.targetGroupMappings;
const targetMapping = currentMappings[index];
targetMapping.containerPort = targetPort;
this.props.notifyAngular('targetGroupMappings', currentMappings);
this.setState({ targetGroupMappings: currentMappings });
};

private removeTargetGroupMapping = (index: number) => {
const currentMappings = this.state.targetGroupMappings;
currentMappings.splice(index, 1);
this.props.notifyAngular('targetGroupMappings', currentMappings);
this.setState({ targetGroupMappings: currentMappings });
};

public render(): React.ReactElement<Container> {
const removeTargetGroupMapping = this.removeTargetGroupMapping;
const updateContainerMappingImage = this.updateContainerMappingImage;
const updateTargetGroupMappingTargetGroup = this.updateTargetGroupMappingTargetGroup;
const updateTargetGroupMappingPort = this.updateTargetGroupMappingPort;

const dockerImages = this.state.dockerImages.map(function(image, index) {
let msg = '';
if (image.fromTrigger || image.fromContext) {
msg = image.fromTrigger ? '(TRIGGER) ' : '(FIND IMAGE RESULT) ';
}
return (
<option key={index} value={image.imageId}>
{msg}
{image.imageId}
</option>
);
});

const newTargetGroupMapping = this.state.targetGroupsAvailable.length ? (
<button className="btn btn-block btn-sm add-new" onClick={this.pushTargetGroupMapping}>
<span className="glyphicon glyphicon-plus-sign" />
Add New Target Group Mapping
</button>
) : (
<div className="sm-label-left">
<Alert color="warning">No target groups found in the selected account/region/VPC</Alert>
</div>
);

const targetGroupsAvailable = this.state.targetGroupsAvailable.map(function(targetGroup, index) {
return (
<option key={index} value={targetGroup}>
{targetGroup}
</option>
);
});

const targetGroupInputs = this.state.targetGroupMappings.map(function(mapping, index) {
return (
<tr key={index}>
<td>
<select
className="form-control input-sm"
value={mapping.targetGroup.toString()}
required={true}
onChange={e => updateTargetGroupMappingTargetGroup(index, e.target.value)}
>
<option value={''}>Select a target group to use...</option>
{targetGroupsAvailable}
</select>
</td>
<td>
<input
type="number"
className="form-control input-sm no-spel"
required={true}
value={mapping.containerPort.toString()}
onChange={e => updateTargetGroupMappingPort(index, e.target.valueAsNumber)}
/>
</td>
<td>
<div className="form-control-static">
<a className="btn-link sm-label" onClick={() => removeTargetGroupMapping(index)}>
<span className="glyphicon glyphicon-trash" />
<span className="sr-only">Remove</span>
</a>
</div>
</td>
</tr>
);
});

return (
<div className="container-fluid form-horizontal">
<div className="form-group">
<div className="col-md-3 sm-label-right">
<b>Container Image</b>
<HelpField id="ecs.containerMappingImage" />
</div>
<div className="col-md-9">
<select
className="form-control input-sm"
value={this.state.imageDescription.imageId}
required={true}
onChange={e => updateContainerMappingImage(e.target.value)}
>
<option value={''}>Select an image to use...</option>
{dockerImages}
</select>
</div>
</div>
<div className="form-group">
<hr />
<div className="sm-label-left">
<b>Target Group Mappings</b>
<HelpField id="ecs.targetGroupMappings" />
</div>
<form name="ecsContainerTargetGroupMappings">
<table className="table table-condensed packed tags">
<thead>
<tr key="header">
<th style={{ width: '80%' }}>
Target group
<HelpField id="ecs.loadBalancer.targetGroup" />
</th>
<th style={{ width: '20%' }}>
Target port
<HelpField id="ecs.loadbalancing.targetPort" />
</th>
<th />
</tr>
</thead>
<tbody>{targetGroupInputs}</tbody>
<tfoot>
<tr>
<td colSpan={4}>{newTargetGroupMapping}</td>
</tr>
</tfoot>
</table>
</form>
</div>
</div>
);
}
}

export const CONTAINER_REACT = 'spinnaker.ecs.serverGroup.configure.wizard.container.react';
module(CONTAINER_REACT, []).component(
'containerReact',
react2angular(Container, ['command', 'notifyAngular', 'configureCommand']),
);

This file was deleted.

Loading

0 comments on commit 5c9ba2c

Please sign in to comment.