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

chore(pipelines): write a GraphViz file with the pipeline structure #23908

Merged
merged 2 commits into from
Jan 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as fs from 'fs';
import * as path from 'path';
import * as cb from '@aws-cdk/aws-codebuild';
import * as cp from '@aws-cdk/aws-codepipeline';
import * as cpa from '@aws-cdk/aws-codepipeline-actions';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as iam from '@aws-cdk/aws-iam';
import { Aws, CfnCapabilities, Duration, PhysicalName, Stack } from '@aws-cdk/core';
import { Aws, CfnCapabilities, Duration, PhysicalName, Stack, Names } from '@aws-cdk/core';
import * as cxapi from '@aws-cdk/cx-api';
import { Construct } from 'constructs';
import { AssetType, FileSet, IFileSetProducer, ManualApprovalStep, ShellStep, StackAsset, StackDeployment, Step } from '../blueprint';
Expand Down Expand Up @@ -423,6 +424,10 @@ export class CodePipeline extends PipelineBase {
this._cloudAssemblyFileSet = graphFromBp.cloudAssemblyFileSet;

this.pipelineStagesAndActionsFromGraph(graphFromBp);

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

private get myCxAsmRoot(): string {
Expand Down
95 changes: 88 additions & 7 deletions packages/@aws-cdk/pipelines/lib/helpers-internal/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,13 +291,21 @@ export class Graph<A> extends GraphNode<A> {
return topoSort(new Set(projectedDependencies.keys()), projectedDependencies);
}

public consoleLog(indent: number = 0) {
process.stdout.write(' '.repeat(indent) + this + depString(this) + '\n');
for (const node of this.nodes) {
if (node instanceof Graph) {
node.consoleLog(indent + 2);
} else {
process.stdout.write(' '.repeat(indent + 2) + node + depString(node) + '\n');
public render() {
const lines = new Array<string>();
recurse(this, '', true);
return lines.join('\n');

function recurse(x: GraphNode<A>, indent: string, last: boolean) {
const bullet = last ? '└─' : '├─';
const follow = last ? ' ' : '│ ';
lines.push(`${indent} ${bullet} ${x}${depString(x)}`);
if (x instanceof Graph) {
let i = 0;
const sortedNodes = Array.prototype.concat.call([], ...x.sortedChildren());
for (const child of sortedNodes) {
recurse(child, `${indent} ${follow} `, i++ == x.nodes.size - 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice one 🙂

}
}
}

Expand All @@ -309,6 +317,79 @@ export class Graph<A> extends GraphNode<A> {
}
}

public renderDot() {
const lines = new Array<string>();

lines.push('digraph G {');
lines.push(' # Arrows represent an "unlocks" relationship (opposite of dependency). So chosen');
lines.push(' # because the layout looks more natural that way.');
lines.push(' # To represent subgraph dependencies, subgraphs are represented by BEGIN/END nodes.');
lines.push(' # To render: `dot -Tsvg input.dot > graph.svg`, open in a browser.');
lines.push(' node [shape="box"];');
for (const child of this.nodes) {
recurse(child);
}
lines.push('}');

return lines.join('\n');

function recurse(node: GraphNode<A>) {
let dependencySource;

if (node instanceof Graph) {
lines.push(`${graphBegin(node)} [shape="cds", style="filled", fillcolor="#b7deff"];`);
lines.push(`${graphEnd(node)} [shape="cds", style="filled", fillcolor="#b7deff"];`);
dependencySource = graphBegin(node);
} else {
dependencySource = nodeLabel(node);
lines.push(`${nodeLabel(node)};`);
}

for (const dep of node.dependencies) {
const dst = dep instanceof Graph ? graphEnd(dep) : nodeLabel(dep);
lines.push(`${dst} -> ${dependencySource};`);
}

if (node instanceof Graph && node.nodes.size > 0) {
for (const child of node.nodes) {
recurse(child);
}

// Add dependency arrows between the "subgraph begin" and the first rank of
// the children, and the last rank of the children and "subgraph end" nodes.
const sortedChildren = node.sortedChildren();
for (const first of sortedChildren[0]) {
const src = first instanceof Graph ? graphBegin(first) : nodeLabel(first);
lines.push(`${graphBegin(node)} -> ${src};`);
}
for (const last of sortedChildren[sortedChildren.length - 1]) {
const dst = last instanceof Graph ? graphEnd(last) : nodeLabel(last);
lines.push(`${dst} -> ${graphEnd(node)};`);
}
}
}

function id(node: GraphNode<A>) {
return node.rootPath().slice(1).map(n => n.id).join('.');
}

function nodeLabel(node: GraphNode<A>) {
return `"${id(node)}"`;
}

function graphBegin(node: Graph<A>) {
return `"BEGIN ${id(node)}"`;
}

function graphEnd(node: Graph<A>) {
return `"END ${id(node)}"`;
}
}

public consoleLog(_indent: number = 0) {
process.stdout.write(this.render() + '\n');
}

/**
* Return the union of all dependencies of the descendants of this graph
*/
Expand Down