puls-dev 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/checker.js +71 -0
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.js +12 -1
- package/dist/core/context.d.ts +14 -0
- package/dist/core/context.js +2 -0
- package/dist/core/decorators.d.ts +2 -0
- package/dist/core/decorators.js +8 -14
- package/dist/core/group.test.d.ts +1 -0
- package/dist/core/group.test.js +94 -0
- package/dist/core/parallel.test.d.ts +1 -0
- package/dist/core/parallel.test.js +215 -0
- package/dist/core/production.test.d.ts +1 -0
- package/dist/core/production.test.js +189 -0
- package/dist/core/provisioner.js +29 -11
- package/dist/core/resource.d.ts +8 -0
- package/dist/core/resource.js +45 -0
- package/dist/core/retry.d.ts +9 -0
- package/dist/core/retry.js +28 -0
- package/dist/core/retry.test.d.ts +1 -0
- package/dist/core/retry.test.js +66 -0
- package/dist/core/secret.d.ts +2 -1
- package/dist/core/secret.js +12 -2
- package/dist/core/stack.js +381 -75
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/providers/aws/api.js +97 -17
- package/dist/providers/aws/ec2.d.ts +3 -0
- package/dist/providers/aws/ec2.js +37 -3
- package/dist/providers/aws/ec2.test.js +5 -3
- package/dist/providers/aws/index.d.ts +2 -0
- package/dist/providers/aws/index.js +2 -0
- package/dist/providers/aws/secrets.js +20 -3
- package/dist/providers/aws/template.d.ts +34 -0
- package/dist/providers/aws/template.js +252 -0
- package/dist/providers/aws/template.test.d.ts +1 -0
- package/dist/providers/aws/template.test.js +208 -0
- package/dist/providers/do/api.d.ts +2 -0
- package/dist/providers/do/api.js +124 -26
- package/dist/providers/do/droplet.js +14 -0
- package/dist/providers/firebase/api.js +92 -29
- package/dist/providers/firebase/list.d.ts +2 -0
- package/dist/providers/firebase/list.js +25 -0
- package/dist/providers/gcp/api.js +88 -14
- package/dist/providers/gcp/index.d.ts +3 -1
- package/dist/providers/gcp/index.js +3 -1
- package/dist/providers/gcp/list.d.ts +2 -0
- package/dist/providers/gcp/list.js +55 -0
- package/dist/providers/gcp/secrets.js +21 -4
- package/dist/providers/gcp/template.d.ts +32 -0
- package/dist/providers/gcp/template.js +252 -0
- package/dist/providers/gcp/template.test.d.ts +1 -0
- package/dist/providers/gcp/template.test.js +227 -0
- package/dist/providers/gcp/vm.d.ts +3 -0
- package/dist/providers/gcp/vm.js +46 -3
- package/dist/providers/proxmox/api.d.ts +1 -0
- package/dist/providers/proxmox/api.js +72 -16
- package/dist/providers/proxmox/index.d.ts +3 -1
- package/dist/providers/proxmox/index.js +14 -1
- package/dist/providers/proxmox/template.d.ts +44 -0
- package/dist/providers/proxmox/template.js +350 -0
- package/dist/providers/proxmox/template.test.d.ts +1 -0
- package/dist/providers/proxmox/template.test.js +215 -0
- package/dist/providers/proxmox/vm.d.ts +3 -0
- package/dist/providers/proxmox/vm.js +43 -11
- package/dist/types/inventory.d.ts +44 -1
- package/package.json +2 -2
package/dist/core/checker.js
CHANGED
|
@@ -123,6 +123,53 @@ function renderAws(inv) {
|
|
|
123
123
|
]);
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
+
function renderGcp(inv) {
|
|
127
|
+
if (inv.vms.length > 0) {
|
|
128
|
+
printSection(`GCP Compute VMs · ${inv.vms.length}`, inv.vms, [
|
|
129
|
+
{ header: "Name", width: 24, render: (v) => v.name },
|
|
130
|
+
{ header: "Zone", width: 15, render: (v) => v.zone },
|
|
131
|
+
{ header: "Machine Type", width: 14, render: (v) => v.machineType },
|
|
132
|
+
{ header: "Status", width: 8, render: (v) => v.status },
|
|
133
|
+
{ header: "IP", width: 15, render: (v) => v.ip },
|
|
134
|
+
]);
|
|
135
|
+
}
|
|
136
|
+
if (inv.rdsInstances.length > 0) {
|
|
137
|
+
printSection(`GCP Cloud SQL · ${inv.rdsInstances.length}`, inv.rdsInstances, [
|
|
138
|
+
{ header: "Name", width: 24, render: (i) => i.name },
|
|
139
|
+
{ header: "Engine", width: 18, render: (i) => i.engine },
|
|
140
|
+
{ header: "Tier", width: 12, render: (i) => i.tier },
|
|
141
|
+
{ header: "Status", width: 10, render: (i) => i.status },
|
|
142
|
+
]);
|
|
143
|
+
}
|
|
144
|
+
if (inv.distributions.length > 0) {
|
|
145
|
+
printSection(`GCP Cloud Run · ${inv.distributions.length}`, inv.distributions, [
|
|
146
|
+
{ header: "Service", width: 24, render: (s) => s.name },
|
|
147
|
+
{ header: "Region", width: 12, render: (s) => s.region },
|
|
148
|
+
{ header: "URL", width: 42, render: (s) => s.url },
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
if (inv.hostedZones.length > 0) {
|
|
152
|
+
printSection(`GCP Cloud DNS · ${inv.hostedZones.length}`, inv.hostedZones, [
|
|
153
|
+
{ header: "Zone", width: 24, render: (z) => z.name },
|
|
154
|
+
{ header: "DNS Name", width: 32, render: (z) => z.dnsName },
|
|
155
|
+
]);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function renderFirebase(inv) {
|
|
159
|
+
if (inv.hostingSites.length > 0) {
|
|
160
|
+
printSection(`Firebase Hosting · ${inv.hostingSites.length}`, inv.hostingSites, [
|
|
161
|
+
{ header: "Site ID", width: 32, render: (s) => s.site },
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
if (inv.functions.length > 0) {
|
|
165
|
+
printSection(`Firebase Functions · ${inv.functions.length}`, inv.functions, [
|
|
166
|
+
{ header: "Function", width: 24, render: (f) => f.name },
|
|
167
|
+
{ header: "Region", width: 12, render: (f) => f.region },
|
|
168
|
+
{ header: "Entry Point", width: 18, render: (f) => f.entryPoint },
|
|
169
|
+
{ header: "Runtime", width: 10, render: (f) => f.runtime },
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
126
173
|
// ─── Checker ──────────────────────────────────────────────────────────────────
|
|
127
174
|
export class Checker {
|
|
128
175
|
async check() {
|
|
@@ -161,6 +208,26 @@ export class Checker {
|
|
|
161
208
|
errors.push({ provider: "aws", message: err.message });
|
|
162
209
|
}));
|
|
163
210
|
}
|
|
211
|
+
if (cfg.providers.gcp?.serviceAccountPath || process.env.GCP_SA) {
|
|
212
|
+
tasks.push(import("../providers/gcp/list.js")
|
|
213
|
+
.then((m) => m.listGcpResources())
|
|
214
|
+
.then((inv) => {
|
|
215
|
+
result.gcp = inv;
|
|
216
|
+
})
|
|
217
|
+
.catch((err) => {
|
|
218
|
+
errors.push({ provider: "gcp", message: err.message });
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
if (cfg.providers.firebase?.serviceAccountPath || process.env.FIREBASE_SA) {
|
|
222
|
+
tasks.push(import("../providers/firebase/list.js")
|
|
223
|
+
.then((m) => m.listFirebaseResources())
|
|
224
|
+
.then((inv) => {
|
|
225
|
+
result.firebase = inv;
|
|
226
|
+
})
|
|
227
|
+
.catch((err) => {
|
|
228
|
+
errors.push({ provider: "firebase", message: err.message });
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
164
231
|
await Promise.all(tasks);
|
|
165
232
|
if (result.proxmox)
|
|
166
233
|
renderProxmox(result.proxmox);
|
|
@@ -168,6 +235,10 @@ export class Checker {
|
|
|
168
235
|
renderDo(result.do);
|
|
169
236
|
if (result.aws)
|
|
170
237
|
renderAws(result.aws);
|
|
238
|
+
if (result.gcp)
|
|
239
|
+
renderGcp(result.gcp);
|
|
240
|
+
if (result.firebase)
|
|
241
|
+
renderFirebase(result.firebase);
|
|
171
242
|
for (const e of errors) {
|
|
172
243
|
console.warn(`\n [!] ${e.provider}: ${e.message}`);
|
|
173
244
|
}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
1
2
|
export interface GlobalConfig {
|
|
2
3
|
dryRun?: boolean;
|
|
4
|
+
parallel?: boolean;
|
|
5
|
+
offline?: boolean;
|
|
3
6
|
providers: {
|
|
4
7
|
do?: {
|
|
5
8
|
token: string;
|
|
@@ -37,6 +40,8 @@ declare class ConfigManager {
|
|
|
37
40
|
set(newConfig: Partial<GlobalConfig>): void;
|
|
38
41
|
get(): GlobalConfig;
|
|
39
42
|
isGlobalDryRun(): boolean;
|
|
43
|
+
isParallelActive(): boolean;
|
|
44
|
+
isOfflineMode(): boolean;
|
|
40
45
|
}
|
|
41
46
|
export declare const Config: ConfigManager;
|
|
42
47
|
export {};
|
package/dist/core/config.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
1
2
|
class ConfigManager {
|
|
2
3
|
config = {
|
|
3
4
|
providers: {},
|
|
4
5
|
};
|
|
5
6
|
set(newConfig) {
|
|
6
|
-
this.config = {
|
|
7
|
+
this.config = {
|
|
8
|
+
...this.config,
|
|
9
|
+
...newConfig,
|
|
10
|
+
providers: { ...this.config.providers, ...newConfig.providers },
|
|
11
|
+
};
|
|
7
12
|
}
|
|
8
13
|
get() {
|
|
9
14
|
return this.config;
|
|
@@ -11,5 +16,11 @@ class ConfigManager {
|
|
|
11
16
|
isGlobalDryRun() {
|
|
12
17
|
return this.config.dryRun ?? false;
|
|
13
18
|
}
|
|
19
|
+
isParallelActive() {
|
|
20
|
+
return this.config.parallel ?? false;
|
|
21
|
+
}
|
|
22
|
+
isOfflineMode() {
|
|
23
|
+
return this.config.offline ?? false;
|
|
24
|
+
}
|
|
14
25
|
}
|
|
15
26
|
export const Config = new ConfigManager();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
export interface HostEntry {
|
|
3
|
+
name: string;
|
|
4
|
+
ip: string;
|
|
5
|
+
user: string;
|
|
6
|
+
sshKey?: string;
|
|
7
|
+
provider: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ResourceContext {
|
|
10
|
+
abortSignal?: AbortSignal;
|
|
11
|
+
hosts?: HostEntry[];
|
|
12
|
+
stackName?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const resourceContextStorage: AsyncLocalStorage<ResourceContext>;
|
package/dist/core/decorators.js
CHANGED
|
@@ -5,27 +5,21 @@ import { Stack } from "./stack.js";
|
|
|
5
5
|
function applyConfig(opts) {
|
|
6
6
|
if (opts.dryRun !== undefined)
|
|
7
7
|
Config.set({ dryRun: opts.dryRun });
|
|
8
|
+
if (opts.parallel !== undefined)
|
|
9
|
+
Config.set({ parallel: opts.parallel });
|
|
10
|
+
if (opts.offline !== undefined)
|
|
11
|
+
Config.set({ offline: opts.offline });
|
|
8
12
|
if (opts.token)
|
|
9
|
-
Config.set({
|
|
10
|
-
providers: { ...Config.get().providers, do: { token: opts.token } },
|
|
11
|
-
});
|
|
13
|
+
Config.set({ providers: { do: { token: opts.token } } });
|
|
12
14
|
if (opts.region)
|
|
13
|
-
Config.set({
|
|
14
|
-
providers: { ...Config.get().providers, aws: { region: opts.region } },
|
|
15
|
-
});
|
|
15
|
+
Config.set({ providers: { aws: { region: opts.region } } });
|
|
16
16
|
if (opts.proxmox)
|
|
17
|
-
Config.set({
|
|
18
|
-
providers: { ...Config.get().providers, proxmox: opts.proxmox },
|
|
19
|
-
});
|
|
17
|
+
Config.set({ providers: { proxmox: opts.proxmox } });
|
|
20
18
|
if (opts.firebase) {
|
|
21
19
|
const sa = JSON.parse(readFileSync(opts.firebase, "utf8"));
|
|
22
20
|
Config.set({
|
|
23
21
|
providers: {
|
|
24
|
-
|
|
25
|
-
firebase: {
|
|
26
|
-
projectId: sa.project_id,
|
|
27
|
-
serviceAccountPath: opts.firebase,
|
|
28
|
-
},
|
|
22
|
+
firebase: { projectId: sa.project_id, serviceAccountPath: opts.firebase },
|
|
29
23
|
},
|
|
30
24
|
});
|
|
31
25
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { test, describe } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { BaseBuilder, createBuilderArray } from "./resource.js";
|
|
4
|
+
import { Stack } from "./stack.js";
|
|
5
|
+
import { Config } from "./config.js";
|
|
6
|
+
class MockResourceBuilder extends BaseBuilder {
|
|
7
|
+
val = 0;
|
|
8
|
+
strVal = "";
|
|
9
|
+
deployCount = 0;
|
|
10
|
+
destroyCount = 0;
|
|
11
|
+
constructor(name) {
|
|
12
|
+
super(name);
|
|
13
|
+
}
|
|
14
|
+
setVal(n) {
|
|
15
|
+
// Overloaded to support array assignment in proxy test
|
|
16
|
+
if (typeof n === "number") {
|
|
17
|
+
this.val = n;
|
|
18
|
+
}
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
setStr(s) {
|
|
22
|
+
if (typeof s === "string") {
|
|
23
|
+
this.strVal = s;
|
|
24
|
+
}
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
async deploy() {
|
|
28
|
+
this.deployCount++;
|
|
29
|
+
return { name: this.name, val: this.val, strVal: this.strVal };
|
|
30
|
+
}
|
|
31
|
+
async destroy() {
|
|
32
|
+
this.destroyCount++;
|
|
33
|
+
return { destroyedName: this.name };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
describe("Resource Builder Groups Unit Tests", () => {
|
|
37
|
+
test("createBuilderArray proxies and chains methods", () => {
|
|
38
|
+
const b1 = new MockResourceBuilder("r1");
|
|
39
|
+
const b2 = new MockResourceBuilder("r2");
|
|
40
|
+
const group = createBuilderArray([b1, b2]);
|
|
41
|
+
// Test method chaining and uniform distribution
|
|
42
|
+
group.setVal(42).setStr("hello");
|
|
43
|
+
assert.strictEqual(b1.val, 42);
|
|
44
|
+
assert.strictEqual(b2.val, 42);
|
|
45
|
+
assert.strictEqual(b1.strVal, "hello");
|
|
46
|
+
assert.strictEqual(b2.strVal, "hello");
|
|
47
|
+
});
|
|
48
|
+
test("createBuilderArray distributes array arguments by index", () => {
|
|
49
|
+
const b1 = new MockResourceBuilder("r1");
|
|
50
|
+
const b2 = new MockResourceBuilder("r2");
|
|
51
|
+
const group = createBuilderArray([b1, b2]);
|
|
52
|
+
// Test index-based distribution
|
|
53
|
+
group.setVal([10, 20]).setStr(["first", "second"]);
|
|
54
|
+
// In MockResourceBuilder, calling setVal with [10, 20] gets distributed by proxy:
|
|
55
|
+
// b1 gets setVal(10) -> val = 10
|
|
56
|
+
// b2 gets setVal(20) -> val = 20
|
|
57
|
+
assert.strictEqual(b1.val, 10);
|
|
58
|
+
assert.strictEqual(b2.val, 20);
|
|
59
|
+
assert.strictEqual(b1.strVal, "first");
|
|
60
|
+
assert.strictEqual(b2.strVal, "second");
|
|
61
|
+
});
|
|
62
|
+
test("Stack deploy gathers array builders and structures outputs as array", async () => {
|
|
63
|
+
Config.set({ dryRun: false });
|
|
64
|
+
const b1 = new MockResourceBuilder("s1");
|
|
65
|
+
const b2 = new MockResourceBuilder("s2");
|
|
66
|
+
class ArrayStack extends Stack {
|
|
67
|
+
servers = createBuilderArray([b1, b2]).setVal([100, 200]);
|
|
68
|
+
}
|
|
69
|
+
const stack = new ArrayStack();
|
|
70
|
+
const result = await stack.deploy();
|
|
71
|
+
assert.strictEqual(b1.deployCount, 1);
|
|
72
|
+
assert.strictEqual(b2.deployCount, 1);
|
|
73
|
+
assert.ok(Array.isArray(result.servers));
|
|
74
|
+
assert.strictEqual(result.servers.length, 2);
|
|
75
|
+
assert.deepStrictEqual(result.servers[0], { name: "s1", val: 100, strVal: "" });
|
|
76
|
+
assert.deepStrictEqual(result.servers[1], { name: "s2", val: 200, strVal: "" });
|
|
77
|
+
});
|
|
78
|
+
test("Stack destroy gathers array builders and structures outputs as array", async () => {
|
|
79
|
+
Config.set({ dryRun: false });
|
|
80
|
+
const b1 = new MockResourceBuilder("s1");
|
|
81
|
+
const b2 = new MockResourceBuilder("s2");
|
|
82
|
+
class ArrayStack extends Stack {
|
|
83
|
+
servers = createBuilderArray([b1, b2]);
|
|
84
|
+
}
|
|
85
|
+
const stack = new ArrayStack();
|
|
86
|
+
const result = await stack.destroy();
|
|
87
|
+
assert.strictEqual(b1.destroyCount, 1);
|
|
88
|
+
assert.strictEqual(b2.destroyCount, 1);
|
|
89
|
+
assert.ok(Array.isArray(result.servers));
|
|
90
|
+
assert.strictEqual(result.servers.length, 2);
|
|
91
|
+
assert.deepStrictEqual(result.servers[0], { destroyedName: "s1" });
|
|
92
|
+
assert.deepStrictEqual(result.servers[1], { destroyedName: "s2" });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { test, describe, beforeEach } from "node:test";
|
|
8
|
+
import assert from "node:assert";
|
|
9
|
+
import { Stack } from "./stack.js";
|
|
10
|
+
import { Deploy } from "./decorators.js";
|
|
11
|
+
import { BaseBuilder } from "./resource.js";
|
|
12
|
+
import { Config } from "./config.js";
|
|
13
|
+
import { Output } from "./output.js";
|
|
14
|
+
// Helper delay
|
|
15
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
class DelayResource extends BaseBuilder {
|
|
17
|
+
delayMs;
|
|
18
|
+
executionLogs;
|
|
19
|
+
out = {
|
|
20
|
+
val: new Output(),
|
|
21
|
+
};
|
|
22
|
+
constructor(name, delayMs, executionLogs) {
|
|
23
|
+
super(name);
|
|
24
|
+
this.delayMs = delayMs;
|
|
25
|
+
this.executionLogs = executionLogs;
|
|
26
|
+
}
|
|
27
|
+
async deploy() {
|
|
28
|
+
this.executionLogs.push(`start:${this.name}`);
|
|
29
|
+
await delay(this.delayMs);
|
|
30
|
+
this.executionLogs.push(`end:${this.name}`);
|
|
31
|
+
this.out.val.resolve(this.name);
|
|
32
|
+
return { name: this.name };
|
|
33
|
+
}
|
|
34
|
+
async destroy() {
|
|
35
|
+
this.executionLogs.push(`destroy:${this.name}`);
|
|
36
|
+
await delay(this.delayMs);
|
|
37
|
+
return { name: this.name, destroyed: true };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
class FailResource extends BaseBuilder {
|
|
41
|
+
async deploy() {
|
|
42
|
+
throw new Error("Failed to deploy!");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
describe("Parallel Resource Deployment Unit Tests", () => {
|
|
46
|
+
let logs = [];
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
Config.set({
|
|
49
|
+
dryRun: false,
|
|
50
|
+
parallel: false,
|
|
51
|
+
providers: {},
|
|
52
|
+
});
|
|
53
|
+
logs = [];
|
|
54
|
+
});
|
|
55
|
+
test("runs sequential deployments by default (verifies base case)", async () => {
|
|
56
|
+
class SeqStack extends Stack {
|
|
57
|
+
r1 = new DelayResource("r1", 30, logs);
|
|
58
|
+
r2 = new DelayResource("r2", 30, logs);
|
|
59
|
+
}
|
|
60
|
+
const start = Date.now();
|
|
61
|
+
const stack = new SeqStack();
|
|
62
|
+
await stack.deploy();
|
|
63
|
+
const duration = Date.now() - start;
|
|
64
|
+
// In sequential, execution is r1 start -> r1 end -> r2 start -> r2 end
|
|
65
|
+
assert.deepStrictEqual(logs, [
|
|
66
|
+
"start:r1",
|
|
67
|
+
"end:r1",
|
|
68
|
+
"start:r2",
|
|
69
|
+
"end:r2"
|
|
70
|
+
]);
|
|
71
|
+
assert.ok(duration >= 60, `Sequential should take at least 60ms, took ${duration}ms`);
|
|
72
|
+
});
|
|
73
|
+
test("runs parallel deployments concurrently when opted in", async () => {
|
|
74
|
+
// Enable parallel globally for the stack run
|
|
75
|
+
Config.set({
|
|
76
|
+
dryRun: false,
|
|
77
|
+
parallel: true,
|
|
78
|
+
providers: {},
|
|
79
|
+
});
|
|
80
|
+
class ParStack extends Stack {
|
|
81
|
+
r1 = new DelayResource("r1", 40, logs);
|
|
82
|
+
r2 = new DelayResource("r2", 40, logs);
|
|
83
|
+
}
|
|
84
|
+
const start = Date.now();
|
|
85
|
+
const stack = new ParStack();
|
|
86
|
+
await stack.deploy();
|
|
87
|
+
const duration = Date.now() - start;
|
|
88
|
+
// In parallel, both start before either finishes:
|
|
89
|
+
// start:r1 and start:r2 will both be printed first.
|
|
90
|
+
assert.strictEqual(logs[0].startsWith("start:"), true);
|
|
91
|
+
assert.strictEqual(logs[1].startsWith("start:"), true);
|
|
92
|
+
assert.ok(logs.includes("end:r1"));
|
|
93
|
+
assert.ok(logs.includes("end:r2"));
|
|
94
|
+
// Total duration should be closer to 40ms than 80ms
|
|
95
|
+
assert.ok(duration < 75, `Parallel should take less than 75ms (sum), took ${duration}ms`);
|
|
96
|
+
});
|
|
97
|
+
test("respects explicit dependsOn() ordering in parallel mode", async () => {
|
|
98
|
+
Config.set({
|
|
99
|
+
dryRun: false,
|
|
100
|
+
parallel: true,
|
|
101
|
+
providers: {},
|
|
102
|
+
});
|
|
103
|
+
class DependencyStack extends Stack {
|
|
104
|
+
// r1 depends on r2
|
|
105
|
+
r1 = new DelayResource("r1", 20, logs);
|
|
106
|
+
r2 = new DelayResource("r2", 20, logs);
|
|
107
|
+
constructor() {
|
|
108
|
+
super();
|
|
109
|
+
this.r1.dependsOn(this.r2);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const stack = new DependencyStack();
|
|
113
|
+
await stack.deploy();
|
|
114
|
+
// Since r1 depends on r2, r2 must fully complete before r1 starts
|
|
115
|
+
const r2StartIndex = logs.indexOf("start:r2");
|
|
116
|
+
const r2EndIndex = logs.indexOf("end:r2");
|
|
117
|
+
const r1StartIndex = logs.indexOf("start:r1");
|
|
118
|
+
assert.ok(r2StartIndex < r2EndIndex);
|
|
119
|
+
assert.ok(r2EndIndex < r1StartIndex);
|
|
120
|
+
});
|
|
121
|
+
test("respects implicit Output waiting in parallel mode", async () => {
|
|
122
|
+
Config.set({
|
|
123
|
+
dryRun: false,
|
|
124
|
+
parallel: true,
|
|
125
|
+
providers: {},
|
|
126
|
+
});
|
|
127
|
+
class OutputAwaitingResource extends BaseBuilder {
|
|
128
|
+
dependentVal;
|
|
129
|
+
executionLogs;
|
|
130
|
+
constructor(name, dependentVal, executionLogs) {
|
|
131
|
+
super(name);
|
|
132
|
+
this.dependentVal = dependentVal;
|
|
133
|
+
this.executionLogs = executionLogs;
|
|
134
|
+
}
|
|
135
|
+
async deploy() {
|
|
136
|
+
this.executionLogs.push(`start:${this.name}`);
|
|
137
|
+
// Blocks on the Output of the other resource!
|
|
138
|
+
const val = await this.dependentVal.get();
|
|
139
|
+
this.executionLogs.push(`end:${this.name}:${val}`);
|
|
140
|
+
return { name: this.name };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
class OutputStack extends Stack {
|
|
144
|
+
r1 = new DelayResource("r1", 30, logs);
|
|
145
|
+
// r2 depends implicitly on r1's output
|
|
146
|
+
r2 = new OutputAwaitingResource("r2", this.r1.out.val, logs);
|
|
147
|
+
}
|
|
148
|
+
const stack = new OutputStack();
|
|
149
|
+
await stack.deploy();
|
|
150
|
+
// r2 starts in parallel, but it cannot end until r1 finishes and resolves the output
|
|
151
|
+
const r2StartIndex = logs.indexOf("start:r2");
|
|
152
|
+
const r1EndIndex = logs.indexOf("end:r1");
|
|
153
|
+
const r2EndIndex = logs.findIndex(line => line.startsWith("end:r2"));
|
|
154
|
+
assert.ok(r2StartIndex >= 0);
|
|
155
|
+
assert.ok(r1EndIndex >= 0);
|
|
156
|
+
assert.ok(r1EndIndex < r2EndIndex, `r1 end (${r1EndIndex}) must be before r2 end (${r2EndIndex})`);
|
|
157
|
+
assert.ok(logs.includes("end:r2:r1"));
|
|
158
|
+
});
|
|
159
|
+
test("runs parallel teardowns in reverse topological order on @Destroy", async () => {
|
|
160
|
+
Config.set({
|
|
161
|
+
dryRun: false,
|
|
162
|
+
parallel: true,
|
|
163
|
+
providers: {},
|
|
164
|
+
});
|
|
165
|
+
class TeardownStack extends Stack {
|
|
166
|
+
r1 = new DelayResource("r1", 20, logs);
|
|
167
|
+
r2 = new DelayResource("r2", 20, logs);
|
|
168
|
+
constructor() {
|
|
169
|
+
super();
|
|
170
|
+
// r2 depends on r1 during deploy (so r1 deploys first).
|
|
171
|
+
// Teardown should destroy r2 first, then r1!
|
|
172
|
+
this.r2.dependsOn(this.r1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const stack = new TeardownStack();
|
|
176
|
+
await stack.destroy();
|
|
177
|
+
const r2DestroyIndex = logs.indexOf("destroy:r2");
|
|
178
|
+
const r1DestroyIndex = logs.indexOf("destroy:r1");
|
|
179
|
+
// r2 depends on r1, so r2 must be fully destroyed BEFORE r1 begins destruction
|
|
180
|
+
assert.ok(r2DestroyIndex < r1DestroyIndex, `r2 destroy (${r2DestroyIndex}) must be before r1 destroy (${r1DestroyIndex})`);
|
|
181
|
+
});
|
|
182
|
+
test("halts execution of dependent resources if a dependency fails", async () => {
|
|
183
|
+
Config.set({
|
|
184
|
+
dryRun: false,
|
|
185
|
+
parallel: true,
|
|
186
|
+
providers: {},
|
|
187
|
+
});
|
|
188
|
+
class FailStack extends Stack {
|
|
189
|
+
r1 = new FailResource("r1");
|
|
190
|
+
r2 = new DelayResource("r2", 30, logs);
|
|
191
|
+
constructor() {
|
|
192
|
+
super();
|
|
193
|
+
this.r2.dependsOn(this.r1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const stack = new FailStack();
|
|
197
|
+
await assert.rejects(async () => {
|
|
198
|
+
await stack.deploy();
|
|
199
|
+
}, /Failed to deploy!/);
|
|
200
|
+
// r2 should never have started because its dependency r1 failed!
|
|
201
|
+
assert.strictEqual(logs.includes("start:r2"), false);
|
|
202
|
+
});
|
|
203
|
+
test("decorator option propagation sets configuration values", async () => {
|
|
204
|
+
// Clear parallel flag
|
|
205
|
+
Config.set({ parallel: false });
|
|
206
|
+
// We define a decorated simple stack
|
|
207
|
+
let SimpleDecoStack = class SimpleDecoStack extends Stack {
|
|
208
|
+
};
|
|
209
|
+
SimpleDecoStack = __decorate([
|
|
210
|
+
Deploy({ parallel: true })
|
|
211
|
+
], SimpleDecoStack);
|
|
212
|
+
// Verify decorator correctly updated global configuration to true
|
|
213
|
+
assert.strictEqual(Config.isParallelActive(), true);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|