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.
Files changed (117) hide show
  1. package/dist/core/checker.js +71 -0
  2. package/dist/core/config.d.ts +6 -0
  3. package/dist/core/config.js +11 -1
  4. package/dist/core/context.d.ts +14 -0
  5. package/dist/core/context.js +2 -0
  6. package/dist/core/decorators.d.ts +4 -0
  7. package/dist/core/decorators.js +56 -30
  8. package/dist/core/hooks.d.ts +21 -0
  9. package/dist/core/hooks.js +116 -0
  10. package/dist/core/hooks.test.d.ts +1 -0
  11. package/dist/core/hooks.test.js +194 -0
  12. package/dist/core/multiregion.test.d.ts +1 -0
  13. package/dist/core/multiregion.test.js +87 -0
  14. package/dist/core/output.d.ts +2 -0
  15. package/dist/core/output.js +9 -2
  16. package/dist/core/parallel.test.d.ts +1 -0
  17. package/dist/core/parallel.test.js +215 -0
  18. package/dist/core/parser.d.ts +10 -0
  19. package/dist/core/parser.js +140 -0
  20. package/dist/core/parser.test.d.ts +1 -0
  21. package/dist/core/parser.test.js +117 -0
  22. package/dist/core/production.test.d.ts +1 -0
  23. package/dist/core/production.test.js +189 -0
  24. package/dist/core/provisioner.d.ts +4 -0
  25. package/dist/core/provisioner.js +123 -0
  26. package/dist/core/resource.d.ts +23 -0
  27. package/dist/core/resource.js +54 -0
  28. package/dist/core/retry.d.ts +9 -0
  29. package/dist/core/retry.js +28 -0
  30. package/dist/core/retry.test.d.ts +1 -0
  31. package/dist/core/retry.test.js +66 -0
  32. package/dist/core/secret.d.ts +41 -0
  33. package/dist/core/secret.js +105 -0
  34. package/dist/core/secret.test.d.ts +1 -0
  35. package/dist/core/secret.test.js +166 -0
  36. package/dist/core/stack.d.ts +4 -3
  37. package/dist/core/stack.js +322 -48
  38. package/dist/index.d.ts +3 -0
  39. package/dist/index.js +3 -0
  40. package/dist/providers/aws/api.js +97 -17
  41. package/dist/providers/aws/ec2.d.ts +51 -0
  42. package/dist/providers/aws/ec2.js +331 -0
  43. package/dist/providers/aws/ec2.test.d.ts +1 -0
  44. package/dist/providers/aws/ec2.test.js +281 -0
  45. package/dist/providers/aws/index.d.ts +4 -0
  46. package/dist/providers/aws/index.js +4 -0
  47. package/dist/providers/aws/route53.d.ts +1 -0
  48. package/dist/providers/aws/route53.js +15 -2
  49. package/dist/providers/aws/route53.test.js +47 -0
  50. package/dist/providers/aws/template.d.ts +34 -0
  51. package/dist/providers/aws/template.js +252 -0
  52. package/dist/providers/aws/template.test.d.ts +1 -0
  53. package/dist/providers/aws/template.test.js +208 -0
  54. package/dist/providers/do/api.d.ts +3 -1
  55. package/dist/providers/do/api.js +126 -27
  56. package/dist/providers/do/app.d.ts +26 -0
  57. package/dist/providers/do/app.js +124 -0
  58. package/dist/providers/do/app.test.d.ts +1 -0
  59. package/dist/providers/do/app.test.js +268 -0
  60. package/dist/providers/do/database.d.ts +44 -0
  61. package/dist/providers/do/database.js +208 -0
  62. package/dist/providers/do/database.test.d.ts +1 -0
  63. package/dist/providers/do/database.test.js +293 -0
  64. package/dist/providers/do/domain.d.ts +2 -0
  65. package/dist/providers/do/domain.js +30 -0
  66. package/dist/providers/do/domain.test.js +49 -0
  67. package/dist/providers/do/droplet.d.ts +9 -0
  68. package/dist/providers/do/droplet.js +146 -8
  69. package/dist/providers/do/droplet.test.js +228 -1
  70. package/dist/providers/do/firewall.d.ts +2 -1
  71. package/dist/providers/do/firewall.js +23 -9
  72. package/dist/providers/do/firewall.test.js +54 -0
  73. package/dist/providers/do/index.d.ts +11 -0
  74. package/dist/providers/do/index.js +8 -0
  75. package/dist/providers/do/spaces.d.ts +27 -0
  76. package/dist/providers/do/spaces.js +142 -0
  77. package/dist/providers/do/spaces.test.d.ts +1 -0
  78. package/dist/providers/do/spaces.test.js +180 -0
  79. package/dist/providers/do/spaces_api.d.ts +2 -0
  80. package/dist/providers/do/spaces_api.js +20 -0
  81. package/dist/providers/do/vpc.d.ts +30 -0
  82. package/dist/providers/do/vpc.js +128 -0
  83. package/dist/providers/do/vpc.test.d.ts +1 -0
  84. package/dist/providers/do/vpc.test.js +258 -0
  85. package/dist/providers/firebase/api.js +92 -29
  86. package/dist/providers/firebase/list.d.ts +2 -0
  87. package/dist/providers/firebase/list.js +25 -0
  88. package/dist/providers/gcp/api.js +88 -14
  89. package/dist/providers/gcp/clouddns.d.ts +1 -0
  90. package/dist/providers/gcp/clouddns.js +15 -2
  91. package/dist/providers/gcp/clouddns.test.js +45 -0
  92. package/dist/providers/gcp/index.d.ts +5 -1
  93. package/dist/providers/gcp/index.js +5 -1
  94. package/dist/providers/gcp/list.d.ts +2 -0
  95. package/dist/providers/gcp/list.js +55 -0
  96. package/dist/providers/gcp/secrets.js +1 -1
  97. package/dist/providers/gcp/template.d.ts +32 -0
  98. package/dist/providers/gcp/template.js +252 -0
  99. package/dist/providers/gcp/template.test.d.ts +1 -0
  100. package/dist/providers/gcp/template.test.js +227 -0
  101. package/dist/providers/gcp/vm.d.ts +48 -0
  102. package/dist/providers/gcp/vm.js +375 -0
  103. package/dist/providers/gcp/vm.test.d.ts +1 -0
  104. package/dist/providers/gcp/vm.test.js +321 -0
  105. package/dist/providers/proxmox/api.d.ts +1 -0
  106. package/dist/providers/proxmox/api.js +72 -16
  107. package/dist/providers/proxmox/index.d.ts +2 -0
  108. package/dist/providers/proxmox/index.js +2 -0
  109. package/dist/providers/proxmox/template.d.ts +44 -0
  110. package/dist/providers/proxmox/template.js +349 -0
  111. package/dist/providers/proxmox/template.test.d.ts +1 -0
  112. package/dist/providers/proxmox/template.test.js +179 -0
  113. package/dist/providers/proxmox/vm.d.ts +7 -4
  114. package/dist/providers/proxmox/vm.js +57 -102
  115. package/dist/providers/proxmox/vm.test.js +77 -0
  116. package/dist/types/inventory.d.ts +44 -1
  117. package/package.json +3 -1
@@ -0,0 +1,375 @@
1
+ import fs from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { BaseBuilder } from "../../core/resource.js";
4
+ import { Output } from "../../core/output.js";
5
+ import { gcpFetch, getProjectId } from "./api.js";
6
+ import { checkPort, runProvisioner } from "../../core/provisioner.js";
7
+ import { getFileHash } from "../proxmox/hash.js";
8
+ import { resourceContextStorage } from "../../core/context.js";
9
+ export class GCPVMBuilder extends BaseBuilder {
10
+ out = {
11
+ ip: new Output(),
12
+ id: new Output(),
13
+ };
14
+ _machineType = "e2-micro";
15
+ _image = "projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts";
16
+ _templateSource;
17
+ _zone = "us-central1-a";
18
+ _network = "global/networks/default";
19
+ _sshKeys = [];
20
+ _provision = [];
21
+ _forceConfigCheck = false;
22
+ resolvedInstanceId;
23
+ resolvedIp;
24
+ constructor(name) {
25
+ super(name);
26
+ this.discoveryPromise = this.discoverVM();
27
+ }
28
+ machineType(type) {
29
+ this._machineType = type;
30
+ return this;
31
+ }
32
+ image(img) {
33
+ this._image = img;
34
+ return this;
35
+ }
36
+ fromTemplate(template) {
37
+ this._templateSource = template;
38
+ this.dependsOn(template);
39
+ return this;
40
+ }
41
+ zone(z) {
42
+ this._zone = z;
43
+ this.discoveryPromise = this.discoverVM();
44
+ return this;
45
+ }
46
+ network(netPath) {
47
+ this._network = netPath;
48
+ return this;
49
+ }
50
+ sshKey(keys) {
51
+ this._sshKeys = keys;
52
+ return this;
53
+ }
54
+ provision(...playbookPaths) {
55
+ this._provision.push(...playbookPaths.flat());
56
+ return this;
57
+ }
58
+ forceConfigCheck() {
59
+ this._forceConfigCheck = true;
60
+ return this;
61
+ }
62
+ async checkPort(ip, port) {
63
+ return checkPort(ip, port);
64
+ }
65
+ async runProvisioner(ip, script) {
66
+ const keysArray = Array.isArray(this._sshKeys) ? this._sshKeys : [this._sshKeys];
67
+ const keyPath = keysArray.find(k => !k.startsWith('ssh-') && !k.startsWith('ecdsa-') && !k.startsWith('sk-'));
68
+ if (!keyPath) {
69
+ throw new Error(`[GCP VM:${this.name}] No SSH private key path found. Pass a file path via .sshKey() to run provisioning.`);
70
+ }
71
+ return runProvisioner(ip, "root", keyPath, script);
72
+ }
73
+ async discoverVM() {
74
+ try {
75
+ const project = getProjectId();
76
+ const zone = this._zone;
77
+ const res = await gcpFetch("https://compute.googleapis.com", `/compute/v1/projects/${project}/zones/${zone}/instances/${this.name}`);
78
+ if (res) {
79
+ this.resolvedInstanceId = res.id;
80
+ const netInterface = (res.networkInterfaces ?? [])[0];
81
+ const extIp = (netInterface?.accessConfigs ?? [])[0]?.natIP;
82
+ this.resolvedIp = extIp;
83
+ if (res.id)
84
+ this.out.id.resolve(res.id);
85
+ if (extIp)
86
+ this.out.ip.resolve(extIp);
87
+ }
88
+ return res ?? null;
89
+ }
90
+ catch (e) {
91
+ if (e.message?.includes("404") ||
92
+ e.message?.includes("403") ||
93
+ e.message?.includes("credentials not configured")) {
94
+ return null;
95
+ }
96
+ throw e;
97
+ }
98
+ }
99
+ async deploy() {
100
+ const dryRun = this.isDryRunActive();
101
+ const existing = await this.discoveryPromise;
102
+ const project = getProjectId();
103
+ const zone = this._zone;
104
+ // Check if machine resizing is needed
105
+ const hasChanges = existing
106
+ ? existing.machineType?.split("/").pop() !== this._machineType
107
+ : true;
108
+ if (await this.checkProtection(hasChanges))
109
+ return null;
110
+ // Parse applied playbooks metadata from GCP metadata items
111
+ const metadataItem = (existing?.metadata?.items ?? []).find((i) => i.key === "puls-provision");
112
+ const appliedHashes = parseGcpMetadataForProvision(metadataItem?.value);
113
+ const declaredPlaybooksWithHashes = this._provision.map((p) => {
114
+ const baseName = p.split("/").pop() ?? p;
115
+ const slug = baseName.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
116
+ return { path: p, slug, hash: getFileHash(p) };
117
+ });
118
+ const playbooksToRun = this._forceConfigCheck
119
+ ? declaredPlaybooksWithHashes
120
+ : declaredPlaybooksWithHashes.filter((p) => {
121
+ const appliedHash = appliedHashes[p.slug];
122
+ return !appliedHash || appliedHash !== p.hash;
123
+ });
124
+ const playbookRunRequired = playbooksToRun.length > 0;
125
+ if (dryRun) {
126
+ console.log(`\nšŸ” [DRY RUN] GCP VM "${this.name}"...`);
127
+ if (!existing) {
128
+ const sourceLabel = this._templateSource ? `Template: ${this._templateSource.name}` : `Image: ${this._image}`;
129
+ console.log(` šŸ“ Plan: Create GCP VM Instance`);
130
+ const details = [
131
+ `Name: ${this.name}`,
132
+ `Machine Type: ${this._machineType}`,
133
+ `Zone: ${this._zone}`,
134
+ `Source: ${sourceLabel}`,
135
+ ];
136
+ if (this._network) {
137
+ details.push(`Network: ${this._network}`);
138
+ }
139
+ if (this._provision.length > 0) {
140
+ details.push(`Provision: ${this._provision.join(", ")}`);
141
+ }
142
+ for (let i = 0; i < details.length; i++) {
143
+ const prefix = i === details.length - 1 ? " └─ " : " ā”œā”€ ";
144
+ console.log(`${prefix}${details[i]}`);
145
+ }
146
+ this.out.id.resolve("PENDING");
147
+ this.out.ip.resolve("0.0.0.0");
148
+ }
149
+ else if (hasChanges || playbookRunRequired) {
150
+ if (hasChanges) {
151
+ console.log(` šŸ“ Plan: Stop and Resize VM ${this.name} → ${this._machineType}`);
152
+ }
153
+ if (playbookRunRequired) {
154
+ console.log(` šŸ“ [PLAN] Run ${playbooksToRun.length} playbook changes on existing GCP VM:`);
155
+ for (const p of playbooksToRun) {
156
+ console.log(` └─ Playbook: ${p.path} (hash: ${p.hash})`);
157
+ }
158
+ }
159
+ }
160
+ else {
161
+ console.log(` āœ… GCP VM "${this.name}" is up to date.`);
162
+ }
163
+ return { name: this.name, id: "PENDING" };
164
+ }
165
+ console.log(`\nā³ Finalizing GCP VM "${this.name}"...`);
166
+ if (!existing) {
167
+ const keysArray = Array.isArray(this._sshKeys) ? this._sshKeys : [this._sshKeys];
168
+ const sshKeysValue = keysArray
169
+ .map((k) => {
170
+ if (k.startsWith("ssh-") || k.startsWith("ecdsa-") || k.startsWith("sk-")) {
171
+ return `root:${k.trim()}`;
172
+ }
173
+ try {
174
+ const path = k.replace(/^~/, homedir());
175
+ const pubPath = path.replace(/\.pub$/, "") + ".pub";
176
+ const keyData = fs.readFileSync(pubPath, "utf-8").trim();
177
+ return `root:${keyData}`;
178
+ }
179
+ catch {
180
+ return `root:${k.trim()}`;
181
+ }
182
+ })
183
+ .join("\n");
184
+ // Compute initial playbooks metadata tag
185
+ const initialHashes = {};
186
+ for (const p of declaredPlaybooksWithHashes) {
187
+ initialHashes[p.slug] = p.hash;
188
+ }
189
+ const initialMetadataVal = mergeGcpMetadataForProvision(initialHashes);
190
+ let activeImage = this._image;
191
+ if (this._templateSource) {
192
+ activeImage = await this._templateSource.out.imageId.get();
193
+ }
194
+ const body = {
195
+ name: this.name,
196
+ machineType: `zones/${zone}/machineTypes/${this._machineType}`,
197
+ disks: [
198
+ {
199
+ boot: true,
200
+ autoDelete: true,
201
+ initializeParams: {
202
+ sourceImage: activeImage,
203
+ },
204
+ },
205
+ ],
206
+ networkInterfaces: [
207
+ {
208
+ network: this._network,
209
+ accessConfigs: [
210
+ {
211
+ name: "External NAT",
212
+ type: "ONE_TO_ONE_NAT",
213
+ },
214
+ ],
215
+ },
216
+ ],
217
+ metadata: {
218
+ items: [
219
+ ...(sshKeysValue ? [{ key: "ssh-keys", value: sshKeysValue }] : []),
220
+ ...(initialMetadataVal ? [{ key: "puls-provision", value: initialMetadataVal }] : []),
221
+ ],
222
+ },
223
+ };
224
+ console.log(`šŸš€ Creating GCP Compute VM Instance "${this.name}"...`);
225
+ await gcpFetch("https://compute.googleapis.com", `/compute/v1/projects/${project}/zones/${zone}/instances`, {
226
+ method: "POST",
227
+ body: JSON.stringify(body),
228
+ });
229
+ // Poll until instance is RUNNING
230
+ await this.waitFor(`GCP VM "${this.name}" to start running`, async () => {
231
+ const current = await this.discoverVM();
232
+ return current && current.status === "RUNNING";
233
+ }, { intervalMs: 10_000, timeoutMs: 300_000 });
234
+ console.log(`šŸš€ GCP VM "${this.name}" is now running.`);
235
+ if (this._provision.length > 0) {
236
+ const activeIp = this.resolvedIp ?? "0.0.0.0";
237
+ if (activeIp === "0.0.0.0") {
238
+ throw new Error(`Failed to resolve IP for new GCP VM "${this.name}" to run playbooks`);
239
+ }
240
+ await this.waitFor(`SSH on ${activeIp} to be ready`, () => this.checkPort(activeIp, 22), { intervalMs: 10_000, timeoutMs: 300_000 });
241
+ for (const playbook of this._provision) {
242
+ await this.runProvisioner(activeIp, playbook);
243
+ }
244
+ }
245
+ }
246
+ else {
247
+ if (hasChanges) {
248
+ console.log(`✨ Resizing GCP VM ${this.name} → ${this._machineType}...`);
249
+ // GCP requires instance to be stopped to resize machineType
250
+ console.log(` šŸ”„ Stopping VM to perform resize...`);
251
+ await gcpFetch("https://compute.googleapis.com", `/compute/v1/projects/${project}/zones/${zone}/instances/${this.name}/stop`, { method: "POST" });
252
+ await this.waitFor(`VM "${this.name}" to stop`, async () => {
253
+ const current = await this.discoverVM();
254
+ return current && current.status === "TERMINATED";
255
+ }, { intervalMs: 10_000, timeoutMs: 300_000 });
256
+ // Perform resize
257
+ await gcpFetch("https://compute.googleapis.com", `/compute/v1/projects/${project}/zones/${zone}/instances/${this.name}/setSize`, {
258
+ method: "POST",
259
+ body: JSON.stringify({
260
+ machineType: `zones/${zone}/machineTypes/${this._machineType}`,
261
+ }),
262
+ });
263
+ // Restart VM
264
+ console.log(` šŸ”„ Restarting VM...`);
265
+ await gcpFetch("https://compute.googleapis.com", `/compute/v1/projects/${project}/zones/${zone}/instances/${this.name}/start`, { method: "POST" });
266
+ await this.waitFor(`VM "${this.name}" to restart`, async () => {
267
+ const current = await this.discoverVM();
268
+ return current && current.status === "RUNNING";
269
+ }, { intervalMs: 10_000, timeoutMs: 300_000 });
270
+ console.log(` āœ… GCP VM resized and restarted successfully.`);
271
+ }
272
+ if (playbookRunRequired) {
273
+ console.log(` šŸ”„ Running ${playbooksToRun.length} playbook changes on GCP VM...`);
274
+ const activeIp = this.resolvedIp ?? "0.0.0.0";
275
+ if (activeIp === "0.0.0.0") {
276
+ throw new Error(`Failed to resolve IP for GCP VM "${this.name}" to run playbooks`);
277
+ }
278
+ await this.waitFor(`SSH on ${activeIp} to be ready`, () => this.checkPort(activeIp, 22), { intervalMs: 10_000, timeoutMs: 300_000 });
279
+ for (const p of playbooksToRun) {
280
+ await this.runProvisioner(activeIp, p.path);
281
+ appliedHashes[p.slug] = p.hash;
282
+ }
283
+ // Re-discover to get fresh metadata fingerprint
284
+ const fresh = await this.discoverVM();
285
+ await this.updateGcpMetadata(fresh, appliedHashes);
286
+ console.log(` āœ… Playbooks applied successfully and metadata updated.`);
287
+ }
288
+ if (!hasChanges && !playbookRunRequired) {
289
+ console.log(`āœ… GCP VM "${this.name}" is up to date.`);
290
+ }
291
+ }
292
+ const context = resourceContextStorage.getStore();
293
+ if (context && context.hosts) {
294
+ const activeIp = this.resolvedIp ?? "0.0.0.0";
295
+ const keysArray = Array.isArray(this._sshKeys) ? this._sshKeys : [this._sshKeys];
296
+ const keyPath = keysArray.find(k => !k.startsWith('ssh-') && !k.startsWith('ecdsa-') && !k.startsWith('sk-'));
297
+ if (!context.hosts.some(h => h.name === this.name)) {
298
+ context.hosts.push({
299
+ name: this.name,
300
+ ip: activeIp,
301
+ user: "root",
302
+ sshKey: keyPath,
303
+ provider: "gcp"
304
+ });
305
+ }
306
+ }
307
+ return {
308
+ name: this.name,
309
+ id: this.resolvedInstanceId,
310
+ ip: this.resolvedIp,
311
+ };
312
+ }
313
+ async destroy() {
314
+ const dryRun = this.isDryRunActive();
315
+ const existing = await this.discoveryPromise;
316
+ const project = getProjectId();
317
+ const zone = this._zone;
318
+ console.log(`\nšŸ—‘ļø Destroying GCP Compute VM "${this.name}"...`);
319
+ if (!existing) {
320
+ console.log(` ─ GCP VM "${this.name}" not found`);
321
+ return { destroyed: false };
322
+ }
323
+ if (dryRun) {
324
+ console.log(` šŸ“ [PLAN] Delete GCP VM "${this.name}"`);
325
+ return { destroyed: this.name };
326
+ }
327
+ console.log(` šŸ”„ Deleting GCP VM "${this.name}"...`);
328
+ await gcpFetch("https://compute.googleapis.com", `/compute/v1/projects/${project}/zones/${zone}/instances/${this.name}`, {
329
+ method: "DELETE",
330
+ });
331
+ console.log(` šŸ—‘ļø Removed GCP VM "${this.name}"`);
332
+ return { destroyed: this.name };
333
+ }
334
+ async updateGcpMetadata(existing, newHashes) {
335
+ const project = getProjectId();
336
+ const zone = this._zone;
337
+ const currentItems = [...(existing.metadata?.items ?? [])];
338
+ const newValue = mergeGcpMetadataForProvision(newHashes);
339
+ const provIdx = currentItems.findIndex((i) => i.key === "puls-provision");
340
+ if (provIdx >= 0) {
341
+ currentItems[provIdx].value = newValue;
342
+ }
343
+ else {
344
+ currentItems.push({ key: "puls-provision", value: newValue });
345
+ }
346
+ await gcpFetch("https://compute.googleapis.com", `/compute/v1/projects/${project}/zones/${zone}/instances/${this.name}/setMetadata`, {
347
+ method: "POST",
348
+ body: JSON.stringify({
349
+ fingerprint: existing.metadata?.fingerprint,
350
+ items: currentItems,
351
+ }),
352
+ });
353
+ }
354
+ }
355
+ export function parseGcpMetadataForProvision(value) {
356
+ if (!value)
357
+ return {};
358
+ const record = {};
359
+ const entries = value.split(",");
360
+ for (const entry of entries) {
361
+ const parts = entry.trim().split("=");
362
+ if (parts.length === 2) {
363
+ const [name, hash] = parts;
364
+ if (name && hash) {
365
+ record[name.trim()] = hash.trim();
366
+ }
367
+ }
368
+ }
369
+ return record;
370
+ }
371
+ export function mergeGcpMetadataForProvision(metadata) {
372
+ return Object.entries(metadata)
373
+ .map(([name, hash]) => `${name}=${hash}`)
374
+ .join(",");
375
+ }
@@ -0,0 +1 @@
1
+ export {};