Skip to content

Commit

Permalink
feat(ecs): docker image selection (#6687)
Browse files Browse the repository at this point in the history
* Rename vertical scaling to container

* feat(ecs): Docker image selection

* Define missing types

* Missing dependency on @spinnaker/docker
  • Loading branch information
clareliguori authored and anotherchrisberry committed Mar 21, 2019
1 parent 09e96a2 commit 1c9e075
Show file tree
Hide file tree
Showing 14 changed files with 295 additions and 54 deletions.
1 change: 1 addition & 0 deletions app/scripts/modules/core/src/region/RegionSelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class RegionSelectField extends React.Component<IRegionSelectFieldProps>

public render() {
const { labelColumns, fieldColumns, account, regions, readOnly, component, field } = this.props;

return (
<div className="form-group">
<div className={`col-md-${labelColumns} sm-label-right`}>Region</div>
Expand Down
1 change: 1 addition & 0 deletions app/scripts/modules/ecs/src/ecs.help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const helpContents: { [key: string]: string } = {
'ecs.loadbalancing.targetPort': '<p>The port on which your application is listening for incoming traffic</p>',
'ecs.iamrole':
'<p>The IAM role that your container (task, in AWS wording) will inherit. </p><p>Define a role only if your application needs to access AWS APIs</p>',
'ecs.dockerimage': 'Docker image for your container, such as nginx:latest',
'ecs.dockerimagecredentials':
'<p>The AWS Secrets Manager secret that contains private registry credentials.</p><p>Define credentials only for private registries other than Amazon ECR.</p>',
'ecs.placementStrategy':
Expand Down
2 changes: 1 addition & 1 deletion app/scripts/modules/ecs/src/ecs.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ angular
ECS_SERVER_GROUP_TRANSFORMER,
// require('./pipeline/stages/cloneServerGroup/ecsCloneServerGroupStage').name, // TODO(Bruno Carrier): We should enable this on Clouddriver before revealing this stage
require('./serverGroup/configure/wizard/advancedSettings/advancedSettings.component').name,
require('./serverGroup/configure/wizard/verticalScaling/verticalScaling.component').name,
require('./serverGroup/configure/wizard/container/container.component').name,
require('./serverGroup/configure/wizard/horizontalScaling/horizontalScaling.component').name,
ECS_SERVER_GROUP_LOGGING,
ECS_NETWORKING_SECTION,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,81 @@ module.exports = angular
function($q, instanceTypeService, ecsServerGroupConfigurationService) {
const CLOUD_PROVIDER = 'ecs';

function reconcileUpstreamImages(image, upstreamImages) {
if (image.fromContext) {
let matchingImage = upstreamImages.find(otherImage => image.stageId === otherImage.stageId);

if (matchingImage) {
image.cluster = matchingImage.cluster;
image.pattern = matchingImage.pattern;
image.repository = matchingImage.repository;
return image;
} else {
return null;
}
} else if (image.fromTrigger) {
let matchingImage = upstreamImages.find(otherImage => {
return (
image.registry === otherImage.registry &&
image.repository === otherImage.repository &&
image.tag === otherImage.tag
);
});

if (matchingImage) {
return image;
} else {
return null;
}
} else {
return image;
}
}

function findUpstreamImages(current, all, visited = {}) {
// This actually indicates a loop in the stage dependencies.
if (visited[current.refId]) {
return [];
} else {
visited[current.refId] = true;
}
let result = [];
if (current.type === 'findImageFromTags') {
result.push({
fromContext: true,
imageLabelOrSha: current.imageLabelOrSha,
stageId: current.refId,
});
}
current.requisiteStageRefIds.forEach(function(id) {
let next = all.find(stage => stage.refId === id);
if (next) {
result = result.concat(findUpstreamImages(next, all, visited));
}
});

return result;
}

function findTriggerImages(triggers) {
let result = triggers
.filter(trigger => {
return trigger.type === 'docker';
})
.map(trigger => {
return {
fromTrigger: true,
repository: trigger.repository,
account: trigger.account,
organization: trigger.organization,
registry: trigger.registry,
tag: trigger.tag,
};
});

return result;
}

function buildNewServerGroupCommand(application, defaults) {
defaults = defaults || {};
var credentialsLoader = AccountService.getCredentialsKeyedByAccount('ecs');
Expand Down Expand Up @@ -92,7 +167,7 @@ module.exports = angular
});
}

function buildServerGroupCommandFromPipeline(application, originalCluster) {
function buildServerGroupCommandFromPipeline(application, originalCluster, current, pipeline) {
var pipelineCluster = _.cloneDeep(originalCluster);
var region = Object.keys(pipelineCluster.availabilityZones)[0];
// var instanceTypeCategoryLoader = instanceTypeService.getCategoryForInstanceType('ecs', pipelineCluster.instanceType);
Expand All @@ -104,6 +179,13 @@ module.exports = angular
var zones = pipelineCluster.availabilityZones[region];
var usePreferredZones = zones.join(',') === command.availabilityZones.join(',');

let contextImages = findUpstreamImages(current, pipeline.stages) || [];
contextImages = contextImages.concat(findTriggerImages(pipeline.triggers));

if (command.docker && command.docker.image) {
command.docker.image = reconcileUpstreamImages(command.docker.image, contextImages);
}

var viewState = {
instanceProfile: asyncData.instanceProfile,
disableImageSelection: true,
Expand All @@ -116,6 +198,7 @@ module.exports = angular
templatingEnabled: true,
existingPipelineCluster: true,
dirty: {},
contextImages: contextImages,
};

var viewOverrides = {
Expand All @@ -132,9 +215,13 @@ module.exports = angular
}

// Only used to prepare view requiring template selecting
function buildNewServerGroupCommandForPipeline() {
function buildNewServerGroupCommandForPipeline(current, pipeline) {
let contextImages = findUpstreamImages(current, pipeline.stages) || [];
contextImages = contextImages.concat(findTriggerImages(pipeline.triggers));

return $q.when({
viewState: {
contextImages: contextImages,
requiresTemplateSelection: true,
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import {
IServerGroupCommandBackingDataFiltered,
IServerGroupCommandDirty,
IServerGroupCommandResult,
IServerGroupCommandViewState,
ISubnet,
LOAD_BALANCER_READ_SERVICE,
LoadBalancerReader,
NameUtils,
SERVER_GROUP_COMMAND_REGISTRY_PROVIDER,
ServerGroupCommandRegistry,
SubnetReader,
Expand All @@ -24,6 +26,7 @@ import {
} from '@spinnaker/core';

import { IAmazonLoadBalancer } from '@spinnaker/amazon';
import { DockerImageReader, IDockerImage } from '@spinnaker/docker';
import { IamRoleReader } from '../../iamRoles/iamRole.read.service';
import { EscClusterReader } from '../../ecsCluster/ecsCluster.read.service';
import { MetricAlarmReader } from '../../metricAlarm/metricAlarm.read.service';
Expand All @@ -43,6 +46,19 @@ export interface IEcsServerGroupCommandResult extends IServerGroupCommandResult
dirty: IEcsServerGroupCommandDirty;
}

export interface IEcsDockerImage extends IDockerImage {
imageId: string;
message: string;
fromTrigger: boolean;
fromContext: boolean;
stageId: string;
imageLabelOrSha: string;
}

export interface IEcsServerGroupCommandViewState extends IServerGroupCommandViewState {
contextImages: IEcsDockerImage[];
}

export interface IEcsServerGroupCommandBackingDataFiltered extends IServerGroupCommandBackingDataFiltered {
targetGroups: string[];
iamRoles: string[];
Expand All @@ -51,6 +67,7 @@ export interface IEcsServerGroupCommandBackingDataFiltered extends IServerGroupC
subnetTypes: string[];
securityGroupNames: string[];
secrets: string[];
images: IEcsDockerImage[];
}

export interface IEcsServerGroupCommandBackingData extends IServerGroupCommandBackingData {
Expand All @@ -63,6 +80,7 @@ export interface IEcsServerGroupCommandBackingData extends IServerGroupCommandBa
// subnetTypes: string;
// securityGroups: string[]
secrets: ISecretDescriptor[];
images: IEcsDockerImage[];
}

export interface IEcsServerGroupCommand extends IServerGroupCommand {
Expand All @@ -71,11 +89,15 @@ export interface IEcsServerGroupCommand extends IServerGroupCommand {
targetGroup: string;
placementStrategyName: string;
placementStrategySequence: IPlacementStrategy[];
imageDescription: IEcsDockerImage;
viewState: IEcsServerGroupCommandViewState;

subnetTypeChanged: (command: IEcsServerGroupCommand) => IServerGroupCommandResult;
placementStrategyNameChanged: (command: IEcsServerGroupCommand) => IServerGroupCommandResult;
// subnetTypeChanged: (command: IEcsServerGroupCommand) => IServerGroupCommandResult;
regionIsDeprecated: (command: IEcsServerGroupCommand) => boolean;

clusterChanged: (command: IServerGroupCommand) => void;
}

export class EcsServerGroupConfigurationService {
Expand Down Expand Up @@ -115,7 +137,7 @@ export class EcsServerGroupConfigurationService {
}

// TODO (Bruno Carrier): Why do we need to inject an Application into this constructor so that the app works? This is strange, and needs investigating
public configureCommand(cmd: IEcsServerGroupCommand): IPromise<void> {
public configureCommand(cmd: IEcsServerGroupCommand, imageQuery = ''): IPromise<void> {
this.applyOverrides('beforeConfiguration', cmd);
cmd.toggleSuspendedProcess = (command: IEcsServerGroupCommand, process: string): void => {
command.suspendedProcesses = command.suspendedProcesses || [];
Expand Down Expand Up @@ -144,6 +166,29 @@ export class EcsServerGroupConfigurationService {
);
};

const imageQueries = cmd.imageDescription ? [this.grabImageAndTag(cmd.imageDescription.imageId)] : [];

if (imageQuery) {
imageQueries.push(imageQuery);
}

let imagesPromise;
if (imageQueries.length) {
imagesPromise = this.$q
.all(
imageQueries.map(q =>
DockerImageReader.findImages({
provider: 'dockerRegistry',
count: 50,
q: q,
}),
),
)
.then(promises => flatten(promises));
} else {
imagesPromise = this.$q.when([]);
}

return this.$q
.all({
credentialsKeyedByAccount: AccountService.getCredentialsKeyedByAccount('ecs'),
Expand All @@ -155,17 +200,23 @@ export class EcsServerGroupConfigurationService {
securityGroups: this.securityGroupReader.getAllSecurityGroups(),
launchTypes: this.$q.when(clone(this.launchTypes)),
secrets: this.secretReader.listSecrets(),
images: imagesPromise,
})
.then((backingData: Partial<IEcsServerGroupCommandBackingData>) => {
backingData.accounts = keys(backingData.credentialsKeyedByAccount);
backingData.filtered = {} as IEcsServerGroupCommandBackingDataFiltered;
if (cmd.viewState.contextImages) {
backingData.images = backingData.images.concat(cmd.viewState.contextImages);
}
cmd.backingData = backingData as IEcsServerGroupCommandBackingData;
this.configureVpcId(cmd);
this.configureAvailableIamRoles(cmd);
this.configureAvailableSubnetTypes(cmd);
this.configureAvailableSecurityGroups(cmd);
this.configureAvailableEcsClusters(cmd);
this.configureAvailableSecrets(cmd);
this.configureAvailableImages(cmd);
this.configureAvailableRegions(cmd);
this.applyOverrides('afterConfiguration', cmd);
this.attachEventHandlers(cmd);
});
Expand All @@ -179,6 +230,44 @@ export class EcsServerGroupConfigurationService {
});
}

public grabImageAndTag(imageId: string): string {
return imageId.split('/').pop();
}

public buildImageId(image: IEcsDockerImage): string {
if (image.fromContext) {
return `${image.imageLabelOrSha}`;
} else if (image.fromTrigger && !image.tag) {
return `${image.registry}/${image.repository} (Tag resolved at runtime)`;
} else {
return `${image.registry}/${image.repository}:${image.tag}`;
}
}

public mapImage(image: IEcsDockerImage): IEcsDockerImage {
if (image.message !== undefined) {
return image;
}

return {
repository: image.repository,
tag: image.tag,
imageId: this.buildImageId(image),
registry: image.registry,
fromContext: image.fromContext,
fromTrigger: image.fromTrigger,
account: image.account,
imageLabelOrSha: image.imageLabelOrSha,
stageId: image.stageId,
message: image.message,
};
}

public configureAvailableImages(command: IEcsServerGroupCommand): void {
// No filtering required, but need to decorate with the displayable image ID
command.backingData.filtered.images = command.backingData.images.map(image => this.mapImage(image));
}

public configureAvailabilityZones(command: IEcsServerGroupCommand): void {
command.backingData.filtered.availabilityZones = find<IRegion>(
command.backingData.credentialsKeyedByAccount[command.credentials].regions,
Expand Down Expand Up @@ -249,6 +338,12 @@ export class EcsServerGroupConfigurationService {
.value();
}

public configureAvailableRegions(command: IEcsServerGroupCommand): void {
const regionsForAccount: IAccountDetails =
command.backingData.credentialsKeyedByAccount[command.credentials] || ({ regions: [] } as IAccountDetails);
command.backingData.filtered.regions = regionsForAccount.regions;
}

public configureAvailableIamRoles(command: IEcsServerGroupCommand): void {
command.backingData.filtered.iamRoles = chain(command.backingData.iamRoles)
.filter({ accountName: command.credentials })
Expand Down Expand Up @@ -399,6 +494,10 @@ export class EcsServerGroupConfigurationService {
return result;
};

cmd.clusterChanged = (command: IEcsServerGroupCommand): void => {
command.moniker = NameUtils.getMoniker(command.application, command.stack, command.freeFormDetails);
};

cmd.credentialsChanged = (command: IEcsServerGroupCommand): IServerGroupCommandResult => {
const result: IEcsServerGroupCommandResult = { dirty: {} };
const backingData = command.backingData;
Expand All @@ -408,10 +507,8 @@ export class EcsServerGroupConfigurationService {
this.configureAvailableSubnetTypes(command);
this.configureAvailableSecurityGroups(command);
this.configureAvailableSecrets(command);
this.configureAvailableRegions(command);

const regionsForAccount: IAccountDetails =
backingData.credentialsKeyedByAccount[command.credentials] || ({ regions: [] } as IAccountDetails);
backingData.filtered.regions = regionsForAccount.regions;
if (!some(backingData.filtered.regions, { name: command.region })) {
command.region = null;
result.dirty.region = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ module.exports = angular
'ecs.serverGroup.basicSettings',
require('./location/basicSettings.html'),
),
verticalScaling: overrideRegistry.getTemplate(
'ecs.serverGroup.verticalScaling',
require('./verticalScaling/verticalScaling.html'),
container: overrideRegistry.getTemplate(
'ecs.serverGroup.container',
require('./container/container.html'),
),
horizontalScaling: overrideRegistry.getTemplate(
'ecs.serverGroup.horizontalScaling',
Expand Down
Loading

0 comments on commit 1c9e075

Please sign in to comment.