puls-dev 0.2.7 → 0.2.9
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 +6 -0
- package/dist/core/config.js +11 -1
- package/dist/core/context.d.ts +14 -0
- package/dist/core/context.js +2 -0
- package/dist/core/decorators.d.ts +4 -0
- package/dist/core/decorators.js +56 -30
- package/dist/core/hooks.d.ts +21 -0
- package/dist/core/hooks.js +116 -0
- package/dist/core/hooks.test.d.ts +1 -0
- package/dist/core/hooks.test.js +194 -0
- package/dist/core/multiregion.test.d.ts +1 -0
- package/dist/core/multiregion.test.js +87 -0
- package/dist/core/output.d.ts +2 -0
- package/dist/core/output.js +9 -2
- package/dist/core/parallel.test.d.ts +1 -0
- package/dist/core/parallel.test.js +215 -0
- package/dist/core/parser.d.ts +10 -0
- package/dist/core/parser.js +140 -0
- package/dist/core/parser.test.d.ts +1 -0
- package/dist/core/parser.test.js +117 -0
- package/dist/core/production.test.d.ts +1 -0
- package/dist/core/production.test.js +189 -0
- package/dist/core/provisioner.d.ts +4 -0
- package/dist/core/provisioner.js +123 -0
- package/dist/core/resource.d.ts +23 -0
- package/dist/core/resource.js +54 -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 +41 -0
- package/dist/core/secret.js +105 -0
- package/dist/core/secret.test.d.ts +1 -0
- package/dist/core/secret.test.js +166 -0
- package/dist/core/stack.d.ts +4 -3
- package/dist/core/stack.js +322 -48
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/providers/aws/api.js +97 -17
- package/dist/providers/aws/ec2.d.ts +51 -0
- package/dist/providers/aws/ec2.js +331 -0
- package/dist/providers/aws/ec2.test.d.ts +1 -0
- package/dist/providers/aws/ec2.test.js +281 -0
- package/dist/providers/aws/index.d.ts +4 -0
- package/dist/providers/aws/index.js +4 -0
- package/dist/providers/aws/route53.d.ts +1 -0
- package/dist/providers/aws/route53.js +15 -2
- package/dist/providers/aws/route53.test.js +47 -0
- 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 +3 -1
- package/dist/providers/do/api.js +126 -27
- package/dist/providers/do/app.d.ts +26 -0
- package/dist/providers/do/app.js +124 -0
- package/dist/providers/do/app.test.d.ts +1 -0
- package/dist/providers/do/app.test.js +268 -0
- package/dist/providers/do/database.d.ts +44 -0
- package/dist/providers/do/database.js +208 -0
- package/dist/providers/do/database.test.d.ts +1 -0
- package/dist/providers/do/database.test.js +293 -0
- package/dist/providers/do/domain.d.ts +2 -0
- package/dist/providers/do/domain.js +30 -0
- package/dist/providers/do/domain.test.js +49 -0
- package/dist/providers/do/droplet.d.ts +9 -0
- package/dist/providers/do/droplet.js +146 -8
- package/dist/providers/do/droplet.test.js +228 -1
- package/dist/providers/do/firewall.d.ts +2 -1
- package/dist/providers/do/firewall.js +23 -9
- package/dist/providers/do/firewall.test.js +54 -0
- package/dist/providers/do/index.d.ts +11 -0
- package/dist/providers/do/index.js +8 -0
- package/dist/providers/do/spaces.d.ts +27 -0
- package/dist/providers/do/spaces.js +142 -0
- package/dist/providers/do/spaces.test.d.ts +1 -0
- package/dist/providers/do/spaces.test.js +180 -0
- package/dist/providers/do/spaces_api.d.ts +2 -0
- package/dist/providers/do/spaces_api.js +20 -0
- package/dist/providers/do/vpc.d.ts +30 -0
- package/dist/providers/do/vpc.js +128 -0
- package/dist/providers/do/vpc.test.d.ts +1 -0
- package/dist/providers/do/vpc.test.js +258 -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/clouddns.d.ts +1 -0
- package/dist/providers/gcp/clouddns.js +15 -2
- package/dist/providers/gcp/clouddns.test.js +45 -0
- package/dist/providers/gcp/index.d.ts +5 -1
- package/dist/providers/gcp/index.js +5 -1
- package/dist/providers/gcp/list.d.ts +2 -0
- package/dist/providers/gcp/list.js +55 -0
- package/dist/providers/gcp/secrets.js +1 -1
- 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 +48 -0
- package/dist/providers/gcp/vm.js +375 -0
- package/dist/providers/gcp/vm.test.d.ts +1 -0
- package/dist/providers/gcp/vm.test.js +321 -0
- package/dist/providers/proxmox/api.d.ts +1 -0
- package/dist/providers/proxmox/api.js +72 -16
- package/dist/providers/proxmox/index.d.ts +2 -0
- package/dist/providers/proxmox/index.js +2 -0
- package/dist/providers/proxmox/template.d.ts +44 -0
- package/dist/providers/proxmox/template.js +349 -0
- package/dist/providers/proxmox/template.test.d.ts +1 -0
- package/dist/providers/proxmox/template.test.js +179 -0
- package/dist/providers/proxmox/vm.d.ts +7 -4
- package/dist/providers/proxmox/vm.js +57 -102
- package/dist/providers/proxmox/vm.test.js +77 -0
- package/dist/types/inventory.d.ts +44 -1
- package/package.json +3 -1
|
@@ -0,0 +1,87 @@
|
|
|
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, Destroy } from "./decorators.js";
|
|
11
|
+
import { BaseBuilder } from "./resource.js";
|
|
12
|
+
import { Config } from "./config.js";
|
|
13
|
+
import { Output } from "./output.js";
|
|
14
|
+
class RegionResource extends BaseBuilder {
|
|
15
|
+
out = {
|
|
16
|
+
region: new Output(),
|
|
17
|
+
};
|
|
18
|
+
async deploy() {
|
|
19
|
+
const activeRegion = Config.get().providers.aws?.region ?? "unknown";
|
|
20
|
+
this.out.region.resolve(activeRegion);
|
|
21
|
+
return { name: this.name, region: activeRegion };
|
|
22
|
+
}
|
|
23
|
+
async destroy() {
|
|
24
|
+
const activeRegion = Config.get().providers.aws?.region ?? "unknown";
|
|
25
|
+
return { name: this.name, destroyed: true, region: activeRegion };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
describe("Multi-Region Deployments Unit Tests", () => {
|
|
29
|
+
let executionLogs = [];
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
Config.set({
|
|
32
|
+
dryRun: false,
|
|
33
|
+
providers: {},
|
|
34
|
+
});
|
|
35
|
+
executionLogs = [];
|
|
36
|
+
});
|
|
37
|
+
test("runs sequential deployments across multiple regions and stores instances in registry", async () => {
|
|
38
|
+
let MultiStack = class MultiStack extends Stack {
|
|
39
|
+
res = new RegionResource("my-region-res").afterDeploy((result) => {
|
|
40
|
+
executionLogs.push(`deploy:${result.region}`);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
MultiStack = __decorate([
|
|
44
|
+
Deploy({
|
|
45
|
+
regions: ["us-east-1", "eu-central-1", "ap-northeast-1"],
|
|
46
|
+
dryRun: false,
|
|
47
|
+
})
|
|
48
|
+
], MultiStack);
|
|
49
|
+
// Wait for the asynchronous macro/microtask queue to resolve Deploy runs
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
51
|
+
// Verify sequential region changes occurred in correct order
|
|
52
|
+
assert.deepStrictEqual(executionLogs, [
|
|
53
|
+
"deploy:us-east-1",
|
|
54
|
+
"deploy:eu-central-1",
|
|
55
|
+
"deploy:ap-northeast-1",
|
|
56
|
+
]);
|
|
57
|
+
// Retrieve specific region stack outputs via Stack.from(cls, region)
|
|
58
|
+
const usStack = Stack.from(MultiStack, "us-east-1");
|
|
59
|
+
const euStack = Stack.from(MultiStack, "eu-central-1");
|
|
60
|
+
const apStack = Stack.from(MultiStack, "ap-northeast-1");
|
|
61
|
+
assert.ok(usStack);
|
|
62
|
+
assert.ok(euStack);
|
|
63
|
+
assert.ok(apStack);
|
|
64
|
+
assert.strictEqual(await usStack.res.out.region.get(), "us-east-1");
|
|
65
|
+
assert.strictEqual(await euStack.res.out.region.get(), "eu-central-1");
|
|
66
|
+
assert.strictEqual(await apStack.res.out.region.get(), "ap-northeast-1");
|
|
67
|
+
});
|
|
68
|
+
test("runs sequential teardowns across multiple regions on @Destroy", async () => {
|
|
69
|
+
let CleanStack = class CleanStack extends Stack {
|
|
70
|
+
res = new RegionResource("my-teardown-res").afterDestroy((result) => {
|
|
71
|
+
executionLogs.push(`destroy:${result.region}`);
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
CleanStack = __decorate([
|
|
75
|
+
Destroy({
|
|
76
|
+
regions: ["us-east-1", "eu-central-1"],
|
|
77
|
+
dryRun: false,
|
|
78
|
+
})
|
|
79
|
+
], CleanStack);
|
|
80
|
+
// Wait for microtask queue to run Destroy
|
|
81
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
82
|
+
assert.deepStrictEqual(executionLogs, [
|
|
83
|
+
"destroy:us-east-1",
|
|
84
|
+
"destroy:eu-central-1",
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
});
|
package/dist/core/output.d.ts
CHANGED
package/dist/core/output.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
export class Output {
|
|
2
2
|
_promise;
|
|
3
3
|
_resolve;
|
|
4
|
+
_reject;
|
|
4
5
|
constructor() {
|
|
5
|
-
this._promise = new Promise(resolve =>
|
|
6
|
+
this._promise = new Promise((resolve, reject) => {
|
|
7
|
+
this._resolve = resolve;
|
|
8
|
+
this._reject = reject;
|
|
9
|
+
});
|
|
6
10
|
}
|
|
7
11
|
resolve(value) {
|
|
8
12
|
this._resolve(value);
|
|
9
13
|
}
|
|
14
|
+
reject(reason) {
|
|
15
|
+
this._reject(reason);
|
|
16
|
+
}
|
|
10
17
|
get() {
|
|
11
18
|
return this._promise;
|
|
12
19
|
}
|
|
13
20
|
// Transform this output into a new Output<U> without awaiting it yourself.
|
|
14
21
|
apply(fn) {
|
|
15
22
|
const out = new Output();
|
|
16
|
-
this._promise.then(v => out.resolve(fn(v)));
|
|
23
|
+
this._promise.then(v => out.resolve(fn(v)), err => out.reject(err));
|
|
17
24
|
return out;
|
|
18
25
|
}
|
|
19
26
|
}
|
|
@@ -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,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Robust, zero-dependency, indentation-aware YAML parser.
|
|
3
|
+
* Parses sequences of key-value maps and nested string arrays.
|
|
4
|
+
*/
|
|
5
|
+
export declare function parseYaml(content: string): any[];
|
|
6
|
+
/**
|
|
7
|
+
* Resolves a file path relative to the current working directory,
|
|
8
|
+
* reads its content, and parses it according to its extension (.json vs .yaml/.yml).
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadRecordsFromFile(filePath: string): any[];
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Strips comments from a YAML/JSON line while preserving comment hashes inside quotes.
|
|
5
|
+
*/
|
|
6
|
+
function stripComments(line) {
|
|
7
|
+
const hashIdx = line.indexOf("#");
|
|
8
|
+
if (hashIdx < 0)
|
|
9
|
+
return line;
|
|
10
|
+
let inQuotes = false;
|
|
11
|
+
let quoteChar = "";
|
|
12
|
+
for (let i = 0; i < hashIdx; i++) {
|
|
13
|
+
const char = line[i];
|
|
14
|
+
if (char === '"' || char === "'") {
|
|
15
|
+
if (!inQuotes) {
|
|
16
|
+
inQuotes = true;
|
|
17
|
+
quoteChar = char;
|
|
18
|
+
}
|
|
19
|
+
else if (char === quoteChar) {
|
|
20
|
+
inQuotes = false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (inQuotes) {
|
|
25
|
+
return line;
|
|
26
|
+
}
|
|
27
|
+
return line.slice(0, hashIdx);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Robust, zero-dependency, indentation-aware YAML parser.
|
|
31
|
+
* Parses sequences of key-value maps and nested string arrays.
|
|
32
|
+
*/
|
|
33
|
+
export function parseYaml(content) {
|
|
34
|
+
const list = [];
|
|
35
|
+
const lines = content.split(/\r?\n/);
|
|
36
|
+
let currentItem = null;
|
|
37
|
+
let activeKey = null;
|
|
38
|
+
let rootIndent = 0;
|
|
39
|
+
for (let line of lines) {
|
|
40
|
+
line = stripComments(line);
|
|
41
|
+
if (!line.trim())
|
|
42
|
+
continue;
|
|
43
|
+
const leadingSpaces = line.length - line.trimStart().length;
|
|
44
|
+
const trimmed = line.trim();
|
|
45
|
+
// Check if it's a bullet item list entry
|
|
46
|
+
if (trimmed.startsWith("-")) {
|
|
47
|
+
const rest = trimmed.slice(1).trim();
|
|
48
|
+
// If it is indented more than the root item and does not match a key-value pattern,
|
|
49
|
+
// treat it as an array item under the active key.
|
|
50
|
+
const isRestKeyValue = /^[a-zA-Z0-9_-]+\s*:/.test(rest);
|
|
51
|
+
if (leadingSpaces > rootIndent && !isRestKeyValue) {
|
|
52
|
+
if (currentItem && activeKey) {
|
|
53
|
+
if (!Array.isArray(currentItem[activeKey])) {
|
|
54
|
+
currentItem[activeKey] = [];
|
|
55
|
+
}
|
|
56
|
+
let val = rest;
|
|
57
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
58
|
+
val = val.slice(1, -1);
|
|
59
|
+
}
|
|
60
|
+
currentItem[activeKey].push(val);
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Otherwise, it represents a new item at the root list level
|
|
65
|
+
if (currentItem) {
|
|
66
|
+
list.push(currentItem);
|
|
67
|
+
}
|
|
68
|
+
currentItem = {};
|
|
69
|
+
rootIndent = leadingSpaces;
|
|
70
|
+
activeKey = null;
|
|
71
|
+
if (rest) {
|
|
72
|
+
const colonIdx = rest.indexOf(":");
|
|
73
|
+
if (colonIdx > 0 && isRestKeyValue) {
|
|
74
|
+
const key = rest.slice(0, colonIdx).trim();
|
|
75
|
+
let value = rest.slice(colonIdx + 1).trim();
|
|
76
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
77
|
+
value = value.slice(1, -1);
|
|
78
|
+
}
|
|
79
|
+
// Convert to number if numeric
|
|
80
|
+
if (/^\d+$/.test(value)) {
|
|
81
|
+
currentItem[key] = parseInt(value, 10);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
currentItem[key] = value;
|
|
85
|
+
}
|
|
86
|
+
activeKey = key;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Key-value pair or list start
|
|
92
|
+
const isKeyValue = /^[a-zA-Z0-9_-]+\s*:/.test(trimmed);
|
|
93
|
+
if (isKeyValue && currentItem) {
|
|
94
|
+
const colonIdx = trimmed.indexOf(":");
|
|
95
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
96
|
+
let value = trimmed.slice(colonIdx + 1).trim();
|
|
97
|
+
if (value === "") {
|
|
98
|
+
// Starts a nested list/array
|
|
99
|
+
currentItem[key] = [];
|
|
100
|
+
activeKey = key;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
104
|
+
value = value.slice(1, -1);
|
|
105
|
+
}
|
|
106
|
+
if (/^\d+$/.test(value)) {
|
|
107
|
+
currentItem[key] = parseInt(value, 10);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
currentItem[key] = value;
|
|
111
|
+
}
|
|
112
|
+
activeKey = key;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (currentItem) {
|
|
118
|
+
list.push(currentItem);
|
|
119
|
+
}
|
|
120
|
+
return list;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolves a file path relative to the current working directory,
|
|
124
|
+
* reads its content, and parses it according to its extension (.json vs .yaml/.yml).
|
|
125
|
+
*/
|
|
126
|
+
export function loadRecordsFromFile(filePath) {
|
|
127
|
+
const absolutePath = path.resolve(process.cwd(), filePath);
|
|
128
|
+
const fileContent = fs.readFileSync(absolutePath, "utf-8");
|
|
129
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
130
|
+
if (ext === ".json") {
|
|
131
|
+
const parsed = JSON.parse(fileContent);
|
|
132
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
133
|
+
}
|
|
134
|
+
else if (ext === ".yaml" || ext === ".yml") {
|
|
135
|
+
return parseYaml(fileContent);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
throw new Error(`Unsupported configuration file format: ${filePath}. Only JSON and YAML/YML files are supported.`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { test, describe } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { parseYaml, loadRecordsFromFile } from "./parser.js";
|
|
6
|
+
describe("YAML & JSON Config Parser", () => {
|
|
7
|
+
test("parses standard simple key-value YAML blocks correctly", () => {
|
|
8
|
+
const yaml = `
|
|
9
|
+
- name: www
|
|
10
|
+
type: CNAME
|
|
11
|
+
value: lb.google.com
|
|
12
|
+
- name: mail
|
|
13
|
+
type: A
|
|
14
|
+
value: 1.2.3.4
|
|
15
|
+
ttl: 600
|
|
16
|
+
`;
|
|
17
|
+
const result = parseYaml(yaml);
|
|
18
|
+
assert.strictEqual(result.length, 2);
|
|
19
|
+
assert.deepStrictEqual(result[0], {
|
|
20
|
+
name: "www",
|
|
21
|
+
type: "CNAME",
|
|
22
|
+
value: "lb.google.com",
|
|
23
|
+
});
|
|
24
|
+
assert.deepStrictEqual(result[1], {
|
|
25
|
+
name: "mail",
|
|
26
|
+
type: "A",
|
|
27
|
+
value: "1.2.3.4",
|
|
28
|
+
ttl: 600,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
test("ignores comments and blank lines in YAML", () => {
|
|
32
|
+
const yaml = `
|
|
33
|
+
# This is a comment at the top
|
|
34
|
+
- name: "@"
|
|
35
|
+
type: TXT # inline comment
|
|
36
|
+
value: "v=spf1 include:_spf.google.com ~all"
|
|
37
|
+
|
|
38
|
+
# Another comment with spaces
|
|
39
|
+
- name: api
|
|
40
|
+
type: CNAME
|
|
41
|
+
value: api.service.com
|
|
42
|
+
`;
|
|
43
|
+
const result = parseYaml(yaml);
|
|
44
|
+
assert.strictEqual(result.length, 2);
|
|
45
|
+
assert.strictEqual(result[0].name, "@");
|
|
46
|
+
assert.strictEqual(result[0].type, "TXT");
|
|
47
|
+
assert.strictEqual(result[0].value, "v=spf1 include:_spf.google.com ~all");
|
|
48
|
+
assert.strictEqual(result[1].name, "api");
|
|
49
|
+
});
|
|
50
|
+
test("parses nested array list values based on indentation", () => {
|
|
51
|
+
const yaml = `
|
|
52
|
+
- type: ingress
|
|
53
|
+
protocol: tcp
|
|
54
|
+
port: 80
|
|
55
|
+
sources:
|
|
56
|
+
- 0.0.0.0/0
|
|
57
|
+
- ::/0
|
|
58
|
+
- type: egress
|
|
59
|
+
protocol: tcp
|
|
60
|
+
port: all
|
|
61
|
+
destinations:
|
|
62
|
+
- 10.0.0.0/8
|
|
63
|
+
`;
|
|
64
|
+
const result = parseYaml(yaml);
|
|
65
|
+
assert.strictEqual(result.length, 2);
|
|
66
|
+
assert.deepStrictEqual(result[0], {
|
|
67
|
+
type: "ingress",
|
|
68
|
+
protocol: "tcp",
|
|
69
|
+
port: 80,
|
|
70
|
+
sources: ["0.0.0.0/0", "::/0"],
|
|
71
|
+
});
|
|
72
|
+
assert.deepStrictEqual(result[1], {
|
|
73
|
+
type: "egress",
|
|
74
|
+
protocol: "tcp",
|
|
75
|
+
port: "all",
|
|
76
|
+
destinations: ["10.0.0.0/8"],
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
test("loads from JSON and YAML files successfully", () => {
|
|
80
|
+
const tempJsonPath = path.resolve(process.cwd(), "temp-test-records.json");
|
|
81
|
+
const tempYamlPath = path.resolve(process.cwd(), "temp-test-records.yaml");
|
|
82
|
+
const jsonContent = JSON.stringify([
|
|
83
|
+
{ name: "api", type: "CNAME", value: "lb.com" }
|
|
84
|
+
]);
|
|
85
|
+
const yamlContent = `
|
|
86
|
+
- name: web
|
|
87
|
+
type: A
|
|
88
|
+
value: 5.6.7.8
|
|
89
|
+
`;
|
|
90
|
+
// Write temp files
|
|
91
|
+
fs.writeFileSync(tempJsonPath, jsonContent, "utf-8");
|
|
92
|
+
fs.writeFileSync(tempYamlPath, yamlContent, "utf-8");
|
|
93
|
+
try {
|
|
94
|
+
const parsedJson = loadRecordsFromFile("temp-test-records.json");
|
|
95
|
+
assert.strictEqual(parsedJson.length, 1);
|
|
96
|
+
assert.deepStrictEqual(parsedJson[0], {
|
|
97
|
+
name: "api",
|
|
98
|
+
type: "CNAME",
|
|
99
|
+
value: "lb.com",
|
|
100
|
+
});
|
|
101
|
+
const parsedYaml = loadRecordsFromFile("temp-test-records.yaml");
|
|
102
|
+
assert.strictEqual(parsedYaml.length, 1);
|
|
103
|
+
assert.deepStrictEqual(parsedYaml[0], {
|
|
104
|
+
name: "web",
|
|
105
|
+
type: "A",
|
|
106
|
+
value: "5.6.7.8",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
// Clean up temp files
|
|
111
|
+
if (fs.existsSync(tempJsonPath))
|
|
112
|
+
fs.unlinkSync(tempJsonPath);
|
|
113
|
+
if (fs.existsSync(tempYamlPath))
|
|
114
|
+
fs.unlinkSync(tempYamlPath);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|