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

feat(pipelines): stack-level steps #16215

Merged
merged 8 commits into from
Sep 8, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
13 changes: 12 additions & 1 deletion packages/@aws-cdk/pipelines/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ manual or automated gates to your pipeline. We recommend putting manual approval
the set of `post` steps.

The following example shows both an automated approval in the form of a `ShellStep`, and
a manual approvel in the form of a `ManualApprovalStep` added to the pipeline. Both must
a manual approval in the form of a `ManualApprovalStep` added to the pipeline. Both must
pass in order to promote from the `PreProd` to the `Prod` environment:

```ts
Expand All @@ -481,6 +481,17 @@ pipeline.addStage(prod, {
});
```

You can also approve changeSets between a stacks `prepare` and `deploy` phases. To achieve this, you can specify a `changeSetApproval` property on a `Stage`:

```ts
pipeline.addStage(prod, {
changeSetApproval: [{
step: new ManualApprovalStep('CheckChangeSet'),
stacks: [ProdStack1, ProdStack2],
kaizencc marked this conversation as resolved.
Show resolved Hide resolved
}],
});
```

#### Using CloudFormation Stack Outputs in approvals

Because many CloudFormation deployments result in the generation of resources with unpredictable
Expand Down
17 changes: 17 additions & 0 deletions packages/@aws-cdk/pipelines/lib/blueprint/stack-deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as cxapi from '@aws-cdk/cx-api';
import { AssetManifestReader, DockerImageManifestEntry, FileManifestEntry } from '../private/asset-manifest';
import { isAssetManifest } from '../private/cloud-assembly-internals';
import { AssetType } from './asset-type';
import { Step } from './step';

/**
* Properties for a `StackDeployment`
Expand Down Expand Up @@ -191,6 +192,11 @@ export class StackDeployment {
*/
public readonly absoluteTemplatePath: string;

/**
* Instructions for additional steps that are run between Prepare and Deploy in specific stacks
*/
public changeSetApproval?: Step;
kaizencc marked this conversation as resolved.
Show resolved Hide resolved

private constructor(props: StackDeploymentProps) {
this.stackArtifactId = props.stackArtifactId;
this.constructPath = props.constructPath;
Expand Down Expand Up @@ -220,6 +226,17 @@ export class StackDeployment {
public addStackDependency(stackDeployment: StackDeployment) {
this.stackDependencies.push(stackDeployment);
}

/**
* Add a changeSet approval step to this stack
* //Question: Should we allow for multiple steps between Prepare and Deploy?
*/
public addChangeSetApproval(step: Step) {
if (this.changeSetApproval) {
throw new Error('changeSetApproval may not be updated more than once');
}
this.changeSetApproval = step;
}
}

/**
Expand Down
28 changes: 27 additions & 1 deletion packages/@aws-cdk/pipelines/lib/blueprint/stage-deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CloudFormationStackArtifact } from '@aws-cdk/cx-api';
import { isStackArtifact } from '../private/cloud-assembly-internals';
import { pipelineSynth } from '../private/construct-internals';
import { StackDeployment } from './stack-deployment';
import { Step } from './step';
import { ChangeSetApproval, Step } from './step';

/**
* Properties for a `StageDeployment`
Expand All @@ -29,6 +29,13 @@ export interface StageDeploymentProps {
* @default - No additional steps
*/
readonly post?: Step[];

/**
* Instructions for additional steps that are run between Prepare and Deploy in specific stacks
*
* @default - No additional instructions
*/
readonly changeSetApproval?: ChangeSetApproval[];
}

/**
Expand Down Expand Up @@ -57,6 +64,19 @@ export class StageDeployment {
const step = StackDeployment.fromArtifact(artifact);
stepFromArtifact.set(artifact, step);
}
if (props.changeSetApproval) {
for (const approval of props.changeSetApproval) {
for (const stack of approval.stacks) {
// getStackByName will throw if stack does not exist
const stackArtifact = assembly.getStackByName(stack.stackName);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I'd rather select the stack by selecting by construct path, that's guaranteed to be unique while stackName is only likely to be unique.

stack.node.path is the path, not sure which assembly.getStack() you need to be using there. It might not exist, in which case you have to iterate over all artifacts, check only for the stack artifacts and compare the nodePath (or hierarchicalId) attributes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

am hoping that the change to assembly.getStackArtifact(stackstep.stack.artifactId) suffices.

const thisStep = stepFromArtifact.get(stackArtifact);
if (!thisStep) {
throw new Error('Logic error: we just added a step for this artifact but it disappeared.');
}
thisStep.addChangeSetApproval(approval.step);
}
}
}

for (const artifact of assembly.stacks) {
const thisStep = stepFromArtifact.get(artifact);
Expand Down Expand Up @@ -95,12 +115,18 @@ export class StageDeployment {
*/
public readonly post: Step[];

/**
* Instructions for additional steps that are run between Prepare and Deploy in specific stacks
*/
public readonly changeSetApproval: ChangeSetApproval[];

private constructor(
/** The stacks deployed in this stage */
public readonly stacks: StackDeployment[], props: StageDeploymentProps = {}) {
this.stageName = props.stageName ?? '';
this.pre = props.pre ?? [];
this.post = props.post ?? [];
this.changeSetApproval = props.changeSetApproval ?? [];
}

/**
Expand Down
17 changes: 16 additions & 1 deletion packages/@aws-cdk/pipelines/lib/blueprint/step.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Token } from '@aws-cdk/core';
import { Stack, Token } from '@aws-cdk/core';
import { FileSet, IFileSetProducer } from './file-set';

/**
Expand Down Expand Up @@ -74,4 +74,19 @@ export abstract class Step implements IFileSetProducer {
protected configurePrimaryOutput(fs: FileSet) {
this._primaryOutput = fs;
}
}

/**
* Instructions for additional steps that are run between Prepare and Deploy in specific stacks
*/
export interface ChangeSetApproval {
kaizencc marked this conversation as resolved.
Show resolved Hide resolved
/**
* The step to run
*/
readonly step: Step,

/**
* The stacks you want the step to run in
*/
readonly stacks: Stack[];
}
9 changes: 8 additions & 1 deletion packages/@aws-cdk/pipelines/lib/blueprint/wave.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as cdk from '@aws-cdk/core';
import { StageDeployment } from './stage-deployment';
import { Step } from './step';
import { ChangeSetApproval, Step } from './step';

/**
* Construction properties for a `Wave`
Expand Down Expand Up @@ -91,6 +91,13 @@ export interface AddStageOpts {
* @default - No additional steps
*/
readonly post?: Step[];

/**
* Instructions for additional steps that are run between Prepare and Deploy in specific stacks
*
* @default - No additional instructions
*/
readonly changeSetApproval?: ChangeSetApproval[];
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export class PipelineGraph {
for (const stack of stage.stacks) {
const stackGraph: AGraph = Graph.of(this.simpleStackName(stack.stackName, stage.stageName), { type: 'stack-group', stack });
const prepareNode: AGraphNode | undefined = this.prepareStep ? GraphNode.of('Prepare', { type: 'prepare', stack }) : undefined;
const changeSetNode: AGraphNode | undefined = stack.changeSetApproval ? GraphNode.of(stack.changeSetApproval.id, { type: 'step', step: stack.changeSetApproval }) : undefined;
const deployNode: AGraphNode = GraphNode.of('Deploy', {
type: 'execute',
stack,
Expand All @@ -147,7 +148,13 @@ export class PipelineGraph {
let firstDeployNode;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

it feels a little wrong to continue calling this firstDeployNode given that it could be a node collection, but I'm not sure if it is that big of a deal.

if (prepareNode) {
stackGraph.add(prepareNode);
deployNode.dependOn(prepareNode);
if (changeSetNode) {
stackGraph.add(changeSetNode);
changeSetNode.dependOn(prepareNode);
deployNode.dependOn(changeSetNode);
} else {
deployNode.dependOn(prepareNode);
}
firstDeployNode = prepareNode;
} else {
firstDeployNode = deployNode;
Expand Down
1 change: 0 additions & 1 deletion packages/@aws-cdk/pipelines/lib/main/pipeline-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ export abstract class PipelineBase extends CoreConstruct {
if (this.built) {
throw new Error('addStage: can\'t add Stages anymore after buildPipeline() has been called');
}

return this.addWave(stage.stageName).addStage(stage, options);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import '@aws-cdk/assert-internal/jest';
import * as cdkp from '../../../lib';
import { Graph, GraphNode, PipelineGraph } from '../../../lib/helpers-internal';
import { flatten } from '../../../lib/private/javascript';
import { AppWithOutput, OneStackApp, TestApp } from '../../testhelpers/test-app';
import { AppWithOutput, AppWithExposedStacks, OneStackApp, TestApp } from '../../testhelpers/test-app';

let app: TestApp;

Expand Down Expand Up @@ -113,6 +113,61 @@ describe('blueprint with wave and stage', () => {
'Stack',
]);
});

test('changeSetApproval gets added inside stack graph', () => {
// GIVEN
const appWithExposedStacks = new AppWithExposedStacks(app, 'Gamma');
const stack = appWithExposedStacks.stacks[0];
blueprint.waves[0].addStage(appWithExposedStacks, {
changeSetApproval: [{
step: new cdkp.ManualApprovalStep('Manual Approval'),
stacks: [stack],
}],
});

// WHEN
const graph = new PipelineGraph(blueprint).graph;

// THEN
expect(childrenAt(graph, 'Wave', 'Gamma', 'Stack1')).toEqual([
'Prepare',
'Manual Approval',
'Deploy',
]);
});

test('changeSetApproval can be added on multiple stacks in a stage', () => {
// GIVEN
const appWithExposedStacks = new AppWithExposedStacks(app, 'Gamma');
const stack1 = appWithExposedStacks.stacks[0];
const stack2 = appWithExposedStacks.stacks[1];
blueprint.waves[0].addStage(appWithExposedStacks, {
changeSetApproval: [{
step: new cdkp.ManualApprovalStep('Manual Approval'),
stacks: [stack1, stack2],
}],
});
// WHEN
const graph = new PipelineGraph(blueprint).graph;

// THEN
expect(childrenAt(graph, 'Wave', 'Gamma', 'Stack1')).toEqual([
'Prepare',
'Manual Approval',
'Deploy',
]);

expect(childrenAt(graph, 'Wave', 'Gamma', 'Stack2')).toEqual([
'Prepare',
'Manual Approval',
'Deploy',
]);

expect(childrenAt(graph, 'Wave', 'Gamma', 'Stack3')).toEqual([
'Prepare',
'Deploy',
]);
});
});

describe('options for other engines', () => {
Expand Down
10 changes: 10 additions & 0 deletions packages/@aws-cdk/pipelines/test/testhelpers/test-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ export class TestApp extends App {
}
}

export class AppWithExposedStacks extends Stage {
public readonly stacks: Stack[];
constructor(scope: Construct, id: string, props?: StageProps) {
super(scope, id, props);
this.stacks = new Array<Stack>();
this.stacks.push(new BucketStack(this, 'Stack1'));
this.stacks.push(new BucketStack(this, 'Stack2'));
this.stacks.push(new BucketStack(this, 'Stack3'));
}
}

export class OneStackApp extends Stage {
constructor(scope: Construct, id: string, props?: StageProps) {
Expand Down