-
Notifications
You must be signed in to change notification settings - Fork 154
/
Copy pathTestStack.ts
190 lines (179 loc) · 5.19 KB
/
TestStack.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import { Console } from 'node:console';
import { readFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
type ICloudAssemblySource,
RequireApproval,
StackSelectionStrategy,
Toolkit,
} from '@aws-cdk/toolkit-lib';
import { App, Stack } from 'aws-cdk-lib';
import { generateTestUniqueName } from './helpers.js';
import type { TestStackProps } from './types.js';
const testConsole = new Console({
stdout: process.stdout,
stderr: process.stderr,
});
/**
* Test stack that can be deployed to the selected environment.
*/
class TestStack {
/**
* Reference to the AWS CDK App object.
* @default new App()
*/
public app: App;
/**
* Outputs of the deployed stack.
*/
public outputs: Record<string, string> = {};
/**
* Reference to the AWS CDK Stack object.
* @default new Stack(this.app, stackName)
*/
public stack: Stack;
/**
* Name of the test stack.
* @example
* Logger-E2E-node18-12345-someFeature
*/
public testName: string;
/**
* @internal
* Reference to the AWS CDK CLI object.
*/
readonly #cli: Toolkit;
/**
* @internal
* Reference to the AWS CDK Cloud Assembly context.
*/
#cx?: ICloudAssemblySource;
public constructor({ stackNameProps, app, stack }: TestStackProps) {
this.testName = generateTestUniqueName({
testName: stackNameProps.testName,
testPrefix: stackNameProps.stackNamePrefix,
});
this.app = app ?? new App();
this.stack =
stack ??
new Stack(this.app, this.testName, {
tags: {
Service: 'Powertools-for-AWS-e2e-tests',
},
});
let lastCreateLog = 0;
let lastDestroyLog = 0;
const creationDeleteLogFrequency = 10000; // 10 seconds
const that = this;
this.#cli = new Toolkit({
color: false,
ioHost: {
/**
* Log messages to the console depending on the log level.
*
* If the `RUNNER_DEBUG` environment variable is set to `1`, all messages are logged.
*
* Otherwise, we log messages that are either warnings or errors as well as periodic
* updates on the stack creation and destruction process.
*
* @param msg - Message to log sent by the CDK CLI
*/
async notify(msg) {
if (process.env.RUNNER_DEBUG === '1') {
testConsole.log(msg);
return;
}
if (msg.message.includes('destroyed') && msg.message.includes('✅')) {
testConsole.log(msg.message);
return;
}
if (msg.message.includes('✅') && !msg.message.includes('deployed')) {
testConsole.log(`${that.testName} deployed successfully`);
return;
}
if (msg.message.includes('CREATE_IN_PROGRESS')) {
if (Date.now() - lastCreateLog < creationDeleteLogFrequency) {
return;
}
lastCreateLog = Date.now();
testConsole.log(`${that.testName} stack is being created...`);
return;
}
if (msg.message.includes('DELETE_IN_PROGRESS')) {
if (Date.now() - lastDestroyLog < creationDeleteLogFrequency) {
return;
}
lastDestroyLog = Date.now();
testConsole.log(`${that.testName} stack is being destroyed...`);
return;
}
if (['warning', 'error'].includes(msg.level)) {
testConsole.log(msg);
}
},
async requestResponse(msg) {
if (
process.env.RUNNER_DEBUG === '1' ||
['warning', 'error'].includes(msg.level)
) {
testConsole.log(msg);
}
return msg.defaultResponse;
},
},
});
}
/**
* Deploy the test stack to the selected environment.
*
* It returns the outputs of the deployed stack.
*/
public async deploy(): Promise<Record<string, string>> {
const outdir = join(tmpdir(), 'powertools-e2e-testing');
const outputFilePath = join(outdir, `${this.stack.stackName}.outputs.json`);
this.#cx = await this.#cli.fromAssemblyBuilder(
async () => this.app.synth(),
{
outdir,
}
);
await this.#cli.deploy(this.#cx, {
stacks: {
strategy: StackSelectionStrategy.ALL_STACKS,
},
outputsFile: outputFilePath,
requireApproval: RequireApproval.NEVER,
});
this.outputs = JSON.parse(await readFile(outputFilePath, 'utf-8'))[
this.stack.stackName
];
return this.outputs;
}
/**
* Destroy the test stack.
*/
public async destroy(): Promise<void> {
if (!this.#cx) {
throw new Error('Cannot destroy stack without a Cloud Assembly');
}
await this.#cli.destroy(this.#cx, {
stacks: {
strategy: StackSelectionStrategy.ALL_STACKS,
},
});
}
/**
* Find and get the value of a StackOutput by its key.
*/
public findAndGetStackOutputValue = (key: string): string => {
const value = Object.keys(this.outputs).find((outputKey) =>
outputKey.includes(key)
);
if (!value) {
throw new Error(`Cannot find output for ${key}`);
}
return this.outputs[value];
};
}
export { TestStack };