UNPKG

@spotinst/spinnaker-deck

Version:

Spinnaker-Deck service, forked with support to Spotinst

341 lines (311 loc) 13.9 kB
import { IQService, module } from 'angular'; import { flatten, forOwn, groupBy, has, head, keyBy, keys, values } from 'lodash'; import { REST } from 'core/api'; import { Application } from 'core/application'; import { ArtifactReferenceService } from 'core/artifact'; import { ProviderServiceDelegate } from 'core/cloudProvider'; import { SETTINGS } from 'core/config/settings'; import { IArtifactExtractor, ICluster, IClusterSummary, IExecution, IExecutionStage, IServerGroup, ITask, } from 'core/domain'; import { FilterModelService } from 'core/filterModel'; import { NameUtils } from 'core/naming'; import { ClusterState } from 'core/state'; import { CORE_SERVERGROUP_SERVERGROUP_TRANSFORMER } from '../serverGroup/serverGroup.transformer'; import { taskMatcher } from './task.matcher'; export class ClusterService { public static $inject = ['$q', 'serverGroupTransformer', 'providerServiceDelegate']; constructor( private $q: IQService, private serverGroupTransformer: any, private providerServiceDelegate: ProviderServiceDelegate, ) {} // Retrieves and normalizes all server groups. If a server group for an unsupported cloud provider (i.e. one that does // not have a server group transformer) is encountered, it will be omitted from the result. public loadServerGroups(application: Application): PromiseLike<IServerGroup[]> { return this.getClusters(application.name).then((clusters: IClusterSummary[]) => { const dataSource = application.getDataSource('serverGroups'); let serverGroupLoader = REST('/applications').path(application.name, 'serverGroups'); dataSource.fetchOnDemand = clusters.length > SETTINGS.onDemandClusterThreshold; if (dataSource.fetchOnDemand) { dataSource.clusters = clusters; serverGroupLoader = serverGroupLoader.query({ clusters: FilterModelService.getCheckValues( ClusterState.filterModel.asFilterModel.sortFilter.clusters, ).join(), }); } else { this.reconcileClusterDeepLink(); } return serverGroupLoader.get().then((serverGroups: IServerGroup[]) => { serverGroups.forEach((sg) => this.addHealthStatusCheck(sg)); serverGroups.forEach((sg) => this.addNameParts(sg)); return this.$q .all(serverGroups.map((sg) => this.serverGroupTransformer.normalizeServerGroup(sg, application))) .then((normalized) => normalized.filter(Boolean)); }); }); } // if the application is deep linked via "clusters:", but the app is not "fetchOnDemand" sized, convert the parameters // to the normal, filterable structure private reconcileClusterDeepLink() { const selectedClusters: string[] = FilterModelService.getCheckValues( ClusterState.filterModel.asFilterModel.sortFilter.clusters, ); if (selectedClusters && selectedClusters.length) { const clusterNames: string[] = []; const accountNames: string[] = []; selectedClusters.forEach((clusterKey) => { const [account, cluster] = clusterKey.split(':'); accountNames.push(account); if (cluster) { clusterNames.push(cluster); } }); if (clusterNames.length) { accountNames.forEach((account) => (ClusterState.filterModel.asFilterModel.sortFilter.account[account] = true)); ClusterState.filterModel.asFilterModel.sortFilter.filter = `clusters:${clusterNames.join()}`; ClusterState.filterModel.asFilterModel.sortFilter.clusters = {}; ClusterState.filterModel.asFilterModel.applyParamsToUrl(); } } } private generateServerGroupLookupKey(serverGroup: IServerGroup): string { const { name, account, region, category } = serverGroup; return [name, account, region, category].join('-'); } public addServerGroupsToApplication(application: Application, serverGroups: IServerGroup[] = []): IServerGroup[] { // map of incoming data const remoteMap = keyBy(serverGroups, this.generateServerGroupLookupKey); // map local cache const localMap = keyBy(application.serverGroups.data, this.generateServerGroupLookupKey); if (application.serverGroups.data) { const data = application.serverGroups.data; // remove any that have dropped off, update any that have changed const toRemove: number[] = []; data.forEach((serverGroup: IServerGroup, idx: number) => { const match = remoteMap[this.generateServerGroupLookupKey(serverGroup)]; if (match) { // Match found between local and incoming data, update but only if needed if (serverGroup.stringVal && match.stringVal && serverGroup.stringVal !== match.stringVal) { data[idx] = match; } } else { // Not found means server group was removed toRemove.push(idx); } }); // IMPORTANT!!! - toRemove must be forEach'ed in decending order, so that we splice backwards. // For example, if we started with [0, 1, 2, 3, 4, 5] and wanted toRemove [0, 1], // Blindly forEach'ing and splicing like so: toRemove.forEach(idx => data.splice(idx, 1)) // would result in the following at each step: // data // [0, 1, 2, 3, 4, 5] // data.splice(0,1); // [1, 2, 3, 4, 5] // data.splice(1,1); // [1, 3, 4, 5] wait, what?? // If toRemove is in ascending order, every splice will cause everything to shift left // and every remaning index will no longer be correct (off by 1 for every iteration) // Works perfect in descending order though. toRemove // ensure indices are in descending order so splice can work properly .sort() .reverse() // splice is necessary to preserve referential equality .forEach((idx) => data.splice(idx, 1)); // add any new ones serverGroups.forEach((serverGroup) => { const match = localMap[this.generateServerGroupLookupKey(serverGroup)]; if (!match) { data.push(serverGroup); } }); return data; } else { return serverGroups; } } public createServerGroupClusters(serverGroups: IServerGroup[]): ICluster[] { const clusters: ICluster[] = []; const groupedByAccount = groupBy(serverGroups, 'account'); forOwn(groupedByAccount, (accountServerGroups, account) => { const groupedByCategory = groupBy(accountServerGroups, 'category'); forOwn(groupedByCategory, (categoryServerGroups, category) => { const groupedByCluster = groupBy(categoryServerGroups, 'cluster'); forOwn(groupedByCluster, (clusterServerGroups, clusterName) => { const cluster: ICluster = { account, category, name: clusterName, serverGroups: clusterServerGroups, cloudProvider: clusterServerGroups[0].cloudProvider, }; this.addHealthCountsToCluster(cluster); clusters.push(cluster); }); }); }); this.addProvidersAndServerGroupsToInstances(serverGroups); return clusters; } public addExecutionsToServerGroups(application: Application): void { const executions = application.runningExecutions?.data ?? []; if (!application.serverGroups.data) { return; // still run if there are no running tasks, since they may have all finished and we need to clear them. } application.serverGroups.data.forEach((serverGroup: IServerGroup) => { serverGroup.runningExecutions = []; executions.forEach((execution: IExecution) => { this.findStagesWithServerGroupInfo(execution.stages).forEach((stage: IExecutionStage) => { const stageServerGroup = stage ? this.extractServerGroupNameFromContext(stage.context) : ''; const stageAccount = stage && stage.context ? stage.context.account || stage.context.credentials : ''; const stageRegion = stage ? this.extractRegionFromContext(stage.context) : ''; if ( stageServerGroup.includes(serverGroup.name) && stageAccount === serverGroup.account && stageRegion === serverGroup.region ) { serverGroup.runningExecutions.push(execution); } }); }); }); } public addTasksToServerGroups(application: Application): void { const runningTasks: ITask[] = application.runningTasks?.data ?? []; if (!application.serverGroups.data) { return; // still run if there are no running tasks, since they may have all finished and we need to clear them. } application.serverGroups.data.forEach((serverGroup: IServerGroup) => { if (!serverGroup.runningTasks) { serverGroup.runningTasks = []; } else { serverGroup.runningTasks.length = 0; } runningTasks.forEach((task) => { if (taskMatcher.taskMatches(task, serverGroup)) { serverGroup.runningTasks.push(task); } }); }); } public isDeployingArtifact(cluster: ICluster): boolean { return cluster.imageSource === 'artifact'; } public defaultArtifactExtractor(): IArtifactExtractor { return { extractArtifacts: (cluster: ICluster) => (this.isDeployingArtifact(cluster) ? [cluster.imageArtifactId] : []), removeArtifact: (cluster: ICluster, artifactId: string) => { ArtifactReferenceService.removeArtifactFromField('imageArtifactId', cluster, artifactId); }, }; } public getArtifactExtractor(cloudProvider: string): IArtifactExtractor { return this.providerServiceDelegate.hasDelegate(cloudProvider, 'serverGroup.artifactExtractor') ? this.providerServiceDelegate.getDelegate<IArtifactExtractor>(cloudProvider, 'serverGroup.artifactExtractor') : this.defaultArtifactExtractor(); } public extractArtifacts(cluster: ICluster): string[] { return this.getArtifactExtractor(cluster.cloudProvider).extractArtifacts(cluster); } public removeArtifact(cluster: ICluster, artifactId: string): void { this.getArtifactExtractor(cluster.cloudProvider).removeArtifact(cluster, artifactId); } private getClusters(application: string): PromiseLike<IClusterSummary[]> { return REST('/applications') .path(application, 'clusters') .get() .then((clustersMap: { [account: string]: string[] }) => { const clusters: IClusterSummary[] = []; Object.keys(clustersMap).forEach((account) => { clustersMap[account].forEach((name) => { clusters.push({ account, name }); }); }); return clusters; }); } private extractServerGroupNameFromContext(context: any): string { return ( head(values(context['deploy.server.groups'])) || context['targetop.asg.disableAsg.name'] || flatten(values(context['outputs.manifestNamesByNamespace'])) || '' ); } public extractRegionFromContext(context: any): string { return ( head(keys(context['deploy.server.groups'] as string)) || head(context['targetop.asg.disableAsg.regions'] as string) || head(keys(context['outputs.manifestNamesByNamespace'])) || '' ); } private findStagesWithServerGroupInfo(stages: IExecutionStage[]): IExecutionStage[] { return (stages || []).filter( (stage) => (['createServerGroup', 'deploy', 'destroyAsg', 'resizeAsg'].includes(stage.type) && has(stage.context, 'deploy.server.groups')) || (stage.type === 'disableAsg' && has(stage.context, 'targetop.asg.disableAsg.name')) || has(stage.context, 'outputs.manifestNamesByNamespace'), ); } private addProvidersAndServerGroupsToInstances(serverGroups: IServerGroup[]) { serverGroups.forEach((serverGroup) => { serverGroup.instances.forEach((instance) => { instance.provider = serverGroup.type || serverGroup.provider; instance.serverGroup = instance.serverGroup || serverGroup.name; instance.vpcId = serverGroup.vpcId; }); }); } private addNameParts(serverGroup: IServerGroup): void { const nameParts = NameUtils.parseServerGroupName(serverGroup.name); if (serverGroup.moniker) { Object.assign(serverGroup, serverGroup.moniker); } else { serverGroup.app = nameParts.application; serverGroup.stack = nameParts.stack; serverGroup.detail = nameParts.freeFormDetails; serverGroup.cluster = nameParts.cluster; } serverGroup.category = 'serverGroup'; } private addHealthStatusCheck(serverGroup: IServerGroup): void { serverGroup.instances.forEach((instance) => { instance.hasHealthStatus = (instance.health || []).some((h) => h.state !== 'Unknown'); }); } private addHealthCountsToCluster(cluster: ICluster): void { cluster.instanceCounts = { up: 0, down: 0, unknown: 0, starting: 0, outOfService: 0, succeeded: 0, failed: 0, total: 0, }; const operand = cluster.serverGroups || []; operand.forEach((serverGroup) => { if (!serverGroup.instanceCounts) { return; } cluster.instanceCounts.total += serverGroup.instanceCounts.total || 0; cluster.instanceCounts.up += serverGroup.instanceCounts.up || 0; cluster.instanceCounts.down += serverGroup.instanceCounts.down || 0; cluster.instanceCounts.unknown += serverGroup.instanceCounts.unknown || 0; cluster.instanceCounts.starting += serverGroup.instanceCounts.starting || 0; cluster.instanceCounts.outOfService += serverGroup.instanceCounts.outOfService || 0; cluster.instanceCounts.succeeded += serverGroup.instanceCounts.succeeded || 0; cluster.instanceCounts.failed += serverGroup.instanceCounts.failed || 0; }); } } export const CLUSTER_SERVICE = 'spinnaker.core.cluster.service'; module(CLUSTER_SERVICE, [CORE_SERVERGROUP_SERVERGROUP_TRANSFORMER]).service('clusterService', ClusterService);