UNPKG

@spotinst/spinnaker-deck

Version:

Spinnaker-Deck service, forked with support to Spotinst

773 lines (685 loc) 29.1 kB
import { mockHttpClient } from 'core/api/mock/jasmine'; import { mock } from 'angular'; import { find } from 'lodash'; import { REACT_MODULE } from 'core/reactShims'; import * as State from 'core/state'; import { ApplicationModelBuilder } from 'core/application/applicationModel.builder'; import { IInstanceCounts, IServerGroup } from 'core/domain'; import { Application } from 'core/application/application.model'; import { CLUSTER_SERVICE, ClusterService } from './cluster.service'; import { SETTINGS } from 'core/config/settings'; const ClusterState = State.ClusterState; describe('Service: Cluster', function () { beforeEach(mock.module(CLUSTER_SERVICE, REACT_MODULE)); let clusterService: ClusterService; let application: Application; function buildTask(config: { status: string; variables: { [key: string]: any } }) { return { status: config.status, getValueFor: (key: string): any => { return find(config.variables, { key }) ? find(config.variables, { key }).value : null; }, }; } beforeEach( mock.inject((_clusterService_: ClusterService) => { clusterService = _clusterService_; application = ApplicationModelBuilder.createApplicationForTests( 'app', { key: 'serverGroups', defaultData: [] }, { key: 'runningExecutions', defaultData: [] }, { key: 'runningTasks', defaultData: [] }, ); application.getDataSource('serverGroups').data = [ { name: 'the-target', account: 'not-the-target', region: 'us-east-1' }, { name: 'the-target', account: 'test', region: 'not-the-target' }, { name: 'the-target', account: 'test', region: 'us-east-1' }, { name: 'not-the-target', account: 'test', region: 'us-east-1' }, { name: 'the-source', account: 'test', region: 'us-east-1' }, ]; }), ); beforeEach(() => State.initialize()); describe('lazy cluster fetching', () => { it('switches to lazy cluster fetching if there are more than the on demand threshold for clusters', async () => { const http = mockHttpClient(); const clusters = [...Array(SETTINGS.onDemandClusterThreshold + 1)]; http.expectGET('/applications/app/clusters').respond(200, { test: clusters }); http.expectGET('/applications/app/serverGroups?clusters=').respond(200, []); let serverGroups: IServerGroup[] = null; clusterService.loadServerGroups(application).then((result: IServerGroup[]) => (serverGroups = result)); await http.flush(); expect(serverGroups).toEqual([]); expect(application.serverGroups.fetchOnDemand).toBe(true); }); it('does boring regular fetching when there are less than the on demand threshold for clusters', async () => { const http = mockHttpClient(); const clusters = Array(SETTINGS.onDemandClusterThreshold); http.expectGET('/applications/app/clusters').respond(200, { test: clusters }); http.expectGET('/applications/app/serverGroups').respond(200, []); let serverGroups: IServerGroup[] = null; clusterService.loadServerGroups(application).then((result: IServerGroup[]) => (serverGroups = result)); await http.flush(); expect(application.serverGroups.fetchOnDemand).toBe(false); expect(serverGroups).toEqual([]); }); it('converts clusters parameter to q and account params when there are fewer than 251 clusters', async () => { const http = mockHttpClient(); spyOn(ClusterState.filterModel.asFilterModel, 'applyParamsToUrl').and.callFake(() => {}); const clusters = Array(250); ClusterState.filterModel.asFilterModel.sortFilter.clusters = { 'test:myapp': true }; http.expectGET('/applications/app/clusters').respond(200, { test: clusters }); http.expectGET('/applications/app/serverGroups').respond(200, []); let serverGroups: IServerGroup[] = null; clusterService.loadServerGroups(application).then((result: IServerGroup[]) => (serverGroups = result)); await http.flush(); expect(application.serverGroups.fetchOnDemand).toBe(false); expect(ClusterState.filterModel.asFilterModel.sortFilter.filter).toEqual('clusters:myapp'); expect(ClusterState.filterModel.asFilterModel.sortFilter.account.test).toBe(true); expect(serverGroups).toEqual([]); }); }); describe('health count rollups', () => { it('aggregates health counts from server groups', () => { application.serverGroups.data = [ { cluster: 'cluster-a', name: 'cluster-a-v001', account: 'test', region: 'us-east-1', instances: [], instanceCounts: { total: 1, up: 1 }, }, { cluster: 'cluster-a', name: 'cluster-a-v001', account: 'test', region: 'us-west-1', instances: [], instanceCounts: { total: 2, down: 2 }, }, { cluster: 'cluster-b', name: 'cluster-b-v001', account: 'test', region: 'us-east-1', instances: [], instanceCounts: { total: 1, starting: 1 }, }, { cluster: 'cluster-b', name: 'cluster-b-v001', account: 'test', region: 'us-west-1', instances: [], instanceCounts: { total: 1, outOfService: 1 }, }, { cluster: 'cluster-b', name: 'cluster-b-v002', account: 'test', region: 'us-west-1', instances: [], instanceCounts: { total: 2, unknown: 1, outOfService: 1 }, }, ]; const clusters = clusterService.createServerGroupClusters(application.serverGroups.data); const cluster0counts: IInstanceCounts = clusters[0].instanceCounts; const cluster1counts: IInstanceCounts = clusters[1].instanceCounts; expect(clusters.length).toBe(2); expect(cluster0counts.total).toBe(3); expect(cluster0counts.up).toBe(1); expect(cluster0counts.down).toBe(2); expect(cluster0counts.starting).toBe(0); expect(cluster0counts.outOfService).toBe(0); expect(cluster0counts.unknown).toBe(0); expect(cluster1counts.total).toBe(4); expect(cluster1counts.up).toBe(0); expect(cluster1counts.down).toBe(0); expect(cluster1counts.starting).toBe(1); expect(cluster1counts.outOfService).toBe(2); expect(cluster1counts.unknown).toBe(1); }); }); describe('addServerGroupsToApplication merging to preserve referential equality', () => { const asgFabricator = (x: string): IServerGroup => ({ cluster: `cluster-${x}`, name: `cluster-${x}-v001`, account: 'test', region: 'us-east-1', category: 'serverGroup', cloudProvider: 'titus', type: 'titus', instances: [], instanceCounts: { total: 1, up: 1, down: 0, starting: 0, succeeded: 0, failed: 0, unknown: 0, outOfService: 0 }, }); it('merges single new server group', () => { // local data application.serverGroups.data = ['mike', 'dustin', 'lucas', 'will'].map(asgFabricator); // remote data const serverGroups = ['mike', 'dustin', 'lucas', 'will', 'eleven'].map(asgFabricator); const merged = clusterService.addServerGroupsToApplication(application, serverGroups); expect(merged.find((sg) => sg.name === 'cluster-mike-v001')).toBeDefined( 'Existing server group should be in merged output1' + JSON.stringify(merged, null, 4), ); expect(merged.find((sg) => sg.name === 'cluster-dustin-v001')).toBeDefined( 'Existing server group should be in merged output2', ); expect(merged.find((sg) => sg.name === 'cluster-lucas-v001')).toBeDefined( 'Existing server group should be in merged output3', ); expect(merged.find((sg) => sg.name === 'cluster-will-v001')).toBeDefined( 'Existing server group should be in merged output4', ); expect(merged.find((sg) => sg.name === 'cluster-eleven-v001')).toBeDefined( 'New server group should be added in merged output', ); }); it('merges multiple new server groups', () => { // local data application.serverGroups.data = ['mike', 'dustin', 'lucas', 'will'].map(asgFabricator); // remote data const serverGroups = ['mike', 'dustin', 'lucas', 'will', 'eleven', 'hopper'].map(asgFabricator); const merged = clusterService.addServerGroupsToApplication(application, serverGroups); expect(merged.find((sg) => sg.name === 'cluster-mike-v001')).toBeDefined( 'Existing server group should be in merged output', ); expect(merged.find((sg) => sg.name === 'cluster-dustin-v001')).toBeDefined( 'Existing server group should be in merged output', ); expect(merged.find((sg) => sg.name === 'cluster-lucas-v001')).toBeDefined( 'Existing server group should be in merged output', ); expect(merged.find((sg) => sg.name === 'cluster-will-v001')).toBeDefined( 'Existing server group should be in merged output', ); expect(merged.find((sg) => sg.name === 'cluster-eleven-v001')).toBeDefined( 'New server group should be added in merged output', ); expect(merged.find((sg) => sg.name === 'cluster-hopper-v001')).toBeDefined( 'New server group should be added in merged output', ); }); it('removes single server group that no longer exists', () => { // local data application.serverGroups.data = ['mike', 'dustin', 'lucas', 'will', 'eleven'].map(asgFabricator); // remote data const serverGroups = ['mike', 'dustin', 'lucas', 'eleven'].map(asgFabricator); const merged = clusterService.addServerGroupsToApplication(application, serverGroups); expect(merged.find((sg) => sg.name === 'cluster-mike-v001')).toBeDefined( 'Remaining server group should be in merged output', ); expect(merged.find((sg) => sg.name === 'cluster-dustin-v001')).toBeDefined( 'Remaining server group should be in merged output', ); expect(merged.find((sg) => sg.name === 'cluster-lucas-v001')).toBeDefined( 'Remaining server group should be in merged output', ); expect(merged.find((sg) => sg.name === 'cluster-eleven-v001')).toBeDefined( 'Remaining server group should be in merged output', ); expect(merged.find((sg) => sg.name === 'cluster-will-v001')).toBeUndefined( 'Removed server group should be absent in merged output', ); }); it('removes multiple server group that no longer exists', () => { // This test is specifically meant to catch a shifting iterative splice // If we started with [0, 1, 2, 3, 4, 5] and wanted toRemove [0, 1], // Blindly forEach'ing and splicing like so: toRemove.forEach(i => data.splice(i, 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] // 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. // local data application.serverGroups.data = ['mike', 'dustin', 'lucas', 'will', 'eleven', 'hopper'].map(asgFabricator); // remote data const serverGroups = ['dustin', 'lucas', 'will', 'hopper'].map(asgFabricator); const merged = clusterService.addServerGroupsToApplication(application, serverGroups); expect(merged.find((sg) => sg.name === 'cluster-dustin-v001')).toBeDefined( 'Remaining server group should be in merged output', ); expect(merged.find((sg) => sg.name === 'cluster-lucas-v001')).toBeDefined( 'Remaining server group should be in merged output', ); expect(merged.find((sg) => sg.name === 'cluster-will-v001')).toBeDefined( 'Remaining server group should be in merged output', ); expect(merged.find((sg) => sg.name === 'cluster-mike-v001')).toBeUndefined( 'Removed server group should be absent in merged output', ); expect(merged.find((sg) => sg.name === 'cluster-eleven-v001')).toBeUndefined( 'Removed server group should be absent in merged output', ); }); }); describe('addTasksToServerGroups', () => { describe('rollback tasks', function () { it('attaches to source and target', function () { application.runningTasks.data = [ buildTask({ status: 'RUNNING', variables: [ { key: 'credentials', value: 'test' }, { key: 'region', value: 'us-east-1' }, { key: 'targetop.asg.disableServerGroup.name', value: 'the-source' }, { key: 'targetop.asg.enableServerGroup.name', value: 'the-target' }, ], }), ]; application.runningTasks.data[0].execution = { stages: [{ type: 'rollbackServerGroup', context: {} }] }; clusterService.addTasksToServerGroups(application); const serverGroups: IServerGroup[] = application.serverGroups.data; expect(serverGroups[0].runningTasks.length).toBe(0); expect(serverGroups[1].runningTasks.length).toBe(0); expect(serverGroups[2].runningTasks.length).toBe(1); expect(serverGroups[3].runningTasks.length).toBe(0); expect(serverGroups[4].runningTasks.length).toBe(1); }); }); describe('createcopylastasg tasks', () => { it('attaches to source and target', () => { application.runningTasks.data = [ buildTask({ status: 'RUNNING', variables: [ { key: 'notification.type', value: 'createcopylastasg' }, { key: 'deploy.account.name', value: 'test' }, { key: 'availabilityZones', value: { 'us-east-1': ['a'] } }, { key: 'deploy.server.groups', value: { 'us-east-1': ['the-target'] } }, { key: 'source', value: { asgName: 'the-source', account: 'test', region: 'us-east-1' } }, ], }), ]; clusterService.addTasksToServerGroups(application); const serverGroups: IServerGroup[] = application.serverGroups.data; expect(serverGroups[0].runningTasks.length).toBe(0); expect(serverGroups[1].runningTasks.length).toBe(0); expect(serverGroups[2].runningTasks.length).toBe(1); expect(serverGroups[3].runningTasks.length).toBe(0); expect(serverGroups[4].runningTasks.length).toBe(1); }); it('still attaches to source when target not found', () => { application.runningTasks.data = [ buildTask({ status: 'RUNNING', variables: [ { key: 'notification.type', value: 'createcopylastasg' }, { key: 'deploy.account.name', value: 'test' }, { key: 'availabilityZones', value: { 'us-east-1': ['a'] } }, { key: 'deploy.server.groups', value: { 'us-east-1': ['not-found-target'] } }, { key: 'source', value: { asgName: 'the-source', account: 'test', region: 'us-east-1' } }, ], }), ]; clusterService.addTasksToServerGroups(application); const serverGroups: IServerGroup[] = application.serverGroups.data; expect(serverGroups[0].runningTasks.length).toBe(0); expect(serverGroups[1].runningTasks.length).toBe(0); expect(serverGroups[2].runningTasks.length).toBe(0); expect(serverGroups[3].runningTasks.length).toBe(0); expect(serverGroups[4].runningTasks.length).toBe(1); }); }); describe('createdeploy', () => { it('attaches to deployed server group', () => { application.runningTasks.data = [ buildTask({ status: 'RUNNING', variables: [ { key: 'notification.type', value: 'createdeploy' }, { key: 'deploy.account.name', value: 'test' }, { key: 'deploy.server.groups', value: { 'us-east-1': ['the-target'] } }, ], }), ]; clusterService.addTasksToServerGroups(application); const serverGroups: IServerGroup[] = application.serverGroups.data; expect(serverGroups[0].runningTasks.length).toBe(0); expect(serverGroups[1].runningTasks.length).toBe(0); expect(serverGroups[2].runningTasks.length).toBe(1); expect(serverGroups[3].runningTasks.length).toBe(0); expect(serverGroups[4].runningTasks.length).toBe(0); }); it('does nothing when target not found', () => { application.runningTasks.data = [ buildTask({ status: 'RUNNING', variables: [ { key: 'notification.type', value: 'createdeploy' }, { key: 'deploy.account.name', value: 'test' }, { key: 'deploy.server.groups', value: { 'us-east-1': ['not-found-target'] } }, ], }), ]; clusterService.addTasksToServerGroups(application); const serverGroups: IServerGroup[] = application.serverGroups.data; expect(serverGroups[0].runningTasks.length).toBe(0); expect(serverGroups[1].runningTasks.length).toBe(0); expect(serverGroups[2].runningTasks.length).toBe(0); expect(serverGroups[3].runningTasks.length).toBe(0); expect(serverGroups[4].runningTasks.length).toBe(0); }); }); describe('can find task in server groups by instance id', () => { [ 'terminateinstances', 'rebootinstances', 'registerinstanceswithloadbalancer', 'deregisterinstancesfromloadbalancer', 'enableinstancesindiscovery', 'disableinstancesindiscovery', ].forEach((name) => { describe(name, () => { it('finds instance within server group (' + name + ')', () => { const serverGroups: IServerGroup[] = application.serverGroups.data; serverGroups[2].instances = [ { name: 'in-1', id: 'in-1', health: null, launchTime: 1, zone: null }, { name: 'in-2', id: 'in-2', health: null, launchTime: 1, zone: null }, ]; serverGroups[4].instances = [ { name: 'in-3', id: 'in-3', health: null, launchTime: 1, zone: null }, { name: 'in-2', id: 'in-2', health: null, launchTime: 1, zone: null }, ]; application.runningTasks.data = [ buildTask({ status: 'RUNNING', variables: [ { key: 'notification.type', value: name }, { key: 'credentials', value: 'test' }, { key: 'region', value: 'us-east-1' }, { key: 'instanceIds', value: ['in-2'] }, ], }), ]; clusterService.addTasksToServerGroups(application); expect(serverGroups[0].runningTasks.length).toBe(0); expect(serverGroups[1].runningTasks.length).toBe(0); expect(serverGroups[2].runningTasks.length).toBe(1); expect(serverGroups[3].runningTasks.length).toBe(0); expect(serverGroups[4].runningTasks.length).toBe(1); }); }); }); }); describe('resizeasg, disableasg, destroyasg, enableasg', () => { beforeEach(() => { this.validateTaskAttached = () => { clusterService.addTasksToServerGroups(application); const serverGroups: IServerGroup[] = application.serverGroups.data; expect(serverGroups[0].runningTasks.length).toBe(0); expect(serverGroups[1].runningTasks.length).toBe(0); expect(serverGroups[2].runningTasks.length).toBe(1); expect(serverGroups[3].runningTasks.length).toBe(0); expect(serverGroups[4].runningTasks.length).toBe(0); }; this.buildCommonTask = (type: string) => { application.runningTasks = { data: [ buildTask({ status: 'RUNNING', variables: [ { key: 'notification.type', value: type }, { key: 'credentials', value: 'test' }, { key: 'regions', value: ['us-east-1'] }, { key: 'asgName', value: 'the-target' }, ], }), ], }; }; }); it('resizeasg', () => { this.buildCommonTask('resizeasg'); this.validateTaskAttached(); }); it('disableasg', () => { this.buildCommonTask('resizeasg'); this.validateTaskAttached(); }); it('destroyasg', () => { this.buildCommonTask('resizeasg'); this.validateTaskAttached(); }); it('enableasg', () => { this.buildCommonTask('resizeasg'); this.validateTaskAttached(); }); it('some unknown task', () => { this.buildCommonTask('someuknownthing'); clusterService.addTasksToServerGroups(application); application.serverGroups.data.forEach((serverGroup: IServerGroup) => { expect(serverGroup.runningTasks.length).toBe(0); }); }); }); describe('extraction region from stage context', function () { it('should return the region from the deploy.server.groups node', function () { const context = { 'deploy.server.groups': { 'us-west-1': ['mahe-prestaging-v001'], }, }; const result = clusterService.extractRegionFromContext(context); expect(result).toBe('us-west-1'); }); it('should return empty string if nothing is extracted', function () { const context = {}; const result = clusterService.extractRegionFromContext(context); expect(result).toBe(''); }); }); describe('add executions to server group for deploy stage', function () { beforeEach(() => { application.serverGroups.data = [ { name: 'foo-v001', account: 'test', region: 'us-west-1', }, ]; }); it('should successfully add a matched execution to a server group', function () { const executions = [ { stages: [ { type: 'deploy', context: { 'deploy.server.groups': { 'us-west-1': ['foo-v001'], }, account: 'test', }, }, ], }, ]; application.runningExecutions.data = executions; clusterService.addExecutionsToServerGroups(application); expect(application.serverGroups.data[0].runningExecutions.length).toBe(1); }); it('should NOT add a execution to a server group if the region does not match', function () { const executions = [ { stages: [ { type: 'deploy', context: { 'deploy.server.groups': { 'us-east-1': ['foo-v001'], }, account: 'test', }, }, ], }, ]; application.runningExecutions.data = executions; clusterService.addExecutionsToServerGroups(application); expect(application.serverGroups.data[0].runningExecutions.length).toBe(0); }); it('should NOT add a execution to a server group if the account does not match', function () { const executions = [ { stages: [ { type: 'deploy', context: { 'deploy.server.groups': { 'us-west-1': ['foo-v001'], }, account: 'prod', }, }, ], }, ]; application.runningExecutions.data = executions; clusterService.addExecutionsToServerGroups(application); expect(application.serverGroups.data[0].runningExecutions.length).toBe(0); }); }); describe('add executions to server group for disableAsg stage', function () { beforeEach(() => { application.serverGroups.data = [ { name: 'foo-v001', account: 'test', region: 'us-west-1', }, ]; }); it('should successfully add a matched execution to a server group', function () { const executions = [ { stages: [ { type: 'disableAsg', context: { 'targetop.asg.disableAsg.name': 'foo-v001', 'targetop.asg.disableAsg.regions': ['us-west-1'], credentials: 'test', }, }, ], }, ]; application.runningExecutions.data = executions; clusterService.addExecutionsToServerGroups(application); expect(application.serverGroups.data[0].runningExecutions.length).toBe(1); }); it('should NOT add a execution to a server group if the region does not match', function () { const executions = [ { stages: [ { type: 'disableAsg', context: { 'targetop.asg.disableAsg.name': 'foo-v001', 'targetop.asg.disableAsg.regions': ['us-east-1'], credentials: 'test', }, }, ], }, ]; application.runningExecutions.data = executions; clusterService.addExecutionsToServerGroups(application); expect(application.serverGroups.data[0].runningExecutions.length).toBe(0); }); it('should NOT add a execution to a server group if the account does not match', function () { const executions = [ { stages: [ { type: 'deploy', context: { 'targetop.asg.disableAsg.name': 'foo-v001', 'targetop.asg.disableAsg.regions': ['us-west-1'], credentials: 'prod', }, }, ], }, ]; application.runningExecutions.data = executions; clusterService.addExecutionsToServerGroups(application); expect(application.serverGroups.data[0].runningExecutions.length).toBe(0); }); }); describe('adding executions to server group for deployManifest stage', () => { beforeEach(() => { application.serverGroups.data = [ { name: 'deployment my-k8s-object', account: 'prod', region: 'default', }, ]; }); it('should add a matched execution to a server group', () => { const executions = [ { stages: [ { type: 'deployManifest', context: { 'outputs.manifestNamesByNamespace': { default: ['deployment my-k8s-object'], }, account: 'prod', }, }, ], }, ]; application.runningExecutions.data = executions; clusterService.addExecutionsToServerGroups(application); expect(application.serverGroups.data[0].runningExecutions.length).toBe(1); }); it('should NOT add a matched execution if the account does not match', () => { const executions = [ { stages: [ { type: 'deployManifest', context: { 'outputs.manifestNamesByNamespace': { default: ['deployment my-k8s-object'], }, account: 'test', }, }, ], }, ]; application.runningExecutions.data = executions; clusterService.addExecutionsToServerGroups(application); expect(application.serverGroups.data[0].runningExecutions.length).toBe(0); }); it('should NOT add a matched execution if the server group name does not match', () => { const executions = [ { stages: [ { type: 'deployManifest', context: { 'outputs.manifestNamesByNamespace': { default: ['deployment my-other-k8s-object'], }, account: 'test', }, }, ], }, ]; application.runningExecutions.data = executions; clusterService.addExecutionsToServerGroups(application); expect(application.serverGroups.data[0].runningExecutions.length).toBe(0); }); }); }); });