puls-dev 0.3.3 → 0.3.5
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/bin/install-shell.d.ts +2 -0
- package/dist/bin/install-shell.js +136 -0
- package/dist/bin/puls.d.ts +1 -0
- package/dist/bin/puls.js +145 -0
- package/dist/core/checker.js +74 -0
- package/dist/core/config.d.ts +3 -0
- package/dist/core/context.d.ts +1 -0
- package/dist/core/decorators.d.ts +1 -0
- package/dist/core/decorators.js +39 -5
- package/dist/core/output.js +8 -1
- package/dist/core/production.test.js +1 -0
- package/dist/core/resource.d.ts +35 -0
- package/dist/core/resource.js +57 -1
- package/dist/core/secret.d.ts +1 -0
- package/dist/core/secret.js +5 -0
- package/dist/core/stack.d.ts +11 -0
- package/dist/core/stack.js +141 -90
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/providers/aws/api.js +3 -0
- package/dist/providers/aws/ec2.d.ts +5 -0
- package/dist/providers/aws/ec2.js +7 -0
- package/dist/providers/aws/lambda.d.ts +5 -0
- package/dist/providers/aws/lambda.js +24 -0
- package/dist/providers/aws/list.js +15 -3
- package/dist/providers/aws/rds.d.ts +9 -0
- package/dist/providers/aws/rds.js +19 -0
- package/dist/providers/do/database.d.ts +9 -0
- package/dist/providers/do/database.js +19 -0
- package/dist/providers/do/domain.js +1 -1
- package/dist/providers/do/droplet.d.ts +10 -0
- package/dist/providers/do/droplet.js +28 -3
- package/dist/providers/do/droplet.test.js +1 -1
- package/dist/providers/do/list.js +25 -2
- package/dist/providers/do/load_balancer.d.ts +5 -0
- package/dist/providers/do/load_balancer.js +7 -0
- package/dist/providers/do/vpc.d.ts +5 -0
- package/dist/providers/do/vpc.js +8 -0
- package/dist/providers/firebase/functions.d.ts +9 -0
- package/dist/providers/firebase/functions.js +28 -0
- package/dist/providers/firebase/list.js +34 -2
- package/dist/providers/gcp/api.js +6 -0
- package/dist/providers/gcp/cloudrun.d.ts +13 -0
- package/dist/providers/gcp/cloudrun.js +30 -0
- package/dist/providers/gcp/cloudsql.d.ts +9 -0
- package/dist/providers/gcp/cloudsql.js +20 -0
- package/dist/providers/gcp/list.js +12 -2
- package/dist/providers/gcp/template.d.ts +3 -0
- package/dist/providers/gcp/template.js +13 -1
- package/dist/providers/gcp/vm.d.ts +8 -0
- package/dist/providers/gcp/vm.js +22 -2
- package/dist/providers/proxmox/api.d.ts +1 -0
- package/dist/providers/proxmox/api.js +18 -3
- package/dist/providers/proxmox/base.d.ts +16 -0
- package/dist/providers/proxmox/base.js +121 -0
- package/dist/providers/proxmox/list.js +8 -1
- package/dist/providers/proxmox/template.d.ts +3 -10
- package/dist/providers/proxmox/template.js +51 -139
- package/dist/providers/proxmox/vm.d.ts +18 -10
- package/dist/providers/proxmox/vm.js +73 -152
- package/dist/types/diff.d.ts +17 -0
- package/dist/types/diff.js +1 -0
- package/dist/types/inventory.d.ts +65 -0
- package/package.json +7 -22
package/dist/core/secret.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { Output } from "./output.js";
|
|
2
2
|
import { Config } from "./config.js";
|
|
3
|
+
import { resourceContextStorage } from "./context.js";
|
|
3
4
|
export const resolvedSecrets = new Set();
|
|
5
|
+
export function clearResolvedSecrets() {
|
|
6
|
+
resolvedSecrets.clear();
|
|
7
|
+
}
|
|
4
8
|
/**
|
|
5
9
|
* Secret represents a lazy, secure credential that is fetched asynchronously
|
|
6
10
|
* at deployment time instead of during the eager construction phase.
|
|
@@ -25,6 +29,7 @@ export class Secret extends Output {
|
|
|
25
29
|
this.resolve(val);
|
|
26
30
|
if (val && val.length >= 3) {
|
|
27
31
|
resolvedSecrets.add(val);
|
|
32
|
+
resourceContextStorage.getStore()?.secrets.add(val);
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
catch (err) {
|
package/dist/core/stack.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
|
+
import type { StackDiff } from "../types/diff.js";
|
|
2
3
|
export declare abstract class Stack {
|
|
3
4
|
/** @internal - called by @Deploy to register the instance for cross-stack references. */
|
|
4
5
|
static _register(cls: Function, instance: Stack, region?: string): void;
|
|
@@ -16,6 +17,16 @@ export declare abstract class Stack {
|
|
|
16
17
|
* }
|
|
17
18
|
*/
|
|
18
19
|
static from<T extends Stack>(cls: new (...args: any[]) => T, region?: string): T;
|
|
20
|
+
/**
|
|
21
|
+
* Compares every declared resource against its live cloud state without
|
|
22
|
+
* making any API writes. Returns a structured `StackDiff` and prints a
|
|
23
|
+
* formatted report to the console.
|
|
24
|
+
*
|
|
25
|
+
* Field-level drift is surfaced for providers that implement `getDiff()`.
|
|
26
|
+
* Resources with no `getDiff()` override show only existence status
|
|
27
|
+
* (missing / in-sync / adopted).
|
|
28
|
+
*/
|
|
29
|
+
diff(): Promise<StackDiff>;
|
|
19
30
|
deploy(): Promise<Record<string, any>>;
|
|
20
31
|
destroy(): Promise<Record<string, any>>;
|
|
21
32
|
}
|
package/dist/core/stack.js
CHANGED
|
@@ -3,6 +3,42 @@ import { BaseBuilder } from "./resource.js";
|
|
|
3
3
|
import { Config } from "./config.js";
|
|
4
4
|
import { resourceContextStorage } from "./context.js";
|
|
5
5
|
import { resolvedSecrets } from "./secret.js";
|
|
6
|
+
async function withRedactedConsole(secrets, fn) {
|
|
7
|
+
const originalLog = console.log;
|
|
8
|
+
const redact = (message) => {
|
|
9
|
+
if (typeof message !== "string")
|
|
10
|
+
return message;
|
|
11
|
+
let result = message;
|
|
12
|
+
for (const secret of secrets) {
|
|
13
|
+
if (secret && secret.length >= 3) {
|
|
14
|
+
const escaped = secret.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
15
|
+
result = result.replace(new RegExp(escaped, "g"), "********");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
};
|
|
20
|
+
console.log = (...args) => {
|
|
21
|
+
const redactedArgs = args.map((arg) => {
|
|
22
|
+
if (typeof arg === "string")
|
|
23
|
+
return redact(arg);
|
|
24
|
+
try {
|
|
25
|
+
const str = String(arg);
|
|
26
|
+
const hasSecret = [...secrets].some((s) => s && s.length >= 3 && str.includes(s));
|
|
27
|
+
if (hasSecret)
|
|
28
|
+
return redact(str);
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
return arg;
|
|
32
|
+
});
|
|
33
|
+
originalLog(...redactedArgs);
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
return await fn();
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
console.log = originalLog;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
6
42
|
const _registry = new Map();
|
|
7
43
|
function formatEntry(val, parentKey) {
|
|
8
44
|
const isSensitiveKey = (k) => /password|secret|token|key/i.test(k);
|
|
@@ -71,6 +107,45 @@ function printOutputs(stackName, outputs) {
|
|
|
71
107
|
}
|
|
72
108
|
console.log(` └${line}┘`);
|
|
73
109
|
}
|
|
110
|
+
function printDiff(diff) {
|
|
111
|
+
console.log(`\n🔍 Diff: ${diff.stackName}`);
|
|
112
|
+
const propWidth = Math.max(...diff.resources.map((r) => r.prop.length), 4);
|
|
113
|
+
const nameWidth = Math.max(...diff.resources.map((r) => r.resource.length), 8);
|
|
114
|
+
for (const r of diff.resources) {
|
|
115
|
+
const prop = r.prop.padEnd(propWidth);
|
|
116
|
+
const name = r.resource.padEnd(nameWidth);
|
|
117
|
+
if (r.status === "in-sync") {
|
|
118
|
+
console.log(` ${prop} ${name} ✅ in-sync`);
|
|
119
|
+
}
|
|
120
|
+
else if (r.status === "adopted") {
|
|
121
|
+
console.log(` ${prop} ${name} 🔗 adopted`);
|
|
122
|
+
}
|
|
123
|
+
else if (r.status === "missing") {
|
|
124
|
+
console.log(` ${prop} ${name} ❌ missing (will create)`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
console.log(` ${prop} ${name} ⚠️ drift`);
|
|
128
|
+
const fieldWidth = Math.max(...r.changes.map((c) => String(c.field).length), 8);
|
|
129
|
+
for (const c of r.changes) {
|
|
130
|
+
const field = String(c.field).padEnd(fieldWidth);
|
|
131
|
+
console.log(` └─ ${field} ${String(c.declared)} → ${c.live}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const driftCount = diff.resources.filter((r) => r.status === "drift").length;
|
|
136
|
+
const missingCount = diff.resources.filter((r) => r.status === "missing").length;
|
|
137
|
+
if (driftCount === 0 && missingCount === 0) {
|
|
138
|
+
console.log(`\n ✅ All ${diff.resources.length} resources are in sync.`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const parts = [];
|
|
142
|
+
if (driftCount > 0)
|
|
143
|
+
parts.push(`${driftCount} drifted`);
|
|
144
|
+
if (missingCount > 0)
|
|
145
|
+
parts.push(`${missingCount} missing`);
|
|
146
|
+
console.log(`\n ⚠️ ${parts.join(", ")} out of ${diff.resources.length} resources.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
74
149
|
export class Stack {
|
|
75
150
|
/** @internal - called by @Deploy to register the instance for cross-stack references. */
|
|
76
151
|
static _register(cls, instance, region) {
|
|
@@ -99,53 +174,67 @@ export class Stack {
|
|
|
99
174
|
throw new Error(`Stack "${cls.name}" ${region ? `for region "${region}" ` : ""}is not registered. Make sure it is decorated with @Deploy and its module is imported before referencing it.`);
|
|
100
175
|
return instance;
|
|
101
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Compares every declared resource against its live cloud state without
|
|
179
|
+
* making any API writes. Returns a structured `StackDiff` and prints a
|
|
180
|
+
* formatted report to the console.
|
|
181
|
+
*
|
|
182
|
+
* Field-level drift is surfaced for providers that implement `getDiff()`.
|
|
183
|
+
* Resources with no `getDiff()` override show only existence status
|
|
184
|
+
* (missing / in-sync / adopted).
|
|
185
|
+
*/
|
|
186
|
+
async diff() {
|
|
187
|
+
const props = Object.getOwnPropertyNames(this);
|
|
188
|
+
const entries = [];
|
|
189
|
+
for (const prop of props) {
|
|
190
|
+
const val = this[prop];
|
|
191
|
+
if (val instanceof BaseBuilder) {
|
|
192
|
+
entries.push({ prop, resource: val });
|
|
193
|
+
}
|
|
194
|
+
else if (Array.isArray(val)) {
|
|
195
|
+
for (const item of val) {
|
|
196
|
+
if (item instanceof BaseBuilder) {
|
|
197
|
+
entries.push({ prop, resource: item });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const resources = [];
|
|
203
|
+
for (const { prop, resource } of entries) {
|
|
204
|
+
const existing = await resource._resolveDiscovery();
|
|
205
|
+
let status;
|
|
206
|
+
let changes = resource.getDiff(existing ?? {});
|
|
207
|
+
if (!existing) {
|
|
208
|
+
status = "missing";
|
|
209
|
+
changes = [];
|
|
210
|
+
}
|
|
211
|
+
else if (existing._adopted === true) {
|
|
212
|
+
status = "adopted";
|
|
213
|
+
changes = [];
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
status = changes.length > 0 ? "drift" : "in-sync";
|
|
217
|
+
}
|
|
218
|
+
resources.push({ prop, resource: resource.name, status, changes });
|
|
219
|
+
}
|
|
220
|
+
const hasDrift = resources.some((r) => r.status === "drift" || r.status === "missing");
|
|
221
|
+
const result = { stackName: this.constructor.name, resources, hasDrift };
|
|
222
|
+
printDiff(result);
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
102
225
|
async deploy() {
|
|
103
226
|
const controller = new AbortController();
|
|
104
227
|
const hosts = [];
|
|
228
|
+
// Snapshot current secrets; new secrets resolved during this run are added via context
|
|
229
|
+
const secrets = new Set(resolvedSecrets);
|
|
105
230
|
const context = {
|
|
106
231
|
abortSignal: controller.signal,
|
|
107
232
|
hosts,
|
|
108
|
-
stackName: this.constructor.name
|
|
233
|
+
stackName: this.constructor.name,
|
|
234
|
+
secrets,
|
|
109
235
|
};
|
|
110
236
|
return resourceContextStorage.run(context, async () => {
|
|
111
|
-
|
|
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 {
|
|
237
|
+
return withRedactedConsole(secrets, async () => {
|
|
149
238
|
console.log(`\n🏗️ Deploying Stack: ${this.constructor.name}`);
|
|
150
239
|
// Stack-level beforeDeploy hook
|
|
151
240
|
if (typeof this.beforeDeploy === "function") {
|
|
@@ -187,11 +276,13 @@ export class Stack {
|
|
|
187
276
|
}
|
|
188
277
|
// 2. Schedule execution
|
|
189
278
|
if (isParallel) {
|
|
190
|
-
const startPromise = Promise.resolve();
|
|
191
279
|
const promises = resources.map(({ prop, resource }) => {
|
|
192
280
|
resource._deployPromise = (async () => {
|
|
193
281
|
try {
|
|
194
|
-
|
|
282
|
+
// Yield so the map() loop finishes assigning all _deployPromise values before
|
|
283
|
+
// any task checks its dependencies - a dependency that appears later in the list
|
|
284
|
+
// would otherwise have an undefined _deployPromise and be silently skipped.
|
|
285
|
+
await Promise.resolve();
|
|
195
286
|
if (controller.signal.aborted) {
|
|
196
287
|
throw new Error("Deployment aborted due to previous failure");
|
|
197
288
|
}
|
|
@@ -287,59 +378,21 @@ export class Stack {
|
|
|
287
378
|
await this.afterDeploy(outputs);
|
|
288
379
|
}
|
|
289
380
|
return outputs;
|
|
290
|
-
}
|
|
291
|
-
finally {
|
|
292
|
-
console.log = originalLog;
|
|
293
|
-
}
|
|
381
|
+
});
|
|
294
382
|
});
|
|
295
383
|
}
|
|
296
384
|
async destroy() {
|
|
297
385
|
const controller = new AbortController();
|
|
298
386
|
const hosts = [];
|
|
387
|
+
const secrets = new Set(resolvedSecrets);
|
|
299
388
|
const context = {
|
|
300
389
|
abortSignal: controller.signal,
|
|
301
390
|
hosts,
|
|
302
|
-
stackName: this.constructor.name
|
|
391
|
+
stackName: this.constructor.name,
|
|
392
|
+
secrets,
|
|
303
393
|
};
|
|
304
394
|
return resourceContextStorage.run(context, async () => {
|
|
305
|
-
|
|
306
|
-
console.log = (...args) => {
|
|
307
|
-
const redact = (message) => {
|
|
308
|
-
if (typeof message !== "string")
|
|
309
|
-
return message;
|
|
310
|
-
let result = message;
|
|
311
|
-
for (const secret of resolvedSecrets) {
|
|
312
|
-
if (secret && secret.length >= 3) {
|
|
313
|
-
const escaped = secret.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
314
|
-
const regex = new RegExp(escaped, 'g');
|
|
315
|
-
result = result.replace(regex, '********');
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
return result;
|
|
319
|
-
};
|
|
320
|
-
const redactedArgs = args.map(arg => {
|
|
321
|
-
if (typeof arg === "string") {
|
|
322
|
-
return redact(arg);
|
|
323
|
-
}
|
|
324
|
-
try {
|
|
325
|
-
const str = String(arg);
|
|
326
|
-
let hasSecret = false;
|
|
327
|
-
for (const secret of resolvedSecrets) {
|
|
328
|
-
if (secret && secret.length >= 3 && str.includes(secret)) {
|
|
329
|
-
hasSecret = true;
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
if (hasSecret) {
|
|
334
|
-
return redact(str);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
catch { }
|
|
338
|
-
return arg;
|
|
339
|
-
});
|
|
340
|
-
originalLog(...redactedArgs);
|
|
341
|
-
};
|
|
342
|
-
try {
|
|
395
|
+
return withRedactedConsole(secrets, async () => {
|
|
343
396
|
console.log(`\n💥 Tearing down Stack: ${this.constructor.name}`);
|
|
344
397
|
// Stack-level beforeDestroy hook
|
|
345
398
|
if (typeof this.beforeDestroy === "function") {
|
|
@@ -374,12 +427,13 @@ export class Stack {
|
|
|
374
427
|
}
|
|
375
428
|
// 2. Schedule execution
|
|
376
429
|
if (isParallel) {
|
|
377
|
-
const startPromise = Promise.resolve();
|
|
378
430
|
// In parallel destroy, await all dependents (reverse dependencies) first
|
|
379
431
|
const promises = resources.map(({ prop, resource }) => {
|
|
380
432
|
resource._destroyPromise = (async () => {
|
|
381
433
|
try {
|
|
382
|
-
|
|
434
|
+
// Yield so the map() loop finishes assigning all _destroyPromise values before
|
|
435
|
+
// any task checks its dependents (same reason as parallel deploy).
|
|
436
|
+
await Promise.resolve();
|
|
383
437
|
if (controller.signal.aborted) {
|
|
384
438
|
throw new Error("Teardown aborted due to previous failure");
|
|
385
439
|
}
|
|
@@ -458,10 +512,7 @@ export class Stack {
|
|
|
458
512
|
await this.afterDestroy(outputs);
|
|
459
513
|
}
|
|
460
514
|
return outputs;
|
|
461
|
-
}
|
|
462
|
-
finally {
|
|
463
|
-
console.log = originalLog;
|
|
464
|
-
}
|
|
515
|
+
});
|
|
465
516
|
});
|
|
466
517
|
}
|
|
467
518
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ export * from "./core/stack.js";
|
|
|
2
2
|
export * from "./core/decorators.js";
|
|
3
3
|
export * from "./core/checker.js";
|
|
4
4
|
export * from "./core/resource.js";
|
|
5
|
-
export { Secret } from "./core/secret.js";
|
|
5
|
+
export { Secret, clearResolvedSecrets } from "./core/secret.js";
|
|
6
6
|
export { Output } from "./core/output.js";
|
|
7
7
|
export * as INVENTORY_TYPES from "./types/inventory.js";
|
|
8
|
+
export type { FieldDiff, ResourceDiff, StackDiff, ResourceStatus } from "./types/diff.js";
|
|
8
9
|
export { SLACK, DISCORD } from "./core/hooks.js";
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export * from "./core/stack.js";
|
|
|
2
2
|
export * from "./core/decorators.js";
|
|
3
3
|
export * from "./core/checker.js";
|
|
4
4
|
export * from "./core/resource.js";
|
|
5
|
-
export { Secret } from "./core/secret.js";
|
|
5
|
+
export { Secret, clearResolvedSecrets } from "./core/secret.js";
|
|
6
6
|
export { Output } from "./core/output.js";
|
|
7
7
|
export * as INVENTORY_TYPES from "./types/inventory.js";
|
|
8
8
|
export { SLACK, DISCORD } from "./core/hooks.js";
|
|
@@ -60,6 +60,9 @@ function createAwsOfflineMock(command) {
|
|
|
60
60
|
get(target, prop) {
|
|
61
61
|
if (prop === "then")
|
|
62
62
|
return undefined;
|
|
63
|
+
// Pagination tokens must be undefined so discovery loops exit cleanly
|
|
64
|
+
if (prop === "NextToken" || prop === "NextMarker" || prop === "Marker" || prop === "ContinuationToken")
|
|
65
|
+
return undefined;
|
|
63
66
|
if (prop === "CertificateArn")
|
|
64
67
|
return "arn:aws:acm:us-east-1:123456789012:certificate/mock-cert-uuid";
|
|
65
68
|
if (prop === "HostedZoneId")
|
|
@@ -29,6 +29,11 @@ export declare class EC2VMBuilder extends BaseBuilder {
|
|
|
29
29
|
sshPrivateKey(path: string): this;
|
|
30
30
|
provision(...playbookPaths: (string | string[])[]): this;
|
|
31
31
|
forceConfigCheck(): this;
|
|
32
|
+
getDiff(existing: any): {
|
|
33
|
+
field: string;
|
|
34
|
+
declared: string;
|
|
35
|
+
live: any;
|
|
36
|
+
}[];
|
|
32
37
|
protected checkPort(ip: string, port: number): Promise<boolean>;
|
|
33
38
|
protected runProvisioner(ip: string, script: string): Promise<void>;
|
|
34
39
|
private discoverVM;
|
|
@@ -67,6 +67,13 @@ export class EC2VMBuilder extends BaseBuilder {
|
|
|
67
67
|
this._forceConfigCheck = true;
|
|
68
68
|
return this;
|
|
69
69
|
}
|
|
70
|
+
getDiff(existing) {
|
|
71
|
+
const diffs = [];
|
|
72
|
+
if (existing.InstanceType !== this._instanceType) {
|
|
73
|
+
diffs.push({ field: "instanceType", declared: this._instanceType, live: existing.InstanceType });
|
|
74
|
+
}
|
|
75
|
+
return diffs;
|
|
76
|
+
}
|
|
70
77
|
async checkPort(ip, port) {
|
|
71
78
|
return checkPort(ip, port);
|
|
72
79
|
}
|
|
@@ -20,6 +20,11 @@ export declare class LambdaBuilder extends BaseBuilder {
|
|
|
20
20
|
timeout(seconds: number): this;
|
|
21
21
|
role(arnOrBuilder: string | IAMRoleBuilder): this;
|
|
22
22
|
env(vars: Record<string, string | SecretsBuilder>): this;
|
|
23
|
+
getDiff(existing: any): {
|
|
24
|
+
field: string;
|
|
25
|
+
declared: string;
|
|
26
|
+
live: any;
|
|
27
|
+
}[];
|
|
23
28
|
private ensureRole;
|
|
24
29
|
private buildZip;
|
|
25
30
|
deploy(): Promise<{
|
|
@@ -78,6 +78,30 @@ export class LambdaBuilder extends BaseBuilder {
|
|
|
78
78
|
this._env = { ...this._env, ...vars };
|
|
79
79
|
return this;
|
|
80
80
|
}
|
|
81
|
+
getDiff(existing) {
|
|
82
|
+
const diffs = [];
|
|
83
|
+
if (existing.Runtime !== this._runtime) {
|
|
84
|
+
diffs.push({ field: "runtime", declared: this._runtime, live: existing.Runtime });
|
|
85
|
+
}
|
|
86
|
+
if (existing.Handler !== this._handler) {
|
|
87
|
+
diffs.push({ field: "handler", declared: this._handler, live: existing.Handler });
|
|
88
|
+
}
|
|
89
|
+
if (existing.MemorySize !== this._memory) {
|
|
90
|
+
diffs.push({ field: "memory", declared: `${this._memory} MB`, live: `${existing.MemorySize} MB` });
|
|
91
|
+
}
|
|
92
|
+
if (existing.Timeout !== this._timeout) {
|
|
93
|
+
diffs.push({ field: "timeout", declared: `${this._timeout}s`, live: `${existing.Timeout}s` });
|
|
94
|
+
}
|
|
95
|
+
const liveEnv = existing.Environment?.Variables ?? {};
|
|
96
|
+
const declaredKeys = Object.keys(this._env);
|
|
97
|
+
const liveKeys = Object.keys(liveEnv);
|
|
98
|
+
const allKeys = new Set([...declaredKeys, ...liveKeys]);
|
|
99
|
+
const envDrift = [...allKeys].filter((k) => String(this._env[k]) !== String(liveEnv[k]));
|
|
100
|
+
if (envDrift.length > 0) {
|
|
101
|
+
diffs.push({ field: "env", declared: `${declaredKeys.length} vars`, live: `${liveKeys.length} vars (${envDrift.length} changed)` });
|
|
102
|
+
}
|
|
103
|
+
return diffs;
|
|
104
|
+
}
|
|
81
105
|
async ensureRole() {
|
|
82
106
|
if (this._roleBuilder) {
|
|
83
107
|
return await this._roleBuilder.out.arn.get();
|
|
@@ -3,16 +3,18 @@ import { ListBucketsCommand } from '@aws-sdk/client-s3';
|
|
|
3
3
|
import { ListFunctionsCommand } from '@aws-sdk/client-lambda';
|
|
4
4
|
import { DescribeDBInstancesCommand } from '@aws-sdk/client-rds';
|
|
5
5
|
import { ListHostedZonesCommand } from '@aws-sdk/client-route-53';
|
|
6
|
-
import {
|
|
6
|
+
import { DescribeInstancesCommand } from '@aws-sdk/client-ec2';
|
|
7
|
+
import { getCFClient, getS3Client, getLambdaClient, getRDSClient, getR53Client, getEC2Client } from './api.js';
|
|
7
8
|
import { Config } from '../../core/config.js';
|
|
8
9
|
export async function listAwsResources() {
|
|
9
10
|
const region = Config.get().providers.aws.region;
|
|
10
|
-
const [cfResult, s3Result, lambdaResult, rdsResult, r53Result] = await Promise.all([
|
|
11
|
+
const [cfResult, s3Result, lambdaResult, rdsResult, r53Result, ec2Result] = await Promise.all([
|
|
11
12
|
getCFClient().send(new ListDistributionsCommand({})),
|
|
12
13
|
getS3Client().send(new ListBucketsCommand({})),
|
|
13
14
|
getLambdaClient().send(new ListFunctionsCommand({ MaxItems: 50 })),
|
|
14
15
|
getRDSClient().send(new DescribeDBInstancesCommand({})),
|
|
15
16
|
getR53Client().send(new ListHostedZonesCommand({})),
|
|
17
|
+
getEC2Client().send(new DescribeInstancesCommand({ MaxResults: 200 })),
|
|
16
18
|
]);
|
|
17
19
|
const distributions = (cfResult.DistributionList?.Items ?? []).map((d) => ({
|
|
18
20
|
id: d.Id,
|
|
@@ -40,5 +42,15 @@ export async function listAwsResources() {
|
|
|
40
42
|
id: z.Id.replace('/hostedzone/', ''),
|
|
41
43
|
recordCount: z.ResourceRecordSetCount ?? 0,
|
|
42
44
|
}));
|
|
43
|
-
|
|
45
|
+
const ec2Instances = (ec2Result.Reservations ?? [])
|
|
46
|
+
.flatMap((r) => r.Instances ?? [])
|
|
47
|
+
.filter((i) => i.State?.Name !== 'terminated')
|
|
48
|
+
.map((i) => ({
|
|
49
|
+
id: i.InstanceId,
|
|
50
|
+
name: i.Tags?.find((t) => t.Key === 'Name')?.Value ?? i.InstanceId,
|
|
51
|
+
type: i.InstanceType ?? 'unknown',
|
|
52
|
+
state: i.State?.Name ?? 'unknown',
|
|
53
|
+
publicIp: i.PublicIpAddress,
|
|
54
|
+
}));
|
|
55
|
+
return { region, distributions, buckets, lambdas, rdsInstances, hostedZones, ec2Instances };
|
|
44
56
|
}
|
|
@@ -24,6 +24,15 @@ export declare class RDSBuilder extends BaseBuilder {
|
|
|
24
24
|
subnets(ids: string[]): this;
|
|
25
25
|
securityGroups(ids: string[]): this;
|
|
26
26
|
publicAccess(enabled?: boolean): this;
|
|
27
|
+
getDiff(existing: any): ({
|
|
28
|
+
field: string;
|
|
29
|
+
declared: string;
|
|
30
|
+
live: any;
|
|
31
|
+
} | {
|
|
32
|
+
field: string;
|
|
33
|
+
declared: boolean;
|
|
34
|
+
live: any;
|
|
35
|
+
})[];
|
|
27
36
|
database(name: string): this;
|
|
28
37
|
credentials(username: string, password: string): this;
|
|
29
38
|
private discoverInstance;
|
|
@@ -54,6 +54,25 @@ export class RDSBuilder extends BaseBuilder {
|
|
|
54
54
|
this._publicAccess = enabled;
|
|
55
55
|
return this;
|
|
56
56
|
}
|
|
57
|
+
getDiff(existing) {
|
|
58
|
+
const diffs = [];
|
|
59
|
+
if (existing.Engine !== this._engine) {
|
|
60
|
+
diffs.push({ field: "engine", declared: this._engine, live: existing.Engine });
|
|
61
|
+
}
|
|
62
|
+
if (existing.EngineVersion !== this._engineVersion) {
|
|
63
|
+
diffs.push({ field: "engineVersion", declared: this._engineVersion, live: existing.EngineVersion });
|
|
64
|
+
}
|
|
65
|
+
if (existing.DBInstanceClass !== this._instanceClass) {
|
|
66
|
+
diffs.push({ field: "instanceClass", declared: this._instanceClass, live: existing.DBInstanceClass });
|
|
67
|
+
}
|
|
68
|
+
if (existing.AllocatedStorage !== this._storage) {
|
|
69
|
+
diffs.push({ field: "storage", declared: `${this._storage} GB`, live: `${existing.AllocatedStorage} GB` });
|
|
70
|
+
}
|
|
71
|
+
if (existing.PubliclyAccessible !== this._publicAccess) {
|
|
72
|
+
diffs.push({ field: "publicAccess", declared: this._publicAccess, live: existing.PubliclyAccessible });
|
|
73
|
+
}
|
|
74
|
+
return diffs;
|
|
75
|
+
}
|
|
57
76
|
database(name) {
|
|
58
77
|
this._dbName = name;
|
|
59
78
|
return this;
|
|
@@ -26,6 +26,15 @@ export declare class DatabaseBuilder extends BaseBuilder {
|
|
|
26
26
|
allowIp(cidr: string): this;
|
|
27
27
|
allowDroplet(dropletId: string): this;
|
|
28
28
|
allowTag(tagName: string): this;
|
|
29
|
+
getDiff(existing: any): ({
|
|
30
|
+
field: string;
|
|
31
|
+
declared: string;
|
|
32
|
+
live: any;
|
|
33
|
+
} | {
|
|
34
|
+
field: string;
|
|
35
|
+
declared: number;
|
|
36
|
+
live: any;
|
|
37
|
+
})[];
|
|
29
38
|
private discoverCluster;
|
|
30
39
|
deploy(): Promise<{
|
|
31
40
|
name: string;
|
|
@@ -58,6 +58,25 @@ export class DatabaseBuilder extends BaseBuilder {
|
|
|
58
58
|
this._firewallRules.push({ type: "tag", value: tagName });
|
|
59
59
|
return this;
|
|
60
60
|
}
|
|
61
|
+
getDiff(existing) {
|
|
62
|
+
const diffs = [];
|
|
63
|
+
if (existing.engine !== this._engine) {
|
|
64
|
+
diffs.push({ field: "engine", declared: this._engine, live: existing.engine });
|
|
65
|
+
}
|
|
66
|
+
if (existing.version !== this._version) {
|
|
67
|
+
diffs.push({ field: "version", declared: this._version, live: existing.version });
|
|
68
|
+
}
|
|
69
|
+
if (existing.size !== this._size) {
|
|
70
|
+
diffs.push({ field: "size", declared: this._size, live: existing.size });
|
|
71
|
+
}
|
|
72
|
+
if (existing.region !== this._region) {
|
|
73
|
+
diffs.push({ field: "region", declared: this._region, live: existing.region });
|
|
74
|
+
}
|
|
75
|
+
if (existing.num_nodes !== this._nodes) {
|
|
76
|
+
diffs.push({ field: "nodes", declared: this._nodes, live: existing.num_nodes });
|
|
77
|
+
}
|
|
78
|
+
return diffs;
|
|
79
|
+
}
|
|
61
80
|
async discoverCluster(name) {
|
|
62
81
|
try {
|
|
63
82
|
const api = getDoApi();
|
|
@@ -104,7 +104,7 @@ export class DomainBuilder extends BaseBuilder {
|
|
|
104
104
|
if (existing) {
|
|
105
105
|
try {
|
|
106
106
|
const res = await api.get(`/domains/${this.domainName}/records?per_page=200`);
|
|
107
|
-
existingRecords = res.domain_records;
|
|
107
|
+
existingRecords = res.domain_records ?? [];
|
|
108
108
|
}
|
|
109
109
|
catch {
|
|
110
110
|
existingRecords = [];
|
|
@@ -12,6 +12,7 @@ export declare class DropletBuilder extends BaseBuilder {
|
|
|
12
12
|
private dropletId?;
|
|
13
13
|
private resolvedIp?;
|
|
14
14
|
private sshKeyPath?;
|
|
15
|
+
private _sshUser?;
|
|
15
16
|
private _provision;
|
|
16
17
|
private _forceConfigCheck;
|
|
17
18
|
constructor(name: string);
|
|
@@ -21,10 +22,19 @@ export declare class DropletBuilder extends BaseBuilder {
|
|
|
21
22
|
image(image: (typeof OS)[keyof typeof OS] | string): this;
|
|
22
23
|
region(region: (typeof REGION)[keyof typeof REGION] | string): this;
|
|
23
24
|
size(size: (typeof SIZE)[keyof typeof SIZE] | string): this;
|
|
25
|
+
sshKey(keyPath: string): this;
|
|
26
|
+
/** @deprecated Use `.sshKey()` instead */
|
|
24
27
|
sslKey(keyPath: string): this;
|
|
28
|
+
sshUser(user: string): this;
|
|
29
|
+
private resolveUser;
|
|
25
30
|
vpc(uuid: string | Output<string>): this;
|
|
26
31
|
provision(...playbookPaths: (string | string[])[]): this;
|
|
27
32
|
forceConfigCheck(): this;
|
|
33
|
+
getDiff(existing: any): {
|
|
34
|
+
field: string;
|
|
35
|
+
declared: any;
|
|
36
|
+
live: any;
|
|
37
|
+
}[];
|
|
28
38
|
protected checkPort(ip: string, port: number): Promise<boolean>;
|
|
29
39
|
protected runProvisioner(ip: string, script: string): Promise<void>;
|
|
30
40
|
private resolveOrRegisterSshKey;
|