@spotinst/spinnaker-deck
Version:
Spinnaker-Deck service, forked with support to Spotinst
357 lines (327 loc) • 13.7 kB
text/typescript
import { module } from 'angular';
import { chain, cloneDeep, flatten, intersection, xor } from 'lodash';
import { $q } from 'ngimport';
import { Subject } from 'rxjs';
import {
IAmazonApplicationLoadBalancer,
IAmazonLoadBalancer,
IAmazonServerGroupCommandDirty,
VpcReader,
} from '@spinnaker/amazon';
import {
AccountService,
Application,
CACHE_INITIALIZER_SERVICE,
CacheInitializerService,
IAccountDetails,
ICluster,
IDeploymentStrategy,
ISecurityGroup,
IServerGroupCommand,
IServerGroupCommandBackingData,
IServerGroupCommandViewState,
IVpc,
LOAD_BALANCER_READ_SERVICE,
LoadBalancerReader,
NameUtils,
SECURITY_GROUP_READER,
SecurityGroupReader,
setMatchingResourceSummary,
} from '@spinnaker/core';
import { IJobDisruptionBudget, ITitusResources } from 'titus/domain';
import { ITitusServiceJobProcesses } from 'titus/domain/ITitusServiceJobProcesses';
export interface ITitusServerGroupCommandBackingData extends IServerGroupCommandBackingData {
accounts: string[];
vpcs: IVpc[];
}
export interface ITitusServerGroupCommandViewState extends IServerGroupCommandViewState {
accountChangedStream: Subject<{}>;
regionChangedStream: Subject<{}>;
groupsRemovedStream: Subject<{}>;
dirty: IAmazonServerGroupCommandDirty;
defaultIamProfile: string;
}
export const defaultJobDisruptionBudget: IJobDisruptionBudget = {
availabilityPercentageLimit: {
percentageOfHealthyContainers: 95,
},
ratePercentagePerInterval: {
intervalMs: 600000,
percentageLimitPerInterval: 5,
},
containerHealthProviders: [{ name: 'eureka' }],
timeWindows: [
{
days: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
hourlyTimeWindows: [{ startHour: 10, endHour: 16 }],
timeZone: 'PST',
},
],
};
export const getDefaultJobDisruptionBudgetForApp = (application: Application): IJobDisruptionBudget => {
const budget = cloneDeep(defaultJobDisruptionBudget);
if (application.attributes && application.attributes.platformHealthOnly) {
budget.containerHealthProviders = [];
}
return budget;
};
export interface ITitusServerGroupCommand extends IServerGroupCommand {
cluster?: ICluster;
disruptionBudget?: IJobDisruptionBudget;
deferredInitialization?: boolean;
registry: string;
imageId: string;
organization: string;
repository: string;
tag?: string;
digest?: string;
image: string;
inService: boolean;
resources: ITitusResources;
efs: {
efsId: string;
mountPoint: string;
mountPerm: string;
efsRelativeMountPoint: string;
};
viewState: ITitusServerGroupCommandViewState;
targetGroups: string[];
removedTargetGroups: string[];
backingData: ITitusServerGroupCommandBackingData;
labels: { [key: string]: string };
containerAttributes: { [key: string]: string };
env: { [key: string]: string };
iamProfile: string;
migrationPolicy: {
type: string;
};
constraints: {
hard: { [key: string]: string };
soft: { [key: string]: string };
};
serviceJobProcesses: ITitusServiceJobProcesses;
}
export class TitusServerGroupConfigurationService {
public static $inject = ['cacheInitializer', 'loadBalancerReader', 'securityGroupReader'];
constructor(
private cacheInitializer: CacheInitializerService,
private loadBalancerReader: LoadBalancerReader,
private securityGroupReader: SecurityGroupReader,
) {}
public configureZones(command: ITitusServerGroupCommand) {
command.backingData.filtered.regions = command.backingData.credentialsKeyedByAccount[command.credentials].regions;
}
private attachEventHandlers(cmd: ITitusServerGroupCommand) {
cmd.credentialsChanged = (command: ITitusServerGroupCommand) => {
const result = { dirty: {} };
const backingData = command.backingData;
this.configureZones(command);
if (command.credentials) {
command.registry = (backingData.credentialsKeyedByAccount[command.credentials] as any).registry;
backingData.filtered.regions = backingData.credentialsKeyedByAccount[command.credentials].regions;
if (!backingData.filtered.regions.some((r) => r.name === command.region)) {
command.region = null;
command.regionChanged(command);
}
} else {
command.region = null;
}
command.viewState.dirty = { ...(command.viewState.dirty || {}), ...result.dirty };
this.configureLoadBalancerOptions(command);
this.configureSecurityGroupOptions(command);
setMatchingResourceSummary(command);
return result;
};
cmd.regionChanged = (command: ITitusServerGroupCommand) => {
this.configureLoadBalancerOptions(command);
this.configureSecurityGroupOptions(command);
setMatchingResourceSummary(command);
return {};
};
cmd.clusterChanged = (command: ITitusServerGroupCommand): void => {
command.moniker = NameUtils.getMoniker(command.application, command.stack, command.freeFormDetails);
setMatchingResourceSummary(command);
};
}
public configureCommand(cmd: ITitusServerGroupCommand) {
cmd.viewState.accountChangedStream = new Subject();
cmd.viewState.regionChangedStream = new Subject();
cmd.viewState.groupsRemovedStream = new Subject();
cmd.viewState.dirty = {};
cmd.onStrategyChange = (command: ITitusServerGroupCommand, strategy: IDeploymentStrategy) => {
// Any strategy other than None or Custom should force traffic to be enabled
if (strategy.key !== '' && strategy.key !== 'custom') {
command.inService = true;
}
};
cmd.image = cmd.viewState.imageId;
return $q
.all([
AccountService.getCredentialsKeyedByAccount('titus'),
this.securityGroupReader.getAllSecurityGroups(),
VpcReader.listVpcs(),
])
.then(([credentialsKeyedByAccount, securityGroups, vpcs]) => {
const backingData: any = {
credentialsKeyedByAccount,
securityGroups,
vpcs,
};
backingData.images = [];
backingData.accounts = Object.keys(credentialsKeyedByAccount);
backingData.filtered = {};
if (cmd.credentials.includes('${')) {
// If our dependency is an expression, the only thing we can really do is to just preserve current selections
backingData.filtered.regions = [{ name: cmd.region }];
} else {
backingData.filtered.regions = credentialsKeyedByAccount[cmd.credentials]?.regions ?? [];
}
cmd.backingData = backingData;
backingData.filtered.securityGroups = this.getRegionalSecurityGroups(cmd);
let securityGroupRefresher: PromiseLike<any> = $q.when();
if (cmd.securityGroups && cmd.securityGroups.length) {
const regionalSecurityGroupIds = backingData.filtered.securityGroups.map((g: ISecurityGroup) => g.id);
if (intersection(cmd.securityGroups, regionalSecurityGroupIds).length < cmd.securityGroups.length) {
securityGroupRefresher = this.refreshSecurityGroups(cmd, false);
}
}
return $q.all([this.refreshLoadBalancers(cmd), securityGroupRefresher]).then(() => {
this.attachEventHandlers(cmd);
});
});
}
private getVpcId(command: ITitusServerGroupCommand): string {
const credentials = this.getCredentials(command);
const match = command.backingData.vpcs.find(
(vpc) =>
vpc.name === credentials.awsVpc &&
vpc.account === credentials.awsAccount &&
vpc.region === this.getRegion(command) &&
vpc.cloudProvider === 'aws',
);
return match ? match.id : null;
}
private getRegionalSecurityGroups(command: ITitusServerGroupCommand): ISecurityGroup[] {
const newSecurityGroups: any = command.backingData.securityGroups[this.getAwsAccount(command)] || { aws: {} };
return chain<ISecurityGroup>(newSecurityGroups.aws[this.getRegion(command)])
.filter({ vpcId: this.getVpcId(command) })
.sortBy('name')
.value();
}
private configureSecurityGroupOptions(command: ITitusServerGroupCommand): void {
const currentOptions = command.backingData.filtered.securityGroups;
if (command.credentials.includes('${') || (command.region && command.region.includes('${'))) {
// If any of our dependencies are expressions, the only thing we can do is preserve current values
command.backingData.filtered.securityGroups = command.securityGroups.map((group) => ({ name: group, id: group }));
} else {
const newRegionalSecurityGroups = this.getRegionalSecurityGroups(command);
const isExpression =
typeof command.securityGroups === 'string' && (command.securityGroups as string).includes('${');
if (currentOptions && command.securityGroups && !isExpression) {
// not initializing - we are actually changing groups
const currentGroupNames: string[] = command.securityGroups.map((groupId: string) => {
const match = currentOptions.find((o) => o.id === groupId);
return match ? match.name : groupId;
});
const matchedGroups = command.securityGroups
.map((groupId: string) => {
const securityGroup: any = currentOptions.find((o) => o.id === groupId || o.name === groupId);
return securityGroup ? securityGroup.name : null;
})
.map((groupName: string) => newRegionalSecurityGroups.find((g) => g.name === groupName))
.filter((group: any) => group);
const matchedGroupNames: string[] = matchedGroups.map((g) => g.name);
const removed: string[] = xor(currentGroupNames, matchedGroupNames);
command.securityGroups = matchedGroups.map((g) => g.id);
if (removed.length) {
command.viewState.dirty.securityGroups = removed;
}
}
command.backingData.filtered.securityGroups = newRegionalSecurityGroups.sort((a, b) => {
if (command.securityGroups.includes(a.id)) {
return -1;
}
if (command.securityGroups.includes(b.id)) {
return 1;
}
return a.name.localeCompare(b.name);
});
}
}
public refreshSecurityGroups(
command: ITitusServerGroupCommand,
skipCommandReconfiguration: boolean,
): PromiseLike<void> {
return this.cacheInitializer.refreshCache('securityGroups').then(() => {
return this.securityGroupReader.getAllSecurityGroups().then((securityGroups: any) => {
command.backingData.securityGroups = securityGroups;
if (!skipCommandReconfiguration) {
this.configureSecurityGroupOptions(command);
}
});
});
}
private getCredentials(command: ITitusServerGroupCommand): IAccountDetails {
return command.backingData.credentialsKeyedByAccount[command.credentials];
}
private getAwsAccount(command: ITitusServerGroupCommand): string {
return this.getCredentials(command).awsAccount;
}
private getRegion(command: ITitusServerGroupCommand): string {
return command.region || (command.cluster ? command.cluster.region : null);
}
public getTargetGroupNames(command: ITitusServerGroupCommand): string[] {
const loadBalancersV2 = this.getLoadBalancerMap(command).filter(
(lb) => lb.loadBalancerType !== 'classic',
) as IAmazonApplicationLoadBalancer[];
const instanceTargetGroups = flatten(
loadBalancersV2.map<any>((lb) => lb.targetGroups.filter((tg) => tg.targetType === 'ip')),
);
return instanceTargetGroups.map((tg) => tg.name).sort();
}
private getLoadBalancerMap(command: ITitusServerGroupCommand): IAmazonLoadBalancer[] {
return chain(command.backingData.loadBalancers)
.map('accounts')
.flattenDeep()
.filter({ name: this.getAwsAccount(command) })
.map('regions')
.flattenDeep()
.filter({ name: this.getRegion(command) })
.map<IAmazonLoadBalancer>('loadBalancers')
.flattenDeep<IAmazonLoadBalancer>()
.value();
}
public configureLoadBalancerOptions(command: ITitusServerGroupCommand) {
const currentTargetGroups = command.targetGroups || [];
if (command.credentials.includes('${') || (command.region && command.region.includes('${'))) {
// If any of our dependencies are expressions, the only thing we can do is preserve current values
command.targetGroups = currentTargetGroups;
(command.backingData.filtered as any).targetGroups = currentTargetGroups;
} else {
const allTargetGroups = this.getTargetGroupNames(command);
if (currentTargetGroups && command.targetGroups) {
const matched = intersection(allTargetGroups, currentTargetGroups);
const removedTargetGroups = xor(matched, currentTargetGroups);
command.targetGroups = intersection(allTargetGroups, matched);
if (removedTargetGroups && removedTargetGroups.length > 0) {
command.viewState.dirty.targetGroups = removedTargetGroups;
} else {
delete command.viewState.dirty.targetGroups;
}
}
(command.backingData.filtered as any).targetGroups = allTargetGroups;
}
}
public refreshLoadBalancers(command: ITitusServerGroupCommand) {
return this.loadBalancerReader.listLoadBalancers('aws').then((loadBalancers) => {
command.backingData.loadBalancers = loadBalancers;
this.configureLoadBalancerOptions(command);
});
}
}
export const TITUS_SERVER_GROUP_CONFIGURATION_SERVICE = 'spinnaker.titus.serverGroup.configure.service';
module(TITUS_SERVER_GROUP_CONFIGURATION_SERVICE, [
CACHE_INITIALIZER_SERVICE,
LOAD_BALANCER_READ_SERVICE,
SECURITY_GROUP_READER,
]).service('titusServerGroupConfigurationService', TitusServerGroupConfigurationService);