Skip to content

Commit

Permalink
feat(stages/webhook): Webhook stage spinnaker/spinnaker#1512
Browse files Browse the repository at this point in the history
Create a new stage for calling an external webhook / service, that will
be useful for having a flexible way of interacting with technologies
that have not yet been integrated with Spinnaker, for example running
AWS lambdas or running Marathon deployments.
  • Loading branch information
Albert Manya committed Apr 7, 2017
1 parent d360f2e commit dcd0c77
Show file tree
Hide file tree
Showing 7 changed files with 318 additions and 0 deletions.
2 changes: 2 additions & 0 deletions app/scripts/modules/core/core.module.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {INFRASTRUCTURE_STATES} from './search/infrastructure/infrastructure.stat
import {VERSION_CHECK_SERVICE} from './config/versionCheck.service';
import {CORE_WIDGETS_MODULE} from './widgets';
import {TRAVIS_STAGE_MODULE} from './pipeline/config/stages/travis/travisStage.module';
import {WEBHOOK_STAGE_MODULE} from './pipeline/config/stages/webhook/webhookStage.module';
import {SETTINGS} from 'core/config/settings';

require('../../../fonts/spinnaker/icons.css');
Expand Down Expand Up @@ -110,6 +111,7 @@ module.exports = angular
require('./pipeline/config/stages/findImageFromTags/findImageFromTagsStage.module.js'),
require('./pipeline/config/stages/jenkins/jenkinsStage.module.js'),
TRAVIS_STAGE_MODULE,
WEBHOOK_STAGE_MODULE,
require('./pipeline/config/stages/manualJudgment/manualJudgmentStage.module.js'),
require('./pipeline/config/stages/tagImage/tagImageStage.module.js'),
require('./pipeline/config/stages/pipeline/pipelineStage.module.js'),
Expand Down
11 changes: 11 additions & 0 deletions app/scripts/modules/core/help/helpContents.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {module, IScope} from 'angular';
import {IStateParamsService} from 'angular-ui-router';
import {get} from 'lodash';

import {
EXECUTION_DETAILS_SECTION_SERVICE,
ExecutionDetailsSectionService
} from 'core/delivery/details/executionDetailsSection.service';

export class WebhookExecutionDetailsCtrl {
static get $inject() {
return ['$stateParams', 'executionDetailsSectionService', '$scope'];
}

public configSections = ['webhookConfig', 'taskStatus'];
public detailsSection: string;
public failureMessage: string;
public progressMessage: string;
public stage: any;

constructor(private $stateParams: IStateParamsService,
private executionDetailsSectionService: ExecutionDetailsSectionService,
private $scope: IScope) {
this.stage = this.$scope.stage;
this.initialize();
this.$scope.$on('$stateChangeSuccess', () => this.initialize());
}

public initialized(): void {
this.detailsSection = get<string>(this.$stateParams, 'details', '');
this.failureMessage = this.getFailureMessage();
this.progressMessage = this.getProgressMessage();
}

private getProgressMessage(): string {
const context = this.stage.context || {},
buildInfo = context.buildInfo || {};
return buildInfo.progressMessage;
}

private getFailureMessage(): string {
let failureMessage = this.stage.failureMessage;
const context = this.stage.context || {},
buildInfo = context.buildInfo || {};
if (buildInfo.status === 'TERMINAL') {
failureMessage = `Webhook failed: ${buildInfo.reason}`;
}
return failureMessage;
}

private initialize(): void {
this.executionDetailsSectionService.synchronizeSection(this.configSections, () => this.initialized());
}
}

export const WEBHOOK_EXECUTION_DETAILS_CONTROLLER = 'spinnaker.core.pipeline.stage.webhook.executionDetails.controller';
module(WEBHOOK_EXECUTION_DETAILS_CONTROLLER, [
EXECUTION_DETAILS_SECTION_SERVICE,
require('core/delivery/details/executionDetailsSectionNav.directive.js'),
]).controller('WebhookExecutionDetailsCtrl', WebhookExecutionDetailsCtrl);
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<div ng-controller="WebhookExecutionDetailsCtrl as ctrl">
<execution-details-section-nav sections="ctrl.configSections"></execution-details-section-nav>
<div class="step-section-details" ng-if="ctrl.detailsSection === 'webhookConfig'">
<div class="row">
<div class="col-md-12">
<h5>Webhook Stage Configuration</h5>
<dl class="dl-narrow dl-horizontal">
<dt>Url</dt>
<dd>{{ctrl.stage.context.url}}</dd>
<dt>Payload</dt>
<dd>{{ctrl.stage.context.payload}}</dd>
</dl>
<dl class="dl-narrow dl-horizontal"
ng-if="ctrl.stage.context.waitForCompletion">
<dt>Status endpoint</dt>
<dd>{{ctrl.stage.context.statusEndpoint}}</dd>
</dl>
</div>
</div>
<stage-failure-message stage="ctrl.stage" is-failed="ctrl.stage.isFailed" message="ctrl.getException(stage) || ctrl.failureMessage"></stage-failure-message>
<div class="well alert-info" ng-if="ctrl.stage.context.progressMessage || ctrl.stage.status">
<h4>Results</h4>
<dl class="dl-narrow dl-horizontal ng-scope">
<dt>Status</dt>
<dd class="ng-binding">{{ctrl.stage.status}}</dd>
<dt>Info</dt>
<dd class="ng-binding">{{ctrl.stage.context.progressMessage}}</dd>
</dl>
</div>
</div>
<div class="step-section-details" ng-if="ctrl.detailsSection === 'taskStatus'">
<div class="row">
<execution-step-details item="ctrl.stage"></execution-step-details>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<div class="form-horizontal">
<stage-config-field label="Webhook URL">
<input type="text" class="form-control input-sm" ng-model="$ctrl.stage.url"/>
</stage-config-field>
<stage-config-field label="Method">
<ui-select ng-model="$ctrl.stage.method" 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="Payload"
help-key="pipeline.config.webhook.payload"
ng-if="$ctrl.stage.method !== 'GET' && $ctrl.stage.method !== 'HEAD'">
<textarea class="code form-control flex-fill"
rows="5"
ng-model="$ctrl.command.payloadJSON"
ng-change="$ctrl.updatePayload()"/>

<div class="form-group row slide-in" ng-if="$ctrl.command.invalid">
<div class="col-sm-9 col-sm-offset-3 error-message">
Error: {{$ctrl.command.errorMessage}}
</div>
</div>
</stage-config-field>
<stage-config-field label="Wait for completion" help-key="pipeline.config.webhook.waitForCompletion">
<input type="checkbox" class="input-sm" name="waitForCompletion"
ng-model="$ctrl.viewState.waitForCompletion"
ng-change="$ctrl.waitForCompletionChanged()"/>
</stage-config-field>
<div ng-class="{collapse: $ctrl.viewState.waitForCompletion !== true, 'collapse.in': !$ctrl.viewState.waitForCompletion === true}">
<div class="form-group">
<div class="col-md-3 sm-label-right">Status URL</div>
<div class="col-md-9 radio">
<label>
<input type="radio"
ng-model="$ctrl.viewState.statusUrlResolution"
ng-change="$ctrl.statusUrlResolutionChanged()"
value="getMethod"
id="statusUrlResolutionIsGetMethod"/>
GET method against webhook URL
<help-field key="pipeline.config.webhook.statusUrlResolutionIsGetMethod"></help-field>
</label>
</div>
<div class="col-md-9 col-md-offset-3 radio">
<label>
<input type="radio"
ng-model="$ctrl.viewState.statusUrlResolution"
ng-change="$ctrl.statusUrlResolutionChanged()"
value="locationHeader"
id="statusUrlResolutionIsLocationHeader"/>
From the Location header
<help-field key="pipeline.config.webhook.statusUrlResolutionIsLocationHeader"></help-field>
</label>
</div>
<div class="col-md-9 col-md-offset-3 radio">
<label>
<input type="radio"
ng-model="$ctrl.viewState.statusUrlResolution"
ng-change="$ctrl.statusUrlResolutionChanged()"
value="webhookResponse"
id="statusUrlResolutionIsWebhookResponse"/>
From webhook's response
<help-field key="pipeline.config.webhook.useStatusUrlFromLocationHeaderFalse"></help-field>
</label>
</div>
<div class="form-group" ng-if="$ctrl.viewState.statusUrlResolution === 'webhookResponse'">
<div class="col-md-offset-3">
<stage-config-field label="Status URL path" help-key="pipeline.config.webhook.statusUrlJsonPath">
<input type="text"
class="form-control input-sm"
ng-model="$ctrl.stage.statusUrlJsonPath"
required />
</stage-config-field>
</div>
</div>
</div>
<stage-config-field label="Status JsonPath"
help-key="pipeline.config.webhook.statusJsonPath">
<input type="text"
class="form-control input-sm"
ng-model="$ctrl.stage.statusJsonPath"/>
</stage-config-field>
<stage-config-field label="Progress location"
help-key="pipeline.config.webhook.progressJsonPath">
<input type="text"
class="form-control input-sm"
ng-model="$ctrl.stage.progressJsonPath"/>
</stage-config-field>
<stage-config-field label="SUCCESS status mapping"
help-key="pipeline.config.webhook.successStatuses">
<input type="text"
class="form-control input-sm"
ng-model="$ctrl.stage.successStatuses"/>
</stage-config-field>
<stage-config-field label="CANCELED status mapping"
help-key="pipeline.config.webhook.canceledStatuses">
<input type="text"
class="form-control input-sm"
ng-model="$ctrl.stage.canceledStatuses"/>
</stage-config-field>
<stage-config-field label="TERMINAL status mapping"
help-key="pipeline.config.webhook.terminalStatuses">
<input type="text"
class="form-control input-sm"
ng-model="$ctrl.stage.terminalStatuses"/>
</stage-config-field>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {module} from 'angular';

import {WEBHOOK_STAGE} from './webhookStage';
import {IGOR_SERVICE} from 'core/ci/igor.service';
import {WEBHOOK_EXECUTION_DETAILS_CONTROLLER} from './webhookExecutionDetails.controller';

export const WEBHOOK_STAGE_MODULE = 'spinnaker.core.pipeline.stage.webhook';
module(WEBHOOK_STAGE_MODULE, [
WEBHOOK_STAGE,
require('../stage.module.js'),
require('../core/stage.core.module.js'),
require('core/utils/timeFormatters.js'),
IGOR_SERVICE,
WEBHOOK_EXECUTION_DETAILS_CONTROLLER,
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {module, extend} from 'angular';

import {JSON_UTILITY_SERVICE, JsonUtilityService} from 'core/utils/json/json.utility.service';

interface IViewState {
waitForCompletion?: boolean;
statusUrlResolution: string;
}

interface ICommand {
errorMessage?: string;
invalid?: boolean;
payloadJSON: string;
}

export class WebhookStage {
static get $inject() {
return ['stage', 'jsonUtilityService'];
}

public command: ICommand;
public viewState: IViewState;
public methods: string[];

constructor(public stage: any,
private jsonUtilityService: JsonUtilityService) {
this.methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'];

this.viewState = {
waitForCompletion: this.stage.waitForCompletion || false,
statusUrlResolution: this.stage.statusUrlResolution || 'getMethod'
};

this.command = {
payloadJSON: this.jsonUtilityService.makeSortedStringFromObject(this.stage.payload || {}),
};
}

public updatePayload(): void {
this.command.invalid = false;
this.command.errorMessage = '';
try {
const parsed = JSON.parse(this.command.payloadJSON);
if (!this.stage.payload) {
this.stage.payload = {};
}
extend(this.stage.payload, parsed);
} catch (e) {
this.command.invalid = true;
this.command.errorMessage = e.message;
}
}

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

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

}

export const WEBHOOK_STAGE = 'spinnaker.core.pipeline.stage.webhookStage';

module(WEBHOOK_STAGE, [
JSON_UTILITY_SERVICE,
require('../../pipelineConfigProvider.js')
]).config((pipelineConfigProvider: any) => {
pipelineConfigProvider.registerStage({
label: 'Webhook',
description: 'Runs a Webhook job',
key: 'webhook',
restartable: true,
controller: 'WebhookStageCtrl',
controllerAs: '$ctrl',
templateUrl: require('./webhookStage.html'),
executionDetailsUrl: require('./webhookExecutionDetails.html'),
validators: [
{type: 'requiredField', fieldName: 'url'},
],
strategy: true,
});
}).controller('WebhookStageCtrl', WebhookStage);

0 comments on commit dcd0c77

Please sign in to comment.