diff --git a/app/scripts/modules/ecs/src/ecs.help.ts b/app/scripts/modules/ecs/src/ecs.help.ts
index 8a6a16ef3ef..27ae11df7a7 100644
--- a/app/scripts/modules/ecs/src/ecs.help.ts
+++ b/app/scripts/modules/ecs/src/ecs.help.ts
@@ -60,6 +60,8 @@ const helpContents: { [key: string]: string } = {
'ecs.containerMappingName':
'
The name of the container. Name should match the containerDefinition.name field as it appears in the Task Definition.
',
'ecs.containerMappingImage': 'The container image the named container should run.
',
+ 'ecs.targetGroupMappings':
+ '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.
',
'ecs.loadBalancedContainer':
'The container in the Task Definition that should receive traffic from the load balancer. Required if a load balancer target group has been specified.
',
'ecs.tags': 'The tags to apply to the task definition and the service',
diff --git a/app/scripts/modules/ecs/src/ecs.module.ts b/app/scripts/modules/ecs/src/ecs.module.ts
index 500cbd6f4d4..614e8f54c24 100644
--- a/app/scripts/modules/ecs/src/ecs.module.ts
+++ b/app/scripts/modules/ecs/src/ecs.module.ts
@@ -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';
@@ -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,
diff --git a/app/scripts/modules/ecs/src/serverGroup/configure/serverGroupConfiguration.service.ts b/app/scripts/modules/ecs/src/serverGroup/configure/serverGroupConfiguration.service.ts
index d4cadaf80af..e5957b711f5 100644
--- a/app/scripts/modules/ecs/src/serverGroup/configure/serverGroupConfiguration.service.ts
+++ b/app/scripts/modules/ecs/src/serverGroup/configure/serverGroupConfiguration.service.ts
@@ -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;
@@ -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;
diff --git a/app/scripts/modules/ecs/src/serverGroup/configure/wizard/container/Container.tsx b/app/scripts/modules/ecs/src/serverGroup/configure/wizard/container/Container.tsx
new file mode 100644
index 00000000000..20d7d4b9d76
--- /dev/null
+++ b/app/scripts/modules/ecs/src/serverGroup/configure/wizard/container/Container.tsx
@@ -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;
+}
+
+interface IContainerState {
+ imageDescription: IEcsDockerImage;
+ computeUnits: number;
+ reservedMemory: number;
+ dockerImages: IEcsDockerImage[];
+ targetGroupsAvailable: string[];
+ targetGroupMappings: IEcsTargetGroupMapping[];
+}
+
+export class Container extends React.Component {
+ 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 => {
+ const imageIdToDescription = new Map();
+ 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 {
+ 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 (
+
+ {msg}
+ {image.imageId}
+
+ );
+ });
+
+ const newTargetGroupMapping = this.state.targetGroupsAvailable.length ? (
+
+
+ Add New Target Group Mapping
+
+ ) : (
+
+
No target groups found in the selected account/region/VPC
+
+ );
+
+ const targetGroupsAvailable = this.state.targetGroupsAvailable.map(function(targetGroup, index) {
+ return (
+
+ {targetGroup}
+
+ );
+ });
+
+ const targetGroupInputs = this.state.targetGroupMappings.map(function(mapping, index) {
+ return (
+
+
+ updateTargetGroupMappingTargetGroup(index, e.target.value)}
+ >
+ Select a target group to use...
+ {targetGroupsAvailable}
+
+
+
+ updateTargetGroupMappingPort(index, e.target.valueAsNumber)}
+ />
+
+
+
+
+
+ );
+ });
+
+ return (
+
+
+
+ Container Image
+
+
+
+ updateContainerMappingImage(e.target.value)}
+ >
+ Select an image to use...
+ {dockerImages}
+
+
+
+
+
+
+ Target Group Mappings
+
+
+
+
+
+ );
+ }
+}
+
+export const CONTAINER_REACT = 'spinnaker.ecs.serverGroup.configure.wizard.container.react';
+module(CONTAINER_REACT, []).component(
+ 'containerReact',
+ react2angular(Container, ['command', 'notifyAngular', 'configureCommand']),
+);
diff --git a/app/scripts/modules/ecs/src/serverGroup/configure/wizard/container/container.component.html b/app/scripts/modules/ecs/src/serverGroup/configure/wizard/container/container.component.html
deleted file mode 100644
index 13e2c531e7b..00000000000
--- a/app/scripts/modules/ecs/src/serverGroup/configure/wizard/container/container.component.html
+++ /dev/null
@@ -1,46 +0,0 @@
-
diff --git a/app/scripts/modules/ecs/src/serverGroup/configure/wizard/container/container.component.js b/app/scripts/modules/ecs/src/serverGroup/configure/wizard/container/container.component.js
deleted file mode 100644
index 06dc5abe273..00000000000
--- a/app/scripts/modules/ecs/src/serverGroup/configure/wizard/container/container.component.js
+++ /dev/null
@@ -1,48 +0,0 @@
-'use strict';
-
-import { module } from 'angular';
-import { Observable, Subject } from 'rxjs';
-
-export const ECS_SERVERGROUP_CONFIGURE_WIZARD_CONTAINER_CONTAINER_COMPONENT =
- 'spinnaker.ecs.serverGroup.configure.wizard.container.component';
-export const name = ECS_SERVERGROUP_CONFIGURE_WIZARD_CONTAINER_CONTAINER_COMPONENT; // for backwards compatibility
-module(ECS_SERVERGROUP_CONFIGURE_WIZARD_CONTAINER_CONTAINER_COMPONENT, [])
- .component('ecsServerGroupContainer', {
- bindings: {
- command: '=',
- application: '=',
- },
- templateUrl: require('./container.component.html'),
- })
- .controller('ecsContainerImageController', [
- '$scope',
- 'ecsServerGroupConfigurationService',
- function($scope, ecsServerGroupConfigurationService) {
- this.groupByRegistry = function(image) {
- if (image) {
- if (image.fromContext) {
- return 'Find Image Result(s)';
- } else if (image.fromTrigger) {
- return 'Images from Trigger(s)';
- } else {
- return image.registry;
- }
- }
- };
-
- function searchImages(cmd, q) {
- return Observable.fromPromise(ecsServerGroupConfigurationService.configureCommand(cmd, q));
- }
-
- const imageSearchResultsStream = new Subject();
-
- imageSearchResultsStream
- .debounceTime(250)
- .switchMap(searchImages)
- .subscribe();
-
- this.searchImages = function(q) {
- imageSearchResultsStream.next(q);
- };
- },
- ]);
diff --git a/app/scripts/modules/ecs/src/serverGroup/configure/wizard/container/container.html b/app/scripts/modules/ecs/src/serverGroup/configure/wizard/container/container.html
index b89b9ca579e..b9a95345bbb 100644
--- a/app/scripts/modules/ecs/src/serverGroup/configure/wizard/container/container.html
+++ b/app/scripts/modules/ecs/src/serverGroup/configure/wizard/container/container.html
@@ -1,7 +1,9 @@
-
diff --git a/app/scripts/modules/ecs/src/serverGroup/configure/wizard/networking/networkingSelector.component.html b/app/scripts/modules/ecs/src/serverGroup/configure/wizard/networking/networkingSelector.component.html
index 102ed085b19..0cbe28080b1 100644
--- a/app/scripts/modules/ecs/src/serverGroup/configure/wizard/networking/networkingSelector.component.html
+++ b/app/scripts/modules/ecs/src/serverGroup/configure/wizard/networking/networkingSelector.component.html
@@ -94,62 +94,4 @@
-
-
-
-
-
- The following target groups could not be found in the selected account/region/VPC and were removed:
-
-
-
- Okay
-
-
-
-
-
-
diff --git a/app/scripts/modules/ecs/src/serverGroup/configure/wizard/taskDefinition/TaskDefinition.tsx b/app/scripts/modules/ecs/src/serverGroup/configure/wizard/taskDefinition/TaskDefinition.tsx
index 84ebe29bb81..4e9fb320436 100644
--- a/app/scripts/modules/ecs/src/serverGroup/configure/wizard/taskDefinition/TaskDefinition.tsx
+++ b/app/scripts/modules/ecs/src/serverGroup/configure/wizard/taskDefinition/TaskDefinition.tsx
@@ -1,12 +1,13 @@
import React from 'react';
import { module, IPromise } from 'angular';
-import { concat, uniq } from 'lodash';
+import { concat, uniq, uniqWith, isEqual } from 'lodash';
import { react2angular } from 'react2angular';
import {
IEcsContainerMapping,
IEcsDockerImage,
IEcsServerGroupCommand,
IEcsTaskDefinitionArtifact,
+ IEcsTargetGroupMapping,
} from '../../serverGroupConfiguration.service';
import {
ArtifactTypePatterns,
@@ -16,6 +17,7 @@ import {
IPipeline,
StageArtifactSelectorDelegate,
} from '@spinnaker/core';
+import { Alert } from 'react-bootstrap';
export interface ITaskDefinitionProps {
command: IEcsServerGroupCommand;
@@ -27,7 +29,9 @@ interface ITaskDefinitionState {
taskDefArtifact: IEcsTaskDefinitionArtifact;
taskDefArtifactAccount: string;
containerMappings: IEcsContainerMapping[];
+ targetGroupMappings: IEcsTargetGroupMapping[];
dockerImages: IEcsDockerImage[];
+ targetGroupsAvailable: string[];
loadBalancedContainer: string;
}
@@ -40,18 +44,40 @@ export class TaskDefinition extends React.Component 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 = {
taskDefArtifact: cmd.taskDefinitionArtifact,
containerMappings: cmd.containerMappings ? cmd.containerMappings : [],
+ targetGroupMappings: cmd.targetGroupMappings,
+ targetGroupsAvailable: cmd.backingData && cmd.backingData.filtered ? cmd.backingData.filtered.targetGroups : [],
dockerImages: cmd.backingData && cmd.backingData.filtered ? cmd.backingData.filtered.images : [],
loadBalancedContainer: cmd.loadBalancedContainer || defaultContainer,
taskDefArtifactAccount: cmd.taskDefinitionArtifactAccount,
};
}
+ // TODO: Separate docker image component used by both TaskDefinition and Container
public componentDidMount() {
this.props.configureCommand('1').then(() => {
- this.setState({ dockerImages: this.props.command.backingData.filtered.images });
+ this.setState({
+ dockerImages: this.props.command.backingData.filtered.images,
+ targetGroupsAvailable: this.props.command.backingData.filtered.targetGroups,
+ });
});
}
@@ -109,6 +135,12 @@ export class TaskDefinition extends React.Component {
+ const targetMaps = this.state.targetGroupMappings;
+ targetMaps.push({ containerName: '', targetGroup: '', containerPort: 80 });
+ this.setState({ targetGroupMappings: targetMaps });
+ };
+
private updateContainerMappingName = (index: number, newName: string) => {
const currentMappings = this.state.containerMappings;
const targetMapping = currentMappings[index];
@@ -131,9 +163,28 @@ export class TaskDefinition extends React.Component {
- this.props.notifyAngular('loadBalancedContainer', containerName);
- this.setState({ loadBalancedContainer: containerName });
+ 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 updateTargetGroupMappingContainer = (index: number, targetContainer: string) => {
+ const currentMappings = this.state.targetGroupMappings;
+ const targetMapping = currentMappings[index];
+ targetMapping.containerName = targetContainer;
+ 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 removeMapping = (index: number) => {
@@ -143,6 +194,13 @@ export class TaskDefinition extends React.Component {
+ const currentMappings = this.state.targetGroupMappings;
+ currentMappings.splice(index, 1);
+ this.props.notifyAngular('targetGroupMappings', currentMappings);
+ this.setState({ targetGroupMappings: currentMappings });
+ };
+
private updatePipeline = (pipeline: IPipeline): void => {
if (pipeline.expectedArtifacts && pipeline.expectedArtifacts.length > 0) {
const oldArtifacts = this.props.command.viewState.pipeline.expectedArtifacts;
@@ -155,8 +213,12 @@ export class TaskDefinition extends React.Component {
const { command } = this.props;
const removeMapping = this.removeMapping;
+ const removeTargetGroupMapping = this.removeTargetGroupMapping;
const updateContainerMappingName = this.updateContainerMappingName;
const updateContainerMappingImage = this.updateContainerMappingImage;
+ const updateTargetGroupMappingContainer = this.updateTargetGroupMappingContainer;
+ const updateTargetGroupMappingTargetGroup = this.updateTargetGroupMappingTargetGroup;
+ const updateTargetGroupMappingPort = this.updateTargetGroupMappingPort;
const dockerImages = this.state.dockerImages.map(function(image, index) {
let msg = '';
@@ -171,6 +233,14 @@ export class TaskDefinition extends React.Component
+ {targetGroup}
+
+ );
+ });
+
const mappingInputs = this.state.containerMappings.map(function(mapping, index) {
return (
@@ -206,14 +276,61 @@ export class TaskDefinition extends React.Component
- {mapping.containerName}
-
+
+
+ updateTargetGroupMappingContainer(index, e.target.value)}
+ />
+
+
+ updateTargetGroupMappingTargetGroup(index, e.target.value)}
+ >
+ Select a target group to use...
+ {targetGroupsAvailable}
+
+
+
+ updateTargetGroupMappingPort(index, e.target.valueAsNumber)}
+ />
+
+
+
+
+
);
});
+ const newTargetGroupMapping = this.state.targetGroupsAvailable.length ? (
+
+
+ Add New Target Group Mapping
+
+ ) : (
+
+
No target groups found in the selected account/region/VPC
+
+ );
+
return (
@@ -236,23 +353,6 @@ export class TaskDefinition extends React.Component
-
-
- Load balanced container
-
-
-
- this.updateLoadBalancedContainer(e.target.value)}
- >
- {'Select a container for load balancer traffic...'}
- {containerOptions}
-
-
-
Container Mappings
@@ -287,6 +387,39 @@ export class TaskDefinition extends React.Component
+
+
+ Target Group Mappings
+
+
+
+
);
}