puls-dev 0.3.3 → 0.3.4
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/puls.d.ts +1 -0
- package/dist/bin/puls.js +123 -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 +23 -5
- package/dist/core/output.js +8 -1
- package/dist/core/production.test.js +1 -0
- package/dist/core/secret.d.ts +1 -0
- package/dist/core/secret.js +5 -0
- package/dist/core/stack.js +54 -90
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/providers/do/droplet.d.ts +5 -0
- package/dist/providers/do/droplet.js +18 -3
- package/dist/providers/do/droplet.test.js +1 -1
- package/dist/providers/gcp/template.d.ts +3 -0
- package/dist/providers/gcp/template.js +13 -1
- package/dist/providers/gcp/vm.d.ts +3 -0
- package/dist/providers/gcp/vm.js +14 -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/template.d.ts +3 -10
- package/dist/providers/proxmox/template.js +51 -139
- package/dist/providers/proxmox/vm.d.ts +5 -10
- package/dist/providers/proxmox/vm.js +57 -152
- package/package.json +6 -21
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/bin/puls.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
function findTsx() {
|
|
12
|
+
let dir = process.cwd();
|
|
13
|
+
while (true) {
|
|
14
|
+
const candidate = path.join(dir, "node_modules", ".bin", "tsx");
|
|
15
|
+
if (existsSync(candidate))
|
|
16
|
+
return candidate;
|
|
17
|
+
const parent = path.dirname(dir);
|
|
18
|
+
if (parent === dir)
|
|
19
|
+
break;
|
|
20
|
+
dir = parent;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function getVersion() {
|
|
25
|
+
try {
|
|
26
|
+
const pkg = require(path.join(__dirname, "../../package.json"));
|
|
27
|
+
return pkg.version;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return "unknown";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const HELP = `
|
|
34
|
+
Usage:
|
|
35
|
+
puls plan <file> Dry-run the stack — prints what would change, no API writes
|
|
36
|
+
puls deploy <file> Deploy the stack
|
|
37
|
+
puls destroy <file> Destroy the stack
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
--parallel Enable parallel resource execution
|
|
41
|
+
--dry-run Force dry-run mode (alias: same as plan)
|
|
42
|
+
--version Print version and exit
|
|
43
|
+
--help Print this help and exit
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
puls plan infra/staging.ts
|
|
47
|
+
puls deploy infra/staging.ts --parallel
|
|
48
|
+
puls destroy infra/staging.ts
|
|
49
|
+
`.trim();
|
|
50
|
+
let parsed;
|
|
51
|
+
try {
|
|
52
|
+
parsed = parseArgs({
|
|
53
|
+
args: process.argv.slice(2),
|
|
54
|
+
options: {
|
|
55
|
+
parallel: { type: "boolean" },
|
|
56
|
+
"dry-run": { type: "boolean" },
|
|
57
|
+
version: { type: "boolean", short: "v" },
|
|
58
|
+
help: { type: "boolean", short: "h" },
|
|
59
|
+
},
|
|
60
|
+
allowPositionals: true,
|
|
61
|
+
strict: true,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
console.error(`Error: ${err.message}`);
|
|
66
|
+
console.error('Run "puls --help" for usage.');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
const { values, positionals } = parsed;
|
|
70
|
+
if (values.version) {
|
|
71
|
+
console.log(`puls v${getVersion()}`);
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
if (values.help || positionals.length === 0) {
|
|
75
|
+
console.log(HELP);
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
const [command, userFile] = positionals;
|
|
79
|
+
const COMMANDS = ["plan", "deploy", "destroy"];
|
|
80
|
+
if (!COMMANDS.includes(command)) {
|
|
81
|
+
console.error(`Error: Unknown command "${command}". Expected: plan, deploy, or destroy.`);
|
|
82
|
+
console.error('Run "puls --help" for usage.');
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
if (!userFile) {
|
|
86
|
+
console.error(`Error: Missing file argument.\nUsage: puls ${command} <file>`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
const resolvedFile = path.resolve(process.cwd(), userFile);
|
|
90
|
+
if (!existsSync(resolvedFile)) {
|
|
91
|
+
console.error(`Error: File not found: ${resolvedFile}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
const childEnv = { ...process.env };
|
|
95
|
+
if (command === "plan" || values["dry-run"]) {
|
|
96
|
+
childEnv.PULS_DRY_RUN = "true";
|
|
97
|
+
}
|
|
98
|
+
if (command === "destroy") {
|
|
99
|
+
childEnv.PULS_MODE = "destroy";
|
|
100
|
+
}
|
|
101
|
+
if (values.parallel) {
|
|
102
|
+
childEnv.PULS_PARALLEL = "true";
|
|
103
|
+
}
|
|
104
|
+
const tsxBin = findTsx() ?? "tsx";
|
|
105
|
+
const child = spawn(tsxBin, [resolvedFile], {
|
|
106
|
+
stdio: "inherit",
|
|
107
|
+
env: childEnv,
|
|
108
|
+
});
|
|
109
|
+
child.on("error", (err) => {
|
|
110
|
+
if (err.code === "ENOENT") {
|
|
111
|
+
console.error("Error: Could not find tsx. Install it in your project:\n\n" +
|
|
112
|
+
" npm install --save-dev tsx\n\n" +
|
|
113
|
+
"or globally:\n\n" +
|
|
114
|
+
" npm install -g tsx");
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
console.error(`Error spawning tsx: ${err.message}`);
|
|
118
|
+
}
|
|
119
|
+
process.exit(1);
|
|
120
|
+
});
|
|
121
|
+
child.on("close", (code) => {
|
|
122
|
+
process.exit(code ?? 1);
|
|
123
|
+
});
|
package/dist/core/config.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface GlobalConfig {
|
|
|
9
9
|
defaultRegion?: string;
|
|
10
10
|
spacesAccessKey?: string;
|
|
11
11
|
spacesSecretKey?: string;
|
|
12
|
+
sshUser?: string;
|
|
12
13
|
};
|
|
13
14
|
aws?: {
|
|
14
15
|
region: string;
|
|
@@ -23,6 +24,7 @@ export interface GlobalConfig {
|
|
|
23
24
|
dnsDomain?: string;
|
|
24
25
|
dnsServers?: string[];
|
|
25
26
|
verifySsl?: boolean;
|
|
27
|
+
sshUser?: string;
|
|
26
28
|
};
|
|
27
29
|
firebase?: {
|
|
28
30
|
projectId: string;
|
|
@@ -32,6 +34,7 @@ export interface GlobalConfig {
|
|
|
32
34
|
projectId?: string;
|
|
33
35
|
serviceAccountPath?: string;
|
|
34
36
|
region?: string;
|
|
37
|
+
sshUser?: string;
|
|
35
38
|
};
|
|
36
39
|
};
|
|
37
40
|
}
|
package/dist/core/context.d.ts
CHANGED
package/dist/core/decorators.js
CHANGED
|
@@ -23,6 +23,11 @@ function applyConfig(opts) {
|
|
|
23
23
|
},
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
+
// CLI env-var overrides — applied last so `puls plan/destroy/--parallel` wins over decorator options
|
|
27
|
+
if (process.env.PULS_DRY_RUN === "true")
|
|
28
|
+
Config.set({ dryRun: true });
|
|
29
|
+
if (process.env.PULS_PARALLEL === "true")
|
|
30
|
+
Config.set({ parallel: true });
|
|
26
31
|
}
|
|
27
32
|
export function Protected(target, propertyKey) {
|
|
28
33
|
Reflect.defineMetadata("protected", true, target, propertyKey);
|
|
@@ -73,15 +78,22 @@ export function Destroy(optsOrTarget, propertyKey) {
|
|
|
73
78
|
export function Deploy(opts = {}) {
|
|
74
79
|
return function (constructor) {
|
|
75
80
|
const regions = opts.regions ?? [];
|
|
81
|
+
const mode = process.env.PULS_MODE;
|
|
76
82
|
if (regions.length > 0) {
|
|
77
83
|
Promise.resolve().then(async () => {
|
|
78
84
|
for (const r of regions) {
|
|
79
|
-
|
|
85
|
+
const label = mode === "destroy" ? "Tearing down" : "Deploying";
|
|
86
|
+
console.log(`\n🌍 [MULTI-REGION] ${label} stack in region: ${r}`);
|
|
80
87
|
applyConfig({ ...opts, region: r });
|
|
81
88
|
const instance = new constructor();
|
|
82
89
|
Stack._register(constructor, instance, r);
|
|
83
|
-
if (
|
|
84
|
-
|
|
90
|
+
if (mode === "destroy") {
|
|
91
|
+
if (typeof instance.destroy === "function")
|
|
92
|
+
await instance.destroy();
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
if (typeof instance.deploy === "function")
|
|
96
|
+
await instance.deploy();
|
|
85
97
|
}
|
|
86
98
|
}
|
|
87
99
|
});
|
|
@@ -91,8 +103,14 @@ export function Deploy(opts = {}) {
|
|
|
91
103
|
const instance = new constructor();
|
|
92
104
|
Stack._register(constructor, instance);
|
|
93
105
|
Promise.resolve().then(async () => {
|
|
94
|
-
if (
|
|
95
|
-
|
|
106
|
+
if (mode === "destroy") {
|
|
107
|
+
if (typeof instance.destroy === "function")
|
|
108
|
+
await instance.destroy();
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
if (typeof instance.deploy === "function")
|
|
112
|
+
await instance.deploy();
|
|
113
|
+
}
|
|
96
114
|
});
|
|
97
115
|
}
|
|
98
116
|
};
|
package/dist/core/output.js
CHANGED
|
@@ -20,7 +20,14 @@ export class Output {
|
|
|
20
20
|
// Transform this output into a new Output<U> without awaiting it yourself.
|
|
21
21
|
apply(fn) {
|
|
22
22
|
const out = new Output();
|
|
23
|
-
this._promise.then(v =>
|
|
23
|
+
this._promise.then(v => {
|
|
24
|
+
try {
|
|
25
|
+
out.resolve(fn(v));
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
out.reject(e);
|
|
29
|
+
}
|
|
30
|
+
}, err => out.reject(err));
|
|
24
31
|
return out;
|
|
25
32
|
}
|
|
26
33
|
}
|
|
@@ -165,6 +165,7 @@ describe("Production Features Unit Tests", () => {
|
|
|
165
165
|
test("Ansible Provisioner Stack-Wide Dynamic Inventory Generation", async () => {
|
|
166
166
|
const context = {
|
|
167
167
|
stackName: "my-test-stack",
|
|
168
|
+
secrets: new Set(),
|
|
168
169
|
hosts: [
|
|
169
170
|
{ name: "web1", ip: "1.2.3.4", user: "root", sshKey: "/path/to/key", provider: "do" },
|
|
170
171
|
{ name: "db1", ip: "5.6.7.8", user: "ubuntu", sshKey: "/path/to/other-key", provider: "aws" }
|
package/dist/core/secret.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Output } from "./output.js";
|
|
2
2
|
export declare const resolvedSecrets: Set<string>;
|
|
3
|
+
export declare function clearResolvedSecrets(): void;
|
|
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.
|
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.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);
|
|
@@ -102,50 +138,16 @@ export class Stack {
|
|
|
102
138
|
async deploy() {
|
|
103
139
|
const controller = new AbortController();
|
|
104
140
|
const hosts = [];
|
|
141
|
+
// Snapshot current secrets; new secrets resolved during this run are added via context
|
|
142
|
+
const secrets = new Set(resolvedSecrets);
|
|
105
143
|
const context = {
|
|
106
144
|
abortSignal: controller.signal,
|
|
107
145
|
hosts,
|
|
108
|
-
stackName: this.constructor.name
|
|
146
|
+
stackName: this.constructor.name,
|
|
147
|
+
secrets,
|
|
109
148
|
};
|
|
110
149
|
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 {
|
|
150
|
+
return withRedactedConsole(secrets, async () => {
|
|
149
151
|
console.log(`\n🏗️ Deploying Stack: ${this.constructor.name}`);
|
|
150
152
|
// Stack-level beforeDeploy hook
|
|
151
153
|
if (typeof this.beforeDeploy === "function") {
|
|
@@ -187,11 +189,13 @@ export class Stack {
|
|
|
187
189
|
}
|
|
188
190
|
// 2. Schedule execution
|
|
189
191
|
if (isParallel) {
|
|
190
|
-
const startPromise = Promise.resolve();
|
|
191
192
|
const promises = resources.map(({ prop, resource }) => {
|
|
192
193
|
resource._deployPromise = (async () => {
|
|
193
194
|
try {
|
|
194
|
-
|
|
195
|
+
// Yield so the map() loop finishes assigning all _deployPromise values before
|
|
196
|
+
// any task checks its dependencies — a dependency that appears later in the list
|
|
197
|
+
// would otherwise have an undefined _deployPromise and be silently skipped.
|
|
198
|
+
await Promise.resolve();
|
|
195
199
|
if (controller.signal.aborted) {
|
|
196
200
|
throw new Error("Deployment aborted due to previous failure");
|
|
197
201
|
}
|
|
@@ -287,59 +291,21 @@ export class Stack {
|
|
|
287
291
|
await this.afterDeploy(outputs);
|
|
288
292
|
}
|
|
289
293
|
return outputs;
|
|
290
|
-
}
|
|
291
|
-
finally {
|
|
292
|
-
console.log = originalLog;
|
|
293
|
-
}
|
|
294
|
+
});
|
|
294
295
|
});
|
|
295
296
|
}
|
|
296
297
|
async destroy() {
|
|
297
298
|
const controller = new AbortController();
|
|
298
299
|
const hosts = [];
|
|
300
|
+
const secrets = new Set(resolvedSecrets);
|
|
299
301
|
const context = {
|
|
300
302
|
abortSignal: controller.signal,
|
|
301
303
|
hosts,
|
|
302
|
-
stackName: this.constructor.name
|
|
304
|
+
stackName: this.constructor.name,
|
|
305
|
+
secrets,
|
|
303
306
|
};
|
|
304
307
|
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 {
|
|
308
|
+
return withRedactedConsole(secrets, async () => {
|
|
343
309
|
console.log(`\n💥 Tearing down Stack: ${this.constructor.name}`);
|
|
344
310
|
// Stack-level beforeDestroy hook
|
|
345
311
|
if (typeof this.beforeDestroy === "function") {
|
|
@@ -374,12 +340,13 @@ export class Stack {
|
|
|
374
340
|
}
|
|
375
341
|
// 2. Schedule execution
|
|
376
342
|
if (isParallel) {
|
|
377
|
-
const startPromise = Promise.resolve();
|
|
378
343
|
// In parallel destroy, await all dependents (reverse dependencies) first
|
|
379
344
|
const promises = resources.map(({ prop, resource }) => {
|
|
380
345
|
resource._destroyPromise = (async () => {
|
|
381
346
|
try {
|
|
382
|
-
|
|
347
|
+
// Yield so the map() loop finishes assigning all _destroyPromise values before
|
|
348
|
+
// any task checks its dependents (same reason as parallel deploy).
|
|
349
|
+
await Promise.resolve();
|
|
383
350
|
if (controller.signal.aborted) {
|
|
384
351
|
throw new Error("Teardown aborted due to previous failure");
|
|
385
352
|
}
|
|
@@ -458,10 +425,7 @@ export class Stack {
|
|
|
458
425
|
await this.afterDestroy(outputs);
|
|
459
426
|
}
|
|
460
427
|
return outputs;
|
|
461
|
-
}
|
|
462
|
-
finally {
|
|
463
|
-
console.log = originalLog;
|
|
464
|
-
}
|
|
428
|
+
});
|
|
465
429
|
});
|
|
466
430
|
}
|
|
467
431
|
}
|
package/dist/index.d.ts
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";
|
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";
|
|
@@ -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,7 +22,11 @@ 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;
|
|
@@ -24,6 +24,7 @@ export class DropletBuilder extends BaseBuilder {
|
|
|
24
24
|
dropletId;
|
|
25
25
|
resolvedIp;
|
|
26
26
|
sshKeyPath;
|
|
27
|
+
_sshUser;
|
|
27
28
|
_provision = [];
|
|
28
29
|
_forceConfigCheck = false;
|
|
29
30
|
constructor(name) {
|
|
@@ -70,10 +71,24 @@ export class DropletBuilder extends BaseBuilder {
|
|
|
70
71
|
this.config.size = size;
|
|
71
72
|
return this;
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
+
sshKey(keyPath) {
|
|
74
75
|
this.sshKeyPath = keyPath.replace('~', homedir());
|
|
75
76
|
return this;
|
|
76
77
|
}
|
|
78
|
+
/** @deprecated Use `.sshKey()` instead */
|
|
79
|
+
sslKey(keyPath) {
|
|
80
|
+
return this.sshKey(keyPath);
|
|
81
|
+
}
|
|
82
|
+
sshUser(user) {
|
|
83
|
+
this._sshUser = user;
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
resolveUser() {
|
|
87
|
+
return (this._sshUser ??
|
|
88
|
+
process.env.DO_SSH_USER ??
|
|
89
|
+
Config.get().providers.do?.sshUser ??
|
|
90
|
+
"root");
|
|
91
|
+
}
|
|
77
92
|
vpc(uuid) {
|
|
78
93
|
this.config.vpc_uuid = uuid;
|
|
79
94
|
return this;
|
|
@@ -91,7 +106,7 @@ export class DropletBuilder extends BaseBuilder {
|
|
|
91
106
|
}
|
|
92
107
|
async runProvisioner(ip, script) {
|
|
93
108
|
const keyPath = this.sshKeyPath ? this.sshKeyPath : undefined;
|
|
94
|
-
return runProvisioner(ip,
|
|
109
|
+
return runProvisioner(ip, this.resolveUser(), keyPath, script);
|
|
95
110
|
}
|
|
96
111
|
async resolveOrRegisterSshKey(api) {
|
|
97
112
|
const pubPath = this.sshKeyPath.replace(/\.pub$/, '') + '.pub';
|
|
@@ -258,7 +273,7 @@ export class DropletBuilder extends BaseBuilder {
|
|
|
258
273
|
context.hosts.push({
|
|
259
274
|
name: this.name,
|
|
260
275
|
ip: activeIp,
|
|
261
|
-
user:
|
|
276
|
+
user: this.resolveUser(),
|
|
262
277
|
sshKey: this.sshKeyPath,
|
|
263
278
|
provider: "do"
|
|
264
279
|
});
|
|
@@ -106,7 +106,7 @@ describe('DropletBuilder Unit Tests', () => {
|
|
|
106
106
|
builder
|
|
107
107
|
.region('nyc3')
|
|
108
108
|
.size('s-1vcpu-1gb')
|
|
109
|
-
.
|
|
109
|
+
.sshKey('~/.ssh/id_rsa.pub');
|
|
110
110
|
const result = await builder.deploy();
|
|
111
111
|
assert.ok(result);
|
|
112
112
|
assert.strictEqual(result.region, 'nyc3');
|
|
@@ -9,6 +9,7 @@ export declare class GCPTemplateBuilder extends BaseBuilder {
|
|
|
9
9
|
private _zone;
|
|
10
10
|
private _network;
|
|
11
11
|
private _sshKeys;
|
|
12
|
+
private _sshUser?;
|
|
12
13
|
private _provision;
|
|
13
14
|
constructor(name: string);
|
|
14
15
|
baseImage(img: string): this;
|
|
@@ -16,6 +17,8 @@ export declare class GCPTemplateBuilder extends BaseBuilder {
|
|
|
16
17
|
zone(z: string): this;
|
|
17
18
|
network(netPath: string): this;
|
|
18
19
|
sshKey(keys: string | string[]): this;
|
|
20
|
+
sshUser(user: string): this;
|
|
21
|
+
private resolveUser;
|
|
19
22
|
provision(...playbookPaths: (string | string[])[]): this;
|
|
20
23
|
private discoverImage;
|
|
21
24
|
protected checkPort(ip: string, port: number): Promise<boolean>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { BaseBuilder } from "../../core/resource.js";
|
|
4
|
+
import { Config } from "../../core/config.js";
|
|
4
5
|
import { Output } from "../../core/output.js";
|
|
5
6
|
import { gcpFetch, getProjectId } from "./api.js";
|
|
6
7
|
import { checkPort, runProvisioner } from "../../core/provisioner.js";
|
|
@@ -15,6 +16,7 @@ export class GCPTemplateBuilder extends BaseBuilder {
|
|
|
15
16
|
_zone = "us-central1-a";
|
|
16
17
|
_network = "global/networks/default";
|
|
17
18
|
_sshKeys = [];
|
|
19
|
+
_sshUser;
|
|
18
20
|
_provision = [];
|
|
19
21
|
constructor(name) {
|
|
20
22
|
super(name);
|
|
@@ -41,6 +43,16 @@ export class GCPTemplateBuilder extends BaseBuilder {
|
|
|
41
43
|
this._sshKeys = keys;
|
|
42
44
|
return this;
|
|
43
45
|
}
|
|
46
|
+
sshUser(user) {
|
|
47
|
+
this._sshUser = user;
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
resolveUser() {
|
|
51
|
+
return (this._sshUser ??
|
|
52
|
+
process.env.GCP_SSH_USER ??
|
|
53
|
+
Config.get().providers.gcp?.sshUser ??
|
|
54
|
+
"root");
|
|
55
|
+
}
|
|
44
56
|
provision(...playbookPaths) {
|
|
45
57
|
this._provision.push(...playbookPaths.flat());
|
|
46
58
|
return this;
|
|
@@ -66,7 +78,7 @@ export class GCPTemplateBuilder extends BaseBuilder {
|
|
|
66
78
|
async runProvisioner(ip, script) {
|
|
67
79
|
const keysArray = Array.isArray(this._sshKeys) ? this._sshKeys : [this._sshKeys];
|
|
68
80
|
const keyPath = keysArray.find(k => !k.startsWith('ssh-') && !k.startsWith('ecdsa-') && !k.startsWith('sk-'));
|
|
69
|
-
return runProvisioner(ip,
|
|
81
|
+
return runProvisioner(ip, this.resolveUser(), keyPath, script);
|
|
70
82
|
}
|
|
71
83
|
async deploy() {
|
|
72
84
|
const dryRun = this.isDryRunActive();
|
|
@@ -12,6 +12,7 @@ export declare class GCPVMBuilder extends BaseBuilder {
|
|
|
12
12
|
private _zone;
|
|
13
13
|
private _network;
|
|
14
14
|
private _sshKeys;
|
|
15
|
+
private _sshUser?;
|
|
15
16
|
private _provision;
|
|
16
17
|
private _forceConfigCheck;
|
|
17
18
|
private resolvedInstanceId?;
|
|
@@ -23,6 +24,8 @@ export declare class GCPVMBuilder extends BaseBuilder {
|
|
|
23
24
|
zone(z: string): this;
|
|
24
25
|
network(netPath: string): this;
|
|
25
26
|
sshKey(keys: string | string[]): this;
|
|
27
|
+
sshUser(user: string): this;
|
|
28
|
+
private resolveUser;
|
|
26
29
|
provision(...playbookPaths: (string | string[])[]): this;
|
|
27
30
|
forceConfigCheck(): this;
|
|
28
31
|
protected checkPort(ip: string, port: number): Promise<boolean>;
|