Skip to content

Commit

Permalink
feat(pipeline_template): Better support for templated pipelines with…
Browse files Browse the repository at this point in the history
… dynamic sources - Take 2 (#4365)

* feat(pipeline_template): Better support for templated pipelines with dynamic sources

* Add "Rendered pipeline" tab to "Edit as JSON" modal for templated pipelines
* Don't display error message when editing templated pipelines with dynamic source - real fix in Orca
* Render execution graph when editing templated pipelines with dynamic source - real fix in Orca
* Display warning in Deck if configuration can't be rendered because no executions have been run
* Display information about which execution used for rendering
* Let the user select which configuration that's used for rendering

Depends on spinnaker/orca#1718 and spinnaker/gate#471

* fix(pipeline_template): Build execution title/numbers was rendered twice

* fix(pipeline_template): PR feedback == --> ===

* PR feedback: don't swallow template exceptions
  • Loading branch information
jervi authored and danielpeach committed Nov 8, 2017
1 parent b04f25f commit 6c6ea94
Show file tree
Hide file tree
Showing 18 changed files with 177 additions and 34 deletions.
2 changes: 1 addition & 1 deletion app/scripts/modules/core/src/delivery/delivery.states.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module(DELIVERY_STATES, [

const pipelineConfig: INestedState = {
name: 'pipelineConfig',
url: '/configure/:pipelineId',
url: '/configure/:pipelineId?executionId',
views: {
'pipelines': {
templateUrl: require('../pipeline/config/pipelineConfig.html'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { BindAll } from 'lodash-decorators';
import { IExecution } from 'core/domain';
import { ReactInjector } from 'core/reactShims';

import './ExecutionBuildNumber.less';
import './ExecutionBuildLink.less';

export interface IExecutionBuildNumberProps {
export interface IExecutionBuildLinkProps {
execution: IExecution;
}

@BindAll()
export class ExecutionBuildNumber extends React.Component<IExecutionBuildNumberProps, {}> {
constructor(props: IExecutionBuildNumberProps) {
export class ExecutionBuildLink extends React.Component<IExecutionBuildLinkProps, {}> {
constructor(props: IExecutionBuildLinkProps) {
super(props);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as React from 'react';
import { BindAll } from 'lodash-decorators';

import { module } from 'angular';
import { react2angular } from 'react2angular';

import { IExecutionBuildLinkProps } from './ExecutionBuildLink';
import { timestamp } from 'core/utils'

export interface IExecutionBuildTitleProps extends IExecutionBuildLinkProps {
defaultToTimestamp?: boolean;
}

@BindAll()
export class ExecutionBuildTitle extends React.Component<IExecutionBuildTitleProps, {}> {

public static defaultProps: Partial<IExecutionBuildTitleProps> = {
defaultToTimestamp: false
};

private hasParentPipeline: boolean;
private hasBuildNumber: boolean;

constructor(props: IExecutionBuildTitleProps) {
super(props);
this.hasParentPipeline = !!props.execution.trigger.parentPipelineName;
this.hasBuildNumber = !!(props.execution.buildInfo && props.execution.buildInfo.number);
}

public render() {
return (
<span>
{ this.hasParentPipeline && (
<span>{this.props.execution.trigger.parentPipelineName}</span>
)}
{ this.hasBuildNumber && !this.hasParentPipeline && (
<span><span className="build-label">Build</span> #{this.props.execution.buildInfo.number}</span>
)}
{ this.props.defaultToTimestamp && !this.hasParentPipeline && !this.hasBuildNumber && (
<span>{timestamp(this.props.execution.startTime)}</span>
)}
</span>
);
}
}

export const EXECUTION_BUILD_TITLE = 'spinnaker.core.delivery.executionbuild.executionbuildtitle';
const ngmodule = module(EXECUTION_BUILD_TITLE, []);

ngmodule.component('executionBuildTitle', react2angular(ExecutionBuildTitle, ['execution', 'defaultToTimestamp']));
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export class ExecutionsTransformerService {
return null;
}

private addBuildInfo(execution: IExecution): void {
public addBuildInfo(execution: IExecution): void {
execution.buildInfo = this.findNearestBuildInfo(execution);

if (has(execution, 'trigger.buildInfo.lastBuild.number')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ReactInjector } from 'core/reactShims';
import { relativeTime, timestamp } from 'core/utils';

import { buildDisplayName } from '../executionBuild/buildDisplayName.filter';
import { ExecutionBuildNumber } from '../executionBuild/ExecutionBuildNumber';
import { ExecutionBuildLink } from '../executionBuild/ExecutionBuildLink';

import './executionStatus.less';

Expand Down Expand Up @@ -109,7 +109,7 @@ export class ExecutionStatus extends React.Component<IExecutionStatusProps, IExe
return (
<div className="execution-status-section">
<span className={`trigger-type ${this.state.sortFilter.groupBy !== name ? 'subheading' : ''}`}>
<h5 className="build-number"><ExecutionBuildNumber execution={execution}/></h5>
<h5 className="build-number"><ExecutionBuildLink execution={execution}/></h5>
<h5 className="execution-type">{this.getExecutionTypeDisplay()}</h5>
</span>
<ul className="trigger-details">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('Controller: editPipelineJsonModal', () => {

function initializeController(pipeline: IPipeline) {
$uibModalInstance = { close: () => {} };
controller = $ctrl(EditPipelineJsonModalCtrl, { $uibModalInstance, pipeline });
controller = $ctrl(EditPipelineJsonModalCtrl, { $uibModalInstance, pipeline, plan: null });
}

it('controller removes name, application, appConfig, all fields and hash keys', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@ export interface IEditPipelineJsonModalCommand {
errorMessage?: string;
invalid?: boolean;
pipelineJSON: string;
pipelinePlanJSON?: string;
locked: boolean;
}

export class EditPipelineJsonModalCtrl implements IController {

public isStrategy: boolean;
public command: IEditPipelineJsonModalCommand;
public mode = 'pipeline'
private immutableFields = ['name', 'application', 'index', 'id', '$$hashKey'];

constructor(private $uibModalInstance: IModalServiceInstance,
private pipeline: IPipeline) {
private pipeline: IPipeline, private plan?: IPipeline) {
'ngInject';
}

Expand Down Expand Up @@ -47,21 +49,31 @@ export class EditPipelineJsonModalCtrl implements IController {
}

public $onInit(): void {
const copy = cloneDeepWith<IPipeline>(this.pipeline, (value: any) => {
if (value && value.$$hashKey) {
delete value.$$hashKey;
}
return undefined; // required for clone operation and typescript happiness
});
this.removeImmutableFields(copy);
const copy = this.clone(this.pipeline);
let copyPlan: IPipeline;
if (this.plan) {
copyPlan = this.clone(this.plan);
}

this.isStrategy = this.pipeline.strategy || false;
this.command = {
pipelineJSON: jsonUtilityService.makeSortedStringFromObject(copy),
pipelinePlanJSON: copyPlan ? jsonUtilityService.makeSortedStringFromObject(copyPlan) : null,
locked: copy.locked
};
}

private clone(pipeline: IPipeline): IPipeline {
const copy = cloneDeepWith<IPipeline>(pipeline, (value: any) => {
if (value && value.$$hashKey) {
delete value.$$hashKey;
}
return undefined; // required for clone operation and typescript happiness
});
this.removeImmutableFields(copy);
return copy;
}

public updatePipeline(): void {
try {
const parsed = JSON.parse(this.command.pipelineJSON);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
<div class="modal-header">
<h3><span ng-if="!command.locked">Edit </span>{{$ctrl.isStrategy === true ? 'Strategy' : 'Pipeline'}} JSON</h3>
</div>
<div class="modal-body flex-fill">
<!-- TODO: remove inline styling below when tabs-basic class has been updated to include list-style: none -->
<ul class="tabs-basic" style="list-style: none" ng-if="$ctrl.command.pipelinePlanJSON">
<li role="presentation" ng-class="{selected: $ctrl.mode === 'pipeline'}"><a ng-click="$ctrl.mode = 'pipeline'">Configuration</a></li>
<li role="presentation" ng-class="{selected: $ctrl.mode === 'renderedPipeline'}"><a ng-click="$ctrl.mode = 'renderedPipeline'">Rendered pipeline</a></li>
</ul>
<div class="modal-body flex-fill" ng-if="$ctrl.mode === 'pipeline'">
<div class="row">
<div class="col-md-12">
<p>
Expand Down Expand Up @@ -34,6 +39,20 @@ <h3><span ng-if="!command.locked">Edit </span>{{$ctrl.isStrategy === true ? 'Str
</div>
</div>
</div>
<div class="modal-body flex-fill" ng-if="$ctrl.mode === 'renderedPipeline'">
<div class="row">
<div class="col-md-12">
<p>
This pipeline is based on a template. The JSON below represents the rendered pipeline.
</p>
</div>
</div>
<form role="form" name="form" class="form-horizontal flex-fill">
<div class="form-group flex-fill">
<textarea class="code form-control flex-fill" ng-model="$ctrl.command.pipelinePlanJSON" disabled></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-default" ng-click="$dismiss()">Cancel</button>
<button class="btn btn-primary"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,20 @@ module.exports = angular.module('spinnaker.core.pipeline.config.controller', [
pipelinesLoaded: false,
};

this.containsJinja = source => source && (source.includes('{{') || source.includes('{%'));

this.initialize = () => {
this.pipelineConfig = _.find(app.pipelineConfigs.data, { id: $stateParams.pipelineId });
if (this.pipelineConfig && this.pipelineConfig.type === 'templatedPipeline') {
this.isTemplatedPipeline = true;
this.hasDynamicSource = this.containsJinja(this.pipelineConfig.config.pipeline.template.source);
if (!this.pipelineConfig.isNew) {
return pipelineTemplateService.getPipelinePlan(this.pipelineConfig)
return pipelineTemplateService.getPipelinePlan(this.pipelineConfig, $stateParams.executionId)
.then(plan => this.pipelinePlan = plan)
.catch(() => this.pipelineConfig.isNew = true);
.catch(error => {
this.templateError = error;
this.pipelineConfig.isNew = true;
});
}
} else if (!this.pipelineConfig) {
this.pipelineConfig = _.find(app.strategyConfigs.data, { id: $stateParams.pipelineId });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
<pipeline-configurer pipeline="vm.pipelineConfig"
plan="vm.pipelinePlan"
is-templated-pipeline="vm.isTemplatedPipeline"
application="vm.application"></pipeline-configurer>
has-dynamic-source="vm.hasDynamicSource"
application="vm.application"
template-error="vm.templateError"></pipeline-configurer>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,32 @@ <h3>
<div class="band band-info" ng-if="isTemplatedPipeline && !pipeline.locked">
<span class="glyphicon glyphicon small glyphicon-lock"></span> Manual edits are not allowed on templated pipelines
</div>

<div class="band band-info" ng-if="hasDynamicSource && pipelineExecutions.length">
<span class="glyphicon glyphicon small glyphicon-wrench"></span> This template has a dynamic source. The
configuration is currently rendered using
<span ng-if="pipelineExecutions.length > 1" class="dropdown" uib-dropdown>
&nbsp;&nbsp;<button type="button" class="btn btn-xs btn-default dropdown-toggle" uib-dropdown-toggle>
<execution-build-title execution="currentExecution" default-to-timestamp="true" ></execution-build-title> <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu" uib-dropdown-menu>
<li ng-repeat="p in pipelineExecutions" ng-class="{disabled: p.id == currentExecution.id}">
<a title="{{p.startTime | timestamp}}" ui-sref="{ executionId: p.id }"><execution-build-title execution="p" default-to-timestamp="true" ></execution-build-title>
<span class="text-muted small">({{p.startTime | relativeTime}})</span>
<span class="glyphicon small glyphicon-ok" ng-if="p.id == currentExecution.id"></span>
</a>
</li>
</ul>
</span>
<span ng-if="pipelineExecutions.length === 1"><execution-build-title execution="currentExecution" default-to-timestamp="true" ></execution-build-title></span>
</div>
<div class="band band-warning" ng-if="pipelineExecutions && !pipelineExecutions.length">
<span class="glyphicon glyphicon small glyphicon-alert"></span> This template has a dynamic source. The
configuration cannot be rendered before the pipeline has executed at least once.
</div>
<div class="band band-warning" ng-if="templateError && pipelineExecutions.length && isTemplatedPipeline">
<span class="glyphicon glyphicon small glyphicon-alert"></span> Could not render the pipeline configuration because
of an error: {{ templateError.data.message }} {{ templateError.data.error }}
</div>
<div class="config-heading-body">
<div class="pipeline-graph-container pipeline-config-graph">
<pipeline-graph view-state="viewState"
Expand Down
34 changes: 30 additions & 4 deletions app/scripts/modules/core/src/pipeline/config/pipelineConfigurer.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import { PIPELINE_CONFIG_SERVICE } from 'core/pipeline/config/services/pipelineC
import { EditPipelineJsonModalCtrl } from './actions/json/editPipelineJsonModal.controller';
import { PIPELINE_CONFIG_VALIDATOR } from './validation/pipelineConfig.validator';
import { PIPELINE_TEMPLATE_SERVICE } from './templates/pipelineTemplate.service';
import { EXECUTION_BUILD_TITLE } from '../../delivery/executionBuild/ExecutionBuildTitle';

module.exports = angular.module('spinnaker.core.pipeline.config.pipelineConfigurer', [
OVERRIDE_REGISTRY,
PIPELINE_CONFIG_SERVICE,
PIPELINE_CONFIG_VALIDATOR,
PIPELINE_TEMPLATE_SERVICE,
EXECUTION_BUILD_TITLE,
])
.directive('pipelineConfigurer', function() {
return {
Expand All @@ -24,13 +26,15 @@ module.exports = angular.module('spinnaker.core.pipeline.config.pipelineConfigur
application: '=',
plan: '<',
isTemplatedPipeline: '<',
hasDynamicSource: '<',
templateError: '<',
},
controller: 'PipelineConfigurerCtrl as pipelineConfigurerCtrl',
templateUrl: require('./pipelineConfigurer.html'),
};
})
.controller('PipelineConfigurerCtrl', function($scope, $uibModal, $timeout, $window, $q,
pipelineConfigValidator, pipelineTemplateService,
.controller('PipelineConfigurerCtrl', function($scope, $uibModal, $timeout, $window, $q, pipelineConfigValidator,
pipelineTemplateService, executionService, executionsTransformer,
pipelineConfigService, viewStateCache, overrideRegistry, $location) {
// For standard pipelines, a 'renderablePipeline' is just the pipeline config.
// For templated pipelines, a 'renderablePipeline' is the pipeline template plan, and '$scope.pipeline' is the template config.
Expand Down Expand Up @@ -173,7 +177,8 @@ module.exports = angular.module('spinnaker.core.pipeline.config.pipelineConfigur
controllerAs: '$ctrl',
size: 'lg modal-fullscreen',
resolve: {
pipeline: () => $scope.renderablePipeline,
pipeline: () => $scope.pipeline,
plan: () => $scope.plan,
}
}).result.then(() => {
$scope.$broadcast('pipeline-json-edited');
Expand Down Expand Up @@ -332,6 +337,7 @@ module.exports = angular.module('spinnaker.core.pipeline.config.pipelineConfigur
pipelineTemplateConfig: () => _.cloneDeep($scope.pipeline),
isNew: () => $scope.pipeline.isNew,
pipelineId: () => $scope.pipeline.id,
executionId: () => $scope.renderablePipeline.executionId,
}
}).result.then(({plan, config}) => {
$scope.pipeline = config;
Expand Down Expand Up @@ -366,6 +372,22 @@ module.exports = angular.module('spinnaker.core.pipeline.config.pipelineConfigur
return msg;
};

this.getPipelineExecutions = () => {
executionService.getExecutionsForConfigIds($scope.pipeline.application, $scope.pipeline.id, 5)
.then(executions => {
executions.forEach(execution => executionsTransformer.addBuildInfo(execution));
$scope.pipelineExecutions = executions;
if ($scope.plan && $scope.plan.executionId) {
$scope.currentExecution = _.find($scope.pipelineExecutions, { id: $scope.plan.executionId });
} else if ($location.search().executionId) {
$scope.currentExecution = _.find($scope.pipelineExecutions, { id: $location.search().executionId });
} else {
$scope.currentExecution = $scope.pipelineExecutions[0];
}
})
.catch(() => $scope.pipelineExecutions = []);
};

this.revertPipelineChanges = () => {
let original = getOriginal();
Object.keys($scope.pipeline).forEach(key => {
Expand Down Expand Up @@ -446,7 +468,11 @@ module.exports = angular.module('spinnaker.core.pipeline.config.pipelineConfigur
$window.onbeforeunload = undefined;
});

if ($scope.isTemplatedPipeline && $scope.pipeline.isNew) {
if ($scope.hasDynamicSource) {
this.getPipelineExecutions();
}

if ($scope.isTemplatedPipeline && $scope.pipeline.isNew && !$scope.hasDynamicSource) {
this.configureTemplate();
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ describe('Controller: ConfigurePipelineTemplateModalCtrl', () => {
}
},
pipelineId: '1234',
executionId: null,
isNew: true,
}) as ConfigurePipelineTemplateModalController;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class ConfigurePipelineTemplateModalController implements IController {

constructor(private $scope: IScope, private $uibModalInstance: IModalInstanceService,
private application: Application, public pipelineTemplateConfig: IPipelineTemplateConfig,
public isNew: boolean, private pipelineId: string) {
public isNew: boolean, private pipelineId: string, private executionId: string) {
'ngInject';
}

Expand Down Expand Up @@ -109,7 +109,7 @@ export class ConfigurePipelineTemplateModalController implements IController {
}

private loadTemplate(): IPromise<void> {
return ReactInjector.pipelineTemplateService.getPipelineTemplateFromSourceUrl(this.source)
return ReactInjector.pipelineTemplateService.getPipelineTemplateFromSourceUrl(this.source, this.executionId, this.pipelineId)
.then(template => { this.template = template });
}

Expand Down
Loading

0 comments on commit 6c6ea94

Please sign in to comment.