Skip to content

Commit

Permalink
feat(monitored deploy): add basic monitored deploy UI (#7426)
Browse files Browse the repository at this point in the history
* Add stage execution details for `notifyDeploymentStarted` and `evaluateDeploymentHealth` stages
* Add monitored deploy to selectable strategies
  • Loading branch information
marchello2000 committed Oct 1, 2019
1 parent 5e25501 commit b55a49a
Show file tree
Hide file tree
Showing 10 changed files with 383 additions and 1 deletion.
8 changes: 7 additions & 1 deletion app/scripts/modules/amazon/src/aws.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,10 @@ module(AMAZON_MODULE, [
});
});

DeploymentStrategyRegistry.registerProvider('aws', ['custom', 'redblack', 'rollingpush', 'rollingredblack']);
DeploymentStrategyRegistry.registerProvider('aws', [
'custom',
'redblack',
'rollingpush',
'rollingredblack',
'monitored',
]);
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>) => {
this.props.command.scaleDown = e.target.checked;
this.forceUpdate();
};

private rollbackOnFailureChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.props.command.rollback.onFailure = e.target.checked;
this.forceUpdate();
};

private maxRemainingAsgsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.props.command.maxRemainingAsgs = parseInt(e.target.value, 10);
this.forceUpdate();
};

private handleDeploymentMonitorChange = (option: Option<string>) => {
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 (
<div className="form-group">
{this.state.deploymentMonitors && (
<div className="col-md-10">
<Select
clearable={false}
required={true}
options={this.state.deploymentMonitors.map(deploymentMonitor => ({
label: deploymentMonitor.name,
value: deploymentMonitor.id,
}))}
placeholder="select deployment monitor"
value={command.deploymentMonitor.id || ''}
onChange={this.handleDeploymentMonitorChange}
/>
</div>
)}
<div className="col-md-12 form-inline">
<label>
Maximum number of server groups to leave
<HelpField id="strategy.redblack.maxRemainingAsgs" />
</label>
<input
className="form-control input-sm"
style={{ width: '50px' }}
type="number"
value={command.maxRemainingAsgs}
onChange={this.maxRemainingAsgsChange}
min="2"
/>
</div>
<div className="col-md-12 checkbox" style={{ marginTop: 0 }}>
<label>
<input type="checkbox" checked={rollbackOnFailure} onChange={this.rollbackOnFailureChange} />
Rollback to previous server group if deployment fails <HelpField id="strategy.monitored.rollback" />
</label>
</div>
<div className="col-md-12 checkbox">
<label>
<input type="checkbox" checked={command.scaleDown} onChange={this.scaleDownChange} />
Scale down replaced server groups to zero instances <HelpField id="strategy.redblack.scaleDown" />
</label>
</div>

{command.scaleDown && (
<div className="col-md-12 form-inline" style={{ marginTop: '5px' }}>
<label>
<span style={{ marginRight: '2px' }}>Wait Before Scale Down</span>
<HelpField content="Time to wait before scaling down all old server groups" />
</label>
<input
className="form-control input-sm"
style={{ width: '60px', marginLeft: '2px', marginRight: '2px' }}
min="0"
type="number"
value={command.delayBeforeScaleDownSec}
onChange={e => this.handleChange('delayBeforeScaleDownSec', e.target.value)}
placeholder="0"
/>
seconds
</div>
)}

<div className="col-md-6" style={{ marginTop: '5px' }}>
<h4>
Percentages
<HelpField id="strategy.monitored.deploySteps" />
</h4>
<NumberList model={command.deploySteps} label="percentage" onChange={this.deployStepsChange} />
</div>
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
},
});
4 changes: 4 additions & 0 deletions app/scripts/modules/core/src/help/help.contents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,10 @@ const helpContents: { [key: string]: string } = {
'<p>Rolling red black will slowly scale up the new server group. It will resize the new server group by each percentage defined.</p>',
'strategy.rollingRedBlack.rollback':
'<p>Disable the new server group and ensure that the previous server group is restored to its original capacity.</p>',
'strategy.monitored.deploySteps':
'<p>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.</p>',
'strategy.monitored.rollback':
'<p>If deploy fails, disable the new server group and ensure that the previous server group is active and restored to its original capacity.</p>',
'loadBalancers.filter.serverGroups': `
<p>Displays all server groups configured to use the load balancer.</p>
<p>If the server group is configured to <em>not</em> add new instances to the load balancer, it will be grayed out.</p>`,
Expand Down
Original file line number Diff line number Diff line change
@@ -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', '<NOT PROVIDED>');
}

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) => (
<React.Fragment key={i}>
<span>{logLine}</span>
<br />
</React.Fragment>
));
}

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 }) => (
<React.Fragment key={key + link + text}>
<dt>{key}</dt>
<dd>
<a href={link} target="_blank">
{text}
</a>
</dd>
</React.Fragment>
));
}

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 = (
<React.Fragment>
<i>&lt;NOT PROVIDED&gt;</i>
</React.Fragment>
);

return (
<ExecutionDetailsSection name={name} current={current}>
<div className="row">
<div className="col-md-12">
<dl className="dl-narrow dl-horizontal">
<dt>Summary</dt>
<dd>{getDeploymentMonitorSummary(stage)}</dd>
<dt className="sp-margin-l-bottom">Deploy %</dt>
<dd className="sp-margin-l-bottom">
<span>{stage.context.currentProgress}%</span>
</dd>
<dt>Details</dt>
<dd>{details || notProvidedFragment}</dd>
<dt className="sp-margin-l-bottom">Log</dt>
<dd className="sp-margin-l-bottom">{log ? <pre>{log}</pre> : notProvidedFragment}</dd>
<dt>More info</dt>
<dd>{additionalDetails ? '' : notProvidedFragment}</dd>
{additionalDetails}
</dl>
{deploymentMonitors && (
<div className="alert alert-info">
<strong>NOTE:</strong> This information is provided by the&nbsp;
<strong>{getDeploymentMonitorName(stage, deploymentMonitors)}</strong> deployment monitor.
<br />
If you are experiencing issues with the analysis, please reach out to{' '}
<a href={getDeploymentMonitorSupportUrl(stage, deploymentMonitors)} target="_blank">
support for {getDeploymentMonitorName(stage, deploymentMonitors)}
</a>
</div>
)}
</div>
</div>

<StageFailureMessage stage={stage} message={stage.failureMessage} />
</ExecutionDetailsSection>
);
}
}
Original file line number Diff line number Diff line change
@@ -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<IDeploymentMonitorDefinition[]> {
return API.all('capabilities')
.all('deploymentMonitors')
.useCache(true)
.get();
}
}
Original file line number Diff line number Diff line change
@@ -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],
});
});
Loading

0 comments on commit b55a49a

Please sign in to comment.