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 ( + + ); + }); + + const newTargetGroupMapping = this.state.targetGroupsAvailable.length ? ( + + ) : ( +

+ No target groups found in the selected account/region/VPC +
+ ); + + const targetGroupsAvailable = this.state.targetGroupsAvailable.map(function(targetGroup, index) { + return ( + + ); + }); + + const targetGroupInputs = this.state.targetGroupMappings.map(function(mapping, index) { + return ( + + + + + + updateTargetGroupMappingPort(index, e.target.valueAsNumber)} + /> + + +
+ removeTargetGroupMapping(index)}> + + Remove + +
+ + + ); + }); + + return ( +
+
+
+ Container Image + +
+
+ +
+
+
+
+
+ Target Group Mappings + +
+
+ + + + + + + + {targetGroupInputs} + + + + + +
+ Target group + + + Target port + + +
{newTargetGroupMapping}
+
+
+
+ ); + } +} + +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 @@ -
-
-
Container Image
-
- - {{ $select.selected.imageId }} - - - - -
-
- -
-
- Reserved Compute Units -
-
- -
-
- -
-
- Reserved Memory -
-
- -
-
-
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 -

-
-
-
-
- Target Group - -
-
-
- No target groups found in the selected account/region/VPC -
- - {{ $select.selected }} - - - - -
-
- -
-
- Target container port -
-
- -
-
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)} + /> + + + + + + updateTargetGroupMappingPort(index, e.target.valueAsNumber)} + /> + + + + + ); }); + const newTargetGroupMapping = this.state.targetGroupsAvailable.length ? ( + + ) : ( +
+ No target groups found in the selected account/region/VPC +
+ ); + return (
@@ -236,23 +353,6 @@ export class TaskDefinition extends React.Component
-
-
- Load balanced container - -
-
- -
-
Container Mappings @@ -287,6 +387,39 @@ export class TaskDefinition extends React.Component
+
+
+ Target Group Mappings + +
+
+ + + + + + + + + {targetGroupInputs} + + + + + +
+ Container name + + + Target group + + + Target port + + +
{newTargetGroupMapping}
+
+
); }