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
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
-
import { createConnection } from "node:net";
|
|
5
4
|
import { BaseBuilder } from "../../core/resource.js";
|
|
6
5
|
import { Config } from "../../core/config.js";
|
|
7
6
|
import { Output } from "../../core/output.js";
|
|
8
7
|
import { getPMClient } from "./api.js";
|
|
9
8
|
import { getFileHash, parseProvisionMetadata, mergeProvisionMetadata } from "./hash.js";
|
|
9
|
+
import { checkPort, runProvisioner } from "../../core/provisioner.js";
|
|
10
|
+
import { resourceContextStorage } from "../../core/context.js";
|
|
10
11
|
export class VMBuilder extends BaseBuilder {
|
|
11
12
|
out = {
|
|
12
13
|
ip: new Output(),
|
|
@@ -16,6 +17,7 @@ export class VMBuilder extends BaseBuilder {
|
|
|
16
17
|
resolvedNode = null;
|
|
17
18
|
resolvedIp = null;
|
|
18
19
|
_image;
|
|
20
|
+
_templateSource;
|
|
19
21
|
_cores = 2;
|
|
20
22
|
_memory = 2048;
|
|
21
23
|
_provision = [];
|
|
@@ -26,6 +28,7 @@ export class VMBuilder extends BaseBuilder {
|
|
|
26
28
|
_ip;
|
|
27
29
|
_sshKeys;
|
|
28
30
|
_machine = "q35";
|
|
31
|
+
_forceConfigCheck = false;
|
|
29
32
|
constructor(name) {
|
|
30
33
|
super(name);
|
|
31
34
|
this.discoveryPromise = this.discoverVm(name);
|
|
@@ -40,7 +43,8 @@ export class VMBuilder extends BaseBuilder {
|
|
|
40
43
|
const config = await pm.get(`/nodes/${match.node}/qemu/${match.vmid}/config`);
|
|
41
44
|
match.description = config.description ?? "";
|
|
42
45
|
}
|
|
43
|
-
catch {
|
|
46
|
+
catch (err) {
|
|
47
|
+
console.warn(` ⚠️ Could not fetch VM config for ${match.vmid}: ${err.message}`);
|
|
44
48
|
match.description = "";
|
|
45
49
|
}
|
|
46
50
|
}
|
|
@@ -56,6 +60,11 @@ export class VMBuilder extends BaseBuilder {
|
|
|
56
60
|
this._image = os;
|
|
57
61
|
return this;
|
|
58
62
|
}
|
|
63
|
+
fromTemplate(template) {
|
|
64
|
+
this._templateSource = template;
|
|
65
|
+
this.dependsOn(template);
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
59
68
|
cores(n) {
|
|
60
69
|
this._cores = n;
|
|
61
70
|
return this;
|
|
@@ -96,6 +105,10 @@ export class VMBuilder extends BaseBuilder {
|
|
|
96
105
|
this._machine = type;
|
|
97
106
|
return this;
|
|
98
107
|
}
|
|
108
|
+
forceConfigCheck() {
|
|
109
|
+
this._forceConfigCheck = true;
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
99
112
|
async deploy() {
|
|
100
113
|
const dryRun = this.isDryRunActive();
|
|
101
114
|
const existing = await this.discoveryPromise;
|
|
@@ -116,10 +129,12 @@ export class VMBuilder extends BaseBuilder {
|
|
|
116
129
|
const baseName = p.split("/").pop() ?? p;
|
|
117
130
|
return { path: p, baseName, hash: getFileHash(p) };
|
|
118
131
|
});
|
|
119
|
-
const playbooksToRun =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
132
|
+
const playbooksToRun = this._forceConfigCheck
|
|
133
|
+
? declaredPlaybooksWithHashes
|
|
134
|
+
: declaredPlaybooksWithHashes.filter((p) => {
|
|
135
|
+
const appliedHash = appliedHashes[p.baseName];
|
|
136
|
+
return !appliedHash || appliedHash !== p.hash;
|
|
137
|
+
});
|
|
123
138
|
if (playbooksToRun.length > 0) {
|
|
124
139
|
console.log(`\n🖥️ Finalizing Proxmox VM "${this.name}"...`);
|
|
125
140
|
console.log(` ✅ VM "${this.name}" already exists (vmid=${existing.vmid}, node=${existing.node}, status=${existing.status})`);
|
|
@@ -171,6 +186,8 @@ export class VMBuilder extends BaseBuilder {
|
|
|
171
186
|
console.log(` 📝 [PLAN] Create VM "${this.name}"`);
|
|
172
187
|
if (this._image)
|
|
173
188
|
console.log(` └─ Image: ${this._image}`);
|
|
189
|
+
if (this._templateSource)
|
|
190
|
+
console.log(` └─ Template: ${this._templateSource.name}`);
|
|
174
191
|
console.log(` └─ Cores: ${this._cores} Memory: ${this._memory} MB Machine: ${this._machine}`);
|
|
175
192
|
if (this._vlan)
|
|
176
193
|
console.log(` └─ VLAN: ${this._vlan}`);
|
|
@@ -184,19 +201,27 @@ export class VMBuilder extends BaseBuilder {
|
|
|
184
201
|
return { name: this.name, vmid: "PENDING" };
|
|
185
202
|
}
|
|
186
203
|
// Find the template - match by VMID (numeric string) or name substring
|
|
204
|
+
let sourceVmid;
|
|
205
|
+
if (this._templateSource) {
|
|
206
|
+
const v = await this._templateSource.out.vmid.get();
|
|
207
|
+
sourceVmid = String(v);
|
|
208
|
+
}
|
|
209
|
+
else if (this._image) {
|
|
210
|
+
sourceVmid = String(this._image);
|
|
211
|
+
}
|
|
187
212
|
const resources = await pm.get("/cluster/resources?type=vm");
|
|
188
|
-
const isVmid =
|
|
189
|
-
const template =
|
|
213
|
+
const isVmid = sourceVmid && /^\d+$/.test(sourceVmid);
|
|
214
|
+
const template = sourceVmid
|
|
190
215
|
? (resources ?? []).find((r) => r.template === 1 &&
|
|
191
216
|
(isVmid
|
|
192
|
-
? String(r.vmid) ===
|
|
193
|
-
: r.name?.includes(
|
|
217
|
+
? String(r.vmid) === sourceVmid
|
|
218
|
+
: r.name?.includes(sourceVmid)))
|
|
194
219
|
: null;
|
|
195
|
-
if (
|
|
196
|
-
throw new Error(`No Proxmox template found matching "${
|
|
220
|
+
if (sourceVmid && !template) {
|
|
221
|
+
throw new Error(`No Proxmox template found matching "${sourceVmid}". ` +
|
|
197
222
|
(isVmid
|
|
198
|
-
? `Check that VMID ${
|
|
199
|
-
: `Create a template whose name contains "${
|
|
223
|
+
? `Check that VMID ${sourceVmid} exists and is marked as a template.`
|
|
224
|
+
: `Create a template whose name contains "${sourceVmid}".`));
|
|
200
225
|
}
|
|
201
226
|
// Resolve target node: explicit → cluster-aware (online & max free RAM) → configured nodes list → template's node → API discovery
|
|
202
227
|
let node = this._node;
|
|
@@ -351,6 +376,19 @@ export class VMBuilder extends BaseBuilder {
|
|
|
351
376
|
if (this._replace) {
|
|
352
377
|
await this.destroyVmByName(this._replace, pm);
|
|
353
378
|
}
|
|
379
|
+
const context = resourceContextStorage.getStore();
|
|
380
|
+
if (context && context.hosts) {
|
|
381
|
+
const activeIp = this.resolvedIp ?? "0.0.0.0";
|
|
382
|
+
if (!context.hosts.some(h => h.name === this.name)) {
|
|
383
|
+
context.hosts.push({
|
|
384
|
+
name: this.name,
|
|
385
|
+
ip: activeIp,
|
|
386
|
+
user: "root",
|
|
387
|
+
sshKey: this.sshKeyPath(),
|
|
388
|
+
provider: "proxmox"
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
354
392
|
return { name: this.name, vmid: this.resolvedVmid, ip: this.resolvedIp };
|
|
355
393
|
}
|
|
356
394
|
async resolveExistingIp(node, vmid, pm) {
|
|
@@ -480,19 +518,11 @@ export class VMBuilder extends BaseBuilder {
|
|
|
480
518
|
proc.on("error", () => resolve(false));
|
|
481
519
|
});
|
|
482
520
|
}
|
|
483
|
-
checkPort(ip, port) {
|
|
484
|
-
return
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
resolve(true);
|
|
489
|
-
});
|
|
490
|
-
socket.on("timeout", () => {
|
|
491
|
-
socket.destroy();
|
|
492
|
-
resolve(false);
|
|
493
|
-
});
|
|
494
|
-
socket.on("error", () => resolve(false));
|
|
495
|
-
});
|
|
521
|
+
async checkPort(ip, port) {
|
|
522
|
+
return checkPort(ip, port);
|
|
523
|
+
}
|
|
524
|
+
async runProvisioner(ip, script) {
|
|
525
|
+
return runProvisioner(ip, "root", this._sshKeys, script);
|
|
496
526
|
}
|
|
497
527
|
sshKeyPath() {
|
|
498
528
|
const keyInput = Array.isArray(this._sshKeys)
|
|
@@ -505,79 +535,4 @@ export class VMBuilder extends BaseBuilder {
|
|
|
505
535
|
? keyInput.replace(/\.pub$/, "")
|
|
506
536
|
: `${homedir()}/.ssh/id_ed25519`).replace(/^~/, homedir());
|
|
507
537
|
}
|
|
508
|
-
runProvisioner(ip, script) {
|
|
509
|
-
const ext = script.split(".").pop()?.toLowerCase();
|
|
510
|
-
if (ext === "sh") {
|
|
511
|
-
throw new Error(`Shell script provisioning (.sh) is no longer supported. ` +
|
|
512
|
-
`Please migrate "${script}" to an Ansible playbook (.yaml/.yml).`);
|
|
513
|
-
}
|
|
514
|
-
if (ext === "pp")
|
|
515
|
-
return this.runPuppet(ip, script);
|
|
516
|
-
return this.runAnsible(ip, script); // .yml / .yaml
|
|
517
|
-
}
|
|
518
|
-
runPuppet(ip, manifest) {
|
|
519
|
-
console.log(` 🔧 Applying Puppet manifest: ${manifest} → ${ip}`);
|
|
520
|
-
const keyPath = this.sshKeyPath();
|
|
521
|
-
// Copy manifest then apply it
|
|
522
|
-
return new Promise((resolve, reject) => {
|
|
523
|
-
const scp = spawn("scp", [
|
|
524
|
-
"-i",
|
|
525
|
-
keyPath,
|
|
526
|
-
"-o",
|
|
527
|
-
"StrictHostKeyChecking=no",
|
|
528
|
-
manifest,
|
|
529
|
-
`root@${ip}:/tmp/manifest.pp`,
|
|
530
|
-
], { stdio: "inherit" });
|
|
531
|
-
scp.on("close", (code) => {
|
|
532
|
-
if (code !== 0) {
|
|
533
|
-
reject(new Error(`scp exited with code ${code}`));
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
const puppet = spawn("ssh", [
|
|
537
|
-
"-i",
|
|
538
|
-
keyPath,
|
|
539
|
-
"-o",
|
|
540
|
-
"StrictHostKeyChecking=no",
|
|
541
|
-
`root@${ip}`,
|
|
542
|
-
"puppet apply /tmp/manifest.pp",
|
|
543
|
-
], { stdio: "inherit" });
|
|
544
|
-
puppet.on("close", (c) => {
|
|
545
|
-
if (c === 0) {
|
|
546
|
-
console.log(` ✅ Provisioning complete`);
|
|
547
|
-
resolve();
|
|
548
|
-
}
|
|
549
|
-
else
|
|
550
|
-
reject(new Error(`puppet apply exited with code ${c}`));
|
|
551
|
-
});
|
|
552
|
-
puppet.on("error", (err) => reject(new Error(`Failed to run puppet: ${err.message}`)));
|
|
553
|
-
});
|
|
554
|
-
scp.on("error", (err) => reject(new Error(`Failed to run scp: ${err.message}`)));
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
runAnsible(ip, playbook) {
|
|
558
|
-
console.log(` 🔧 Running Ansible: ${playbook} → ${ip}`);
|
|
559
|
-
const keyPath = this.sshKeyPath();
|
|
560
|
-
return new Promise((resolve, reject) => {
|
|
561
|
-
const proc = spawn("ansible-playbook", [
|
|
562
|
-
playbook,
|
|
563
|
-
"-i",
|
|
564
|
-
`${ip},`,
|
|
565
|
-
"-u",
|
|
566
|
-
"root",
|
|
567
|
-
"--private-key",
|
|
568
|
-
keyPath,
|
|
569
|
-
"--ssh-extra-args",
|
|
570
|
-
"-o StrictHostKeyChecking=no -o ConnectTimeout=30",
|
|
571
|
-
], { stdio: "inherit" });
|
|
572
|
-
proc.on("close", (code) => {
|
|
573
|
-
if (code === 0) {
|
|
574
|
-
console.log(` ✅ Provisioning complete`);
|
|
575
|
-
resolve();
|
|
576
|
-
}
|
|
577
|
-
else
|
|
578
|
-
reject(new Error(`ansible-playbook exited with code ${code}`));
|
|
579
|
-
});
|
|
580
|
-
proc.on("error", (err) => reject(new Error(`Failed to run ansible-playbook: ${err.message}`)));
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
538
|
}
|
|
@@ -1,9 +1,20 @@
|
|
|
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
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
1
10
|
import { test, describe, beforeEach, afterEach } from "node:test";
|
|
2
11
|
import assert from "node:assert";
|
|
3
12
|
import { ProxmoxApiClient } from "./api.js";
|
|
4
13
|
import { VMBuilder } from "./vm.js";
|
|
5
14
|
import { Config } from "../../core/config.js";
|
|
6
15
|
import { getFileHash, parseProvisionMetadata, mergeProvisionMetadata } from "./hash.js";
|
|
16
|
+
import { Stack } from "../../core/stack.js";
|
|
17
|
+
import { ForceConfigCheck } from "../../core/decorators.js";
|
|
7
18
|
describe("Proxmox VMBuilder Unit Tests", () => {
|
|
8
19
|
let originalGet;
|
|
9
20
|
let originalPost;
|
|
@@ -263,6 +274,72 @@ describe("Proxmox VMBuilder Unit Tests", () => {
|
|
|
263
274
|
assert.strictEqual(updateConfigCall.body.description, expectedDescription);
|
|
264
275
|
assert.ok(expectedDescription.startsWith("User notes preserved"));
|
|
265
276
|
});
|
|
277
|
+
test("forceConfigCheck() builder method forces playbook execution even if hashes match", async () => {
|
|
278
|
+
const nginxHash = getFileHash("playbooks/nginx.yaml");
|
|
279
|
+
const descriptionNotes = mergeProvisionMetadata("User notes", {
|
|
280
|
+
"nginx.yaml": nginxHash,
|
|
281
|
+
});
|
|
282
|
+
mockGetResponses["/cluster/resources?type=vm"] = [
|
|
283
|
+
{ name: "force-vm", vmid: 200, node: "pve1", template: 0, status: "running" },
|
|
284
|
+
];
|
|
285
|
+
mockGetResponses["/nodes/pve1/qemu/200/config"] = {
|
|
286
|
+
description: descriptionNotes,
|
|
287
|
+
};
|
|
288
|
+
const builder = new VMBuilder("force-vm")
|
|
289
|
+
.ip("10.8.10.95")
|
|
290
|
+
.provision("playbooks/nginx.yaml")
|
|
291
|
+
.forceConfigCheck();
|
|
292
|
+
const provisionCalls = [];
|
|
293
|
+
// Overrides
|
|
294
|
+
builder.waitFor = async (label, condition) => {
|
|
295
|
+
return await condition();
|
|
296
|
+
};
|
|
297
|
+
builder.checkPort = async () => true;
|
|
298
|
+
builder.checkCloudInit = async () => true;
|
|
299
|
+
builder.runProvisioner = async (ip, script) => {
|
|
300
|
+
provisionCalls.push({ ip, script });
|
|
301
|
+
};
|
|
302
|
+
const deployResult = await builder.deploy();
|
|
303
|
+
assert.strictEqual(deployResult.vmid, 200);
|
|
304
|
+
// Verify playbook WAS executed because of forceConfigCheck()
|
|
305
|
+
assert.strictEqual(provisionCalls.length, 1);
|
|
306
|
+
assert.strictEqual(provisionCalls[0].script, "playbooks/nginx.yaml");
|
|
307
|
+
});
|
|
308
|
+
test("ForceConfigCheck decorator forces playbook execution even if hashes match", async () => {
|
|
309
|
+
const nginxHash = getFileHash("playbooks/nginx.yaml");
|
|
310
|
+
const descriptionNotes = mergeProvisionMetadata("User notes", {
|
|
311
|
+
"nginx.yaml": nginxHash,
|
|
312
|
+
});
|
|
313
|
+
mockGetResponses["/cluster/resources?type=vm"] = [
|
|
314
|
+
{ name: "force-dec-vm", vmid: 200, node: "pve1", template: 0, status: "running" },
|
|
315
|
+
];
|
|
316
|
+
mockGetResponses["/nodes/pve1/qemu/200/config"] = {
|
|
317
|
+
description: descriptionNotes,
|
|
318
|
+
};
|
|
319
|
+
const provisionCalls = [];
|
|
320
|
+
class TestStack extends Stack {
|
|
321
|
+
server = new VMBuilder("force-dec-vm")
|
|
322
|
+
.ip("10.8.10.95")
|
|
323
|
+
.provision("playbooks/nginx.yaml");
|
|
324
|
+
}
|
|
325
|
+
__decorate([
|
|
326
|
+
ForceConfigCheck,
|
|
327
|
+
__metadata("design:type", Object)
|
|
328
|
+
], TestStack.prototype, "server", void 0);
|
|
329
|
+
const stack = new TestStack();
|
|
330
|
+
stack.server.waitFor = async (label, condition) => {
|
|
331
|
+
return await condition();
|
|
332
|
+
};
|
|
333
|
+
stack.server.checkPort = async () => true;
|
|
334
|
+
stack.server.checkCloudInit = async () => true;
|
|
335
|
+
stack.server.runProvisioner = async (ip, script) => {
|
|
336
|
+
provisionCalls.push({ ip, script });
|
|
337
|
+
};
|
|
338
|
+
await stack.deploy();
|
|
339
|
+
// Verify playbook WAS executed because of @ForceConfigCheck decorator
|
|
340
|
+
assert.strictEqual(provisionCalls.length, 1);
|
|
341
|
+
assert.strictEqual(provisionCalls[0].script, "playbooks/nginx.yaml");
|
|
342
|
+
});
|
|
266
343
|
});
|
|
267
344
|
describe("Proxmox Provision Hash & Metadata Utilities", () => {
|
|
268
345
|
test("parseProvisionMetadata parses valid, invalid, and empty strings", () => {
|
|
@@ -75,13 +75,56 @@ export interface AwsInventory {
|
|
|
75
75
|
rdsInstances: AwsRdsInstance[];
|
|
76
76
|
hostedZones: AwsHostedZone[];
|
|
77
77
|
}
|
|
78
|
+
export interface GcpVM {
|
|
79
|
+
name: string;
|
|
80
|
+
zone: string;
|
|
81
|
+
machineType: string;
|
|
82
|
+
status: string;
|
|
83
|
+
ip: string;
|
|
84
|
+
}
|
|
85
|
+
export interface GcpCloudSQL {
|
|
86
|
+
name: string;
|
|
87
|
+
engine: string;
|
|
88
|
+
tier: string;
|
|
89
|
+
status: string;
|
|
90
|
+
}
|
|
91
|
+
export interface GcpCloudRun {
|
|
92
|
+
name: string;
|
|
93
|
+
region: string;
|
|
94
|
+
url: string;
|
|
95
|
+
}
|
|
96
|
+
export interface GcpCloudDNS {
|
|
97
|
+
name: string;
|
|
98
|
+
dnsName: string;
|
|
99
|
+
}
|
|
100
|
+
export interface GcpInventory {
|
|
101
|
+
vms: GcpVM[];
|
|
102
|
+
rdsInstances: GcpCloudSQL[];
|
|
103
|
+
distributions: GcpCloudRun[];
|
|
104
|
+
hostedZones: GcpCloudDNS[];
|
|
105
|
+
}
|
|
106
|
+
export interface FirebaseHosting {
|
|
107
|
+
site: string;
|
|
108
|
+
}
|
|
109
|
+
export interface FirebaseFunction {
|
|
110
|
+
name: string;
|
|
111
|
+
region: string;
|
|
112
|
+
entryPoint: string;
|
|
113
|
+
runtime: string;
|
|
114
|
+
}
|
|
115
|
+
export interface FirebaseInventory {
|
|
116
|
+
hostingSites: FirebaseHosting[];
|
|
117
|
+
functions: FirebaseFunction[];
|
|
118
|
+
}
|
|
78
119
|
export interface InventoryError {
|
|
79
|
-
provider: "proxmox" | "do" | "aws";
|
|
120
|
+
provider: "proxmox" | "do" | "aws" | "gcp" | "firebase";
|
|
80
121
|
message: string;
|
|
81
122
|
}
|
|
82
123
|
export interface InventoryResult {
|
|
83
124
|
proxmox?: ProxmoxInventory;
|
|
84
125
|
do?: DoInventory;
|
|
85
126
|
aws?: AwsInventory;
|
|
127
|
+
gcp?: GcpInventory;
|
|
128
|
+
firebase?: FirebaseInventory;
|
|
86
129
|
errors: InventoryError[];
|
|
87
130
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "puls-dev",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "Intent-driven infrastructure-as-code with eager discovery and no state files.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"@aws-sdk/client-secrets-manager": "^3.1053.0",
|
|
71
71
|
"@aws-sdk/client-sns": "^3.1053.0",
|
|
72
72
|
"@aws-sdk/client-sqs": "^3.1053.0",
|
|
73
|
+
"@aws-sdk/client-ssm": "^3.1053.0",
|
|
73
74
|
"@types/node": "^25.6.2",
|
|
74
75
|
"google-auth-library": "^10.6.2",
|
|
75
76
|
"ts-node": "^10.9.2",
|
|
@@ -94,6 +95,7 @@
|
|
|
94
95
|
"@aws-sdk/client-secrets-manager": "^3.1053.0",
|
|
95
96
|
"@aws-sdk/client-sns": "^3.1053.0",
|
|
96
97
|
"@aws-sdk/client-sqs": "^3.1053.0",
|
|
98
|
+
"@aws-sdk/client-ssm": "^3.1053.0",
|
|
97
99
|
"dotenv": "^17.4.2",
|
|
98
100
|
"google-auth-library": "^10.6.2",
|
|
99
101
|
"reflect-metadata": "^0.2.2",
|