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 #9038

Closed
Closed
1 change: 1 addition & 0 deletions app/scripts/modules/core/src/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface IFeatures {
snapshots?: boolean;
savePipelinesStageEnabled?: boolean;
functions?: boolean;
kubernetesRawResources?: boolean;
}

export interface IDockerInsightSettings {
Expand Down
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;
}
6 changes: 5 additions & 1 deletion app/scripts/modules/core/src/help/help.contents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,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 Expand Up @@ -357,7 +361,7 @@ const helpContents: { [key: string]: string } = {
'pipeline.config.trigger.runAsUser':
"The current user must have access to the specified service account, and the service account must have access to the current application. Otherwise, you'll receive an 'Access is denied' error.",
'pipeline.config.trigger.authorizedUser':
"The current user must have the permission to approve the manual judgment stage. Otherwise, you'll not be able continue to the next pipeline stage.",
"The current user must have the permission to approve the manual judgment stage. Otherwise, you'll not be able continue to the next pipeline stage.",
'pipeline.config.script.repoUrl':
'<p>Path to the repo hosting the scripts in Stash. (e.g. <samp>CDL/mimir-scripts</samp>). Leave empty to use the default.</p>',
'pipeline.config.script.repoBranch':
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
44 changes: 23 additions & 21 deletions app/scripts/modules/core/src/pipeline/config/stages/stage.module.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { OVERRIDE_TIMEOUT_COMPONENT } from './overrideTimeout/overrideTimeout.mo
import { ApplicationReader } from 'core/application/service/ApplicationReader';
import { CORE_PIPELINE_CONFIG_STAGES_OPTIONALSTAGE_OPTIONALSTAGE_DIRECTIVE } from './optionalStage/optionalStage.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_COMMON_STAGECONFIGFIELD_STAGECONFIGFIELD_DIRECTIVE } from './common/stageConfigField/stageConfigField.directive';

export const CORE_PIPELINE_CONFIG_STAGES_STAGE_MODULE = 'spinnaker.core.pipeline.config.stage';
Expand All @@ -34,6 +35,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 Expand Up @@ -135,29 +137,29 @@ module(CORE_PIPELINE_CONFIG_STAGES_STAGE_MODULE, [
});
};

$scope.getApplicationPermissions = function() {
ApplicationReader.getApplicationPermissions($scope.application.name).then(result => {
appPermissions = result;
if (appPermissions) {
const readArray = appPermissions.READ || [];
const writeArray = appPermissions.WRITE || [];
const executeArray = appPermissions.EXECUTE || [];
appRoles = _.union(readArray, writeArray, executeArray);
appRoles = Array.from(new Set(appRoles));
$scope.updateAvailableStageRoles();
}
});
$scope.getApplicationPermissions = function () {
ApplicationReader.getApplicationPermissions($scope.application.name).then((result) => {
appPermissions = result;
if (appPermissions) {
const readArray = appPermissions.READ || [];
const writeArray = appPermissions.WRITE || [];
const executeArray = appPermissions.EXECUTE || [];
appRoles = _.union(readArray, writeArray, executeArray);
appRoles = Array.from(new Set(appRoles));
$scope.updateAvailableStageRoles();
}
});
};

$scope.updateAvailableStageRoles = function() {
$scope.options.stageRoles = appRoles.map(function(value, index) {
return {
name: value,
roleId: value,
id: index,
available: true,
};
});
$scope.updateAvailableStageRoles = function () {
$scope.options.stageRoles = appRoles.map(function (value, index) {
return {
name: value,
roleId: value,
id: index,
available: true,
};
});
};

this.editStageJson = () => {
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 @@ -529,6 +529,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
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import { ReactComponent as spMenuCanaryConfig } from './vectors/spMenuCanaryConf
import { ReactComponent as spMenuCanaryReport } from './vectors/spMenuCanaryReport.svg';
import { ReactComponent as spMenuClusters } from './vectors/spMenuClusters.svg';
import { ReactComponent as spMenuConfig } from './vectors/spMenuConfig.svg';
import { ReactComponent as spMenuK8s } from './vectors/spMenuK8s.svg';
import { ReactComponent as spMenuLoadBalancers } from './vectors/spMenuLoadBalancers.svg';
import { ReactComponent as spMenuMeme } from './vectors/spMenuMeme.svg';
import { ReactComponent as spMenuPager } from './vectors/spMenuPager.svg';
Expand Down Expand Up @@ -215,6 +216,7 @@ export const iconsByName = {
spMenuCanaryReport,
spMenuClusters,
spMenuConfig,
spMenuK8s,
spMenuLoadBalancers,
spMenuMeme,
spMenuPager,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/scripts/modules/kubernetes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './serverGroup';
export * from './securityGroup';
export * from './manifest';
export * from './loadBalancer';
export * from './rawResource';
19 changes: 16 additions & 3 deletions app/scripts/modules/kubernetes/src/kubernetes.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { module } from 'angular';

import { CloudProviderRegistry, STAGE_ARTIFACT_SELECTOR_COMPONENT_REACT, YAML_EDITOR_COMPONENT } from '@spinnaker/core';
import {
CloudProviderRegistry,
STAGE_ARTIFACT_SELECTOR_COMPONENT_REACT,
SETTINGS,
YAML_EDITOR_COMPONENT,
} from '@spinnaker/core';

import { KUBERNETES_MANIFEST_DELETE_CTRL } from './manifest/delete/delete.controller';
import { KUBERNETES_MANIFEST_SCALE_CTRL } from './manifest/scale/scale.controller';
Expand Down Expand Up @@ -38,6 +43,8 @@ import { KUBERNETES_DISABLE_MANIFEST_STAGE } from './pipelines/stages/traffic/di
import { KubernetesSecurityGroupReader } from './securityGroup/securityGroup.reader';
import { KUBERNETES_ROLLING_RESTART } from './manifest/rollout/RollingRestart';

import { KUBERNETS_RAW_RESOURCE_MODULE } from './rawResource';

import kubernetesLogo from './logo/kubernetes.logo.svg';

import './validation/applicationName.validator';
Expand All @@ -54,7 +61,7 @@ templates.keys().forEach(function (key) {

export const KUBERNETES_MODULE = 'spinnaker.kubernetes';

module(KUBERNETES_MODULE, [
const requires = [
KUBERNETES_INSTANCE_DETAILS_CTRL,
KUBERNETES_LOAD_BALANCER_DETAILS_CTRL,
KUBERNETES_SECURITY_GROUP_DETAILS_CTRL,
Expand Down Expand Up @@ -91,7 +98,13 @@ module(KUBERNETES_MODULE, [
KUBERNETES_DISABLE_MANIFEST_STAGE,
STAGE_ARTIFACT_SELECTOR_COMPONENT_REACT,
KUBERNETES_ROLLING_RESTART,
]).config(() => {
];

if (SETTINGS.feature.kubernetesRawResources) {
requires.push(KUBERNETS_RAW_RESOURCE_MODULE);
}

module(KUBERNETES_MODULE, requires).config(() => {
CloudProviderRegistry.registerProvider('kubernetes', {
name: 'Kubernetes',
logo: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.K8sResources {
width: 100%;
.StandardFieldLayout {
width: 25%;
.StandardFieldLayout_Label {
min-width: 72px;
}
}
}
Loading