From e0a86392e7841b574a31eb6535d4a5d065535dad Mon Sep 17 00:00:00 2001 From: Stuart Pollock Date: Sat, 23 Mar 2019 12:59:35 -0400 Subject: [PATCH] fix(cf): Repair Rollback Cluster pipeline stage (#6743) Cloud Foundry's Reactified implementation of AccountRegionClusterSelector did not account for moniker selection. The moniker field is required by Orca to properly perform the rollback. spinnaker/spinnaker#4180 Co-Authored-By: Stu Pollock --- .../modules/cloudfoundry/src/cf.module.ts | 3 +- ...CloudfoundryRollbackClusterStageConfig.tsx | 55 +++++-------------- .../cloudfoundryRollbackClusterStage.html | 6 -- ...cloudfoundryRollbackClusterStage.module.ts | 49 +++++------------ .../AccountRegionClusterSelector.spec.tsx | 25 +++++++-- .../AccountRegionClusterSelector.tsx | 20 ++++++- 6 files changed, 71 insertions(+), 87 deletions(-) delete mode 100644 app/scripts/modules/cloudfoundry/src/pipeline/stages/rollbackCluster/cloudfoundryRollbackClusterStage.html diff --git a/app/scripts/modules/cloudfoundry/src/cf.module.ts b/app/scripts/modules/cloudfoundry/src/cf.module.ts index ffd647a5e3a..5826ec75368 100644 --- a/app/scripts/modules/cloudfoundry/src/cf.module.ts +++ b/app/scripts/modules/cloudfoundry/src/cf.module.ts @@ -36,7 +36,7 @@ import { CLOUD_FOUNDRY_ENABLE_ASG_STAGE } from './pipeline/stages/enableAsg/clou import './pipeline/stages/mapLoadBalancers/cloudfoundryMapLoadBalancersStage.module'; import './pipeline/stages/unmapLoadBalancers/cloudfoundryUnmapLoadBalancersStage.module'; import { CLOUD_FOUNDRY_RESIZE_ASG_STAGE } from './pipeline/stages/resizeAsg/cloudfoundryResizeAsgStage.module'; -import { CLOUD_FOUNDRY_ROLLBACK_CLUSTER_STAGE } from './pipeline/stages/rollbackCluster/cloudfoundryRollbackClusterStage.module'; +import './pipeline/stages/rollbackCluster/cloudfoundryRollbackClusterStage.module'; import './pipeline/stages/shareService/cloudfoundryShareServiceStage.module'; import './pipeline/stages/unshareService/cloudfoundryUnshareServiceStage.module'; import { CloudFoundryCreateServerGroupModal } from 'cloudfoundry/serverGroup/configure/wizard/CreateServerGroupModal'; @@ -58,7 +58,6 @@ module(CLOUD_FOUNDRY_MODULE, [ CLOUD_FOUNDRY_LOAD_BALANCER_MODULE, CLOUD_FOUNDRY_REACT_MODULE, CLOUD_FOUNDRY_RESIZE_ASG_STAGE, - CLOUD_FOUNDRY_ROLLBACK_CLUSTER_STAGE, CLOUD_FOUNDRY_SEARCH_FORMATTER, CLOUD_FOUNDRY_SERVER_GROUP_COMMAND_BUILDER, CLOUD_FOUNDRY_SERVER_GROUP_TRANSFORMER, diff --git a/app/scripts/modules/cloudfoundry/src/pipeline/stages/rollbackCluster/CloudfoundryRollbackClusterStageConfig.tsx b/app/scripts/modules/cloudfoundry/src/pipeline/stages/rollbackCluster/CloudfoundryRollbackClusterStageConfig.tsx index 40f54c135b8..ec8fa63894b 100644 --- a/app/scripts/modules/cloudfoundry/src/pipeline/stages/rollbackCluster/CloudfoundryRollbackClusterStageConfig.tsx +++ b/app/scripts/modules/cloudfoundry/src/pipeline/stages/rollbackCluster/CloudfoundryRollbackClusterStageConfig.tsx @@ -1,13 +1,5 @@ import * as React from 'react'; -import { - AccountService, - Application, - IAccount, - IPipeline, - IRegion, - IStageConfigProps, - StageConfigField, -} from '@spinnaker/core'; +import { AccountService, IAccount, IPipeline, IStageConfigProps, StageConfigField } from '@spinnaker/core'; import { AccountRegionClusterSelector } from 'cloudfoundry/presentation'; @@ -17,13 +9,6 @@ export interface ICloudfoundryRollbackClusterStageProps extends IStageConfigProp export interface ICloudfoundryRollbackClusterStageConfigState { accounts: IAccount[]; - application: Application; - cloudProvider: string; - credentials: string; - pipeline: IPipeline; - regions: IRegion[]; - targetHealthyRollbackPercentage: number; - waitTimeBetweenRegions: number; } export class CloudfoundryRollbackClusterStageConfig extends React.Component< @@ -33,49 +18,39 @@ export class CloudfoundryRollbackClusterStageConfig extends React.Component< constructor(props: ICloudfoundryRollbackClusterStageProps) { super(props); - Object.assign(props.stage, { + this.props.updateStageField({ + cloudProvider: 'cloudfoundry', + regions: this.props.stage.regions || [], targetHealthyRollbackPercentage: 100, }); - this.props.stage.regions = this.props.stage.regions || []; - - this.state = { - accounts: [], - application: props.application, - cloudProvider: 'cloudfoundry', - credentials: props.stage.credentials, - pipeline: props.pipeline, - regions: [], - targetHealthyRollbackPercentage: props.stage.targetHealthyRollbackPercentage, - waitTimeBetweenRegions: props.stage.waitTimeBetweenRegions, - }; + this.state = { accounts: [] }; } public componentDidMount = (): void => { AccountService.listAccounts('cloudfoundry').then(accounts => { - this.setState({ accounts: accounts }); + this.setState({ accounts }); }); - this.props.stageFieldUpdated(); }; private waitTimeBetweenRegionsUpdated = (event: React.ChangeEvent): void => { const time = parseInt(event.target.value || '0', 10); - this.setState({ waitTimeBetweenRegions: time }); - this.props.stage.waitTimeBetweenRegions = time; - this.props.stageFieldUpdated(); + this.props.updateStageField({ waitTimeBetweenRegions: time }); }; private componentUpdate = (stage: any): void => { - this.props.stage.credentials = stage.credentials; - this.props.stage.regions = stage.regions; - this.props.stage.cluster = stage.cluster; - this.props.stageFieldUpdated(); + this.props.updateStageField({ + credentials: stage.credentials, + regions: stage.regions, + cluster: stage.cluster, + moniker: stage.moniker, + }); }; public render() { - const { stage } = this.props; + const { application, pipeline, stage } = this.props; const { waitTimeBetweenRegions } = stage; - const { accounts, application, pipeline } = this.state; + const { accounts } = this.state; return (
{!pipeline.strategy && ( diff --git a/app/scripts/modules/cloudfoundry/src/pipeline/stages/rollbackCluster/cloudfoundryRollbackClusterStage.html b/app/scripts/modules/cloudfoundry/src/pipeline/stages/rollbackCluster/cloudfoundryRollbackClusterStage.html deleted file mode 100644 index b45c39475ff..00000000000 --- a/app/scripts/modules/cloudfoundry/src/pipeline/stages/rollbackCluster/cloudfoundryRollbackClusterStage.html +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/app/scripts/modules/cloudfoundry/src/pipeline/stages/rollbackCluster/cloudfoundryRollbackClusterStage.module.ts b/app/scripts/modules/cloudfoundry/src/pipeline/stages/rollbackCluster/cloudfoundryRollbackClusterStage.module.ts index e3f4edeff2e..5187c3e6dac 100644 --- a/app/scripts/modules/cloudfoundry/src/pipeline/stages/rollbackCluster/cloudfoundryRollbackClusterStage.module.ts +++ b/app/scripts/modules/cloudfoundry/src/pipeline/stages/rollbackCluster/cloudfoundryRollbackClusterStage.module.ts @@ -1,36 +1,17 @@ -import { IController, IScope, module } from 'angular'; -import { react2angular } from 'react2angular'; - import { CloudfoundryRollbackClusterStageConfig } from './CloudfoundryRollbackClusterStageConfig'; -import { Application, IStage, Registry } from '@spinnaker/core'; - -class CloudFoundryRollbackClusterStageCtrl implements IController { - public static $inject = ['$scope', 'application']; - constructor(public $scope: IScope, private application: Application) { - this.$scope.application = this.application; - } -} +import { IStage, Registry } from '@spinnaker/core'; -export const CLOUD_FOUNDRY_ROLLBACK_CLUSTER_STAGE = 'spinnaker.cloudfoundry.pipeline.stage.rollbackClusterStage'; -module(CLOUD_FOUNDRY_ROLLBACK_CLUSTER_STAGE, []) - .config(function() { - Registry.pipeline.registerStage({ - accountExtractor: (stage: IStage) => stage.context.credentials, - configAccountExtractor: (stage: IStage) => [stage.credentials], - provides: 'rollbackCluster', - key: 'rollbackCluster', - cloudProvider: 'cloudfoundry', - templateUrl: require('./cloudfoundryRollbackClusterStage.html'), - controller: 'cfRollbackClusterStageCtrl', - validators: [ - { type: 'requiredField', fieldName: 'cluster' }, - { type: 'requiredField', fieldName: 'regions' }, - { type: 'requiredField', fieldName: 'credentials', fieldLabel: 'account' }, - ], - }); - }) - .component( - 'cfRollbackClusterStage', - react2angular(CloudfoundryRollbackClusterStageConfig, ['application', 'pipeline', 'stage', 'stageFieldUpdated']), - ) - .controller('cfRollbackClusterStageCtrl', CloudFoundryRollbackClusterStageCtrl); +Registry.pipeline.registerStage({ + accountExtractor: (stage: IStage) => stage.context.credentials, + configAccountExtractor: (stage: IStage) => [stage.credentials], + provides: 'rollbackCluster', + key: 'rollbackCluster', + cloudProvider: 'cloudfoundry', + component: CloudfoundryRollbackClusterStageConfig, + controller: 'cfRollbackClusterStageCtrl', + validators: [ + { type: 'requiredField', preventSave: true, fieldName: 'cluster' }, + { type: 'requiredField', preventSave: true, fieldName: 'regions' }, + { type: 'requiredField', preventSave: true, fieldName: 'credentials', fieldLabel: 'account' }, + ], +}); diff --git a/app/scripts/modules/cloudfoundry/src/presentation/widgets/accountRegionClusterSelector/AccountRegionClusterSelector.spec.tsx b/app/scripts/modules/cloudfoundry/src/presentation/widgets/accountRegionClusterSelector/AccountRegionClusterSelector.spec.tsx index c7c146762a7..99236b49568 100644 --- a/app/scripts/modules/cloudfoundry/src/presentation/widgets/accountRegionClusterSelector/AccountRegionClusterSelector.spec.tsx +++ b/app/scripts/modules/cloudfoundry/src/presentation/widgets/accountRegionClusterSelector/AccountRegionClusterSelector.spec.tsx @@ -2,10 +2,15 @@ import * as React from 'react'; import { mock, noop, IScope } from 'angular'; import { mount, shallow } from 'enzyme'; -import { Application, ApplicationDataSource } from 'core/application'; -import { IServerGroup } from 'core/domain'; -import { APPLICATION_MODEL_BUILDER, ApplicationModelBuilder } from 'core/application/applicationModel.builder'; -import { REACT_MODULE } from 'core/reactShims'; +import { + Application, + APPLICATION_MODEL_BUILDER, + ApplicationModelBuilder, + ApplicationDataSource, + IMoniker, + IServerGroup, + REACT_MODULE, +} from '@spinnaker/core'; import { AccountRegionClusterSelector, IAccountRegionClusterSelectorProps } from './AccountRegionClusterSelector'; @@ -22,6 +27,7 @@ describe('', () => { region, instances: [{ health: null, id: 'instance-id', launchTime: 0, name: 'instance-name', zone: 'GMT' }], instanceCounts: { up: 1, down: 0, starting: 0, succeeded: 1, failed: 0, unknown: 0, outOfService: 0 }, + moniker: { app: 'my-app', cluster, detail: 'my-detail', stack: 'my-stack', sequence: 1 }, } as IServerGroup; } @@ -237,6 +243,7 @@ describe('', () => { it('the cluster value is updated in the component when cluster is changed', () => { let cluster = ''; + let moniker: IMoniker = { app: '' }; const accountRegionClusterProps: IAccountRegionClusterSelectorProps = { accounts: [ { @@ -257,6 +264,7 @@ describe('', () => { clusterField: 'newCluster', onComponentUpdate: (value: any) => { cluster = value.newCluster; + moniker = value.moniker; }, component: { cluster: 'app-stack-detailOne', @@ -265,6 +273,14 @@ describe('', () => { }, }; + const expectedMoniker = { + app: 'my-app', + cluster: 'app-stack-detailThree', + detail: 'my-detail', + stack: 'my-stack', + sequence: null, + } as IMoniker; + const component = mount( , ); @@ -285,6 +301,7 @@ describe('', () => { $scope.$digest(); expect(cluster).toBe('app-stack-detailThree'); + expect(moniker).toEqual(expectedMoniker); }); it('initialize with form names', () => { diff --git a/app/scripts/modules/cloudfoundry/src/presentation/widgets/accountRegionClusterSelector/AccountRegionClusterSelector.tsx b/app/scripts/modules/cloudfoundry/src/presentation/widgets/accountRegionClusterSelector/AccountRegionClusterSelector.tsx index bfc994656c9..1e76f61612e 100644 --- a/app/scripts/modules/cloudfoundry/src/presentation/widgets/accountRegionClusterSelector/AccountRegionClusterSelector.tsx +++ b/app/scripts/modules/cloudfoundry/src/presentation/widgets/accountRegionClusterSelector/AccountRegionClusterSelector.tsx @@ -1,11 +1,14 @@ import * as React from 'react'; +import { first, isNil, uniq } from 'lodash'; + import Select, { Option } from 'react-select'; import { Application, AppListExtractor, IAccount, + IMoniker, IServerGroup, IServerGroupFilter, StageConfigField, @@ -113,10 +116,25 @@ export class AccountRegionClusterSelector extends React.Component< }; public onClusterUpdate = (option: Option): void => { + const clusterName = option.value; + const filterByCluster = AppListExtractor.monikerClusterNameFilter(clusterName); + const clusterMoniker = first(uniq(AppListExtractor.getMonikers([this.props.application], filterByCluster))); + let moniker: IMoniker; + + if (isNil(clusterMoniker)) { + // remove the moniker from the stage if one doesn't exist. + moniker = undefined; + } else { + // clusters don't contain sequences, so null it out. + clusterMoniker.sequence = null; + moniker = clusterMoniker; + } + this.props.onComponentUpdate && this.props.onComponentUpdate({ ...this.props.component, - [this.state.clusterField]: option.value, + [this.state.clusterField]: clusterName, + moniker, }); };