Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(Execution): Allow ignoring failed stages #9039

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/scripts/modules/core/src/domain/IOrchestratedItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ export interface IOrchestratedItem extends ITimedItem {
isCanceled: boolean;
isSuspended: boolean;
isPaused: boolean;
isHalted: boolean;
runningTime: string;
}
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 @@ -80,6 +80,10 @@ const helpContents: { [key: string]: string } = {
<p>When this option is enabled, stage will only execute when the supplied expression evaluates true.</p>
<p>The expression <em>does not</em> need to be wrapped in \${ and }.</p>
<p>If this expression evaluates to false, the stages following this stage will still execute.</p>`,
'pipeline.config.allowIgnoreFailure': `
<p>When this option is enabled, users will be able to manually ignore the stage if it failed.</p>
<p>You should use this only for stages that other stages don't closely depend on.</p>
<p>For example, if later stages depend on the outputs of this stage, you should not allow that option.</p>`,
'pipeline.config.checkPreconditions.failPipeline': `
<p><strong>Checked</strong> - the overall pipeline will fail whenever this precondition is false.</p>
<p><strong>Unchecked</strong> - the overall pipeline will continue executing but this particular branch will stop.</p>`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ export class OrchestratedItemTransformer {
isPaused: {
get: (): boolean => item.status === 'PAUSED',
},
isHalted: {
get: (): boolean => ['TERMINAL', 'CANCELED', 'STOPPED'].includes(item.status),
},
status: {
// Returns either SUCCEEDED, RUNNING, FAILED, CANCELED, or NOT_STARTED
get: (): string => this.normalizeStatus(item),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div>
<div class="form-group">
<div class="col-md-8 col-md-offset-1">
<div class="checkbox pull-left">
<label>
<input type="checkbox" ng-false-value="false" ng-model="$ctrl.stage.allowIgnoreFailure" />
<strong>Allow users to ignore the failure of the stage</strong>
<help-field key="pipeline.config.allowIgnoreFailure"></help-field>
</label>
</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';

import { module } from 'angular';

export const CORE_PIPELINE_CONFIG_STAGES_ALLOWIGNOREFAILURE_ALLOWIGNOREFAILURE_DIRECTIVE =
'spinnaker.core.pipeline.stage.allowIgnoreFailure.directive';
export const name = CORE_PIPELINE_CONFIG_STAGES_ALLOWIGNOREFAILURE_ALLOWIGNOREFAILURE_DIRECTIVE; // for backwards compatibility
module(CORE_PIPELINE_CONFIG_STAGES_ALLOWIGNOREFAILURE_ALLOWIGNOREFAILURE_DIRECTIVE, []).component(
'allowIgnoreFailure',
{
bindings: {
stage: '<',
},
templateUrl: require('./allowIgnoreFailure.directive.html'),
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<h5 class="execution-details-title">
Stage details: {{stageSummary.name || stageSummary.type }}

<div ng-if="$ctrl.isRestartable(stage) || $ctrl.canManuallySkip()" uib-dropdown class="btn-group pull-right">
<div ng-if="$ctrl.hasActions(stage)" uib-dropdown class="btn-group pull-right">
<button
type="button"
class="btn btn-default btn-sm dropdown-toggle"
Expand Down Expand Up @@ -37,6 +37,16 @@ <h5 class="execution-details-title">
>Skip {{ $ctrl.getTopLevelStage().name }}</a
>
</li>
<li ng-if="$ctrl.canIgnoreFailure()">
<a
href
analytics-on="click"
analytics-category="Pipeline"
analytics-event="Stage ignore failure clicked"
ng-click="$ctrl.openIgnoreStageFailureModal()"
>Ignore {{ stage.name }} Failure</a
>
</li>
</ul>
</div>
</h5>
Expand All @@ -49,6 +59,11 @@ <h6 ng-if="$ctrl.getTopLevelStage().context.manualSkip" uib-tooltip="{{$ctrl.get
Manually skipped by {{$ctrl.getTopLevelStage().lastModified.user}} &mdash;
{{$ctrl.getTopLevelStage().lastModified.lastModifiedTime | timestamp}}
</h6>
<h6 ng-if="stage.context.ignoreFailureDetails" uib-tooltip="{{stage.context.ignoreFailureDetails.reason}}">
Failure ignored manually by {{stage.context.ignoreFailureDetails.by}} &mdash;
{{stage.context.ignoreFailureDetails.time | timestamp}} {{stage.context.ignoreFailureDetails.previousException?
'Previous exception:' + stage.context.ignoreFailureDetails.previousException : ''}}
</h6>

<table class="table">
<thead>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ <h4 ng-bind="stage.name || '[new stage]'"></h4>
></override-timeout>
<fail-on-failed-expressions stage="stage"></fail-on-failed-expressions>
<optional-stage stage="stage"></optional-stage>
<allow-ignore-failure stage="stage"></allow-ignore-failure>
</page-section>
<page-section
key="notification"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { EditStageJsonModal } from './common/EditStageJsonModal';
import { BASE_EXECUTION_DETAILS_CTRL } from './common/baseExecutionDetails.controller';
import { CORE_PIPELINE_CONFIG_STAGES_COMMON_STAGECONFIGFIELD_STAGECONFIGFIELD_DIRECTIVE } from './common/stageConfigField/stageConfigField.directive';
import { CORE_PIPELINE_CONFIG_STAGES_FAILONFAILEDEXPRESSIONS_FAILONFAILEDEXPRESSIONS_DIRECTIVE } from './failOnFailedExpressions/failOnFailedExpressions.directive';
import { CORE_PIPELINE_CONFIG_STAGES_ALLOWIGNOREFAILURE_ALLOWIGNOREFAILURE_DIRECTIVE } from './allowIgnoreFailure/allowIgnoreFailure.directive';
import { CORE_PIPELINE_CONFIG_STAGES_OPTIONALSTAGE_OPTIONALSTAGE_DIRECTIVE } from './optionalStage/optionalStage.directive';
import { OVERRRIDE_FAILURE } from './overrideFailure/overrideFailure.module';
import { OVERRIDE_TIMEOUT_COMPONENT } from './overrideTimeout/overrideTimeout.module';
Expand All @@ -35,6 +36,7 @@ module(CORE_PIPELINE_CONFIG_STAGES_STAGE_MODULE, [
CORE_PIPELINE_CONFIG_STAGES_OPTIONALSTAGE_OPTIONALSTAGE_DIRECTIVE,
CORE_PIPELINE_CONFIG_STAGES_FAILONFAILEDEXPRESSIONS_FAILONFAILEDEXPRESSIONS_DIRECTIVE,
CORE_PIPELINE_CONFIG_STAGES_COMMON_STAGECONFIGFIELD_STAGECONFIGFIELD_DIRECTIVE,
CORE_PIPELINE_CONFIG_STAGES_ALLOWIGNOREFAILURE_ALLOWIGNOREFAILURE_DIRECTIVE
])
.directive('pipelineConfigStage', function () {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ export class StageSummaryController implements IController {
return index === this.getCurrentStep();
}

public hasActions(stage?: IStage): boolean {
return this.isRestartable(stage) || this.canManuallySkip() || this.canIgnoreFailure();
}

public isRestartable(stage?: IStage): boolean {
if (stage.isRunning || stage.isCompleted) {
return false;
Expand All @@ -91,6 +95,10 @@ export class StageSummaryController implements IController {
return this.stage.isRunning && topLevelStage && topLevelStage.context.canManuallySkip;
}

public canIgnoreFailure(): boolean {
return this.stage.isHalted && this.stage.context.allowIgnoreFailure;
}

public getTopLevelStage(): IExecutionStage {
let parentStageId = this.stage.parentStageId;
let topLevelStage: IExecutionStage = this.stage;
Expand All @@ -101,6 +109,32 @@ export class StageSummaryController implements IController {
return topLevelStage;
}

public openIgnoreStageFailureModal(): void {
ConfirmationModalService.confirm({
header: 'Really ignore this failure?',
buttonText: 'Ignore',
askForReason: true,
submitJustWithReason: true,
body: `<div class="alert alert-warning">
<b>Warning:</b> Ignoring this failure may have unpredictable results.
<ul>
<li>Downstream stages that depend on the outputs of this stage may fail or behave unexpectedly.</li>
</ul>
</div>
`,
submitMethod: (reason: object) =>
this.executionService
.ignoreStageFailureInExecution(this.execution.id, this.stage.id, reason)
.then(() =>
this.executionService.waitUntilExecutionMatches(this.execution.id, (execution) => {
const updatedStage = execution.stages.find((stage) => stage.id === this.stage.id);
return updatedStage && updatedStage.status === 'FAILED_CONTINUE';
}),
)
.then((updated) => this.executionService.updateExecution(this.application, updated)),
});
}

public openManualSkipStageModal(): void {
const topLevelStage = this.getTopLevelStage();
ConfirmationModalService.confirm({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,10 @@ export class ExecutionService {
return REST('/pipelines').path(executionId, 'stages', stageId).patch(data);
}

public ignoreStageFailureInExecution(executionId: string, stageId: string, reason: object): PromiseLike<any> {
return REST('/pipelines').path(executionId, 'stages', stageId, 'ignoreFailure').put(reason);
}

private stringifyExecution(execution: IExecution): string {
const transient = { ...execution };
transient.stages = transient.stages.filter((s) => s.status !== 'SUCCEEDED' && s.status !== 'NOT_STARTED');
Expand Down