puls-dev 0.2.6 → 0.2.8
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/README.md +1 -1
- package/dist/core/config.d.ts +2 -0
- package/dist/core/decorators.d.ts +2 -0
- package/dist/core/decorators.js +48 -16
- 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/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/provisioner.d.ts +4 -0
- package/dist/core/provisioner.js +105 -0
- package/dist/core/resource.d.ts +16 -0
- package/dist/core/resource.js +44 -0
- package/dist/core/secret.d.ts +40 -0
- package/dist/core/secret.js +95 -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 +50 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/providers/aws/ec2.d.ts +48 -0
- package/dist/providers/aws/ec2.js +297 -0
- package/dist/providers/aws/ec2.test.d.ts +1 -0
- package/dist/providers/aws/ec2.test.js +279 -0
- package/dist/providers/aws/index.d.ts +2 -0
- package/dist/providers/aws/index.js +2 -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/do/api.d.ts +1 -1
- package/dist/providers/do/api.js +2 -1
- 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 +132 -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/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 +3 -1
- package/dist/providers/gcp/index.js +3 -1
- package/dist/providers/gcp/vm.d.ts +45 -0
- package/dist/providers/gcp/vm.js +332 -0
- package/dist/providers/gcp/vm.test.d.ts +1 -0
- package/dist/providers/gcp/vm.test.js +321 -0
- package/dist/providers/proxmox/hash.d.ts +3 -0
- package/dist/providers/proxmox/hash.js +46 -0
- package/dist/providers/proxmox/vm.d.ts +8 -7
- package/dist/providers/proxmox/vm.js +126 -106
- package/dist/providers/proxmox/vm.test.js +224 -0
- package/package.json +3 -1
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { test, describe, beforeEach, afterEach, mock } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { GoogleAuth } from "google-auth-library";
|
|
5
|
+
import { GCPVMBuilder } from "./vm.js";
|
|
6
|
+
import { Config } from "../../core/config.js";
|
|
7
|
+
import { getFileHash } from "../proxmox/hash.js";
|
|
8
|
+
describe("GCPVMBuilder Unit Tests", () => {
|
|
9
|
+
let originalFetch;
|
|
10
|
+
let fetchCalls = [];
|
|
11
|
+
let mockResponses = {};
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
Config.set({
|
|
14
|
+
dryRun: false,
|
|
15
|
+
providers: {
|
|
16
|
+
gcp: {
|
|
17
|
+
projectId: "my-gcp-project",
|
|
18
|
+
serviceAccountPath: "/fake/sa.json",
|
|
19
|
+
region: "us-central1",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
originalFetch = globalThis.fetch;
|
|
24
|
+
fetchCalls = [];
|
|
25
|
+
mockResponses = {};
|
|
26
|
+
globalThis.fetch = async (input, init) => {
|
|
27
|
+
const url = String(input);
|
|
28
|
+
const method = init?.method ?? "GET";
|
|
29
|
+
let body;
|
|
30
|
+
if (init?.body) {
|
|
31
|
+
if (typeof init.body === "string") {
|
|
32
|
+
try {
|
|
33
|
+
body = JSON.parse(init.body);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
body = init.body;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
body = "[Binary/Buffer Body]";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const headers = init?.headers;
|
|
44
|
+
fetchCalls.push({ url, method, body, headers });
|
|
45
|
+
const matchKey = Object.keys(mockResponses)
|
|
46
|
+
.filter((key) => {
|
|
47
|
+
const [mMethod, mPath] = key.split(" ");
|
|
48
|
+
return method === mMethod && url.includes(mPath);
|
|
49
|
+
})
|
|
50
|
+
.sort((a, b) => b.split(" ")[1].length - a.split(" ")[1].length)[0];
|
|
51
|
+
if (matchKey) {
|
|
52
|
+
const resp = mockResponses[matchKey];
|
|
53
|
+
return {
|
|
54
|
+
ok: resp.status >= 200 && resp.status < 300,
|
|
55
|
+
status: resp.status,
|
|
56
|
+
json: async () => resp.body,
|
|
57
|
+
text: async () => JSON.stringify(resp.body),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
status: 404,
|
|
63
|
+
json: async () => ({ error: { message: `Endpoint not mocked: ${method} ${url}` } }),
|
|
64
|
+
text: async () => `Endpoint not mocked: ${method} ${url}`,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
mock.method(GoogleAuth.prototype, "getClient", async () => {
|
|
68
|
+
return {
|
|
69
|
+
getAccessToken: async () => ({ token: "fake-gcp-token" }),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
mock.method(fs, "readFileSync", () => {
|
|
73
|
+
return "ssh-rsa AAAA_FAKE_GCP_PUBLIC_KEY test@gcp.com";
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
globalThis.fetch = originalFetch;
|
|
78
|
+
mock.restoreAll();
|
|
79
|
+
});
|
|
80
|
+
test("handles discovery when VM does not exist", async () => {
|
|
81
|
+
mockResponses["GET /instances/my-gcp-vm"] = {
|
|
82
|
+
status: 404,
|
|
83
|
+
body: { error: { message: "Not Found" } },
|
|
84
|
+
};
|
|
85
|
+
const builder = new GCPVMBuilder("my-gcp-vm");
|
|
86
|
+
const existing = await builder.discoveryPromise;
|
|
87
|
+
assert.strictEqual(existing, null);
|
|
88
|
+
const getCall = fetchCalls.find((c) => c.method === "GET" && c.url.includes("/instances/my-gcp-vm"));
|
|
89
|
+
assert.ok(getCall);
|
|
90
|
+
});
|
|
91
|
+
test("discovers VM successfully when it exists", async () => {
|
|
92
|
+
mockResponses["GET /instances/my-gcp-vm"] = {
|
|
93
|
+
status: 200,
|
|
94
|
+
body: {
|
|
95
|
+
id: "vm-123456",
|
|
96
|
+
name: "my-gcp-vm",
|
|
97
|
+
status: "RUNNING",
|
|
98
|
+
networkInterfaces: [
|
|
99
|
+
{
|
|
100
|
+
accessConfigs: [{ natIP: "34.56.78.90" }],
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
metadata: {
|
|
104
|
+
items: [{ key: "puls-provision", value: "nginx-yaml=abc123123123" }],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
const builder = new GCPVMBuilder("my-gcp-vm");
|
|
109
|
+
const existing = await builder.discoveryPromise;
|
|
110
|
+
assert.ok(existing);
|
|
111
|
+
assert.strictEqual(existing.id, "vm-123456");
|
|
112
|
+
const resolvedId = await builder.out.id.get();
|
|
113
|
+
const resolvedIp = await builder.out.ip.get();
|
|
114
|
+
assert.strictEqual(resolvedId, "vm-123456");
|
|
115
|
+
assert.strictEqual(resolvedIp, "34.56.78.90");
|
|
116
|
+
});
|
|
117
|
+
test("runs in dry-run mode safely and logs plan", async () => {
|
|
118
|
+
Config.set({
|
|
119
|
+
dryRun: true,
|
|
120
|
+
providers: {
|
|
121
|
+
gcp: { projectId: "my-gcp-project", serviceAccountPath: "/fake/sa.json" },
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
mockResponses["GET /instances/new-gcp-vm"] = {
|
|
125
|
+
status: 404,
|
|
126
|
+
body: { error: { message: "Not Found" } },
|
|
127
|
+
};
|
|
128
|
+
const builder = new GCPVMBuilder("new-gcp-vm")
|
|
129
|
+
.machineType("e2-medium")
|
|
130
|
+
.zone("europe-west1-b")
|
|
131
|
+
.provision("playbooks/nginx.yaml");
|
|
132
|
+
const res = await builder.deploy();
|
|
133
|
+
assert.deepStrictEqual(res, { name: "new-gcp-vm", id: "PENDING" });
|
|
134
|
+
const resolvedId = await builder.out.id.get();
|
|
135
|
+
const resolvedIp = await builder.out.ip.get();
|
|
136
|
+
assert.strictEqual(resolvedId, "PENDING");
|
|
137
|
+
assert.strictEqual(resolvedIp, "0.0.0.0");
|
|
138
|
+
// Ensure no writes were performed
|
|
139
|
+
const writeCalls = fetchCalls.filter((c) => c.method === "POST" || c.method === "DELETE");
|
|
140
|
+
assert.strictEqual(writeCalls.length, 0);
|
|
141
|
+
});
|
|
142
|
+
test("creates a new VM instance and runs playbooks successfully", async () => {
|
|
143
|
+
mockResponses["GET /instances/new-vm"] = {
|
|
144
|
+
status: 404,
|
|
145
|
+
body: { error: { message: "Not Found" } },
|
|
146
|
+
};
|
|
147
|
+
mockResponses["POST /instances"] = {
|
|
148
|
+
status: 200,
|
|
149
|
+
body: { id: "op-111", status: "DONE" },
|
|
150
|
+
};
|
|
151
|
+
// Subsequents GET during wait loop returns RUNNING
|
|
152
|
+
let getCount = 0;
|
|
153
|
+
globalThis.fetch = async (input, init) => {
|
|
154
|
+
const url = String(input);
|
|
155
|
+
const method = init?.method ?? "GET";
|
|
156
|
+
const body = init?.body ? JSON.parse(init.body) : undefined;
|
|
157
|
+
fetchCalls.push({ url, method, body });
|
|
158
|
+
if (method === "GET" && url.includes("/instances/new-vm")) {
|
|
159
|
+
const createCall = fetchCalls.find(c => c.method === "POST" && c.url.includes("/instances"));
|
|
160
|
+
if (!createCall) {
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
status: 404,
|
|
164
|
+
json: async () => ({ error: { message: "Not Found" } }),
|
|
165
|
+
text: async () => JSON.stringify({ error: { message: "Not Found" } }),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
getCount++;
|
|
169
|
+
const data = {
|
|
170
|
+
id: "new-vm-uuid",
|
|
171
|
+
name: "new-vm",
|
|
172
|
+
status: getCount > 1 ? "RUNNING" : "PROVISIONING",
|
|
173
|
+
networkInterfaces: [
|
|
174
|
+
{
|
|
175
|
+
accessConfigs: [{ natIP: "35.200.10.20" }],
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
return {
|
|
180
|
+
ok: true,
|
|
181
|
+
status: 200,
|
|
182
|
+
json: async () => data,
|
|
183
|
+
text: async () => JSON.stringify(data),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (method === "POST" && url.includes("/instances")) {
|
|
187
|
+
const opData = { id: "op-111", status: "DONE" };
|
|
188
|
+
return {
|
|
189
|
+
ok: true,
|
|
190
|
+
status: 200,
|
|
191
|
+
json: async () => opData,
|
|
192
|
+
text: async () => JSON.stringify(opData),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return { ok: false, status: 404 };
|
|
196
|
+
};
|
|
197
|
+
const builder = new GCPVMBuilder("new-vm")
|
|
198
|
+
.machineType("e2-medium")
|
|
199
|
+
.zone("us-central1-a")
|
|
200
|
+
.sshKey("~/.ssh/id_rsa.pub")
|
|
201
|
+
.provision("playbooks/nginx.yaml");
|
|
202
|
+
const provisionCalls = [];
|
|
203
|
+
builder.waitFor = async (label, condition) => {
|
|
204
|
+
return await condition();
|
|
205
|
+
};
|
|
206
|
+
builder.checkPort = async () => true;
|
|
207
|
+
builder.runProvisioner = async (ip, script) => {
|
|
208
|
+
provisionCalls.push(script);
|
|
209
|
+
};
|
|
210
|
+
const res = await builder.deploy();
|
|
211
|
+
assert.ok(res);
|
|
212
|
+
assert.strictEqual(res.id, "new-vm-uuid");
|
|
213
|
+
assert.strictEqual(res.ip, "35.200.10.20");
|
|
214
|
+
assert.strictEqual(provisionCalls.length, 1);
|
|
215
|
+
assert.strictEqual(provisionCalls[0], "playbooks/nginx.yaml");
|
|
216
|
+
const createCall = fetchCalls.find((c) => c.method === "POST" && c.url.includes("/instances"));
|
|
217
|
+
assert.ok(createCall);
|
|
218
|
+
assert.strictEqual(createCall.body.name, "new-vm");
|
|
219
|
+
assert.strictEqual(createCall.body.machineType, "zones/us-central1-a/machineTypes/e2-medium");
|
|
220
|
+
const sshMetadata = createCall.body.metadata.items.find((i) => i.key === "ssh-keys");
|
|
221
|
+
assert.strictEqual(sshMetadata.value, "root:ssh-rsa AAAA_FAKE_GCP_PUBLIC_KEY test@gcp.com");
|
|
222
|
+
const provMetadata = createCall.body.metadata.items.find((i) => i.key === "puls-provision");
|
|
223
|
+
assert.ok(provMetadata.value.startsWith("nginx-yaml="));
|
|
224
|
+
});
|
|
225
|
+
test("skips playbook execution on existing VM if hashes match", async () => {
|
|
226
|
+
const nginxHash = getFileHash("playbooks/nginx.yaml");
|
|
227
|
+
mockResponses["GET /instances/exist-vm"] = {
|
|
228
|
+
status: 200,
|
|
229
|
+
body: {
|
|
230
|
+
id: "exist-vm-id",
|
|
231
|
+
name: "exist-vm",
|
|
232
|
+
status: "RUNNING",
|
|
233
|
+
machineType: "zones/us-central1-a/machineTypes/e2-micro",
|
|
234
|
+
networkInterfaces: [
|
|
235
|
+
{
|
|
236
|
+
accessConfigs: [{ natIP: "35.200.10.30" }],
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
metadata: {
|
|
240
|
+
items: [{ key: "puls-provision", value: `nginx-yaml=${nginxHash}` }],
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
const builder = new GCPVMBuilder("exist-vm")
|
|
245
|
+
.machineType("e2-micro")
|
|
246
|
+
.zone("us-central1-a")
|
|
247
|
+
.provision("playbooks/nginx.yaml");
|
|
248
|
+
const provisionCalls = [];
|
|
249
|
+
builder.runProvisioner = async (ip, script) => {
|
|
250
|
+
provisionCalls.push(script);
|
|
251
|
+
};
|
|
252
|
+
await builder.deploy();
|
|
253
|
+
// No playbooks should run
|
|
254
|
+
assert.strictEqual(provisionCalls.length, 0);
|
|
255
|
+
// No setMetadata call
|
|
256
|
+
const setMetaCall = fetchCalls.find((c) => c.method === "POST" && c.url.includes("/setMetadata"));
|
|
257
|
+
assert.strictEqual(setMetaCall, undefined);
|
|
258
|
+
});
|
|
259
|
+
test("executes playbooks on existing VM if hashes differ, updating metadata", async () => {
|
|
260
|
+
mockResponses["GET /instances/exist-diff-vm"] = {
|
|
261
|
+
status: 200,
|
|
262
|
+
body: {
|
|
263
|
+
id: "exist-diff-vm-id",
|
|
264
|
+
name: "exist-diff-vm",
|
|
265
|
+
status: "RUNNING",
|
|
266
|
+
machineType: "zones/us-central1-a/machineTypes/e2-micro",
|
|
267
|
+
networkInterfaces: [
|
|
268
|
+
{
|
|
269
|
+
accessConfigs: [{ natIP: "35.200.10.40" }],
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
metadata: {
|
|
273
|
+
fingerprint: "old-fingerprint-123",
|
|
274
|
+
items: [{ key: "puls-provision", value: "nginx-yaml=abc123123123" }],
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
mockResponses["POST /instances/exist-diff-vm/setMetadata"] = {
|
|
279
|
+
status: 200,
|
|
280
|
+
body: {},
|
|
281
|
+
};
|
|
282
|
+
const builder = new GCPVMBuilder("exist-diff-vm")
|
|
283
|
+
.machineType("e2-micro")
|
|
284
|
+
.zone("us-central1-a")
|
|
285
|
+
.provision("playbooks/nginx.yaml");
|
|
286
|
+
const provisionCalls = [];
|
|
287
|
+
builder.waitFor = async (label, condition) => {
|
|
288
|
+
return await condition();
|
|
289
|
+
};
|
|
290
|
+
builder.checkPort = async () => true;
|
|
291
|
+
builder.runProvisioner = async (ip, script) => {
|
|
292
|
+
provisionCalls.push(script);
|
|
293
|
+
};
|
|
294
|
+
await builder.deploy();
|
|
295
|
+
assert.strictEqual(provisionCalls.length, 1);
|
|
296
|
+
assert.strictEqual(provisionCalls[0], "playbooks/nginx.yaml");
|
|
297
|
+
// Verify setMetadata was dispatched with new hashes and correct fingerprint
|
|
298
|
+
const setMetaCall = fetchCalls.find((c) => c.method === "POST" && c.url.includes("/setMetadata"));
|
|
299
|
+
assert.ok(setMetaCall);
|
|
300
|
+
assert.strictEqual(setMetaCall.body.fingerprint, "old-fingerprint-123");
|
|
301
|
+
const provMetadata = setMetaCall.body.items.find((i) => i.key === "puls-provision");
|
|
302
|
+
const expectedHash = getFileHash("playbooks/nginx.yaml");
|
|
303
|
+
assert.strictEqual(provMetadata.value, `nginx-yaml=${expectedHash}`);
|
|
304
|
+
});
|
|
305
|
+
test("destroys VM successfully", async () => {
|
|
306
|
+
mockResponses["GET /instances/delete-vm"] = {
|
|
307
|
+
status: 200,
|
|
308
|
+
body: { id: "delete-vm-id", name: "delete-vm" },
|
|
309
|
+
};
|
|
310
|
+
mockResponses["DELETE /instances/delete-vm"] = {
|
|
311
|
+
status: 200,
|
|
312
|
+
body: {},
|
|
313
|
+
};
|
|
314
|
+
const builder = new GCPVMBuilder("delete-vm");
|
|
315
|
+
await builder.discoveryPromise;
|
|
316
|
+
const res = await builder.destroy();
|
|
317
|
+
assert.deepStrictEqual(res, { destroyed: "delete-vm" });
|
|
318
|
+
const deleteCall = fetchCalls.find((c) => c.method === "DELETE" && c.url.includes("/instances/delete-vm"));
|
|
319
|
+
assert.ok(deleteCall);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
export function getFileHash(filePath) {
|
|
4
|
+
try {
|
|
5
|
+
if (!fs.existsSync(filePath)) {
|
|
6
|
+
// Stable hash fallback for virtual playbooks or missing dry-run files
|
|
7
|
+
return crypto.createHash("sha256").update(filePath).digest("hex").slice(0, 12);
|
|
8
|
+
}
|
|
9
|
+
const content = fs.readFileSync(filePath);
|
|
10
|
+
return crypto.createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return "unknown";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function parseProvisionMetadata(description) {
|
|
17
|
+
if (!description)
|
|
18
|
+
return {};
|
|
19
|
+
const match = description.match(/\[puls-provision:\s*([^\]]+)\]/);
|
|
20
|
+
if (!match)
|
|
21
|
+
return {};
|
|
22
|
+
const record = {};
|
|
23
|
+
const entries = match[1].split(",");
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const parts = entry.trim().split("=");
|
|
26
|
+
if (parts.length === 2) {
|
|
27
|
+
const [name, hash] = parts;
|
|
28
|
+
if (name && hash) {
|
|
29
|
+
record[name.trim()] = hash.trim();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return record;
|
|
34
|
+
}
|
|
35
|
+
export function mergeProvisionMetadata(description, metadata) {
|
|
36
|
+
const metaString = Object.entries(metadata)
|
|
37
|
+
.map(([name, hash]) => `${name}=${hash}`)
|
|
38
|
+
.join(",");
|
|
39
|
+
const block = `[puls-provision: ${metaString}]`;
|
|
40
|
+
const regex = /\[puls-provision:\s*[^\]]+\]/;
|
|
41
|
+
if (regex.test(description)) {
|
|
42
|
+
return description.replace(regex, block);
|
|
43
|
+
}
|
|
44
|
+
const trimmed = description.trim();
|
|
45
|
+
return trimmed ? `${trimmed}\n\n${block}` : block;
|
|
46
|
+
}
|
|
@@ -12,7 +12,7 @@ export declare class VMBuilder extends BaseBuilder {
|
|
|
12
12
|
private _image?;
|
|
13
13
|
private _cores;
|
|
14
14
|
private _memory;
|
|
15
|
-
private _provision
|
|
15
|
+
private _provision;
|
|
16
16
|
private _replace?;
|
|
17
17
|
private _node?;
|
|
18
18
|
private _storage?;
|
|
@@ -20,12 +20,13 @@ export declare class VMBuilder extends BaseBuilder {
|
|
|
20
20
|
private _ip?;
|
|
21
21
|
private _sshKeys?;
|
|
22
22
|
private _machine;
|
|
23
|
+
private _forceConfigCheck;
|
|
23
24
|
constructor(name: string);
|
|
24
25
|
private discoverVm;
|
|
25
26
|
image(os: OSImage): this;
|
|
26
27
|
cores(n: number): this;
|
|
27
28
|
memory(mb: number): this;
|
|
28
|
-
provision(
|
|
29
|
+
provision(...playbookPaths: (string | string[])[]): this;
|
|
29
30
|
replace(oldVmName: string): this;
|
|
30
31
|
node(n: string): this;
|
|
31
32
|
storage(pool: string): this;
|
|
@@ -33,11 +34,12 @@ export declare class VMBuilder extends BaseBuilder {
|
|
|
33
34
|
ip(address: string): this;
|
|
34
35
|
sshKey(keys: string | readonly string[]): this;
|
|
35
36
|
machine(type: "q35" | "i440fx"): this;
|
|
37
|
+
forceConfigCheck(): this;
|
|
36
38
|
deploy(): Promise<{
|
|
37
39
|
name: string;
|
|
38
40
|
vmid: number | null;
|
|
39
41
|
node: string | null;
|
|
40
|
-
ip
|
|
42
|
+
ip: string;
|
|
41
43
|
} | {
|
|
42
44
|
name: string;
|
|
43
45
|
vmid: string;
|
|
@@ -49,14 +51,13 @@ export declare class VMBuilder extends BaseBuilder {
|
|
|
49
51
|
ip: string | null;
|
|
50
52
|
node?: undefined;
|
|
51
53
|
}>;
|
|
54
|
+
private resolveExistingIp;
|
|
52
55
|
destroy(): Promise<any>;
|
|
53
56
|
private waitForTask;
|
|
54
57
|
private destroyVmByName;
|
|
55
58
|
private resolvePublicKeys;
|
|
56
59
|
private checkCloudInit;
|
|
57
|
-
|
|
60
|
+
protected checkPort(ip: string, port: number): Promise<boolean>;
|
|
61
|
+
protected runProvisioner(ip: string, script: string): Promise<void>;
|
|
58
62
|
private sshKeyPath;
|
|
59
|
-
private runProvisioner;
|
|
60
|
-
private runPuppet;
|
|
61
|
-
private runAnsible;
|
|
62
63
|
}
|