diff --git a/app/scripts/modules/amazon/src/aws.module.ts b/app/scripts/modules/amazon/src/aws.module.ts index 188174e039c..1887783f3d9 100644 --- a/app/scripts/modules/amazon/src/aws.module.ts +++ b/app/scripts/modules/amazon/src/aws.module.ts @@ -147,4 +147,10 @@ module(AMAZON_MODULE, [ }); }); -DeploymentStrategyRegistry.registerProvider('aws', ['custom', 'redblack', 'rollingpush', 'rollingredblack']); +DeploymentStrategyRegistry.registerProvider('aws', [ + 'custom', + 'redblack', + 'rollingpush', + 'rollingredblack', + 'monitored', +]); diff --git a/app/scripts/modules/core/src/deploymentStrategy/deploymentStrategy.module.ts b/app/scripts/modules/core/src/deploymentStrategy/deploymentStrategy.module.ts index 95b8310cbd5..719b0119bfe 100644 --- a/app/scripts/modules/core/src/deploymentStrategy/deploymentStrategy.module.ts +++ b/app/scripts/modules/core/src/deploymentStrategy/deploymentStrategy.module.ts @@ -5,6 +5,7 @@ import './strategies/highlander/highlander.strategy'; import './strategies/none/none.strategy'; import './strategies/redblack/redblack.strategy'; import './strategies/rollingredblack/rollingredblack.strategy'; +import './strategies/monitored/monitored.strategy'; import { DEPLOYMENT_STRATEGY_SELECTOR_COMPONENT } from './deploymentStrategySelector.component'; diff --git a/app/scripts/modules/core/src/deploymentStrategy/strategies/monitored/AdditionalFields.tsx b/app/scripts/modules/core/src/deploymentStrategy/strategies/monitored/AdditionalFields.tsx new file mode 100644 index 00000000000..05a0e1285e9 --- /dev/null +++ b/app/scripts/modules/core/src/deploymentStrategy/strategies/monitored/AdditionalFields.tsx @@ -0,0 +1,158 @@ +import * as React from 'react'; +import Select, { Option } from 'react-select'; +import { set } from 'lodash'; + +import { IDeploymentStrategyAdditionalFieldsProps } from 'core/deploymentStrategy/deploymentStrategy.registry'; +import { HelpField } from 'core/help/HelpField'; +import { NgReact } from 'core/reactShims'; +import { IServerGroupCommand } from 'core/serverGroup'; +import { + DeploymentMonitorReader, + IDeploymentMonitorDefinition, +} from 'core/pipeline/config/stages/monitoreddeploy/DeploymentMonitorReader'; + +export interface IMonitoredDeployCommand extends IServerGroupCommand { + delayBeforeScaleDownSec: string; + rollback: { + onFailure: boolean; + }; + maxRemainingAsgs: number; + scaleDown: boolean; + deploySteps: number[] | string; + deploymentMonitor: { + id: string; + parameters: {}; + }; +} + +export interface IMonitoredDeployStrategyAdditionalFieldsProps extends IDeploymentStrategyAdditionalFieldsProps { + command: IMonitoredDeployCommand; +} + +export interface IMonitoredDeployStrategyAdditionalFieldsState { + deploymentMonitors: IDeploymentMonitorDefinition[]; +} + +export class AdditionalFields extends React.Component< + IMonitoredDeployStrategyAdditionalFieldsProps, + IMonitoredDeployStrategyAdditionalFieldsState +> { + public state: IMonitoredDeployStrategyAdditionalFieldsState = { + deploymentMonitors: [], + }; + + public componentDidMount() { + DeploymentMonitorReader.getDeploymentMonitors().then(deploymentMonitors => { + this.setState({ deploymentMonitors }); + }); + } + + private deployStepsChange = (model: number[] | string) => { + this.props.command.deploySteps = model; + this.forceUpdate(); + }; + + private scaleDownChange = (e: React.ChangeEvent) => { + this.props.command.scaleDown = e.target.checked; + this.forceUpdate(); + }; + + private rollbackOnFailureChange = (e: React.ChangeEvent) => { + this.props.command.rollback.onFailure = e.target.checked; + this.forceUpdate(); + }; + + private maxRemainingAsgsChange = (e: React.ChangeEvent) => { + this.props.command.maxRemainingAsgs = parseInt(e.target.value, 10); + this.forceUpdate(); + }; + + private handleDeploymentMonitorChange = (option: Option) => { + this.props.command.deploymentMonitor.id = option.value; + this.forceUpdate(); + }; + + private handleChange = (key: string, value: string) => { + set(this.props.command, key, value); + this.forceUpdate(); + }; + + public render() { + const { NumberList } = NgReact; + const { command } = this.props; + const rollbackOnFailure = command.rollback && command.rollback.onFailure; + + return ( +
+ {this.state.deploymentMonitors && ( +
+ +
+
+ +
+
+ +
+ + {command.scaleDown && ( +
+ + this.handleChange('delayBeforeScaleDownSec', e.target.value)} + placeholder="0" + /> + seconds +
+ )} + +
+

+ Percentages + +

+ +
+
+ ); + } +} diff --git a/app/scripts/modules/core/src/deploymentStrategy/strategies/monitored/monitored.strategy.ts b/app/scripts/modules/core/src/deploymentStrategy/strategies/monitored/monitored.strategy.ts new file mode 100644 index 00000000000..1ea5f2044f4 --- /dev/null +++ b/app/scripts/modules/core/src/deploymentStrategy/strategies/monitored/monitored.strategy.ts @@ -0,0 +1,20 @@ +import { DeploymentStrategyRegistry } from 'core/deploymentStrategy/deploymentStrategy.registry'; + +import { AdditionalFields } from './AdditionalFields'; + +DeploymentStrategyRegistry.registerStrategy({ + label: 'Monitored Deploy', + description: `Creates a new version of this server group, then incrementally resizes the new server group while monitoring progress using a deployment monitor.`, + key: 'monitored', + providerRestricted: true, + additionalFields: ['deploySteps', 'scaleDown'], + AdditionalFieldsComponent: AdditionalFields, + initializationMethod: command => { + if (!command.deploySteps) { + command.deploySteps = [10, 40, 100]; + command.rollback.onFailure = true; + command.deploymentMonitor = { id: '' }; + command.maxRemainingAsgs = 2; + } + }, +}); diff --git a/app/scripts/modules/core/src/help/help.contents.ts b/app/scripts/modules/core/src/help/help.contents.ts index 7a0e9b0f762..41a61c2f17b 100644 --- a/app/scripts/modules/core/src/help/help.contents.ts +++ b/app/scripts/modules/core/src/help/help.contents.ts @@ -359,6 +359,10 @@ const helpContents: { [key: string]: string } = { '

Rolling red black will slowly scale up the new server group. It will resize the new server group by each percentage defined.

', 'strategy.rollingRedBlack.rollback': '

Disable the new server group and ensure that the previous server group is restored to its original capacity.

', + 'strategy.monitored.deploySteps': + '

Monitored Deploy will scale up the new server group as specified by these per cent steps. After each step, the health of the new server group will be evaluated by the specified deployment monitor.

', + 'strategy.monitored.rollback': + '

If deploy fails, disable the new server group and ensure that the previous server group is active and restored to its original capacity.

', 'loadBalancers.filter.serverGroups': `

Displays all server groups configured to use the load balancer.

If the server group is configured to not add new instances to the load balancer, it will be grayed out.

`, diff --git a/app/scripts/modules/core/src/pipeline/config/stages/monitoreddeploy/DeploymentMonitorExecutionDetails.tsx b/app/scripts/modules/core/src/pipeline/config/stages/monitoreddeploy/DeploymentMonitorExecutionDetails.tsx new file mode 100644 index 00000000000..475b3b9faec --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/stages/monitoreddeploy/DeploymentMonitorExecutionDetails.tsx @@ -0,0 +1,141 @@ +import * as React from 'react'; +import { get, isEmpty } from 'lodash'; + +import { StageFailureMessage, IExecutionDetailsSectionProps, ExecutionDetailsSection } from 'core/pipeline'; +import { IExecutionStage } from 'core/domain'; +import { + DeploymentMonitorReader, + IDeploymentMonitorDefinition, +} from 'core/pipeline/config/stages/monitoreddeploy/DeploymentMonitorReader'; + +interface IAdditionalData { + link: string; + text: string; + key: string; +} + +function getDeploymentMonitorSummary(stage: IExecutionStage) { + const { context } = stage; + + return get(context, 'deploymentMonitorReasons.summary', ''); +} + +function getDeploymentMonitorName(stage: IExecutionStage, monitors: IDeploymentMonitorDefinition[]) { + return monitors.find(x => x.id === stage.context.deploymentMonitor.id).name; +} + +function getDeploymentMonitorSupportUrl(stage: IExecutionStage, monitors: IDeploymentMonitorDefinition[]) { + return monitors.find(x => x.id === stage.context.deploymentMonitor.id).supportContact; +} + +function getDeploymentMonitorDetails(stage: IExecutionStage) { + const { context } = stage; + + return get(context, 'deploymentMonitorReasons.reason.message', null); +} + +function getDeploymentMonitorLog(stage: IExecutionStage) { + const { context } = stage; + const logArray = get(context, 'deploymentMonitorReasons.reason.logSummary', null); + + if (isEmpty(logArray)) { + return null; + } + + return logArray.map((logLine: string, i: number) => ( + + {logLine} +
+
+ )); +} + +function getDeploymentMonitorAdditionalDetails(stage: IExecutionStage) { + const { context } = stage; + const additionalData = get(context, 'deploymentMonitorReasons.reason.additionalData', []) as IAdditionalData[]; + + if (!isEmpty(additionalData)) { + return additionalData.map(({ key, link, text }) => ( + +
{key}
+
+ + {text} + +
+
+ )); + } + + return null; +} + +interface IDeploymentMonitorExecutionDetailsSectionState { + deploymentMonitors: IDeploymentMonitorDefinition[]; +} + +export class DeploymentMonitorExecutionDetails extends React.Component< + IExecutionDetailsSectionProps, + IDeploymentMonitorExecutionDetailsSectionState +> { + public static title = 'evaluateDeploymentHealth'; + public state: IDeploymentMonitorExecutionDetailsSectionState = { deploymentMonitors: null }; + + public componentDidMount(): void { + DeploymentMonitorReader.getDeploymentMonitors().then(deploymentMonitors => { + this.setState({ deploymentMonitors }); + }); + } + + public render() { + const { stage, current, name } = this.props; + const { deploymentMonitors } = this.state; + + const additionalDetails = getDeploymentMonitorAdditionalDetails(stage); + const log = getDeploymentMonitorLog(stage); + const details = getDeploymentMonitorDetails(stage); + + const notProvidedFragment = ( + + <NOT PROVIDED> + + ); + + return ( + +
+
+
+
Summary
+
{getDeploymentMonitorSummary(stage)}
+
Deploy %
+
+ {stage.context.currentProgress}% +
+
Details
+
{details || notProvidedFragment}
+
Log
+
{log ?
{log}
: notProvidedFragment}
+
More info
+
{additionalDetails ? '' : notProvidedFragment}
+ {additionalDetails} +
+ {deploymentMonitors && ( +
+ NOTE: This information is provided by the  + {getDeploymentMonitorName(stage, deploymentMonitors)} deployment monitor. +
+ If you are experiencing issues with the analysis, please reach out to{' '} + + support for {getDeploymentMonitorName(stage, deploymentMonitors)} + +
+ )} +
+
+ + +
+ ); + } +} diff --git a/app/scripts/modules/core/src/pipeline/config/stages/monitoreddeploy/DeploymentMonitorReader.ts b/app/scripts/modules/core/src/pipeline/config/stages/monitoreddeploy/DeploymentMonitorReader.ts new file mode 100644 index 00000000000..383d0b95ec2 --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/stages/monitoreddeploy/DeploymentMonitorReader.ts @@ -0,0 +1,18 @@ +import { IPromise } from 'angular'; + +import { API } from 'core/api'; + +export interface IDeploymentMonitorDefinition { + id: string; + name: string; + supportContact: string; +} + +export class DeploymentMonitorReader { + public static getDeploymentMonitors(): IPromise { + return API.all('capabilities') + .all('deploymentMonitors') + .useCache(true) + .get(); + } +} diff --git a/app/scripts/modules/core/src/pipeline/config/stages/monitoreddeploy/evaluateHealthStage.ts b/app/scripts/modules/core/src/pipeline/config/stages/monitoreddeploy/evaluateHealthStage.ts new file mode 100644 index 00000000000..a4b4db79a0d --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/stages/monitoreddeploy/evaluateHealthStage.ts @@ -0,0 +1,15 @@ +import { module } from 'angular'; + +import { Registry } from 'core/registry'; + +import { DeploymentMonitorExecutionDetails } from './DeploymentMonitorExecutionDetails'; + +export const EVALUATE_HEALTH_STAGE = 'spinnaker.core.pipeline.stage.monitored.evaluatehealthstage'; + +module(EVALUATE_HEALTH_STAGE, []).config(() => { + Registry.pipeline.registerStage({ + synthetic: true, + key: 'evaluateDeploymentHealth', + executionDetailsSections: [DeploymentMonitorExecutionDetails], + }); +}); diff --git a/app/scripts/modules/core/src/pipeline/config/stages/monitoreddeploy/notifyDeployStartingStage.ts b/app/scripts/modules/core/src/pipeline/config/stages/monitoreddeploy/notifyDeployStartingStage.ts new file mode 100644 index 00000000000..45f24f1ee21 --- /dev/null +++ b/app/scripts/modules/core/src/pipeline/config/stages/monitoreddeploy/notifyDeployStartingStage.ts @@ -0,0 +1,15 @@ +import { module } from 'angular'; + +import { Registry } from 'core/registry'; + +import { DeploymentMonitorExecutionDetails } from './DeploymentMonitorExecutionDetails'; + +export const NOTIFY_DEPLOY_STARTING_STAGE = 'spinnaker.core.pipeline.stage.monitored.notifydeploystartingstage'; + +module(NOTIFY_DEPLOY_STARTING_STAGE, []).config(() => { + Registry.pipeline.registerStage({ + synthetic: true, + key: 'notifyDeployStarting', + executionDetailsSections: [DeploymentMonitorExecutionDetails], + }); +}); diff --git a/app/scripts/modules/core/src/pipeline/pipeline.module.ts b/app/scripts/modules/core/src/pipeline/pipeline.module.ts index a05ecb2a52e..023a62f2909 100644 --- a/app/scripts/modules/core/src/pipeline/pipeline.module.ts +++ b/app/scripts/modules/core/src/pipeline/pipeline.module.ts @@ -1,6 +1,8 @@ import { module } from 'angular'; import { APPLY_SOURCE_SERVER_GROUP_CAPACITY_STAGE } from './config/stages/applySourceServerGroupCapacity/applySourceServerGroupCapacityStage.module'; +import { EVALUATE_HEALTH_STAGE } from './config/stages/monitoreddeploy/evaluateHealthStage'; +import { NOTIFY_DEPLOY_STARTING_STAGE } from './config/stages/monitoreddeploy/notifyDeployStartingStage'; import './config/stages/bakeManifest/bakeManifestStage'; import { CHECK_PRECONDITIONS_STAGE_MODULE } from './config/stages/checkPreconditions/checkPreconditionsStage.module'; import { CLONE_SERVER_GROUP_STAGE } from './config/stages/cloneServerGroup/cloneServerGroupStage.module'; @@ -103,6 +105,8 @@ module(PIPELINE_MODULE, [ require('./config/stages/waitForParentTasks/waitForParentTasks').name, CREATE_LOAD_BALANCER_STAGE, APPLY_SOURCE_SERVER_GROUP_CAPACITY_STAGE, + EVALUATE_HEALTH_STAGE, + NOTIFY_DEPLOY_STARTING_STAGE, require('./config/preconditions/preconditions.module').name, require('./config/preconditions/types/clusterSize/clusterSize.precondition.type.module').name, require('./config/preconditions/types/expression/expression.precondition.type.module').name,