@spotinst/spinnaker-deck
Version:
Spinnaker-Deck service, forked with support to Spotinst
646 lines (594 loc) • 25.2 kB
text/typescript
import { chain, filter, flatten, map } from 'lodash';
import { $q } from 'ngimport';
import {
AccountService,
Application,
IHealth,
IInstance,
IServerGroup,
IVpc,
NameUtils,
SETTINGS,
} from '@spinnaker/core';
import { AWSProviderSettings } from 'amazon/aws.settings';
import {
IALBListenerCertificate,
IAmazonApplicationLoadBalancer,
IAmazonApplicationLoadBalancerUpsertCommand,
IAmazonClassicLoadBalancer,
IAmazonClassicLoadBalancerUpsertCommand,
IAmazonLoadBalancer,
IAmazonNetworkLoadBalancerUpsertCommand,
IAmazonServerGroup,
IApplicationLoadBalancerSourceData,
IClassicListenerDescription,
IClassicLoadBalancerSourceData,
INetworkLoadBalancerSourceData,
ITargetGroup,
} from 'amazon/domain';
import { VpcReader } from 'amazon/vpc/VpcReader';
export class AwsLoadBalancerTransformer {
private updateHealthCounts(container: IServerGroup | ITargetGroup | IAmazonLoadBalancer): void {
const instances = container.instances;
container.instanceCounts = {
up: instances.filter((instance) => instance.health[0].state === 'InService').length,
down: instances.filter((instance) => instance.healthState === 'Down').length,
outOfService: instances.filter((instance) => instance.healthState === 'OutOfService').length,
starting: undefined,
succeeded: undefined,
failed: undefined,
unknown: undefined,
};
if ((container as ITargetGroup | IAmazonLoadBalancer).serverGroups) {
const serverGroupInstances = flatten((container as ITargetGroup).serverGroups.map((sg) => sg.instances));
container.instanceCounts.up = serverGroupInstances.filter(
(instance) => instance.health[0].state === 'InService',
).length;
container.instanceCounts.down = serverGroupInstances.filter((instance) => instance.healthState === 'Down').length;
container.instanceCounts.outOfService = serverGroupInstances.filter(
(instance) => instance.healthState === 'OutOfService',
).length;
}
}
private transformInstance(instance: IInstance, provider: string, account: string, region: string): void {
// instance in this case should be some form if instance source data, but force to 'any' type to fix
// instnace health in load balancers until we can actually shape this bit properly
const health: IHealth = (instance.health as any) || ({} as IHealth);
if (health.state === 'healthy') {
// Target groups use 'healthy' instead of 'InService' and a lot of deck expects InService
// to surface health in the UI; just set it as InService since we don't really care the
// specific state name... yet
health.state = 'InService';
}
instance.provider = provider;
instance.account = account;
instance.region = region;
instance.healthState = health.state ? (health.state === 'InService' ? 'Up' : 'Down') : 'OutOfService';
instance.health = [health];
}
private addVpcNameToContainer(
container: IAmazonLoadBalancer | ITargetGroup,
): (vpcs: IVpc[]) => IAmazonLoadBalancer | ITargetGroup {
return (vpcs: IVpc[]) => {
const match = vpcs.find((test) => test.id === container.vpcId);
container.vpcName = match ? match.name : '';
return container;
};
}
private normalizeServerGroups(
serverGroups: IServerGroup[],
container: IAmazonLoadBalancer | ITargetGroup,
containerType: string,
healthType: string,
): void {
serverGroups.forEach((serverGroup) => {
serverGroup.account = serverGroup.account || container.account;
serverGroup.region = serverGroup.region || container.region;
serverGroup.cloudProvider = serverGroup.cloudProvider || container.cloudProvider;
if (serverGroup.detachedInstances) {
serverGroup.detachedInstances = (serverGroup.detachedInstances as any).map((instanceId: string) => {
return { id: instanceId } as IInstance;
});
serverGroup.instances = serverGroup.instances.concat(serverGroup.detachedInstances);
} else {
serverGroup.detachedInstances = [];
}
serverGroup.instances.forEach((instance) => {
this.transformInstance(instance, container.type, container.account, container.region);
(instance as any)[containerType] = [container.name];
(instance.health as any).type = healthType;
});
this.updateHealthCounts(serverGroup);
});
}
private normalizeTargetGroup(targetGroup: ITargetGroup): PromiseLike<ITargetGroup> {
this.normalizeServerGroups(targetGroup.serverGroups, targetGroup, 'targetGroups', 'TargetGroup');
const activeServerGroups = filter(targetGroup.serverGroups, { isDisabled: false });
targetGroup.provider = targetGroup.type;
targetGroup.instances = chain(activeServerGroups).map('instances').flatten<IInstance>().value();
targetGroup.detachedInstances = chain(activeServerGroups).map('detachedInstances').flatten<IInstance>().value();
this.updateHealthCounts(targetGroup);
return $q.all([VpcReader.listVpcs(), AccountService.listAllAccounts()]).then(([vpcs, accounts]) => {
const tg = this.addVpcNameToContainer(targetGroup)(vpcs) as ITargetGroup;
tg.serverGroups = tg.serverGroups.map((serverGroup) => {
const account = accounts.find((x) => x.name === serverGroup.account);
const cloudProvider = (account && account.cloudProvider) || serverGroup.cloudProvider;
serverGroup.cloudProvider = cloudProvider;
serverGroup.instances.forEach((instance) => {
instance.cloudProvider = cloudProvider;
instance.provider = cloudProvider;
});
return { ...serverGroup, cloudProvider };
});
return tg;
});
}
private normalizeActions(loadBalancer: IAmazonApplicationLoadBalancer) {
if (loadBalancer.loadBalancerType === 'application') {
const alb = loadBalancer as IAmazonApplicationLoadBalancer;
// Sort the actions by the order specified since amazon does not return them in order of order
alb.listeners.forEach((l) => {
l.defaultActions.sort((a, b) => a.order - b.order);
l.rules.forEach((r) => r.actions.sort((a, b) => a.order - b.order));
});
}
}
public normalizeLoadBalancer(loadBalancer: IAmazonLoadBalancer): PromiseLike<IAmazonLoadBalancer> {
this.normalizeServerGroups(loadBalancer.serverGroups, loadBalancer, 'loadBalancers', 'LoadBalancer');
let serverGroups = loadBalancer.serverGroups;
if ((loadBalancer as IAmazonApplicationLoadBalancer).targetGroups) {
const appLoadBalancer = loadBalancer as IAmazonApplicationLoadBalancer;
appLoadBalancer.targetGroups.forEach((targetGroup) => this.normalizeTargetGroup(targetGroup));
serverGroups = flatten<IAmazonServerGroup>(map(appLoadBalancer.targetGroups, 'serverGroups'));
}
loadBalancer.loadBalancerType = loadBalancer.loadBalancerType || 'classic';
loadBalancer.provider = loadBalancer.type;
this.normalizeActions(loadBalancer as IAmazonApplicationLoadBalancer);
const activeServerGroups = filter(serverGroups, { isDisabled: false });
loadBalancer.instances = chain(activeServerGroups).map('instances').flatten<IInstance>().value();
loadBalancer.detachedInstances = chain(activeServerGroups).map('detachedInstances').flatten<IInstance>().value();
this.updateHealthCounts(loadBalancer);
return VpcReader.listVpcs().then(
(vpcs: IVpc[]) => this.addVpcNameToContainer(loadBalancer)(vpcs) as IAmazonLoadBalancer,
);
}
public static convertClassicLoadBalancerForEditing(
loadBalancer: IAmazonClassicLoadBalancer,
): IAmazonClassicLoadBalancerUpsertCommand {
const toEdit: IAmazonClassicLoadBalancerUpsertCommand = {
availabilityZones: undefined,
isInternal: loadBalancer.isInternal,
region: loadBalancer.region,
cloudProvider: loadBalancer.cloudProvider,
credentials: loadBalancer.credentials || loadBalancer.account,
listeners: loadBalancer.listeners,
loadBalancerType: 'classic',
name: loadBalancer.name,
regionZones: loadBalancer.availabilityZones,
securityGroups: loadBalancer.securityGroups,
vpcId: loadBalancer.vpcId,
healthCheck: undefined,
healthTimeout: loadBalancer.healthTimeout,
healthInterval: loadBalancer.healthInterval,
healthyThreshold: loadBalancer.healthyThreshold,
unhealthyThreshold: loadBalancer.unhealthyThreshold,
healthCheckProtocol: loadBalancer.healthCheckProtocol,
healthCheckPort: loadBalancer.healthCheckPort,
healthCheckPath: loadBalancer.healthCheckPath,
idleTimeout: loadBalancer.idleTimeout || 60,
subnetType: loadBalancer.subnetType,
};
if (loadBalancer.elb) {
const elb = loadBalancer.elb as IClassicLoadBalancerSourceData;
toEdit.securityGroups = elb.securityGroups;
toEdit.vpcId = elb.vpcid || elb.vpcId;
if (elb.listenerDescriptions) {
toEdit.listeners = elb.listenerDescriptions.map(
(description: any): IClassicListenerDescription => {
const listener = description.listener;
if (listener.sslcertificateId) {
const splitCertificateId = listener.sslcertificateId.split('/');
listener.sslcertificateId = splitCertificateId[1];
listener.sslCertificateType = splitCertificateId[0].split(':')[2];
}
return {
internalProtocol: listener.instanceProtocol,
internalPort: listener.instancePort,
externalProtocol: listener.protocol,
externalPort: listener.loadBalancerPort,
sslCertificateId: listener.sslcertificateId,
sslCertificateName: listener.sslcertificateId,
sslCertificateType: listener.sslCertificateType,
policyNames: description.policyNames,
};
},
);
}
if (elb.healthCheck && elb.healthCheck.target) {
toEdit.healthTimeout = elb.healthCheck.timeout;
toEdit.healthInterval = elb.healthCheck.interval;
toEdit.healthyThreshold = elb.healthCheck.healthyThreshold;
toEdit.unhealthyThreshold = elb.healthCheck.unhealthyThreshold;
const healthCheck = elb.healthCheck.target;
const protocolIndex = healthCheck.indexOf(':');
let pathIndex = healthCheck.indexOf('/');
if (pathIndex === -1) {
pathIndex = healthCheck.length;
}
if (protocolIndex !== -1) {
toEdit.healthCheckProtocol = healthCheck.substring(0, protocolIndex);
const healthCheckPort = Number(healthCheck.substring(protocolIndex + 1, pathIndex));
toEdit.healthCheckPath = healthCheck.substring(pathIndex);
if (!isNaN(healthCheckPort)) {
toEdit.healthCheckPort = healthCheckPort;
}
}
}
}
return toEdit;
}
public static convertApplicationLoadBalancerForEditing(
loadBalancer: IAmazonApplicationLoadBalancer,
): IAmazonApplicationLoadBalancerUpsertCommand {
const applicationName = NameUtils.parseLoadBalancerName(loadBalancer.name).application;
// Since we build up toEdit as we go, much easier to declare as any, then cast at return time.
const toEdit: IAmazonApplicationLoadBalancerUpsertCommand = {
availabilityZones: undefined,
isInternal: loadBalancer.isInternal,
region: loadBalancer.region,
loadBalancerType: 'application',
cloudProvider: loadBalancer.cloudProvider,
credentials: loadBalancer.account || loadBalancer.credentials,
listeners: [],
targetGroups: [],
name: loadBalancer.name,
regionZones: loadBalancer.availabilityZones,
securityGroups: [],
subnetType: loadBalancer.subnetType,
vpcId: undefined,
idleTimeout: loadBalancer.idleTimeout || 60,
deletionProtection: loadBalancer.deletionProtection || false,
ipAddressType: loadBalancer.ipAddressType || 'ipv4',
dualstack: loadBalancer.ipAddressType === 'dualstack',
};
if (loadBalancer.elb) {
const elb = loadBalancer.elb as IApplicationLoadBalancerSourceData;
toEdit.securityGroups = elb.securityGroups;
toEdit.vpcId = elb.vpcid || elb.vpcId;
// Convert listeners
if (elb.listeners) {
toEdit.listeners = elb.listeners.map((listener) => {
const certificates: IALBListenerCertificate[] = [];
if (listener.certificates) {
listener.certificates.forEach((cert) => {
const certArnParts = cert.certificateArn.split(':');
const certParts = certArnParts[5].split('/');
certificates.push({
certificateArn: cert.certificateArn,
type: certArnParts[2],
name: certParts[1],
});
});
}
(listener.defaultActions || []).forEach((action) => {
if (action.targetGroupName) {
action.targetGroupName = action.targetGroupName.replace(`${applicationName}-`, '');
}
action.redirectActionConfig = action.redirectConfig;
});
// Remove the default rule because it already exists in defaultActions
listener.rules = (listener.rules || []).filter((l) => !l.default);
listener.rules.forEach((rule) => {
(rule.actions || []).forEach((action) => {
if (action.targetGroupName) {
action.targetGroupName = action.targetGroupName.replace(`${applicationName}-`, '');
}
action.redirectActionConfig = action.redirectConfig;
});
(rule.conditions || []).forEach((condition) => {
if (condition.field === 'http-request-method') {
condition.values = condition.httpRequestMethodConfig.values;
}
});
rule.conditions = rule.conditions || [];
});
// Sort listener.rules by priority.
listener.rules.sort((a, b) => (a.priority as number) - (b.priority as number));
return {
protocol: listener.protocol,
port: listener.port,
defaultActions: listener.defaultActions,
certificates,
rules: listener.rules || [],
sslPolicy: listener.sslPolicy,
};
});
}
// Convert target groups
if (elb.targetGroups) {
toEdit.targetGroups = elb.targetGroups.map((targetGroup: any) => {
return {
name: targetGroup.targetGroupName.replace(`${applicationName}-`, ''),
protocol: targetGroup.protocol,
port: targetGroup.port,
targetType: targetGroup.targetType,
healthCheckProtocol: targetGroup.healthCheckProtocol,
healthCheckPort: targetGroup.healthCheckPort,
healthCheckPath: targetGroup.healthCheckPath,
healthCheckTimeout: targetGroup.healthCheckTimeoutSeconds,
healthCheckInterval: targetGroup.healthCheckIntervalSeconds,
healthyThreshold: targetGroup.healthyThresholdCount,
unhealthyThreshold: targetGroup.unhealthyThresholdCount,
attributes: {
deregistrationDelay: Number(targetGroup.attributes['deregistration_delay.timeout_seconds']),
stickinessEnabled: targetGroup.attributes['stickiness.enabled'] === 'true',
stickinessType: targetGroup.attributes['stickiness.type'],
stickinessDuration: Number(targetGroup.attributes['stickiness.lb_cookie.duration_seconds']),
multiValueHeadersEnabled: targetGroup.attributes['lambda.multi_value_headers.enabled'] === 'true',
},
};
});
}
}
return toEdit;
}
public static convertNetworkLoadBalancerForEditing(
loadBalancer: IAmazonApplicationLoadBalancer,
): IAmazonNetworkLoadBalancerUpsertCommand {
const applicationName = NameUtils.parseLoadBalancerName(loadBalancer.name).application;
// Since we build up toEdit as we go, much easier to declare as any, then cast at return time.
const toEdit: IAmazonNetworkLoadBalancerUpsertCommand = {
availabilityZones: undefined,
isInternal: loadBalancer.isInternal,
region: loadBalancer.region,
loadBalancerType: 'network',
cloudProvider: loadBalancer.cloudProvider,
credentials: loadBalancer.account || loadBalancer.credentials,
listeners: [],
targetGroups: [],
name: loadBalancer.name,
regionZones: loadBalancer.availabilityZones,
securityGroups: [],
subnetType: loadBalancer.subnetType,
vpcId: undefined,
deletionProtection: loadBalancer.deletionProtection,
loadBalancingCrossZone: loadBalancer.loadBalancingCrossZone,
ipAddressType: loadBalancer.ipAddressType || 'ipv4',
dualstack: loadBalancer.ipAddressType === 'dualstack',
};
if (loadBalancer.elb) {
const elb = loadBalancer.elb as INetworkLoadBalancerSourceData;
toEdit.securityGroups = elb.securityGroups;
toEdit.vpcId = elb.vpcid || elb.vpcId;
// Convert listeners
if (elb.listeners) {
toEdit.listeners = elb.listeners.map((listener) => {
const certificates: IALBListenerCertificate[] = [];
if (listener.certificates) {
listener.certificates.forEach((cert) => {
const certArnParts = cert.certificateArn.split(':');
const certParts = certArnParts[5].split('/');
certificates.push({
certificateArn: cert.certificateArn,
type: certArnParts[2],
name: certParts[1],
});
});
}
(listener.defaultActions || []).forEach((action) => {
if (action.targetGroupName) {
action.targetGroupName = action.targetGroupName.replace(`${applicationName}-`, '');
}
});
// Remove the default rule because it already exists in defaultActions
listener.rules = (listener.rules || []).filter((l) => !l.default);
listener.rules.forEach((rule) => {
(rule.actions || []).forEach((action) => {
if (action.targetGroupName) {
action.targetGroupName = action.targetGroupName.replace(`${applicationName}-`, '');
}
});
rule.conditions = rule.conditions || [];
});
// Sort listener.rules by priority.
listener.rules.sort((a, b) => (a.priority as number) - (b.priority as number));
return {
protocol: listener.protocol,
port: listener.port,
defaultActions: listener.defaultActions,
certificates,
rules: listener.rules || [],
sslPolicy: listener.sslPolicy,
};
});
}
// Convert target groups
if (elb.targetGroups) {
toEdit.targetGroups = elb.targetGroups.map((targetGroup: any) => {
return {
name: targetGroup.targetGroupName.replace(`${applicationName}-`, ''),
protocol: targetGroup.protocol,
port: targetGroup.port,
targetType: targetGroup.targetType,
healthCheckProtocol: targetGroup.healthCheckProtocol,
healthCheckPort: targetGroup.healthCheckPort,
healthCheckTimeout: targetGroup.healthCheckTimeoutSeconds,
healthCheckInterval: targetGroup.healthCheckIntervalSeconds,
healthyThreshold: targetGroup.healthyThresholdCount,
unhealthyThreshold: targetGroup.unhealthyThresholdCount,
healthCheckPath: targetGroup.healthCheckPath,
attributes: {
deregistrationDelay: Number(targetGroup.attributes['deregistration_delay.timeout_seconds']),
deregistrationDelayConnectionTermination: Boolean(
targetGroup.attributes['deregistration_delay.connection_termination.enabled'] === 'true',
),
},
};
});
}
}
return toEdit;
}
public static constructNewClassicLoadBalancerTemplate(
application: Application,
): IAmazonClassicLoadBalancerUpsertCommand {
const defaultCredentials = application.defaultCredentials.aws || AWSProviderSettings.defaults.account;
const defaultRegion = application.defaultRegions.aws || AWSProviderSettings.defaults.region;
const defaultSubnetType = AWSProviderSettings.defaults.subnetType;
return {
availabilityZones: undefined,
name: '',
stack: '',
detail: '',
loadBalancerType: 'classic',
isInternal: false,
cloudProvider: 'aws',
credentials: defaultCredentials,
region: defaultRegion,
vpcId: null,
subnetType: defaultSubnetType,
healthCheck: undefined,
healthCheckProtocol: 'HTTP',
healthCheckPort: 7001,
healthCheckPath: '/healthcheck',
healthTimeout: 5,
healthInterval: 10,
healthyThreshold: 10,
unhealthyThreshold: 2,
idleTimeout: 60,
regionZones: [],
securityGroups: [],
listeners: [
{
externalPort: 80,
externalProtocol: 'HTTP',
internalPort: 7001,
internalProtocol: 'HTTP',
},
],
};
}
public static constructNewApplicationLoadBalancerTemplate(
application: Application,
): IAmazonApplicationLoadBalancerUpsertCommand {
const defaultCredentials = application.defaultCredentials.aws || AWSProviderSettings.defaults.account;
const defaultRegion = application.defaultRegions.aws || AWSProviderSettings.defaults.region;
const defaultSubnetType = AWSProviderSettings.defaults.subnetType;
const defaultPort = application.attributes.instancePort || SETTINGS.defaultInstancePort;
const defaultTargetGroupName = `targetgroup`;
return {
name: '',
availabilityZones: undefined,
stack: '',
detail: '',
loadBalancerType: 'application',
ipAddressType: 'ipv4',
dualstack: false,
isInternal: false,
cloudProvider: 'aws',
credentials: defaultCredentials,
region: defaultRegion,
vpcId: null,
subnetType: defaultSubnetType,
idleTimeout: 60,
deletionProtection: false,
targetGroups: [
{
name: defaultTargetGroupName,
protocol: 'HTTP',
port: defaultPort,
targetType: 'instance',
healthCheckProtocol: 'HTTP',
healthCheckPort: 'traffic-port',
healthCheckPath: '/healthcheck',
healthCheckTimeout: 5,
healthCheckInterval: 10,
healthyThreshold: 10,
unhealthyThreshold: 2,
attributes: {
deregistrationDelay: 300,
stickinessEnabled: false,
stickinessType: 'lb_cookie',
stickinessDuration: 8400,
multiValueHeadersEnabled: false,
},
},
],
regionZones: [],
securityGroups: [],
listeners: [
{
certificates: [],
protocol: 'HTTP',
port: 80,
defaultActions: [
{
type: 'forward',
targetGroupName: defaultTargetGroupName,
},
],
rules: [],
},
],
};
}
public static constructNewNetworkLoadBalancerTemplate(
application: Application,
): IAmazonNetworkLoadBalancerUpsertCommand {
const defaultCredentials = application.defaultCredentials.aws || AWSProviderSettings.defaults.account;
const defaultRegion = application.defaultRegions.aws || AWSProviderSettings.defaults.region;
const defaultSubnetType = AWSProviderSettings.defaults.subnetType;
const defaultTargetGroupName = `targetgroup`;
return {
name: '',
availabilityZones: undefined,
stack: '',
detail: '',
loadBalancerType: 'network',
isInternal: false,
ipAddressType: 'ipv4',
dualstack: false,
cloudProvider: 'aws',
credentials: defaultCredentials,
region: defaultRegion,
vpcId: null,
subnetType: defaultSubnetType,
deletionProtection: false,
loadBalancingCrossZone: true,
securityGroups: [],
targetGroups: [
{
name: defaultTargetGroupName,
protocol: 'TCP',
port: 7001,
targetType: 'instance',
healthCheckProtocol: 'TCP',
healthCheckPath: '/healthcheck',
healthCheckPort: 'traffic-port',
healthCheckTimeout: 5,
healthCheckInterval: 10,
healthyThreshold: 10,
unhealthyThreshold: 10,
attributes: {
deregistrationDelay: 300,
},
},
],
regionZones: [],
listeners: [
{
certificates: [],
protocol: 'TCP',
port: 80,
defaultActions: [
{
type: 'forward',
targetGroupName: defaultTargetGroupName,
},
],
rules: [],
},
],
};
}
}