Skip to content

Commit

Permalink
feat(webhooks): add support for cancellation to webhooks (#7289)
Browse files Browse the repository at this point in the history
for webhooks that have `waitForCompletion=true` (i.e. monitored webhook) it is
desirable to have an indication of cancellation (either user cancels execution
or the execution is terminated due to some failure)

corresponding `orca` PR: spinnaker/orca#3069
  • Loading branch information
marchello2000 authored Aug 1, 2019
1 parent 10617c5 commit 7368523
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 5 deletions.
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 @@ -403,6 +403,8 @@ const helpContents: { [key: string]: string } = {
'markdown.examples':
'Some examples of markdown syntax: <br/> *<em>emphasis</em>* <br/> **<b>strong</b>** <br/> [link text](http://url-goes-here)',
'pipeline.config.webhook.payload': 'JSON payload to be added to the webhook call.',
'pipeline.config.webhook.cancelPayload':
'JSON payload to be added to the webhook call when it is called in response to a cancellation.',
'pipeline.config.webhook.waitForCompletion':
'If not checked, we consider the stage succeeded if the webhook returns an HTTP status code 2xx, otherwise it will be failed. If checked, it will poll a status url (defined below) to determine the progress of the stage.',
'pipeline.config.webhook.statusUrlResolutionIsGetMethod': "Use the webhook's URL with GET method as status endpoint.",
Expand All @@ -429,6 +431,8 @@ const helpContents: { [key: string]: string } = {
'pipeline.config.webhook.customHeaders': 'Key-value pairs to be sent as additional headers to the service.',
'pipeline.config.webhook.failFastCodes':
'Comma-separated HTTP status codes (4xx or 5xx) that will cause this webhook stage to fail without retrying.',
'pipeline.config.webhook.signalCancellation':
'Trigger a specific webhook if this stage is cancelled by user or due to pipeline failure',
'pipeline.config.parameter.label': '(Optional): a label to display when users are triggering the pipeline manually',
'pipeline.config.parameter.description': `(Optional): if supplied, will be displayed to users as a tooltip
when triggering the pipeline manually. You can include HTML in this field.`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,5 +211,55 @@
>
<input type="text" class="form-control input-sm" ng-model="$ctrl.stage.terminalStatuses" />
</stage-config-field>

<div class="form-group" ng-if="$ctrl.displayField('signalCancellation')">
<div class="col-md-8 col-md-offset-1">
<div class="checkbox pull-left">
<label>
<input
type="checkbox"
name="signalCancellation"
ng-model="$ctrl.viewState.signalCancellation"
ng-change="$ctrl.signalCancellationChanged()"
/>
<strong>Signal on cancellation</strong>
<help-field key="pipeline.config.webhook.signalCancellation"></help-field>
</label>
</div>
</div>
</div>
<div
ng-class="{collapse: $ctrl.viewState.signalCancellation !== true, 'collapse.in': !$ctrl.viewState.signalCancellation === true}"
>
<stage-config-field label="Cancellation URL" ng-if="$ctrl.displayField('cancelEndpoint')">
<input type="text" class="form-control input-sm" ng-model="$ctrl.stage.cancelEndpoint" />
</stage-config-field>
<stage-config-field label="Method" ng-if="$ctrl.displayField('cancelMethod')">
<ui-select ng-model="$ctrl.stage.cancelMethod" class="form-control input-sm">
<ui-select-match placeholder="Select a method...">{{$select.selected}}</ui-select-match>
<ui-select-choices repeat="method in $ctrl.methods | filter: $select.search">
<span ng-bind-html="method | highlight: $select.search"></span>
</ui-select-choices>
</ui-select>
</stage-config-field>
<stage-config-field
label="Cancellation payload"
help-key="pipeline.config.webhook.cancelPayload"
ng-if="$ctrl.stage.cancelMethod !== 'GET' && $ctrl.stage.cancelMethod !== 'HEAD' && $ctrl.displayField('cancelPayload')"
>
<textarea
class="code form-control flex-fill"
rows="5"
ng-model="$ctrl.cancelCommand.payloadJSON"
ng-change="$ctrl.updateCancelPayload()"
></textarea>

<div class="form-group row slide-in" ng-if="$ctrl.cancelCommand.invalid">
<div class="col-sm-9 col-sm-offset-3 error-message">
Error: {{$ctrl.cancelCommand.errorMessage}}
</div>
</div>
</stage-config-field>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface IWebhookStageViewState {
statusUrlResolution: string;
failFastStatusCodes: string;
retryStatusCodes: string;
signalCancellation?: boolean;
}

export interface IWebhookStageCommand {
Expand Down Expand Up @@ -43,6 +44,7 @@ interface IPreconfiguredWebhook {

export class WebhookStage implements IController {
public command: IWebhookStageCommand;
public cancelCommand: IWebhookStageCommand;
public viewState: IWebhookStageViewState;
public methods: string[];
public preconfiguredProperties: string[];
Expand All @@ -64,6 +66,10 @@ export class WebhookStage implements IController {
payloadJSON: JsonUtils.makeSortedStringFromObject(this.stage.payload || {}),
};

this.cancelCommand = {
payloadJSON: JsonUtils.makeSortedStringFromObject(this.stage.cancelPayload || {}),
};

this.stage.statusUrlResolution = this.viewState.statusUrlResolution;

const stageConfig = Registry.pipeline.getStageConfig(this.stage);
Expand All @@ -89,20 +95,54 @@ export class WebhookStage implements IController {
}

public updatePayload(): void {
this.command.invalid = false;
this.command.errorMessage = '';
const payload = WebhookStage.checkAndGetPayload(this.command);

if (payload !== undefined) {
this.stage.payload = payload;
}
}

public updateCancelPayload(): void {
const payload = WebhookStage.checkAndGetPayload(this.cancelCommand);

if (payload !== undefined) {
this.stage.cancelPayload = payload;
}
}

private static checkAndGetPayload(command: IWebhookStageCommand): void {
command.invalid = false;
command.errorMessage = '';

try {
this.stage.payload = this.command.payloadJSON ? JSON.parse(this.command.payloadJSON) : null;
return command.payloadJSON ? JSON.parse(command.payloadJSON) : null;
} catch (e) {
this.command.invalid = true;
this.command.errorMessage = e.message;
command.invalid = true;
command.errorMessage = e.message;
}

return undefined;
}

public waitForCompletionChanged(): void {
this.stage.waitForCompletion = this.viewState.waitForCompletion;
}

public signalCancellationChanged(): void {
if (!this.viewState.signalCancellation) {
// Reset the data to "defaults" when user disables this option
delete this.stage.cancelEndpoint;
delete this.stage.cancelMethod;
delete this.stage.cancelPayload;

this.cancelCommand = {
payloadJSON: JsonUtils.makeSortedStringFromObject(this.stage.cancelPayload || {}),
};
} else {
this.stage.cancelMethod = 'POST';
}
}

public statusUrlResolutionChanged(): void {
this.stage.statusUrlResolution = this.viewState.statusUrlResolution;
}
Expand Down

0 comments on commit 7368523

Please sign in to comment.