-
Notifications
You must be signed in to change notification settings - Fork 900
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(monitored deploy): add basic monitored deploy UI (#7426)
* Add stage execution details for `notifyDeploymentStarted` and `evaluateDeploymentHealth` stages * Add monitored deploy to selectable strategies
- Loading branch information
1 parent
5e25501
commit b55a49a
Showing
10 changed files
with
383 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
158 changes: 158 additions & 0 deletions
158
app/scripts/modules/core/src/deploymentStrategy/strategies/monitored/AdditionalFields.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
app/scripts/modules/core/src/deploymentStrategy/strategies/monitored/monitored.strategy.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
141 changes: 141 additions & 0 deletions
141
...les/core/src/pipeline/config/stages/monitoreddeploy/DeploymentMonitorExecutionDetails.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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><NOT PROVIDED></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 | ||
<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> | ||
); | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
...cripts/modules/core/src/pipeline/config/stages/monitoreddeploy/DeploymentMonitorReader.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
app/scripts/modules/core/src/pipeline/config/stages/monitoreddeploy/evaluateHealthStage.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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], | ||
}); | ||
}); |
Oops, something went wrong.