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
|
@@ -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 {};
|