From ed61a594c148dd7574b7d426f7b988f1667ae551 Mon Sep 17 00:00:00 2001 From: Justin Reynolds Date: Wed, 1 Feb 2017 09:11:29 -0800 Subject: [PATCH] (netflix) Add Isolated Testing Target stage --- .../canary/canaryConfigSelector.directive.js | 2 +- .../fastPropertyPod.component.js | 2 +- .../globalFastPropertyPods.component.js | 2 +- .../createFastPropertyWizard.controller.ts | 2 +- .../help/netflixHelpContents.registry.js | 9 + app/scripts/modules/netflix/netflix.module.js | 3 + .../editVip.modal.controller.ts | 27 ++ .../isolatedTestingTarget/editVip.modal.html | 26 ++ .../isolatedTestingTargetStage.html | 61 +++++ .../isolatedTestingTargetStage.module.ts | 13 + .../isolatedTestingTargetStage.ts | 252 ++++++++++++++++++ 11 files changed, 395 insertions(+), 4 deletions(-) create mode 100644 app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/editVip.modal.controller.ts create mode 100644 app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/editVip.modal.html create mode 100644 app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/isolatedTestingTargetStage.html create mode 100644 app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/isolatedTestingTargetStage.module.ts create mode 100644 app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/isolatedTestingTargetStage.ts diff --git a/app/scripts/modules/netflix/canary/canaryConfigSelector.directive.js b/app/scripts/modules/netflix/canary/canaryConfigSelector.directive.js index d680b90fbdb..d05c73d7b29 100644 --- a/app/scripts/modules/netflix/canary/canaryConfigSelector.directive.js +++ b/app/scripts/modules/netflix/canary/canaryConfigSelector.directive.js @@ -5,7 +5,7 @@ import _ from 'lodash'; let angular = require('angular'); module.exports = angular - .module('spinnaker.netfilx.widgits.canaryConfigSelector.directive', [ + .module('spinnaker.netflix.widgits.canaryConfigSelector.directive', [ require('./canary.read.service'), ]) .directive('canaryConfigSelector', () => { diff --git a/app/scripts/modules/netflix/fastProperties/fastPropertyPod.component.js b/app/scripts/modules/netflix/fastProperties/fastPropertyPod.component.js index 3374debd70f..356cf703f4a 100644 --- a/app/scripts/modules/netflix/fastProperties/fastPropertyPod.component.js +++ b/app/scripts/modules/netflix/fastProperties/fastPropertyPod.component.js @@ -3,7 +3,7 @@ let angular = require('angular'); module.exports = angular - .module('spinnaker.netfilx.fastProperty.pod.component', [ + .module('spinnaker.netflix.fastProperty.pod.component', [ require('angular-ui-router'), ]) .component('fastPropertyPod', { diff --git a/app/scripts/modules/netflix/fastProperties/globalFastPropertyPods.component.js b/app/scripts/modules/netflix/fastProperties/globalFastPropertyPods.component.js index 36077e6911c..622ededea80 100644 --- a/app/scripts/modules/netflix/fastProperties/globalFastPropertyPods.component.js +++ b/app/scripts/modules/netflix/fastProperties/globalFastPropertyPods.component.js @@ -4,7 +4,7 @@ let angular = require('angular'); import _ from 'lodash'; module.exports = angular - .module('spinnaker.netfilx.globalFastProperty.pods.component', [ + .module('spinnaker.netflix.globalFastProperty.pods.component', [ require('angular-ui-router'), ]) .component('globalFastPropertyPods', { diff --git a/app/scripts/modules/netflix/fastProperties/wizard/createFastPropertyWizard.controller.ts b/app/scripts/modules/netflix/fastProperties/wizard/createFastPropertyWizard.controller.ts index 2c3332f51f6..f87decf8bb6 100644 --- a/app/scripts/modules/netflix/fastProperties/wizard/createFastPropertyWizard.controller.ts +++ b/app/scripts/modules/netflix/fastProperties/wizard/createFastPropertyWizard.controller.ts @@ -63,7 +63,7 @@ class CreateFastPropertyWizardController { } -export const CREATE_FAST_PROPERTY_WIZARD_CONTROLLER = 'spinnaker.netfilx.fastProperty.createWizard.controller'; +export const CREATE_FAST_PROPERTY_WIZARD_CONTROLLER = 'spinnaker.netflix.fastProperty.createWizard.controller'; module(CREATE_FAST_PROPERTY_WIZARD_CONTROLLER, [ FAST_PROPERTY_DETAILS_COMPONENT, diff --git a/app/scripts/modules/netflix/help/netflixHelpContents.registry.js b/app/scripts/modules/netflix/help/netflixHelpContents.registry.js index bcbcad18944..6b86531cb86 100644 --- a/app/scripts/modules/netflix/help/netflixHelpContents.registry.js +++ b/app/scripts/modules/netflix/help/netflixHelpContents.registry.js @@ -29,6 +29,15 @@ module.exports = angular contents: `

The name of the package you want installed (without any version identifiers).

If your build produces a deb file named "myapp_1.27-h343", you would want to enter "myapp" here.

` }, + { + key: 'pipeline.config.isolatedTestingTarget.clusters', + contents: `

These clusters will allow you to select a cluster that you want to mimic, and will clone the properties + and override the VIP to provide an isolated testing target.

` + }, + { + key: 'pipeline.config.isolatedTestingTarget.vips', + contents: `

These VIPs will show the value of the VIP of the old cluster that is being copied from and the VIP that will be given to the new cluster.

` + }, { key: 'chaos.documentation', contents: `

Chaos Monkey documentation can be found diff --git a/app/scripts/modules/netflix/netflix.module.js b/app/scripts/modules/netflix/netflix.module.js index e0c30bc292b..26b741c8614 100644 --- a/app/scripts/modules/netflix/netflix.module.js +++ b/app/scripts/modules/netflix/netflix.module.js @@ -1,6 +1,8 @@ import {APPLICATION_DATA_SOURCE_REGISTRY} from 'core/application/service/applicationDataSource.registry'; import {CLOUD_PROVIDER_REGISTRY} from 'core/cloudProvider/cloudProvider.registry'; import {TABLEAU_STATES} from './tableau/tableau.states'; +import {ISOLATED_TESTING_TARGET_STAGE_MODULE} from './pipeline/stage/isolatedTestingTarget/isolatedTestingTargetStage.module'; + let angular = require('angular'); @@ -20,6 +22,7 @@ module.exports = angular require('./instance/aws/netflixAwsInstanceDetails.controller.js'), require('./instance/titus/netflixTitusInstanceDetails.controller.js'), require('./pipeline/stage/canary/canaryStage.module.js'), + ISOLATED_TESTING_TARGET_STAGE_MODULE, require('./pipeline/stage/acaTask/acaTaskStage.module'), require('./pipeline/stage/properties'), require('./pipeline/stage/quickPatchAsg/quickPatchAsgStage.module.js'), diff --git a/app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/editVip.modal.controller.ts b/app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/editVip.modal.controller.ts new file mode 100644 index 00000000000..71d64028878 --- /dev/null +++ b/app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/editVip.modal.controller.ts @@ -0,0 +1,27 @@ +import {module} from 'angular'; +import {IModalServiceInstance} from 'angular-ui-bootstrap'; + +class EditVipModalCtrl { + public cancel: (reason?: any) => void; + public invalid: boolean = false; + public errorMessage: string = null; + + constructor(public vip: string, private $uibModalInstance: IModalServiceInstance) { + this.cancel = $uibModalInstance.dismiss; + } + + public update(): void { + if (this.vip.length === 0) { + this.invalid = true; + this.errorMessage = 'VIP must have a value'; + } else { + this.$uibModalInstance.close(this.vip); + } + } +} + +export const EDIT_VIP_MODAL_CONTROLLER = 'spinnaker.netflix.pipeline.stage.isolatedTestingTarget.editVip'; + +module(EDIT_VIP_MODAL_CONTROLLER, []) + .controller('EditVipModalCtrl', EditVipModalCtrl); + diff --git a/app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/editVip.modal.html b/app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/editVip.modal.html new file mode 100644 index 00000000000..a24f1094de4 --- /dev/null +++ b/app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/editVip.modal.html @@ -0,0 +1,26 @@ +

+ + + diff --git a/app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/isolatedTestingTargetStage.html b/app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/isolatedTestingTargetStage.html new file mode 100644 index 00000000000..905e94a65aa --- /dev/null +++ b/app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/isolatedTestingTargetStage.html @@ -0,0 +1,61 @@ +
+

Isolated Testing Target Config

+ +
Owner
+
+ + + + +
Clusters
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
LocationNew ClusterVIPs Actions
+ + {{isolatedTestingTargetStageCtrl.getRegion(cluster)}} + + {{isolatedTestingTargetStageCtrl.getClusterName(cluster)}} +
+ Edit +
+ Overridden: {{isolatedTestingTargetStageCtrl.getClusterOldVIPs(cluster)}} Edit +
+ New: {{isolatedTestingTargetStageCtrl.getClusterNewVIPs(cluster)}} Edit +
+ + + +
+ +
+
+
+
+
diff --git a/app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/isolatedTestingTargetStage.module.ts b/app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/isolatedTestingTargetStage.module.ts new file mode 100644 index 00000000000..1c11c6581ed --- /dev/null +++ b/app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/isolatedTestingTargetStage.module.ts @@ -0,0 +1,13 @@ +import {module} from 'angular'; + +import {ACCOUNT_SERVICE} from 'core/account/account.service'; +import {NAMING_SERVICE} from 'core/naming/naming.service'; +import {ISOLATED_TESTING_TARGET_STAGE} from './isolatedTestingTargetStage'; + +export const ISOLATED_TESTING_TARGET_STAGE_MODULE = 'spinnaker.netflix.pipeline.stage.isolatedTestingTarget'; + +module(ISOLATED_TESTING_TARGET_STAGE_MODULE, [ + ISOLATED_TESTING_TARGET_STAGE, + ACCOUNT_SERVICE, + NAMING_SERVICE, +]); diff --git a/app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/isolatedTestingTargetStage.ts b/app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/isolatedTestingTargetStage.ts new file mode 100644 index 00000000000..b8dd6d611a4 --- /dev/null +++ b/app/scripts/modules/netflix/pipeline/stage/isolatedTestingTarget/isolatedTestingTargetStage.ts @@ -0,0 +1,252 @@ +import {module} from 'angular'; +import {filter, flatten, map, each} from 'lodash'; +import {IModalService} from 'angular-ui-bootstrap'; + +import {AuthenticationService} from 'core/authentication/authentication.service'; +import {CLOUD_PROVIDER_REGISTRY, CloudProviderRegistry} from 'core/cloudProvider/cloudProvider.registry'; +import {ICluster, IStage, ServerGroup} from 'core/domain/index'; +import {SERVER_GROUP_COMMAND_BUILDER_SERVICE} from 'core/serverGroup/configure/common/serverGroupCommandBuilder.service'; +import {SERVER_GROUP_READER, ServerGroupReader} from 'core/serverGroup/serverGroupReader.service'; +import {NAMING_SERVICE, NamingService} from 'core/naming/naming.service'; +import {EDIT_VIP_MODAL_CONTROLLER} from './editVip.modal.controller'; + +interface IVipOverride { + oldVip: string; + oldSecureVip: string; + newVip: string; + newSecureVip: string; + [key: string]: string; // This makes the linter happy when an object of shape IVipOverride is referenced with a variable instead of dot notation +} + +interface IIsolatedTestingTargetStage extends IStage { + clusters: ICluster[]; + vipOverrides: { + [clusterId: string]: IVipOverride; + }; + username: string; +} + +class IsolatedTestingTargetStageCtrl { + static get $inject() { + return ['$scope', '$uibModal', 'stage', 'namingService', 'providerSelectionService', + 'authenticationService', 'cloudProviderRegistry', 'serverGroupCommandBuilder', + 'serverGroupReader', 'awsServerGroupTransformer']; + } + + constructor(private $scope: any, + private $uibModal: IModalService, + private stage: IIsolatedTestingTargetStage, + private namingService: NamingService, + private providerSelectionService: any, + authenticationService: AuthenticationService, + private cloudProviderRegistry: CloudProviderRegistry, + private serverGroupCommandBuilder: any, + private serverGroupReader: ServerGroupReader, + private awsServerGroupTransformer: any) { + + const user = authenticationService.getAuthenticatedUser(); + $scope.stage = stage; + $scope.stage.owner = $scope.stage.owner || (user.authenticated ? user.name : null); + + $scope.stage.username = user.name.includes('@') ? user.name.substring(0, user.name.lastIndexOf('@')) : user.name; + } + + public getClusterOldVIPs(cluster: any): string { + const vips = this.stage.vipOverrides[this.getClusterId(cluster)]; + return vips ? [vips.oldVip, vips.oldSecureVip].filter(n => n).join(', ') : ''; + }; + + public getClusterNewVIPs(cluster: any): string { + const vips = this.stage.vipOverrides[this.getClusterId(cluster)]; + return vips ? [vips.newVip, vips.newSecureVip].filter(n => n).join(', ') : ''; + }; + + public getClusterName(cluster: any): string { + return this.namingService.getClusterName(cluster.application, cluster.stack, cluster.freeFormDetails); + } + + private buildServerGroupCommand(selectedProvider: string): ng.IPromise { + return this.serverGroupCommandBuilder.buildNewServerGroupCommandForPipeline(selectedProvider) + .then((command: any) => { + this.configureServerGroupCommandForEditing(command); + command.viewState.overrides = { + useSourceCapacity: false, + capacity: { + min: 1, max: 1, desired: 1, + }, + loadBalancers: [], + freeFormDetails: 'itt-' + this.stage.username, + maxRemainingAsgs: 2 + }; + command.viewState.disableNoTemplateSelection = true; + command.viewState.customTemplateMessage = 'Select a template to configure the isolated testing target cluster. ' + + 'If you want to configure the server groups differently, you can do so by clicking ' + + '"Edit" after adding the cluster.'; + return command; + }); + } + + private applyCommandToStage(command: any): void { + // Push the cluster config into the list of clusters + const cluster = this.awsServerGroupTransformer.convertServerGroupCommandToDeployConfiguration(command); + this.stage.clusters.push(cluster); + + // If the ASG we are cloning has a VIP, generate the new VIP + if (command.source && command.source.asgName && command.source.region && command.source.account) { + this.serverGroupReader.getServerGroup(command.application, command.source.account, command.source.region, command.source.asgName).then((asgDetails: ServerGroup) => { + if (asgDetails) { + const discoveryHealth = filter(flatten(map(asgDetails.instances, 'health')), { type: 'Discovery' }); + + let oldVip: string, oldSecureVip: string; + each(discoveryHealth, (health: any) => { + oldVip = oldVip || health.vipAddress; + oldSecureVip = oldSecureVip || health.secureVipAddress; + }); + + const newVip = this.generateNewVip(oldVip, command.freeFormDetails); + const newSecureVip = this.generateNewVip(oldSecureVip, command.freeFormDetails); + this.stage.vipOverrides[this.getClusterId(cluster)] = { oldVip, oldSecureVip, newVip, newSecureVip }; + } + }); + } + } + + public addCluster(): void { + this.stage.clusters = this.stage.clusters || []; + this.stage.vipOverrides = this.stage.vipOverrides || {}; + this.providerSelectionService.selectProvider(this.$scope.application).then((selectedProvider: string) => { + let config = this.cloudProviderRegistry.getValue(selectedProvider, 'serverGroup'); + this.$uibModal.open({ + templateUrl: config.cloneServerGroupTemplateUrl, + controller: `${config.cloneServerGroupController} as ctrl`, + size: 'lg', + resolve: { + title: () => 'Add Isolated Testing Target Cluster', + application: () => this.$scope.application, + serverGroupCommand: () => this.buildServerGroupCommand(selectedProvider), + } + }).result.then((command: any) => this.applyCommandToStage(command)); + }); + } + + public editCluster(cluster: any, index: number): void { + cluster.provider = cluster.provider || 'aws'; + let config = this.cloudProviderRegistry.getValue(cluster.provider, 'serverGroup'); + this.$uibModal.open({ + templateUrl: config.cloneServerGroupTemplateUrl, + controller: `${config.cloneServerGroupController} as ctrl`, + size: 'lg', + resolve: { + title: () => 'Configure Isolated Testing Target Cluster', + application: () => this.$scope.application, + serverGroupCommand: () => { + return this.serverGroupCommandBuilder.buildServerGroupCommandFromPipeline(this.$scope.application, cluster) + .then((command: any) => this.configureServerGroupCommandForEditing(command)); + } + } + }).result.then((command: any) => { + this.stage.clusters[index] = this.awsServerGroupTransformer.convertServerGroupCommandToDeployConfiguration(command); + }); + } + + public deleteCluster(index: number): void { + this.stage.clusters.splice(index, 1); + } + + public editVip(cluster: any, vipType: 'oldVip' | 'newVip'): void { + const clusterVip = this.stage.vipOverrides[this.getClusterId(cluster)]; + this.$uibModal.open({ + templateUrl: require('./editVip.modal.html'), + controller: 'EditVipModalCtrl as vm', + size: 'md', + resolve: { + vip: () => clusterVip[vipType] + } + }).result.then(newVip => { + clusterVip[vipType] = newVip; + }); + } + + private getClusterId(cluster: any): string { + return `${this.getRegion(cluster)}::${cluster.account}::${this.getClusterName(cluster)}`; + } + + private configureServerGroupCommandForEditing(command: any) { + command.viewState.disableStrategySelection = false; + command.viewState.hideClusterNamePreview = false; + command.viewState.readOnlyFields = { credentials: true, region: true, subnet: true }; + + return command; + } + + private generateNewVip(oldVip: string, details: string) { + if (!oldVip) { return undefined; } + + let vipParts = oldVip.split(':'); + + // Check if the last part is a port + const lastPart = vipParts.pop(); + if (!Number(lastPart)) { + vipParts.push(lastPart); + } + vipParts = [vipParts.join(':')]; + + // add the isolatedTestingTarget custom vip piece + vipParts.push(`-${details}`); + + if (Number(lastPart)) { + vipParts.push(`:${lastPart}`); + } + + return vipParts.join(''); + } + + // TODO: Extract into utility class + private getRegion(cluster: any): string { + if (cluster.region) { + return cluster.region; + } + const availabilityZones = cluster.availabilityZones; + if (availabilityZones) { + const regions = Object.keys(availabilityZones); + if (regions && regions.length) { + return regions[0]; + } + } + return 'n/a'; + } +} + +export const ISOLATED_TESTING_TARGET_STAGE = 'spinnaker.netflix.pipeline.stage.isolatedTestingTargetStage'; + +module(ISOLATED_TESTING_TARGET_STAGE, [ + require('core/pipeline/config/pipelineConfigProvider'), + require('core/application/listExtractor/listExtractor.service'), + EDIT_VIP_MODAL_CONTROLLER, + NAMING_SERVICE, + CLOUD_PROVIDER_REGISTRY, + SERVER_GROUP_COMMAND_BUILDER_SERVICE, + SERVER_GROUP_READER, + require('core/config/settings.js'), +]) + .config((pipelineConfigProvider: any, settings: any) => { + if (settings.feature && settings.feature.netflixMode) { + pipelineConfigProvider.registerStage({ + label: 'Isolated Testing Target', + description: 'Launches a cluster with a unique VIP to allow for testing on an isolated cluster', + key: 'isolatedTestingTarget', + cloudProviders: ['aws'], + templateUrl: require('./isolatedTestingTargetStage.html'), + controller: 'IsolatedTestingTargetStageCtrl', + controllerAs: 'isolatedTestingTargetStageCtrl', + validators: [ + { + type: 'stageBeforeType', + stageTypes: ['bake', 'findAmi', 'findImage'], + message: 'You must have a Bake or Find AMI stage before an Isolated Testing Target stage.' + }, + ], + }); + } + }) + .controller('IsolatedTestingTargetStageCtrl', IsolatedTestingTargetStageCtrl);