puls-dev 0.2.8 ā 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 +4 -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 +2 -0
- package/dist/core/decorators.js +8 -14
- 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 +7 -0
- package/dist/core/resource.js +10 -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 +308 -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/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 +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 +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 +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 +3 -0
- package/dist/providers/proxmox/vm.js +40 -9
- package/dist/types/inventory.d.ts +44 -1
- package/package.json +1 -1
package/dist/core/resource.d.ts
CHANGED
|
@@ -4,11 +4,18 @@ export declare abstract class BaseBuilder {
|
|
|
4
4
|
protected localDryRun: boolean | null;
|
|
5
5
|
protected discoveryPromise: Promise<any>;
|
|
6
6
|
protected sidecars: BaseBuilder[];
|
|
7
|
+
/** @internal */
|
|
8
|
+
_deployPromise: Promise<any>;
|
|
9
|
+
/** @internal */
|
|
10
|
+
_destroyPromise?: Promise<any>;
|
|
11
|
+
/** @internal */
|
|
12
|
+
_dependencies: BaseBuilder[];
|
|
7
13
|
private _beforeDeployHooks;
|
|
8
14
|
private _afterDeployHooks;
|
|
9
15
|
private _beforeDestroyHooks;
|
|
10
16
|
private _afterDestroyHooks;
|
|
11
17
|
constructor(name: string);
|
|
18
|
+
dependsOn(resource: BaseBuilder): this;
|
|
12
19
|
protect(): this;
|
|
13
20
|
dryRun(enabled?: boolean): this;
|
|
14
21
|
beforeDeploy(callback: () => Promise<void> | void): this;
|
package/dist/core/resource.js
CHANGED
|
@@ -5,6 +5,12 @@ export class BaseBuilder {
|
|
|
5
5
|
localDryRun = null;
|
|
6
6
|
discoveryPromise;
|
|
7
7
|
sidecars = [];
|
|
8
|
+
/** @internal */
|
|
9
|
+
_deployPromise;
|
|
10
|
+
/** @internal */
|
|
11
|
+
_destroyPromise;
|
|
12
|
+
/** @internal */
|
|
13
|
+
_dependencies = [];
|
|
8
14
|
_beforeDeployHooks = [];
|
|
9
15
|
_afterDeployHooks = [];
|
|
10
16
|
_beforeDestroyHooks = [];
|
|
@@ -12,6 +18,10 @@ export class BaseBuilder {
|
|
|
12
18
|
constructor(name) {
|
|
13
19
|
this.name = name;
|
|
14
20
|
}
|
|
21
|
+
dependsOn(resource) {
|
|
22
|
+
this._dependencies.push(resource);
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
15
25
|
protect() {
|
|
16
26
|
this.isProtected = true;
|
|
17
27
|
return this;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface RetryOptions {
|
|
2
|
+
maxAttempts?: number;
|
|
3
|
+
initialDelayMs?: number;
|
|
4
|
+
backoffFactor?: number;
|
|
5
|
+
jitter?: boolean;
|
|
6
|
+
retryable?: (error: any) => boolean;
|
|
7
|
+
delayFn?: (ms: number) => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export async function withRetry(fn, options = {}) {
|
|
2
|
+
const maxAttempts = options.maxAttempts ?? 5;
|
|
3
|
+
const initialDelayMs = options.initialDelayMs ?? 1000;
|
|
4
|
+
const backoffFactor = options.backoffFactor ?? 2;
|
|
5
|
+
const useJitter = options.jitter ?? true;
|
|
6
|
+
const retryable = options.retryable ?? (() => true);
|
|
7
|
+
const delayFn = options.delayFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
8
|
+
let attempt = 0;
|
|
9
|
+
while (true) {
|
|
10
|
+
try {
|
|
11
|
+
return await fn();
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
attempt++;
|
|
15
|
+
if (attempt >= maxAttempts || !retryable(error)) {
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
let delay = initialDelayMs * Math.pow(backoffFactor, attempt - 1);
|
|
19
|
+
if (useJitter) {
|
|
20
|
+
// Add random jitter offset of +/- 10%
|
|
21
|
+
const jitterAmount = (Math.random() * 0.2 - 0.1) * delay;
|
|
22
|
+
delay += jitterAmount;
|
|
23
|
+
}
|
|
24
|
+
console.warn(` ā ļø Transient error encountered: ${error.message || error}. Retrying in ${Math.round(delay)}ms (attempt ${attempt}/${maxAttempts})...`);
|
|
25
|
+
await delayFn(delay);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { test, describe } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { withRetry } from "./retry.js";
|
|
4
|
+
describe("Retry & Backoff Engine Unit Tests", () => {
|
|
5
|
+
test("resolves immediately if target function succeeds on the first try", async () => {
|
|
6
|
+
let calls = 0;
|
|
7
|
+
const res = await withRetry(async () => {
|
|
8
|
+
calls++;
|
|
9
|
+
return "success";
|
|
10
|
+
});
|
|
11
|
+
assert.strictEqual(res, "success");
|
|
12
|
+
assert.strictEqual(calls, 1);
|
|
13
|
+
});
|
|
14
|
+
test("retries up to max attempts and then throws the final error", async () => {
|
|
15
|
+
let calls = 0;
|
|
16
|
+
const delayTimes = [];
|
|
17
|
+
await assert.rejects(withRetry(async () => {
|
|
18
|
+
calls++;
|
|
19
|
+
throw new Error(`fail-${calls}`);
|
|
20
|
+
}, {
|
|
21
|
+
maxAttempts: 3,
|
|
22
|
+
jitter: false,
|
|
23
|
+
delayFn: async (ms) => {
|
|
24
|
+
delayTimes.push(ms);
|
|
25
|
+
},
|
|
26
|
+
}), /fail-3/);
|
|
27
|
+
assert.strictEqual(calls, 3);
|
|
28
|
+
// Exponential backoff verification:
|
|
29
|
+
// Delay 1: 1000ms * 2^0 = 1000ms
|
|
30
|
+
// Delay 2: 1000ms * 2^1 = 2000ms
|
|
31
|
+
assert.deepStrictEqual(delayTimes, [1000, 2000]);
|
|
32
|
+
});
|
|
33
|
+
test("immediately stops retrying and throws on non-retryable errors", async () => {
|
|
34
|
+
let calls = 0;
|
|
35
|
+
await assert.rejects(withRetry(async () => {
|
|
36
|
+
calls++;
|
|
37
|
+
if (calls === 1)
|
|
38
|
+
throw new Error("transient");
|
|
39
|
+
throw new Error("fatal");
|
|
40
|
+
}, {
|
|
41
|
+
maxAttempts: 5,
|
|
42
|
+
retryable: (err) => err.message === "transient",
|
|
43
|
+
delayFn: async () => { },
|
|
44
|
+
}), /fatal/);
|
|
45
|
+
assert.strictEqual(calls, 2);
|
|
46
|
+
});
|
|
47
|
+
test("includes randomized jitter when options.jitter is enabled", async () => {
|
|
48
|
+
const delayTimes = [];
|
|
49
|
+
await assert.rejects(withRetry(async () => {
|
|
50
|
+
throw new Error("fail");
|
|
51
|
+
}, {
|
|
52
|
+
maxAttempts: 3,
|
|
53
|
+
initialDelayMs: 1000,
|
|
54
|
+
backoffFactor: 2,
|
|
55
|
+
jitter: true,
|
|
56
|
+
delayFn: async (ms) => {
|
|
57
|
+
delayTimes.push(ms);
|
|
58
|
+
},
|
|
59
|
+
}), /fail/);
|
|
60
|
+
assert.strictEqual(delayTimes.length, 2);
|
|
61
|
+
// With 10% jitter, first delay (1000) should be between 900 and 1100
|
|
62
|
+
assert.ok(delayTimes[0] >= 900 && delayTimes[0] <= 1100);
|
|
63
|
+
// Second delay (2000) should be between 1800 and 2200
|
|
64
|
+
assert.ok(delayTimes[1] >= 1800 && delayTimes[1] <= 2200);
|
|
65
|
+
});
|
|
66
|
+
});
|
package/dist/core/secret.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Output } from "./output.js";
|
|
2
|
+
export declare const resolvedSecrets: Set<string>;
|
|
2
3
|
/**
|
|
3
4
|
* Secret represents a lazy, secure credential that is fetched asynchronously
|
|
4
5
|
* at deployment time instead of during the eager construction phase.
|
|
@@ -11,7 +12,7 @@ export declare class Secret extends Output<string> {
|
|
|
11
12
|
* Helper method to seamlessly unpack either a static string, a standard Output, or a Secret.
|
|
12
13
|
* Call this within resource builder deploy() methods.
|
|
13
14
|
*/
|
|
14
|
-
static resolve(val: string | Output<string> | Secret
|
|
15
|
+
static resolve(val: string | Output<string> | Secret): Promise<string>;
|
|
15
16
|
/**
|
|
16
17
|
* Fetches a secret from a local environment variable.
|
|
17
18
|
*/
|
package/dist/core/secret.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Output } from "./output.js";
|
|
2
2
|
import { Config } from "./config.js";
|
|
3
|
+
export const resolvedSecrets = new Set();
|
|
3
4
|
/**
|
|
4
5
|
* Secret represents a lazy, secure credential that is fetched asynchronously
|
|
5
6
|
* at deployment time instead of during the eager construction phase.
|
|
@@ -15,11 +16,16 @@ export class Secret extends Output {
|
|
|
15
16
|
try {
|
|
16
17
|
if (Config.isGlobalDryRun()) {
|
|
17
18
|
// Resolve immediately to a secure placeholder during dry-run testing
|
|
18
|
-
|
|
19
|
+
const placeholder = `[SECRET:${this.secretName}]`;
|
|
20
|
+
this.resolve(placeholder);
|
|
21
|
+
resolvedSecrets.add(placeholder);
|
|
19
22
|
return;
|
|
20
23
|
}
|
|
21
24
|
const val = await fetcher();
|
|
22
25
|
this.resolve(val);
|
|
26
|
+
if (val && val.length >= 3) {
|
|
27
|
+
resolvedSecrets.add(val);
|
|
28
|
+
}
|
|
23
29
|
}
|
|
24
30
|
catch (err) {
|
|
25
31
|
this.reject(err);
|
|
@@ -31,7 +37,11 @@ export class Secret extends Output {
|
|
|
31
37
|
*/
|
|
32
38
|
static async resolve(val) {
|
|
33
39
|
if (val instanceof Output) {
|
|
34
|
-
|
|
40
|
+
const resolved = await val.get();
|
|
41
|
+
if (val instanceof Secret && resolved && resolved.length >= 3) {
|
|
42
|
+
resolvedSecrets.add(resolved);
|
|
43
|
+
}
|
|
44
|
+
return resolved;
|
|
35
45
|
}
|
|
36
46
|
return val;
|
|
37
47
|
}
|
package/dist/core/stack.js
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
2
|
import { BaseBuilder } from "./resource.js";
|
|
3
|
+
import { Config } from "./config.js";
|
|
4
|
+
import { resourceContextStorage } from "./context.js";
|
|
5
|
+
import { resolvedSecrets } from "./secret.js";
|
|
3
6
|
const _registry = new Map();
|
|
4
|
-
function formatEntry(val) {
|
|
7
|
+
function formatEntry(val, parentKey) {
|
|
8
|
+
const isSensitiveKey = (k) => /password|secret|token|key/i.test(k);
|
|
9
|
+
if (parentKey && isSensitiveKey(parentKey)) {
|
|
10
|
+
return { primary: "********" };
|
|
11
|
+
}
|
|
5
12
|
if (!val || typeof val !== "object")
|
|
6
13
|
return { primary: String(val) };
|
|
14
|
+
const redactedVal = { ...val };
|
|
15
|
+
for (const k of Object.keys(redactedVal)) {
|
|
16
|
+
if (isSensitiveKey(k)) {
|
|
17
|
+
redactedVal[k] = "********";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
7
20
|
// Known shapes
|
|
8
|
-
if ("destroyed" in
|
|
9
|
-
return { primary:
|
|
10
|
-
if (
|
|
11
|
-
return { primary:
|
|
12
|
-
if (
|
|
13
|
-
return { primary: `${
|
|
14
|
-
if (
|
|
15
|
-
return { primary:
|
|
16
|
-
if (
|
|
17
|
-
return { primary:
|
|
21
|
+
if ("destroyed" in redactedVal)
|
|
22
|
+
return { primary: redactedVal.destroyed ? "šļø destroyed" : "ā not found" };
|
|
23
|
+
if (redactedVal.zone)
|
|
24
|
+
return { primary: redactedVal.zone };
|
|
25
|
+
if (redactedVal.name && redactedVal.id)
|
|
26
|
+
return { primary: `${redactedVal.name} [${redactedVal.id}]` };
|
|
27
|
+
if (redactedVal.name)
|
|
28
|
+
return { primary: redactedVal.name };
|
|
29
|
+
if (redactedVal.arn)
|
|
30
|
+
return { primary: redactedVal.arn };
|
|
18
31
|
// Generic: pull all scalar values
|
|
19
|
-
const pairs = Object.entries(
|
|
32
|
+
const pairs = Object.entries(redactedVal).filter(([, v]) => typeof v === "string" || typeof v === "number");
|
|
20
33
|
if (pairs.length === 0)
|
|
21
|
-
return { primary: JSON.stringify(
|
|
34
|
+
return { primary: JSON.stringify(redactedVal) };
|
|
22
35
|
// Try compact inline (values only, dot-separated)
|
|
23
36
|
const inline = pairs.map(([, v]) => v).join(" Ā· ");
|
|
24
37
|
if (inline.length <= 52)
|
|
@@ -35,7 +48,7 @@ function printOutputs(stackName, outputs) {
|
|
|
35
48
|
const keyWidth = Math.max(...Object.keys(outputs).map((k) => k.length));
|
|
36
49
|
const rows = Object.entries(outputs).map(([key, val]) => ({
|
|
37
50
|
key,
|
|
38
|
-
...formatEntry(val),
|
|
51
|
+
...formatEntry(val, key),
|
|
39
52
|
}));
|
|
40
53
|
// textWidth = width of row text content (without the 2-space padding on each side)
|
|
41
54
|
const textWidth = Math.max(...rows.flatMap(({ key, primary, sub }) => [
|
|
@@ -87,75 +100,295 @@ export class Stack {
|
|
|
87
100
|
return instance;
|
|
88
101
|
}
|
|
89
102
|
async deploy() {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const hosts = [];
|
|
105
|
+
const context = {
|
|
106
|
+
abortSignal: controller.signal,
|
|
107
|
+
hosts,
|
|
108
|
+
stackName: this.constructor.name
|
|
109
|
+
};
|
|
110
|
+
return resourceContextStorage.run(context, async () => {
|
|
111
|
+
const originalLog = console.log;
|
|
112
|
+
console.log = (...args) => {
|
|
113
|
+
const redact = (message) => {
|
|
114
|
+
if (typeof message !== "string")
|
|
115
|
+
return message;
|
|
116
|
+
let result = message;
|
|
117
|
+
for (const secret of resolvedSecrets) {
|
|
118
|
+
if (secret && secret.length >= 3) {
|
|
119
|
+
const escaped = secret.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
120
|
+
const regex = new RegExp(escaped, 'g');
|
|
121
|
+
result = result.replace(regex, '********');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
};
|
|
126
|
+
const redactedArgs = args.map(arg => {
|
|
127
|
+
if (typeof arg === "string") {
|
|
128
|
+
return redact(arg);
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const str = String(arg);
|
|
132
|
+
let hasSecret = false;
|
|
133
|
+
for (const secret of resolvedSecrets) {
|
|
134
|
+
if (secret && secret.length >= 3 && str.includes(secret)) {
|
|
135
|
+
hasSecret = true;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (hasSecret) {
|
|
140
|
+
return redact(str);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch { }
|
|
144
|
+
return arg;
|
|
145
|
+
});
|
|
146
|
+
originalLog(...redactedArgs);
|
|
147
|
+
};
|
|
148
|
+
try {
|
|
149
|
+
console.log(`\nšļø Deploying Stack: ${this.constructor.name}`);
|
|
150
|
+
// Stack-level beforeDeploy hook
|
|
151
|
+
if (typeof this.beforeDeploy === "function") {
|
|
152
|
+
console.log(` ā” Running Stack-level beforeDeploy hook...`);
|
|
153
|
+
await this.beforeDeploy();
|
|
154
|
+
}
|
|
155
|
+
const props = Object.getOwnPropertyNames(this);
|
|
156
|
+
const outputs = {};
|
|
157
|
+
const isParallel = Config.isParallelActive();
|
|
158
|
+
// 1. Gather all resources
|
|
159
|
+
const resources = [];
|
|
160
|
+
for (const prop of props) {
|
|
161
|
+
const val = this[prop];
|
|
162
|
+
if (val instanceof BaseBuilder) {
|
|
163
|
+
resources.push({ prop, resource: val });
|
|
164
|
+
// Apply metadata properties eagerly
|
|
165
|
+
const isProtected = Reflect.getMetadata("protected", this, prop);
|
|
166
|
+
if (isProtected)
|
|
167
|
+
val.protect();
|
|
168
|
+
const forceConfigCheck = Reflect.getMetadata("forceConfigCheck", this, prop);
|
|
169
|
+
if (forceConfigCheck && typeof val.forceConfigCheck === "function") {
|
|
170
|
+
val.forceConfigCheck();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
108
173
|
}
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
174
|
+
// 2. Schedule execution
|
|
175
|
+
if (isParallel) {
|
|
176
|
+
const startPromise = Promise.resolve();
|
|
177
|
+
const promises = resources.map(({ prop, resource }) => {
|
|
178
|
+
resource._deployPromise = (async () => {
|
|
179
|
+
try {
|
|
180
|
+
await startPromise;
|
|
181
|
+
if (controller.signal.aborted) {
|
|
182
|
+
throw new Error("Deployment aborted due to previous failure");
|
|
183
|
+
}
|
|
184
|
+
// Wait for explicit dependencies
|
|
185
|
+
for (const dep of resource._dependencies) {
|
|
186
|
+
if (dep._deployPromise) {
|
|
187
|
+
await dep._deployPromise;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (controller.signal.aborted) {
|
|
191
|
+
throw new Error("Deployment aborted due to previous failure");
|
|
192
|
+
}
|
|
193
|
+
// Execute hooks and deploy
|
|
194
|
+
const isDestroyed = Reflect.getMetadata("destroy", this, prop);
|
|
195
|
+
let res;
|
|
196
|
+
if (isDestroyed) {
|
|
197
|
+
await resource._runBeforeDestroy();
|
|
198
|
+
res = await resource.destroy();
|
|
199
|
+
await resource._runAfterDestroy(res);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
await resource._runBeforeDeploy();
|
|
203
|
+
res = await resource.deploy();
|
|
204
|
+
await resource._runAfterDeploy(res);
|
|
205
|
+
}
|
|
206
|
+
outputs[prop] = res;
|
|
207
|
+
return res;
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
controller.abort();
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
})();
|
|
214
|
+
return resource._deployPromise;
|
|
215
|
+
});
|
|
216
|
+
await Promise.all(promises);
|
|
114
217
|
}
|
|
115
218
|
else {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
219
|
+
// Sequential mode
|
|
220
|
+
for (const { prop, resource } of resources) {
|
|
221
|
+
if (controller.signal.aborted) {
|
|
222
|
+
throw new Error("Deployment aborted due to previous failure");
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const isDestroyed = Reflect.getMetadata("destroy", this, prop);
|
|
226
|
+
let res;
|
|
227
|
+
if (isDestroyed) {
|
|
228
|
+
await resource._runBeforeDestroy();
|
|
229
|
+
res = await resource.destroy();
|
|
230
|
+
await resource._runAfterDestroy(res);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
await resource._runBeforeDeploy();
|
|
234
|
+
res = await resource.deploy();
|
|
235
|
+
await resource._runAfterDeploy(res);
|
|
236
|
+
}
|
|
237
|
+
outputs[prop] = res;
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
controller.abort();
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
119
244
|
}
|
|
120
|
-
outputs
|
|
245
|
+
printOutputs(this.constructor.name, outputs);
|
|
246
|
+
// Stack-level afterDeploy hook
|
|
247
|
+
if (typeof this.afterDeploy === "function") {
|
|
248
|
+
console.log(` ā” Running Stack-level afterDeploy hook...`);
|
|
249
|
+
await this.afterDeploy(outputs);
|
|
250
|
+
}
|
|
251
|
+
return outputs;
|
|
121
252
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
console.log(` ā” Running Stack-level afterDeploy hook...`);
|
|
127
|
-
await this.afterDeploy(outputs);
|
|
128
|
-
}
|
|
129
|
-
return outputs;
|
|
253
|
+
finally {
|
|
254
|
+
console.log = originalLog;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
130
257
|
}
|
|
131
258
|
async destroy() {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
259
|
+
const controller = new AbortController();
|
|
260
|
+
const hosts = [];
|
|
261
|
+
const context = {
|
|
262
|
+
abortSignal: controller.signal,
|
|
263
|
+
hosts,
|
|
264
|
+
stackName: this.constructor.name
|
|
265
|
+
};
|
|
266
|
+
return resourceContextStorage.run(context, async () => {
|
|
267
|
+
const originalLog = console.log;
|
|
268
|
+
console.log = (...args) => {
|
|
269
|
+
const redact = (message) => {
|
|
270
|
+
if (typeof message !== "string")
|
|
271
|
+
return message;
|
|
272
|
+
let result = message;
|
|
273
|
+
for (const secret of resolvedSecrets) {
|
|
274
|
+
if (secret && secret.length >= 3) {
|
|
275
|
+
const escaped = secret.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
276
|
+
const regex = new RegExp(escaped, 'g');
|
|
277
|
+
result = result.replace(regex, '********');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
};
|
|
282
|
+
const redactedArgs = args.map(arg => {
|
|
283
|
+
if (typeof arg === "string") {
|
|
284
|
+
return redact(arg);
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const str = String(arg);
|
|
288
|
+
let hasSecret = false;
|
|
289
|
+
for (const secret of resolvedSecrets) {
|
|
290
|
+
if (secret && secret.length >= 3 && str.includes(secret)) {
|
|
291
|
+
hasSecret = true;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (hasSecret) {
|
|
296
|
+
return redact(str);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch { }
|
|
300
|
+
return arg;
|
|
301
|
+
});
|
|
302
|
+
originalLog(...redactedArgs);
|
|
303
|
+
};
|
|
304
|
+
try {
|
|
305
|
+
console.log(`\nš„ Tearing down Stack: ${this.constructor.name}`);
|
|
306
|
+
// Stack-level beforeDestroy hook
|
|
307
|
+
if (typeof this.beforeDestroy === "function") {
|
|
308
|
+
console.log(` ā” Running Stack-level beforeDestroy hook...`);
|
|
309
|
+
await this.beforeDestroy();
|
|
310
|
+
}
|
|
311
|
+
const props = Object.getOwnPropertyNames(this).reverse();
|
|
312
|
+
const outputs = {};
|
|
313
|
+
const isParallel = Config.isParallelActive();
|
|
314
|
+
// 1. Gather all resources
|
|
315
|
+
const resources = [];
|
|
316
|
+
for (const prop of props) {
|
|
317
|
+
const val = this[prop];
|
|
318
|
+
if (val instanceof BaseBuilder) {
|
|
319
|
+
if (Reflect.getMetadata("protected", this, prop)) {
|
|
320
|
+
console.log(` š Skipping protected resource "${prop}"`);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
resources.push({ prop, resource: val });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// 2. Schedule execution
|
|
327
|
+
if (isParallel) {
|
|
328
|
+
const startPromise = Promise.resolve();
|
|
329
|
+
// In parallel destroy, await all dependents (reverse dependencies) first
|
|
330
|
+
const promises = resources.map(({ prop, resource }) => {
|
|
331
|
+
resource._destroyPromise = (async () => {
|
|
332
|
+
try {
|
|
333
|
+
await startPromise;
|
|
334
|
+
if (controller.signal.aborted) {
|
|
335
|
+
throw new Error("Teardown aborted due to previous failure");
|
|
336
|
+
}
|
|
337
|
+
// Wait for all resources that explicitly declare this one as a dependency
|
|
338
|
+
const dependents = resources.filter(r => r.resource._dependencies.includes(resource));
|
|
339
|
+
for (const dep of dependents) {
|
|
340
|
+
if (dep.resource._destroyPromise) {
|
|
341
|
+
await dep.resource._destroyPromise;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (controller.signal.aborted) {
|
|
345
|
+
throw new Error("Teardown aborted due to previous failure");
|
|
346
|
+
}
|
|
347
|
+
// Execute teardown
|
|
348
|
+
await resource._runBeforeDestroy();
|
|
349
|
+
const res = await resource.destroy();
|
|
350
|
+
await resource._runAfterDestroy(res);
|
|
351
|
+
outputs[prop] = res;
|
|
352
|
+
return res;
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
controller.abort();
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
358
|
+
})();
|
|
359
|
+
return resource._destroyPromise;
|
|
360
|
+
});
|
|
361
|
+
await Promise.all(promises);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
// Sequential mode
|
|
365
|
+
for (const { prop, resource } of resources) {
|
|
366
|
+
if (controller.signal.aborted) {
|
|
367
|
+
throw new Error("Teardown aborted due to previous failure");
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
await resource._runBeforeDestroy();
|
|
371
|
+
const res = await resource.destroy();
|
|
372
|
+
await resource._runAfterDestroy(res);
|
|
373
|
+
outputs[prop] = res;
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
controller.abort();
|
|
377
|
+
throw err;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
146
380
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
381
|
+
printOutputs(this.constructor.name, outputs);
|
|
382
|
+
// Stack-level afterDestroy hook
|
|
383
|
+
if (typeof this.afterDestroy === "function") {
|
|
384
|
+
console.log(` ā” Running Stack-level afterDestroy hook...`);
|
|
385
|
+
await this.afterDestroy(outputs);
|
|
386
|
+
}
|
|
387
|
+
return outputs;
|
|
151
388
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
console.log(` ā” Running Stack-level afterDestroy hook...`);
|
|
157
|
-
await this.afterDestroy(outputs);
|
|
158
|
-
}
|
|
159
|
-
return outputs;
|
|
389
|
+
finally {
|
|
390
|
+
console.log = originalLog;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
160
393
|
}
|
|
161
394
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ export * from "./core/decorators.js";
|
|
|
3
3
|
export * from "./core/checker.js";
|
|
4
4
|
export * from "./core/resource.js";
|
|
5
5
|
export { Secret } from "./core/secret.js";
|
|
6
|
+
export { Output } from "./core/output.js";
|
|
6
7
|
export * as INVENTORY_TYPES from "./types/inventory.js";
|
|
7
8
|
export { SLACK, DISCORD } from "./core/hooks.js";
|
package/dist/index.js
CHANGED
|
@@ -3,5 +3,6 @@ export * from "./core/decorators.js";
|
|
|
3
3
|
export * from "./core/checker.js";
|
|
4
4
|
export * from "./core/resource.js";
|
|
5
5
|
export { Secret } from "./core/secret.js";
|
|
6
|
+
export { Output } from "./core/output.js";
|
|
6
7
|
export * as INVENTORY_TYPES from "./types/inventory.js";
|
|
7
8
|
export { SLACK, DISCORD } from "./core/hooks.js";
|