Skip to content

Commit ec73c39

Browse files
authored
chore(pipelines): write a GraphViz file with the pipeline structure (#23908)
Add a `pipeline.dot` file to the cloud assembly containing the graph structure of the pipeline. This change is a `chore`, not a `feat`, as I want this to be a debugging aid, but I don't want to service feature requests on it. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 642b4ac commit ec73c39

File tree

2 files changed

+94
-8
lines changed

2 files changed

+94
-8
lines changed

packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import * as fs from 'fs';
12
import * as path from 'path';
23
import * as cb from '@aws-cdk/aws-codebuild';
34
import * as cp from '@aws-cdk/aws-codepipeline';
45
import * as cpa from '@aws-cdk/aws-codepipeline-actions';
56
import * as ec2 from '@aws-cdk/aws-ec2';
67
import * as iam from '@aws-cdk/aws-iam';
7-
import { Aws, CfnCapabilities, Duration, PhysicalName, Stack } from '@aws-cdk/core';
8+
import { Aws, CfnCapabilities, Duration, PhysicalName, Stack, Names } from '@aws-cdk/core';
89
import * as cxapi from '@aws-cdk/cx-api';
910
import { Construct } from 'constructs';
1011
import { AssetType, FileSet, IFileSetProducer, ManualApprovalStep, ShellStep, StackAsset, StackDeployment, Step } from '../blueprint';
@@ -423,6 +424,10 @@ export class CodePipeline extends PipelineBase {
423424
this._cloudAssemblyFileSet = graphFromBp.cloudAssemblyFileSet;
424425

425426
this.pipelineStagesAndActionsFromGraph(graphFromBp);
427+
428+
// Write a dotfile for the pipeline layout
429+
const dotFile = `${Names.uniqueId(this)}.dot`;
430+
fs.writeFileSync(path.join(this.myCxAsmRoot, dotFile), graphFromBp.graph.renderDot().replace(/input\.dot/, dotFile), { encoding: 'utf-8' });
426431
}
427432

428433
private get myCxAsmRoot(): string {

packages/@aws-cdk/pipelines/lib/helpers-internal/graph.ts

+88-7
Original file line numberDiff line numberDiff line change
@@ -291,13 +291,21 @@ export class Graph<A> extends GraphNode<A> {
291291
return topoSort(new Set(projectedDependencies.keys()), projectedDependencies);
292292
}
293293

294-
public consoleLog(indent: number = 0) {
295-
process.stdout.write(' '.repeat(indent) + this + depString(this) + '\n');
296-
for (const node of this.nodes) {
297-
if (node instanceof Graph) {
298-
node.consoleLog(indent + 2);
299-
} else {
300-
process.stdout.write(' '.repeat(indent + 2) + node + depString(node) + '\n');
294+
public render() {
295+
const lines = new Array<string>();
296+
recurse(this, '', true);
297+
return lines.join('\n');
298+
299+
function recurse(x: GraphNode<A>, indent: string, last: boolean) {
300+
const bullet = last ? '└─' : '├─';
301+
const follow = last ? ' ' : '│ ';
302+
lines.push(`${indent} ${bullet} ${x}${depString(x)}`);
303+
if (x instanceof Graph) {
304+
let i = 0;
305+
const sortedNodes = Array.prototype.concat.call([], ...x.sortedChildren());
306+
for (const child of sortedNodes) {
307+
recurse(child, `${indent} ${follow} `, i++ == x.nodes.size - 1);
308+
}
301309
}
302310
}
303311

@@ -309,6 +317,79 @@ export class Graph<A> extends GraphNode<A> {
309317
}
310318
}
311319

320+
public renderDot() {
321+
const lines = new Array<string>();
322+
323+
lines.push('digraph G {');
324+
lines.push(' # Arrows represent an "unlocks" relationship (opposite of dependency). So chosen');
325+
lines.push(' # because the layout looks more natural that way.');
326+
lines.push(' # To represent subgraph dependencies, subgraphs are represented by BEGIN/END nodes.');
327+
lines.push(' # To render: `dot -Tsvg input.dot > graph.svg`, open in a browser.');
328+
lines.push(' node [shape="box"];');
329+
for (const child of this.nodes) {
330+
recurse(child);
331+
}
332+
lines.push('}');
333+
334+
return lines.join('\n');
335+
336+
function recurse(node: GraphNode<A>) {
337+
let dependencySource;
338+
339+
if (node instanceof Graph) {
340+
lines.push(`${graphBegin(node)} [shape="cds", style="filled", fillcolor="#b7deff"];`);
341+
lines.push(`${graphEnd(node)} [shape="cds", style="filled", fillcolor="#b7deff"];`);
342+
dependencySource = graphBegin(node);
343+
} else {
344+
dependencySource = nodeLabel(node);
345+
lines.push(`${nodeLabel(node)};`);
346+
}
347+
348+
for (const dep of node.dependencies) {
349+
const dst = dep instanceof Graph ? graphEnd(dep) : nodeLabel(dep);
350+
lines.push(`${dst} -> ${dependencySource};`);
351+
}
352+
353+
if (node instanceof Graph && node.nodes.size > 0) {
354+
for (const child of node.nodes) {
355+
recurse(child);
356+
}
357+
358+
// Add dependency arrows between the "subgraph begin" and the first rank of
359+
// the children, and the last rank of the children and "subgraph end" nodes.
360+
const sortedChildren = node.sortedChildren();
361+
for (const first of sortedChildren[0]) {
362+
const src = first instanceof Graph ? graphBegin(first) : nodeLabel(first);
363+
lines.push(`${graphBegin(node)} -> ${src};`);
364+
}
365+
for (const last of sortedChildren[sortedChildren.length - 1]) {
366+
const dst = last instanceof Graph ? graphEnd(last) : nodeLabel(last);
367+
lines.push(`${dst} -> ${graphEnd(node)};`);
368+
}
369+
}
370+
}
371+
372+
function id(node: GraphNode<A>) {
373+
return node.rootPath().slice(1).map(n => n.id).join('.');
374+
}
375+
376+
function nodeLabel(node: GraphNode<A>) {
377+
return `"${id(node)}"`;
378+
}
379+
380+
function graphBegin(node: Graph<A>) {
381+
return `"BEGIN ${id(node)}"`;
382+
}
383+
384+
function graphEnd(node: Graph<A>) {
385+
return `"END ${id(node)}"`;
386+
}
387+
}
388+
389+
public consoleLog(_indent: number = 0) {
390+
process.stdout.write(this.render() + '\n');
391+
}
392+
312393
/**
313394
* Return the union of all dependencies of the descendants of this graph
314395
*/

0 commit comments

Comments
 (0)