@highstate/common 0.9.16 → 0.9.19
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/{chunk-HZBJ6LLS.js → chunk-WDYIUWYZ.js} +659 -267
- package/dist/chunk-WDYIUWYZ.js.map +1 -0
- package/dist/highstate.manifest.json +12 -8
- package/dist/index.js +1 -1
- package/dist/units/access-point/index.js +16 -0
- package/dist/units/access-point/index.js.map +1 -0
- package/dist/units/databases/existing-mariadb/index.js +17 -0
- package/dist/units/databases/existing-mariadb/index.js.map +1 -0
- package/dist/units/databases/existing-mongodb/index.js +17 -0
- package/dist/units/databases/existing-mongodb/index.js.map +1 -0
- package/dist/units/databases/existing-postgresql/index.js +17 -0
- package/dist/units/databases/existing-postgresql/index.js.map +1 -0
- package/dist/units/dns/record-set/index.js +22 -11
- package/dist/units/dns/record-set/index.js.map +1 -1
- package/dist/units/existing-server/index.js +12 -12
- package/dist/units/existing-server/index.js.map +1 -1
- package/dist/units/network/l3-endpoint/index.js +1 -1
- package/dist/units/network/l3-endpoint/index.js.map +1 -1
- package/dist/units/network/l4-endpoint/index.js +1 -1
- package/dist/units/network/l4-endpoint/index.js.map +1 -1
- package/dist/units/script/index.js +1 -1
- package/dist/units/script/index.js.map +1 -1
- package/dist/units/server-dns/index.js +1 -1
- package/dist/units/server-dns/index.js.map +1 -1
- package/dist/units/server-patch/index.js +1 -1
- package/dist/units/server-patch/index.js.map +1 -1
- package/dist/units/ssh/key-pair/index.js +6 -6
- package/dist/units/ssh/key-pair/index.js.map +1 -1
- package/package.json +61 -8
- package/src/shared/access-point.ts +110 -0
- package/src/shared/command.ts +310 -69
- package/src/shared/dns.ts +150 -90
- package/src/shared/files.ts +34 -34
- package/src/shared/gateway.ts +117 -0
- package/src/shared/impl-ref.ts +123 -0
- package/src/shared/index.ts +4 -0
- package/src/shared/network.ts +41 -27
- package/src/shared/passwords.ts +38 -2
- package/src/shared/ssh.ts +261 -126
- package/src/shared/tls.ts +123 -0
- package/src/units/access-point/index.ts +12 -0
- package/src/units/databases/existing-mariadb/index.ts +14 -0
- package/src/units/databases/existing-mongodb/index.ts +14 -0
- package/src/units/databases/existing-postgresql/index.ts +14 -0
- package/src/units/dns/record-set/index.ts +21 -11
- package/src/units/existing-server/index.ts +12 -17
- package/src/units/ssh/key-pair/index.ts +6 -6
- package/dist/chunk-HZBJ6LLS.js.map +0 -1
@@ -1,22 +1,23 @@
|
|
1
|
-
import { toPromise, output,
|
2
|
-
import { uniqueBy,
|
1
|
+
import { ComponentResource, Resource, toPromise, output, interpolate, normalizeInputsAndMap, fileFromString, secret, asset, normalizeInputs } from '@highstate/pulumi';
|
2
|
+
import { uniqueBy, flat, groupBy } from 'remeda';
|
3
|
+
import { homedir, tmpdir } from 'node:os';
|
3
4
|
import { local, remote } from '@pulumi/command';
|
4
|
-
import '@
|
5
|
-
import {
|
5
|
+
import { sha256 } from '@noble/hashes/sha2';
|
6
|
+
import { z, getOrCreate, stripNullish, HighstateSignature } from '@highstate/contract';
|
7
|
+
import { randomBytes, bytesToHex } from '@noble/hashes/utils';
|
6
8
|
import { secureMask } from 'micro-key-producer/password.js';
|
7
9
|
import getKeys, { PrivateExport } from 'micro-key-producer/ssh.js';
|
8
10
|
import { randomBytes as randomBytes$1 } from 'micro-key-producer/utils.js';
|
9
|
-
import {
|
11
|
+
import { createHash } from 'node:crypto';
|
12
|
+
import { createReadStream } from 'node:fs';
|
10
13
|
import { mkdtemp, writeFile, cp, rm, stat, rename, mkdir } from 'node:fs/promises';
|
11
14
|
import { join, dirname, basename, extname } from 'node:path';
|
12
|
-
import { createReadStream } from 'node:fs';
|
13
|
-
import { pipeline } from 'node:stream/promises';
|
14
15
|
import { Readable } from 'node:stream';
|
15
|
-
import {
|
16
|
+
import { pipeline } from 'node:stream/promises';
|
16
17
|
import { minimatch } from 'minimatch';
|
17
|
-
import { HighstateSignature } from '@highstate/contract';
|
18
18
|
import * as tar from 'tar';
|
19
19
|
import unzipper from 'unzipper';
|
20
|
+
import { network } from '@highstate/library';
|
20
21
|
|
21
22
|
// src/shared/network.ts
|
22
23
|
function l3EndpointToString(l3Endpoint) {
|
@@ -123,7 +124,7 @@ async function requireInputL4Endpoint(rawEndpoint, inputEndpoint) {
|
|
123
124
|
}
|
124
125
|
throw new Error("No endpoint provided");
|
125
126
|
}
|
126
|
-
function
|
127
|
+
function l3EndpointToL4(l3Endpoint, port, protocol = "tcp") {
|
127
128
|
return {
|
128
129
|
...parseL3Endpoint(l3Endpoint),
|
129
130
|
port,
|
@@ -138,7 +139,7 @@ function filterEndpoints(endpoints, filter, types) {
|
|
138
139
|
} else if (endpoints.some((endpoint) => endpoint.visibility === "external")) {
|
139
140
|
endpoints = endpoints.filter((endpoint) => endpoint.visibility === "external");
|
140
141
|
}
|
141
|
-
if (types
|
142
|
+
if (types?.length) {
|
142
143
|
endpoints = endpoints.filter((endpoint) => types.includes(endpoint.type));
|
143
144
|
}
|
144
145
|
return endpoints;
|
@@ -184,30 +185,31 @@ function parseL7Endpoint(l7Endpoint) {
|
|
184
185
|
}
|
185
186
|
async function updateEndpoints(currentEndpoints, endpoints, inputEndpoints, mode = "prepend") {
|
186
187
|
const resolvedCurrentEndpoints = await toPromise(currentEndpoints);
|
187
|
-
const
|
188
|
-
const newEndpoints = uniqueBy(
|
189
|
-
//
|
190
|
-
[...endpoints.map(parseL34Endpoint), ...resolvedInputEndpoints],
|
191
|
-
(endpoint) => l34EndpointToString(endpoint)
|
192
|
-
);
|
188
|
+
const newEndpoints = await parseEndpoints(endpoints, inputEndpoints);
|
193
189
|
if (mode === "replace") {
|
194
190
|
return newEndpoints;
|
195
191
|
}
|
196
192
|
return uniqueBy(
|
197
|
-
//
|
198
193
|
[...newEndpoints, ...resolvedCurrentEndpoints],
|
199
|
-
|
194
|
+
l34EndpointToString
|
200
195
|
);
|
201
196
|
}
|
202
|
-
function
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
197
|
+
async function parseEndpoints(endpoints, inputEndpoints) {
|
198
|
+
const resolvedInputEndpoints = await toPromise(inputEndpoints);
|
199
|
+
return uniqueBy(
|
200
|
+
[...endpoints?.map(parseL34Endpoint) ?? [], ...resolvedInputEndpoints ?? []],
|
201
|
+
l34EndpointToString
|
202
|
+
);
|
203
|
+
}
|
204
|
+
function getServerConnection(ssh) {
|
205
|
+
return output(ssh).apply((ssh2) => ({
|
206
|
+
host: l3EndpointToString(ssh2.endpoints[0]),
|
207
|
+
port: ssh2.endpoints[0].port,
|
208
|
+
user: ssh2.user,
|
209
|
+
password: ssh2.password,
|
210
|
+
privateKey: ssh2.keyPair?.privateKey,
|
209
211
|
dialErrorLimit: 3,
|
210
|
-
hostKey:
|
212
|
+
hostKey: ssh2.hostKey
|
211
213
|
}));
|
212
214
|
}
|
213
215
|
function createCommand(command) {
|
@@ -216,76 +218,234 @@ function createCommand(command) {
|
|
216
218
|
}
|
217
219
|
return command;
|
218
220
|
}
|
221
|
+
function wrapWithWorkDir(dir) {
|
222
|
+
if (!dir) {
|
223
|
+
return (command) => output(command);
|
224
|
+
}
|
225
|
+
return (command) => interpolate`cd "${dir}" && ${command}`;
|
226
|
+
}
|
227
|
+
function wrapWithEnvironment(environment) {
|
228
|
+
if (!environment) {
|
229
|
+
return (command) => output(command);
|
230
|
+
}
|
231
|
+
return (command) => output({ command, environment }).apply(({ command: command2, environment: environment2 }) => {
|
232
|
+
if (!environment2 || Object.keys(environment2).length === 0) {
|
233
|
+
return command2;
|
234
|
+
}
|
235
|
+
const envExport = Object.entries(environment2).map(([key, value]) => `export ${key}="${value}"`).join(" && ");
|
236
|
+
return `${envExport} && ${command2}`;
|
237
|
+
});
|
238
|
+
}
|
239
|
+
function wrapWithWaitFor(timeout = 300, interval = 5) {
|
240
|
+
return (command) => (
|
241
|
+
// TOD: escape the command
|
242
|
+
interpolate`timeout ${timeout} bash -c 'while ! ${createCommand(command)}; do sleep ${interval}; done'`
|
243
|
+
);
|
244
|
+
}
|
245
|
+
function applyUpdateTriggers(env, triggers) {
|
246
|
+
return output({ env, triggers }).apply(({ env: env2, triggers: triggers2 }) => {
|
247
|
+
if (!triggers2) {
|
248
|
+
return env2;
|
249
|
+
}
|
250
|
+
const hash = sha256(JSON.stringify(triggers2));
|
251
|
+
const hashHex = Buffer.from(hash).toString("hex");
|
252
|
+
return {
|
253
|
+
...env2,
|
254
|
+
HIGHSTATE_UPDATE_TRIGGER_HASH: hashHex
|
255
|
+
};
|
256
|
+
});
|
257
|
+
}
|
219
258
|
var Command = class _Command extends ComponentResource {
|
220
|
-
command;
|
221
259
|
stdout;
|
222
260
|
stderr;
|
223
261
|
constructor(name, args, opts) {
|
224
262
|
super("highstate:common:Command", name, args, opts);
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
{
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
263
|
+
const environment = applyUpdateTriggers(
|
264
|
+
args.environment,
|
265
|
+
args.updateTriggers
|
266
|
+
);
|
267
|
+
const command = args.host === "local" ? new local.Command(
|
268
|
+
name,
|
269
|
+
{
|
270
|
+
create: output(args.create).apply(createCommand),
|
271
|
+
update: args.update ? output(args.update).apply(createCommand) : void 0,
|
272
|
+
delete: args.delete ? output(args.delete).apply(createCommand) : void 0,
|
273
|
+
logging: args.logging,
|
274
|
+
triggers: args.triggers ? output(args.triggers).apply(flat) : void 0,
|
275
|
+
dir: args.cwd ?? homedir(),
|
276
|
+
environment,
|
277
|
+
stdin: args.stdin
|
278
|
+
},
|
279
|
+
{ ...opts, parent: this }
|
280
|
+
) : new remote.Command(
|
281
|
+
name,
|
282
|
+
{
|
283
|
+
connection: output(args.host).apply((server) => {
|
284
|
+
if (!server.ssh) {
|
285
|
+
throw new Error(`The server "${server.hostname}" has no SSH credentials`);
|
286
|
+
}
|
287
|
+
return getServerConnection(server.ssh);
|
288
|
+
}),
|
289
|
+
create: output(args.create).apply(createCommand).apply(wrapWithWorkDir(args.cwd)).apply(wrapWithEnvironment(environment)),
|
290
|
+
update: args.update ? output(args.update).apply(createCommand).apply(wrapWithWorkDir(args.cwd)).apply(wrapWithEnvironment(environment)) : void 0,
|
291
|
+
delete: args.delete ? output(args.delete).apply(createCommand).apply(wrapWithWorkDir(args.cwd)).apply(wrapWithEnvironment(environment)) : void 0,
|
292
|
+
logging: args.logging,
|
293
|
+
triggers: args.triggers ? output(args.triggers).apply(flat) : void 0,
|
294
|
+
stdin: args.stdin,
|
295
|
+
addPreviousOutputInEnv: false
|
296
|
+
// TODO: does not work if server do not define AcceptEnv
|
297
|
+
// environment,
|
298
|
+
},
|
299
|
+
{ ...opts, parent: this }
|
300
|
+
);
|
301
|
+
this.stdout = command.stdout;
|
302
|
+
this.stderr = command.stderr;
|
303
|
+
this.registerOutputs({
|
304
|
+
stdout: this.stdout,
|
305
|
+
stderr: this.stderr
|
255
306
|
});
|
256
|
-
this.stdout = this.command.stdout;
|
257
|
-
this.stderr = this.command.stderr;
|
258
307
|
}
|
308
|
+
/**
|
309
|
+
* Waits for the command to complete and returns its output.
|
310
|
+
* The standard output will be returned.
|
311
|
+
*/
|
312
|
+
async wait() {
|
313
|
+
return await toPromise(this.stdout);
|
314
|
+
}
|
315
|
+
/**
|
316
|
+
* Creates a command that writes the given content to a file on the host.
|
317
|
+
* The file will be created if it does not exist, and overwritten if it does.
|
318
|
+
*
|
319
|
+
* Use for small text files like configuration files.
|
320
|
+
*/
|
259
321
|
static createTextFile(name, options, opts) {
|
260
|
-
return
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
{
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
);
|
271
|
-
return command;
|
272
|
-
});
|
322
|
+
return new _Command(
|
323
|
+
name,
|
324
|
+
{
|
325
|
+
host: options.host,
|
326
|
+
create: interpolate`mkdir -p $(dirname "${options.path}") && cat > ${options.path}`,
|
327
|
+
delete: interpolate`rm -rf ${options.path}`,
|
328
|
+
stdin: options.content
|
329
|
+
},
|
330
|
+
opts
|
331
|
+
);
|
273
332
|
}
|
333
|
+
/**
|
334
|
+
* Creates a command that waits for a file to be created and then reads its content.
|
335
|
+
* This is useful for waiting for a file to be generated by another process.
|
336
|
+
*
|
337
|
+
* Use for small text files like configuration files.
|
338
|
+
*/
|
274
339
|
static receiveTextFile(name, options, opts) {
|
275
|
-
return
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
340
|
+
return new _Command(
|
341
|
+
name,
|
342
|
+
{
|
343
|
+
host: options.host,
|
344
|
+
create: interpolate`while ! test -f "${options.path}"; do sleep 1; done; cat "${options.path}"`,
|
345
|
+
logging: "stderr"
|
346
|
+
},
|
347
|
+
opts
|
348
|
+
);
|
349
|
+
}
|
350
|
+
/**
|
351
|
+
* Creates a command that waits for a condition to be met.
|
352
|
+
* The command will run until the condition is met or the timeout is reached.
|
353
|
+
*
|
354
|
+
* The condition is considered met if the command returns a zero exit code.
|
355
|
+
*
|
356
|
+
* @param name The name of the command resource.
|
357
|
+
* @param args The arguments for the command, including the condition to check.
|
358
|
+
* @param opts Optional resource options.
|
359
|
+
*/
|
360
|
+
static waitFor(name, args, opts) {
|
361
|
+
return new _Command(
|
362
|
+
name,
|
363
|
+
{
|
364
|
+
...args,
|
365
|
+
create: output(args.create).apply(wrapWithWaitFor(args.timeout, args.interval)),
|
366
|
+
update: args.update ? output(args.update).apply(wrapWithWaitFor(args.timeout, args.interval)) : void 0,
|
367
|
+
delete: args.delete ? output(args.delete).apply(wrapWithWaitFor(args.timeout, args.interval)) : void 0
|
368
|
+
},
|
369
|
+
opts
|
370
|
+
);
|
371
|
+
}
|
372
|
+
};
|
373
|
+
var ImplementationMediator = class {
|
374
|
+
constructor(path, inputSchema, outputSchema) {
|
375
|
+
this.path = path;
|
376
|
+
this.inputSchema = inputSchema;
|
377
|
+
this.outputSchema = outputSchema;
|
378
|
+
}
|
379
|
+
implement(dataSchema, func) {
|
380
|
+
return async (input, data) => {
|
381
|
+
const parsedInput = this.inputSchema.safeParse(input);
|
382
|
+
if (!parsedInput.success) {
|
383
|
+
throw new Error(
|
384
|
+
`Invalid input for implementation "${this.path}": ${parsedInput.error.message}`
|
385
|
+
);
|
386
|
+
}
|
387
|
+
const parsedData = dataSchema.safeParse(data);
|
388
|
+
if (!parsedData.success) {
|
389
|
+
throw new Error(
|
390
|
+
`Invalid data for implementation "${this.path}": ${parsedData.error.message}`
|
391
|
+
);
|
392
|
+
}
|
393
|
+
const result = await func(parsedInput.data, parsedData.data);
|
394
|
+
const parsedResult = this.outputSchema.safeParse(result);
|
395
|
+
if (!parsedResult.success) {
|
396
|
+
throw new Error(
|
397
|
+
`Invalid output from implementation "${this.path}": ${parsedResult.error.message}`
|
398
|
+
);
|
399
|
+
}
|
400
|
+
return parsedResult.data;
|
401
|
+
};
|
402
|
+
}
|
403
|
+
async call(implRef, input) {
|
404
|
+
const resolvedImplRef = await toPromise(implRef);
|
405
|
+
const resolvedInput = await toPromise(input);
|
406
|
+
const importPath = `${resolvedImplRef.package}/impl/${this.path}`;
|
407
|
+
let impl;
|
408
|
+
try {
|
409
|
+
impl = await import(importPath);
|
410
|
+
} catch (error) {
|
411
|
+
throw new Error(`Failed to import module "${importPath}" required by implementation.`, {
|
412
|
+
cause: error
|
413
|
+
});
|
414
|
+
}
|
415
|
+
const funcs = Object.entries(impl).filter((value) => typeof value[1] === "function");
|
416
|
+
if (funcs.length === 0) {
|
417
|
+
throw new Error(`No implementation functions found in module "${importPath}".`);
|
418
|
+
}
|
419
|
+
if (funcs.length > 1) {
|
420
|
+
throw new Error(
|
421
|
+
`Multiple implementation functions found in module "${importPath}": ${funcs.map((func) => func[0]).join(", ")}. Ensure only one function is exported.`
|
284
422
|
);
|
285
|
-
|
286
|
-
|
423
|
+
}
|
424
|
+
const [funcName, implFunc] = funcs[0];
|
425
|
+
let result;
|
426
|
+
try {
|
427
|
+
result = await implFunc(resolvedInput, resolvedImplRef.data);
|
428
|
+
} catch (error) {
|
429
|
+
console.error(`Error in implementation function "${funcName}":`, error);
|
430
|
+
throw new Error(`Implementation function "${funcName}" failed`);
|
431
|
+
}
|
432
|
+
const parsedResult = this.outputSchema.safeParse(result);
|
433
|
+
if (!parsedResult.success) {
|
434
|
+
throw new Error(
|
435
|
+
`Implementation function "${funcName}" returned invalid result: ${parsedResult.error.message}`
|
436
|
+
);
|
437
|
+
}
|
438
|
+
return parsedResult.data;
|
439
|
+
}
|
440
|
+
callOutput(implRef, input) {
|
441
|
+
return output(this.call(implRef, input));
|
287
442
|
}
|
288
443
|
};
|
444
|
+
var dnsRecordMediator = new ImplementationMediator(
|
445
|
+
"dns-record",
|
446
|
+
z.object({ name: z.string(), args: z.custom() }),
|
447
|
+
z.instanceof(ComponentResource)
|
448
|
+
);
|
289
449
|
function getTypeByEndpoint(endpoint) {
|
290
450
|
switch (endpoint.type) {
|
291
451
|
case "ipv4":
|
@@ -302,53 +462,73 @@ var DnsRecord = class extends ComponentResource {
|
|
302
462
|
*/
|
303
463
|
dnsRecord;
|
304
464
|
/**
|
305
|
-
* The
|
465
|
+
* The commands to be executed after the DNS record is created/updated.
|
306
466
|
*
|
307
|
-
*
|
467
|
+
* These commands will wait for the DNS record to be resolved to the specified value.
|
308
468
|
*/
|
309
469
|
waitCommands;
|
310
470
|
constructor(name, args, opts) {
|
311
471
|
super("highstate:common:DnsRecord", name, args, opts);
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
);
|
472
|
+
const l3Endpoint = output(args.value).apply((value) => parseL3Endpoint(value));
|
473
|
+
const resolvedValue = l3Endpoint.apply(l3EndpointToString);
|
474
|
+
const resolvedType = args.type ? output(args.type) : l3Endpoint.apply(getTypeByEndpoint);
|
475
|
+
this.dnsRecord = output(args.provider).apply((provider) => {
|
476
|
+
return dnsRecordMediator.call(provider.implRef, {
|
477
|
+
name,
|
478
|
+
args: {
|
479
|
+
name: args.name,
|
480
|
+
priority: args.priority,
|
481
|
+
ttl: args.ttl,
|
482
|
+
value: resolvedValue,
|
483
|
+
type: resolvedType
|
484
|
+
}
|
485
|
+
});
|
326
486
|
});
|
327
|
-
this.waitCommands = output(
|
328
|
-
|
329
|
-
|
487
|
+
this.waitCommands = output({
|
488
|
+
waitAt: args.waitAt,
|
489
|
+
resolvedType,
|
490
|
+
proxied: args.proxied
|
491
|
+
}).apply(({ waitAt, resolvedType: resolvedType2, proxied }) => {
|
492
|
+
if (resolvedType2 === "CNAME") {
|
493
|
+
return [];
|
494
|
+
}
|
495
|
+
const resolvedHosts = waitAt ? [waitAt].flat() : [];
|
496
|
+
if (proxied) {
|
497
|
+
return resolvedHosts.map((host) => {
|
498
|
+
const hostname = host === "local" ? "local" : host.hostname;
|
499
|
+
return new Command(
|
500
|
+
`${name}.wait-for-dns.${hostname}`,
|
501
|
+
{
|
502
|
+
host,
|
503
|
+
create: [
|
504
|
+
interpolate`while ! getent hosts "${args.name}";`,
|
505
|
+
interpolate`do echo "Waiting for DNS record \"${args.name}\" to be available...";`,
|
506
|
+
`sleep 5;`,
|
507
|
+
`done`
|
508
|
+
]
|
509
|
+
},
|
510
|
+
{ parent: this }
|
511
|
+
);
|
512
|
+
});
|
513
|
+
}
|
514
|
+
return resolvedHosts.map((host) => {
|
330
515
|
const hostname = host === "local" ? "local" : host.hostname;
|
331
516
|
return new Command(
|
332
|
-
`${name}-
|
517
|
+
`${name}.wait-for-dns.${hostname}`,
|
333
518
|
{
|
334
519
|
host,
|
335
|
-
create:
|
336
|
-
|
520
|
+
create: [
|
521
|
+
interpolate`while ! getent hosts "${args.name}" | grep "${resolvedValue}";`,
|
522
|
+
interpolate`do echo "Waiting for DNS record \"${args.name}" to resolve to "${resolvedValue}"...";`,
|
523
|
+
`sleep 5;`,
|
524
|
+
`done`
|
525
|
+
]
|
337
526
|
},
|
338
527
|
{ parent: this }
|
339
528
|
);
|
340
529
|
});
|
341
530
|
});
|
342
531
|
}
|
343
|
-
static create(name, args, opts) {
|
344
|
-
return output(args).apply(async (args2) => {
|
345
|
-
const providerType = args2.provider.type;
|
346
|
-
const implName = `${capitalize(providerType)}DnsRecord`;
|
347
|
-
const implModule = await import(`@highstate/${providerType}`);
|
348
|
-
const implClass = implModule[implName];
|
349
|
-
return new implClass(name, args2, opts);
|
350
|
-
});
|
351
|
-
}
|
352
532
|
};
|
353
533
|
var DnsRecordSet = class _DnsRecordSet extends ComponentResource {
|
354
534
|
/**
|
@@ -356,34 +536,63 @@ var DnsRecordSet = class _DnsRecordSet extends ComponentResource {
|
|
356
536
|
*/
|
357
537
|
dnsRecords;
|
358
538
|
/**
|
359
|
-
* The
|
539
|
+
* The flat list of all wait commands for the DNS records.
|
360
540
|
*/
|
361
541
|
waitCommands;
|
362
|
-
constructor(name,
|
363
|
-
super("highstate:common:DnsRecordSet", name,
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
)
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
return output(
|
374
|
-
args2.providers.filter((provider) => recordName.endsWith(provider.domain)).flatMap((provider) => {
|
375
|
-
return values.map((value) => {
|
376
|
-
const l3Endpoint = parseL3Endpoint(value);
|
377
|
-
return DnsRecord.create(
|
378
|
-
`${provider.type}-from-${recordName}-to-${l3EndpointToString(l3Endpoint)}`,
|
379
|
-
{ name: recordName, ...args2, value: l3Endpoint, provider },
|
380
|
-
opts
|
381
|
-
);
|
382
|
-
});
|
383
|
-
})
|
384
|
-
);
|
542
|
+
constructor(name, args, opts) {
|
543
|
+
super("highstate:common:DnsRecordSet", name, args, opts);
|
544
|
+
const matchedProviders = output({
|
545
|
+
providers: args.providers,
|
546
|
+
name: args.name ?? name
|
547
|
+
}).apply(({ providers }) => {
|
548
|
+
const matchedProviders2 = providers.filter((provider) => name.endsWith(provider.domain));
|
549
|
+
if (matchedProviders2.length === 0) {
|
550
|
+
throw new Error(`No DNS provider matched the domain "${name}"`);
|
551
|
+
}
|
552
|
+
return matchedProviders2;
|
385
553
|
});
|
386
|
-
|
554
|
+
this.dnsRecords = normalizeInputsAndMap(args.value, args.values, (value) => {
|
555
|
+
return output({
|
556
|
+
name: args.name ?? name,
|
557
|
+
providers: matchedProviders
|
558
|
+
}).apply(({ name: name2, providers }) => {
|
559
|
+
return providers.flatMap((provider) => {
|
560
|
+
const l3Endpoint = parseL3Endpoint(value);
|
561
|
+
return new DnsRecord(
|
562
|
+
`${name2}.${provider.id}.${l3EndpointToString(l3Endpoint)}`,
|
563
|
+
{
|
564
|
+
name: name2,
|
565
|
+
provider,
|
566
|
+
value: l3Endpoint,
|
567
|
+
type: args.type ?? getTypeByEndpoint(l3Endpoint),
|
568
|
+
proxied: args.proxied,
|
569
|
+
ttl: args.ttl,
|
570
|
+
priority: args.priority,
|
571
|
+
waitAt: args.waitAt
|
572
|
+
},
|
573
|
+
{ parent: this }
|
574
|
+
);
|
575
|
+
});
|
576
|
+
});
|
577
|
+
}).apply(flat);
|
578
|
+
this.waitCommands = this.dnsRecords.apply((records) => records.flatMap((record) => record.waitCommands)).apply(flat);
|
579
|
+
}
|
580
|
+
static dnsRecordSetCache = /* @__PURE__ */ new Map();
|
581
|
+
/**
|
582
|
+
* Creates a DNS record set for the specified endpoints and waits for it to be resolved.
|
583
|
+
*
|
584
|
+
* If a DNS record set with the same name already exists, it will be reused.
|
585
|
+
*
|
586
|
+
* @param name The name of the DNS record set.
|
587
|
+
* @param args The arguments for the DNS record set.
|
588
|
+
* @param opts The options for the resource.
|
589
|
+
*/
|
590
|
+
static createOnce(name, args, opts) {
|
591
|
+
return getOrCreate(
|
592
|
+
_DnsRecordSet.dnsRecordSetCache,
|
593
|
+
name,
|
594
|
+
() => new _DnsRecordSet(name, args, opts)
|
595
|
+
);
|
387
596
|
}
|
388
597
|
};
|
389
598
|
async function updateEndpointsWithFqdn(endpoints, fqdn, fqdnEndpointFilter, patchMode, dnsProviders) {
|
@@ -395,7 +604,7 @@ async function updateEndpointsWithFqdn(endpoints, fqdn, fqdnEndpointFilter, patc
|
|
395
604
|
};
|
396
605
|
}
|
397
606
|
const filteredEndpoints = filterEndpoints(resolvedEndpoints, fqdnEndpointFilter);
|
398
|
-
const dnsRecordSet = DnsRecordSet
|
607
|
+
const dnsRecordSet = new DnsRecordSet(fqdn, {
|
399
608
|
providers: dnsProviders,
|
400
609
|
values: filteredEndpoints,
|
401
610
|
waitAt: "local"
|
@@ -435,6 +644,16 @@ async function updateEndpointsWithFqdn(endpoints, fqdn, fqdnEndpointFilter, patc
|
|
435
644
|
function generatePassword() {
|
436
645
|
return secureMask.apply(randomBytes(32)).password;
|
437
646
|
}
|
647
|
+
function generateKey(format = "hex") {
|
648
|
+
const bytes = randomBytes(32);
|
649
|
+
if (format === "raw") {
|
650
|
+
return bytes;
|
651
|
+
}
|
652
|
+
if (format === "base64") {
|
653
|
+
return Buffer.from(bytes).toString("base64");
|
654
|
+
}
|
655
|
+
return bytesToHex(bytes);
|
656
|
+
}
|
438
657
|
|
439
658
|
// assets/images.json
|
440
659
|
var terminal_ssh = {
|
@@ -442,63 +661,48 @@ var terminal_ssh = {
|
|
442
661
|
};
|
443
662
|
|
444
663
|
// src/shared/ssh.ts
|
445
|
-
function createSshTerminal(credentials) {
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
664
|
+
async function createSshTerminal(credentials) {
|
665
|
+
const resolvedCredentials = await toPromise(credentials);
|
666
|
+
const command = ["ssh", "-tt", "-o", "UserKnownHostsFile=/known_hosts"];
|
667
|
+
const endpoint = resolvedCredentials.endpoints[0];
|
668
|
+
command.push("-p", endpoint.port.toString());
|
669
|
+
if (resolvedCredentials.keyPair) {
|
670
|
+
command.push("-i", "/private_key");
|
671
|
+
}
|
672
|
+
command.push(`${resolvedCredentials.user}@${l3EndpointToString(endpoint)}`);
|
673
|
+
if (resolvedCredentials.password) {
|
674
|
+
command.unshift("sshpass", "-f", "/password");
|
675
|
+
}
|
676
|
+
return output({
|
677
|
+
name: "ssh",
|
678
|
+
meta: {
|
679
|
+
title: "Shell",
|
680
|
+
description: "Connect to the server via SSH.",
|
681
|
+
icon: "gg:remote"
|
682
|
+
},
|
683
|
+
spec: {
|
684
|
+
image: terminal_ssh.image,
|
685
|
+
command,
|
686
|
+
files: stripNullish({
|
687
|
+
"/password": resolvedCredentials.password ? fileFromString("password", resolvedCredentials.password, { isSecret: true }) : void 0,
|
688
|
+
"/private_key": resolvedCredentials.keyPair?.privateKey ? fileFromString("private_key", resolvedCredentials.keyPair.privateKey, {
|
689
|
+
isSecret: true,
|
690
|
+
mode: 384
|
691
|
+
}) : void 0,
|
692
|
+
"/known_hosts": fileFromString(
|
693
|
+
"known_hosts",
|
694
|
+
`${l3EndpointToString(endpoint)} ${resolvedCredentials.hostKey}`,
|
695
|
+
{ mode: 420 }
|
696
|
+
)
|
697
|
+
})
|
459
698
|
}
|
460
|
-
return {
|
461
|
-
name: "ssh",
|
462
|
-
meta: {
|
463
|
-
title: "Shell",
|
464
|
-
description: "Connect to the server via SSH",
|
465
|
-
icon: "gg:remote"
|
466
|
-
},
|
467
|
-
spec: {
|
468
|
-
image: terminal_ssh.image,
|
469
|
-
command,
|
470
|
-
files: {
|
471
|
-
"/password": credentials2.password,
|
472
|
-
"/private_key": credentials2.keyPair?.privateKey && {
|
473
|
-
content: {
|
474
|
-
type: "embedded",
|
475
|
-
value: credentials2.keyPair?.privateKey
|
476
|
-
},
|
477
|
-
meta: {
|
478
|
-
name: "private_key",
|
479
|
-
mode: 384
|
480
|
-
}
|
481
|
-
},
|
482
|
-
"/known_hosts": {
|
483
|
-
content: {
|
484
|
-
type: "embedded",
|
485
|
-
value: `${l3EndpointToString(endpoint)} ${credentials2.hostKey}`
|
486
|
-
},
|
487
|
-
meta: {
|
488
|
-
name: "known_hosts",
|
489
|
-
mode: 420
|
490
|
-
}
|
491
|
-
}
|
492
|
-
}
|
493
|
-
}
|
494
|
-
};
|
495
699
|
});
|
496
700
|
}
|
497
|
-
function
|
701
|
+
function generateSshPrivateKey() {
|
498
702
|
const seed = randomBytes$1(32);
|
499
|
-
return getKeys(seed).privateKey;
|
703
|
+
return secret(getKeys(seed).privateKey);
|
500
704
|
}
|
501
|
-
function
|
705
|
+
function sshPrivateKeyToKeyPair(privateKeyString) {
|
502
706
|
return output(privateKeyString).apply((privateKeyString2) => {
|
503
707
|
const privateKeyStruct = PrivateExport.decode(privateKeyString2);
|
504
708
|
const privKey = privateKeyStruct.keys[0].privKey.privKey;
|
@@ -511,60 +715,88 @@ function privateKeyToKeyPair(privateKeyString) {
|
|
511
715
|
});
|
512
716
|
});
|
513
717
|
}
|
514
|
-
function
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
718
|
+
async function createServerBundle(options) {
|
719
|
+
const server = await createServerEntity(options);
|
720
|
+
const ssh = await toPromise(server.ssh);
|
721
|
+
return {
|
722
|
+
server,
|
723
|
+
terminal: ssh ? await createSshTerminal(ssh) : void 0
|
724
|
+
};
|
520
725
|
}
|
521
|
-
function createServerEntity(
|
726
|
+
async function createServerEntity({
|
727
|
+
name,
|
728
|
+
fallbackHostname,
|
729
|
+
endpoints,
|
730
|
+
sshArgs = { enabled: true, port: 22, user: "root" },
|
731
|
+
sshPassword,
|
732
|
+
sshPrivateKey,
|
733
|
+
sshKeyPair,
|
734
|
+
pingInterval,
|
735
|
+
pingTimeout,
|
736
|
+
waitForPing,
|
737
|
+
waitForSsh,
|
738
|
+
sshCheckInterval,
|
739
|
+
sshCheckTimeout
|
740
|
+
}) {
|
741
|
+
if (endpoints.length === 0) {
|
742
|
+
throw new Error("At least one L3 endpoint is required to create a server entity");
|
743
|
+
}
|
744
|
+
fallbackHostname ??= name;
|
745
|
+
waitForSsh ??= sshArgs.enabled;
|
746
|
+
waitForPing ??= !waitForSsh;
|
747
|
+
if (waitForPing) {
|
748
|
+
await Command.waitFor(`${name}.ping`, {
|
749
|
+
host: "local",
|
750
|
+
create: `ping -c 1 ${l3EndpointToString(endpoints[0])}`,
|
751
|
+
timeout: pingTimeout ?? 300,
|
752
|
+
interval: pingInterval ?? 5,
|
753
|
+
triggers: [Date.now()]
|
754
|
+
}).wait();
|
755
|
+
}
|
756
|
+
if (!sshArgs.enabled) {
|
757
|
+
return output({
|
758
|
+
hostname: name,
|
759
|
+
endpoints
|
760
|
+
});
|
761
|
+
}
|
762
|
+
const sshHost = sshArgs?.host ?? l3EndpointToString(endpoints[0]);
|
763
|
+
if (waitForSsh) {
|
764
|
+
await Command.waitFor(`${name}.ssh`, {
|
765
|
+
host: "local",
|
766
|
+
create: `nc -zv ${sshHost} ${sshArgs.port}`,
|
767
|
+
timeout: sshCheckTimeout ?? 300,
|
768
|
+
interval: sshCheckInterval ?? 5,
|
769
|
+
triggers: [Date.now()]
|
770
|
+
}).wait();
|
771
|
+
}
|
522
772
|
const connection = output({
|
523
|
-
host:
|
524
|
-
port:
|
525
|
-
user:
|
773
|
+
host: sshHost,
|
774
|
+
port: sshArgs.port,
|
775
|
+
user: sshArgs.user,
|
526
776
|
password: sshPassword,
|
527
|
-
privateKey: sshPrivateKey,
|
777
|
+
privateKey: sshKeyPair ? output(sshKeyPair).privateKey : sshPrivateKey,
|
528
778
|
dialErrorLimit: 3
|
529
779
|
});
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
endpoints: [endpoint]
|
534
|
-
});
|
535
|
-
}
|
536
|
-
const command = new local.Command("check-ssh", {
|
537
|
-
create: `nc -zv ${l3EndpointToString(endpoint)} ${sshPort} && echo "up" || echo "down"`,
|
780
|
+
const hostnameResult = new remote.Command("hostname", {
|
781
|
+
connection,
|
782
|
+
create: "hostname",
|
538
783
|
triggers: [Date.now()]
|
539
784
|
});
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
785
|
+
const hostKeyResult = new remote.Command("host-key", {
|
786
|
+
connection,
|
787
|
+
create: "cat /etc/ssh/ssh_host_ed25519_key.pub",
|
788
|
+
triggers: [Date.now()]
|
789
|
+
});
|
790
|
+
return output({
|
791
|
+
endpoints,
|
792
|
+
hostname: hostnameResult.stdout.apply((x) => x.trim()),
|
793
|
+
ssh: {
|
794
|
+
endpoints: [l3EndpointToL4(sshHost, sshArgs.port ?? 22)],
|
795
|
+
user: sshArgs.user ?? "root",
|
796
|
+
hostKey: hostKeyResult.stdout.apply((x) => x.trim()),
|
797
|
+
password: connection.password,
|
798
|
+
keyPair: sshKeyPair ? sshKeyPair : sshPrivateKey ? sshPrivateKeyToKeyPair(sshPrivateKey) : void 0
|
546
799
|
}
|
547
|
-
const hostnameResult = new remote.Command("hostname", {
|
548
|
-
connection,
|
549
|
-
create: "hostname",
|
550
|
-
triggers: [Date.now()]
|
551
|
-
});
|
552
|
-
const hostKeyResult = new remote.Command("host-key", {
|
553
|
-
connection,
|
554
|
-
create: "cat /etc/ssh/ssh_host_ed25519_key.pub",
|
555
|
-
triggers: [Date.now()]
|
556
|
-
});
|
557
|
-
return output({
|
558
|
-
endpoints: [endpoint],
|
559
|
-
hostname: hostnameResult.stdout.apply((x) => x.trim()),
|
560
|
-
ssh: {
|
561
|
-
endpoints: [l3ToL4Endpoint(endpoint, sshPort)],
|
562
|
-
user: sshUser,
|
563
|
-
hostKey: hostKeyResult.stdout.apply((x) => x.trim()),
|
564
|
-
password: sshPassword,
|
565
|
-
keyPair: sshPrivateKey ? privateKeyToKeyPair(sshPrivateKey) : void 0
|
566
|
-
}
|
567
|
-
});
|
568
800
|
});
|
569
801
|
}
|
570
802
|
function assetFromFile(file) {
|
@@ -579,7 +811,7 @@ function assetFromFile(file) {
|
|
579
811
|
"Artifact-based files cannot be converted to Pulumi assets directly. Use MaterializedFile instead."
|
580
812
|
);
|
581
813
|
}
|
582
|
-
if (file.
|
814
|
+
if (file.content.isBinary) {
|
583
815
|
throw new Error(
|
584
816
|
"Cannot create asset from inline binary file content. Please open an issue if you need this feature."
|
585
817
|
);
|
@@ -646,11 +878,14 @@ var MaterializedFile = class _MaterializedFile {
|
|
646
878
|
constructor(entity, parent) {
|
647
879
|
this.entity = entity;
|
648
880
|
this.parent = parent;
|
881
|
+
this.artifactMeta = {
|
882
|
+
title: `Materialized file "${entity.meta.name}"`
|
883
|
+
};
|
649
884
|
}
|
650
885
|
_tmpPath;
|
651
886
|
_path;
|
652
887
|
_disposed = false;
|
653
|
-
artifactMeta
|
888
|
+
artifactMeta;
|
654
889
|
get path() {
|
655
890
|
return this._path;
|
656
891
|
}
|
@@ -664,7 +899,7 @@ var MaterializedFile = class _MaterializedFile {
|
|
664
899
|
}
|
665
900
|
switch (this.entity.content.type) {
|
666
901
|
case "embedded": {
|
667
|
-
const content = this.entity.
|
902
|
+
const content = this.entity.content.isBinary ? Buffer.from(this.entity.content.value, "base64") : this.entity.content.value;
|
668
903
|
await writeFile(this._path, content, { mode: this.entity.meta.mode });
|
669
904
|
break;
|
670
905
|
}
|
@@ -673,21 +908,20 @@ var MaterializedFile = class _MaterializedFile {
|
|
673
908
|
break;
|
674
909
|
}
|
675
910
|
case "remote": {
|
676
|
-
const response = await
|
911
|
+
const response = await fetch(l7EndpointToString(this.entity.content.endpoint));
|
677
912
|
if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`);
|
678
913
|
const arrayBuffer = await response.arrayBuffer();
|
679
914
|
await writeFile(this._path, Buffer.from(arrayBuffer), { mode: this.entity.meta.mode });
|
680
915
|
break;
|
681
916
|
}
|
682
917
|
case "artifact": {
|
683
|
-
const artifactData = this.entity.content[HighstateSignature.Artifact];
|
684
918
|
const artifactPath = process.env.HIGHSTATE_ARTIFACT_READ_PATH;
|
685
919
|
if (!artifactPath) {
|
686
920
|
throw new Error(
|
687
921
|
"HIGHSTATE_ARTIFACT_READ_PATH environment variable is not set but required for artifact content"
|
688
922
|
);
|
689
923
|
}
|
690
|
-
const tgzPath = join(artifactPath, `${
|
924
|
+
const tgzPath = join(artifactPath, `${this.entity.content.hash}.tgz`);
|
691
925
|
const readStream = createReadStream(tgzPath);
|
692
926
|
await unarchiveFromStream(readStream, dirname(this._path), "tar");
|
693
927
|
break;
|
@@ -743,18 +977,15 @@ var MaterializedFile = class _MaterializedFile {
|
|
743
977
|
name: this.entity.meta.name,
|
744
978
|
mode: fileStats.mode & 511,
|
745
979
|
// extract only permission bits
|
746
|
-
size: fileStats.size
|
747
|
-
isBinary: this.entity.meta.isBinary
|
748
|
-
// keep original binary flag as we can't reliably detect this from filesystem
|
980
|
+
size: fileStats.size
|
749
981
|
};
|
750
982
|
return {
|
751
983
|
meta: newMeta,
|
752
984
|
content: {
|
753
985
|
type: "artifact",
|
754
|
-
[HighstateSignature.Artifact]:
|
755
|
-
|
756
|
-
|
757
|
-
}
|
986
|
+
[HighstateSignature.Artifact]: true,
|
987
|
+
hash: hashValue,
|
988
|
+
meta: await toPromise(this.artifactMeta)
|
758
989
|
}
|
759
990
|
};
|
760
991
|
} finally {
|
@@ -777,8 +1008,7 @@ var MaterializedFile = class _MaterializedFile {
|
|
777
1008
|
meta: {
|
778
1009
|
name,
|
779
1010
|
mode,
|
780
|
-
size: 0
|
781
|
-
isBinary: false
|
1011
|
+
size: 0
|
782
1012
|
},
|
783
1013
|
content: {
|
784
1014
|
type: "embedded",
|
@@ -809,12 +1039,15 @@ var MaterializedFolder = class _MaterializedFolder {
|
|
809
1039
|
constructor(entity, parent) {
|
810
1040
|
this.entity = entity;
|
811
1041
|
this.parent = parent;
|
1042
|
+
this.artifactMeta = {
|
1043
|
+
title: `Materialized folder "${entity.meta.name}"`
|
1044
|
+
};
|
812
1045
|
}
|
813
1046
|
_tmpPath;
|
814
1047
|
_path;
|
815
1048
|
_disposed = false;
|
816
1049
|
_disposables = [];
|
817
|
-
artifactMeta
|
1050
|
+
artifactMeta;
|
818
1051
|
get path() {
|
819
1052
|
return this._path;
|
820
1053
|
}
|
@@ -853,7 +1086,7 @@ var MaterializedFolder = class _MaterializedFolder {
|
|
853
1086
|
break;
|
854
1087
|
}
|
855
1088
|
case "remote": {
|
856
|
-
const response = await
|
1089
|
+
const response = await fetch(l7EndpointToString(this.entity.content.endpoint));
|
857
1090
|
if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`);
|
858
1091
|
if (!response.body) throw new Error("Response body is empty");
|
859
1092
|
const url = new URL(l7EndpointToString(this.entity.content.endpoint));
|
@@ -886,14 +1119,13 @@ var MaterializedFolder = class _MaterializedFolder {
|
|
886
1119
|
break;
|
887
1120
|
}
|
888
1121
|
case "artifact": {
|
889
|
-
const artifactData = this.entity.content[HighstateSignature.Artifact];
|
890
1122
|
const artifactPath = process.env.HIGHSTATE_ARTIFACT_READ_PATH;
|
891
1123
|
if (!artifactPath) {
|
892
1124
|
throw new Error(
|
893
1125
|
"HIGHSTATE_ARTIFACT_READ_PATH environment variable is not set but required for artifact content"
|
894
1126
|
);
|
895
1127
|
}
|
896
|
-
const tgzPath = join(artifactPath, `${
|
1128
|
+
const tgzPath = join(artifactPath, `${this.entity.content.hash}.tgz`);
|
897
1129
|
const readStream = createReadStream(tgzPath);
|
898
1130
|
await unarchiveFromStream(readStream, dirname(this._path), "tar");
|
899
1131
|
break;
|
@@ -972,11 +1204,10 @@ var MaterializedFolder = class _MaterializedFolder {
|
|
972
1204
|
return {
|
973
1205
|
meta: newMeta,
|
974
1206
|
content: {
|
1207
|
+
[HighstateSignature.Artifact]: true,
|
975
1208
|
type: "artifact",
|
976
|
-
|
977
|
-
|
978
|
-
meta: await toPromise(this.artifactMeta)
|
979
|
-
}
|
1209
|
+
hash: hashValue,
|
1210
|
+
meta: await toPromise(this.artifactMeta)
|
980
1211
|
}
|
981
1212
|
};
|
982
1213
|
} finally {
|
@@ -1033,7 +1264,7 @@ async function fetchFileSize(endpoint) {
|
|
1033
1264
|
);
|
1034
1265
|
}
|
1035
1266
|
const url = l7EndpointToString(endpoint);
|
1036
|
-
const response = await
|
1267
|
+
const response = await fetch(url, { method: "HEAD" });
|
1037
1268
|
if (!response.ok) {
|
1038
1269
|
throw new Error(`Failed to fetch file size: ${response.statusText}`);
|
1039
1270
|
}
|
@@ -1042,7 +1273,7 @@ async function fetchFileSize(endpoint) {
|
|
1042
1273
|
throw new Error("Content-Length header is missing in the response");
|
1043
1274
|
}
|
1044
1275
|
const size = parseInt(contentLength, 10);
|
1045
|
-
if (isNaN(size)) {
|
1276
|
+
if (Number.isNaN(size)) {
|
1046
1277
|
throw new Error(`Invalid Content-Length value: ${contentLength}`);
|
1047
1278
|
}
|
1048
1279
|
return size;
|
@@ -1051,7 +1282,168 @@ function getNameByEndpoint(endpoint) {
|
|
1051
1282
|
const parsedEndpoint = parseL7Endpoint(endpoint);
|
1052
1283
|
return parsedEndpoint.resource ? basename(parsedEndpoint.resource) : "";
|
1053
1284
|
}
|
1285
|
+
var gatewayRouteMediator = new ImplementationMediator(
|
1286
|
+
"gateway-route",
|
1287
|
+
z.object({
|
1288
|
+
name: z.string(),
|
1289
|
+
spec: z.custom(),
|
1290
|
+
opts: z.custom().optional()
|
1291
|
+
}),
|
1292
|
+
z.object({
|
1293
|
+
resource: z.instanceof(Resource),
|
1294
|
+
endpoints: network.l3EndpointEntity.schema.array()
|
1295
|
+
})
|
1296
|
+
);
|
1297
|
+
var GatewayRoute = class extends ComponentResource {
|
1298
|
+
/**
|
1299
|
+
* The underlying resource created by the implementation.
|
1300
|
+
*/
|
1301
|
+
resource;
|
1302
|
+
/**
|
1303
|
+
* The endpoints of the gateway which serve this route.
|
1304
|
+
*
|
1305
|
+
* In most cases, this will be a single endpoint of the gateway shared for all routes.
|
1306
|
+
*/
|
1307
|
+
endpoints;
|
1308
|
+
constructor(name, args, opts) {
|
1309
|
+
super("highstate:common:GatewayRoute", name, args, opts);
|
1310
|
+
const { resource, endpoints } = gatewayRouteMediator.callOutput(output(args.gateway).implRef, {
|
1311
|
+
name,
|
1312
|
+
spec: args,
|
1313
|
+
opts: { ...opts, parent: this }
|
1314
|
+
});
|
1315
|
+
this.resource = resource;
|
1316
|
+
this.endpoints = endpoints;
|
1317
|
+
}
|
1318
|
+
};
|
1319
|
+
var tlsCertificateMediator = new ImplementationMediator(
|
1320
|
+
"tls-certificate",
|
1321
|
+
z.object({
|
1322
|
+
name: z.string(),
|
1323
|
+
spec: z.custom(),
|
1324
|
+
opts: z.custom().optional()
|
1325
|
+
}),
|
1326
|
+
z.instanceof(Resource)
|
1327
|
+
);
|
1328
|
+
var TlsCertificate = class _TlsCertificate extends ComponentResource {
|
1329
|
+
/**
|
1330
|
+
* The underlying resource created by the implementation.
|
1331
|
+
*/
|
1332
|
+
resource;
|
1333
|
+
constructor(name, args, opts) {
|
1334
|
+
super("highstate:common:TlsCertificate", name, args, opts);
|
1335
|
+
const issuers = normalizeInputs(args.issuer, args.issuers);
|
1336
|
+
this.resource = output({
|
1337
|
+
issuers,
|
1338
|
+
commonName: args.commonName,
|
1339
|
+
dnsNames: args.dnsNames
|
1340
|
+
}).apply(async ({ issuers: issuers2, commonName, dnsNames }) => {
|
1341
|
+
const matchedIssuer = issuers2.find((issuer) => {
|
1342
|
+
if (commonName && !commonName.endsWith(issuer.domain)) {
|
1343
|
+
return false;
|
1344
|
+
}
|
1345
|
+
if (dnsNames && !dnsNames.every((name2) => name2.endsWith(issuer.domain))) {
|
1346
|
+
return false;
|
1347
|
+
}
|
1348
|
+
return true;
|
1349
|
+
});
|
1350
|
+
if (!matchedIssuer) {
|
1351
|
+
throw new Error(
|
1352
|
+
`No TLS issuer matched the common name "${commonName}" and DNS names "${dnsNames?.join(", ") ?? ""}"`
|
1353
|
+
);
|
1354
|
+
}
|
1355
|
+
return await tlsCertificateMediator.call(matchedIssuer.implRef, {
|
1356
|
+
name,
|
1357
|
+
spec: args
|
1358
|
+
});
|
1359
|
+
});
|
1360
|
+
}
|
1361
|
+
static tlsCertificateCache = /* @__PURE__ */ new Map();
|
1362
|
+
/**
|
1363
|
+
* Creates a TLS certificate for the specified common name and DNS names.
|
1364
|
+
*
|
1365
|
+
* If a TLS certificate with the same name already exists, it will be reused.
|
1366
|
+
*
|
1367
|
+
* @param name The name of the TLS certificate.
|
1368
|
+
* @param args The arguments for the TLS certificate.
|
1369
|
+
* @param opts The options for the resource.
|
1370
|
+
*/
|
1371
|
+
static createOnce(name, args, opts) {
|
1372
|
+
return getOrCreate(
|
1373
|
+
_TlsCertificate.tlsCertificateCache,
|
1374
|
+
name,
|
1375
|
+
() => new _TlsCertificate(name, args, opts)
|
1376
|
+
);
|
1377
|
+
}
|
1378
|
+
};
|
1379
|
+
var AccessPointRoute = class extends ComponentResource {
|
1380
|
+
/**
|
1381
|
+
* The created gateway route.
|
1382
|
+
*/
|
1383
|
+
route;
|
1384
|
+
/**
|
1385
|
+
* The DNS record set created for the route.
|
1386
|
+
*
|
1387
|
+
* May be shared between multiple routes with the same FQDN.
|
1388
|
+
*/
|
1389
|
+
dnsRecordSet;
|
1390
|
+
/**
|
1391
|
+
* The TLS certificate created for the route.
|
1392
|
+
*
|
1393
|
+
* May be shared between multiple routes with the same FQDN.
|
1394
|
+
*/
|
1395
|
+
tlsCertificate;
|
1396
|
+
constructor(name, args, opts) {
|
1397
|
+
super("highstate:common:AccessPointRoute", name, args, opts);
|
1398
|
+
if (args.fqdn && args.type === "http" && !args.insecure) {
|
1399
|
+
this.tlsCertificate = output(args.accessPoint).apply((accessPoint) => {
|
1400
|
+
if (accessPoint.tlsIssuers.length === 0) {
|
1401
|
+
return void 0;
|
1402
|
+
}
|
1403
|
+
return TlsCertificate.createOnce(
|
1404
|
+
name,
|
1405
|
+
{
|
1406
|
+
issuers: accessPoint.tlsIssuers,
|
1407
|
+
dnsNames: args.fqdn ? [args.fqdn] : [],
|
1408
|
+
nativeData: args.tlsCertificateNativeData
|
1409
|
+
},
|
1410
|
+
{ ...opts, parent: this }
|
1411
|
+
);
|
1412
|
+
});
|
1413
|
+
}
|
1414
|
+
this.route = new GatewayRoute(
|
1415
|
+
name,
|
1416
|
+
{
|
1417
|
+
...args,
|
1418
|
+
gateway: output(args.accessPoint).gateway,
|
1419
|
+
tlsCertificate: this.tlsCertificate,
|
1420
|
+
nativeData: args.gatewayNativeData
|
1421
|
+
},
|
1422
|
+
{ ...opts, parent: this }
|
1423
|
+
);
|
1424
|
+
if (args.fqdn) {
|
1425
|
+
this.dnsRecordSet = output(args.accessPoint).apply(async (accessPoint) => {
|
1426
|
+
if (accessPoint.dnsProviders.length === 0) {
|
1427
|
+
return void 0;
|
1428
|
+
}
|
1429
|
+
const fqdn = await toPromise(args.fqdn);
|
1430
|
+
if (!fqdn) {
|
1431
|
+
return void 0;
|
1432
|
+
}
|
1433
|
+
return DnsRecordSet.createOnce(
|
1434
|
+
fqdn,
|
1435
|
+
{
|
1436
|
+
providers: output(args.accessPoint).dnsProviders,
|
1437
|
+
values: this.route.endpoints,
|
1438
|
+
waitAt: "local"
|
1439
|
+
},
|
1440
|
+
{ ...opts, parent: this }
|
1441
|
+
);
|
1442
|
+
});
|
1443
|
+
}
|
1444
|
+
}
|
1445
|
+
};
|
1054
1446
|
|
1055
|
-
export { Command, DnsRecord, DnsRecordSet, MaterializedFile, MaterializedFolder, archiveFromFolder, assetFromFile, createServerEntity, createSshTerminal, fetchFileSize, filterEndpoints, generatePassword,
|
1056
|
-
//# sourceMappingURL=chunk-
|
1057
|
-
//# sourceMappingURL=chunk-
|
1447
|
+
export { AccessPointRoute, Command, DnsRecord, DnsRecordSet, GatewayRoute, ImplementationMediator, MaterializedFile, MaterializedFolder, TlsCertificate, archiveFromFolder, assetFromFile, createServerBundle, createServerEntity, createSshTerminal, dnsRecordMediator, fetchFileSize, filterEndpoints, gatewayRouteMediator, generateKey, generatePassword, generateSshPrivateKey, getNameByEndpoint, getServerConnection, l34EndpointToString, l3EndpointToCidr, l3EndpointToL4, l3EndpointToString, l4EndpointToString, l4EndpointWithProtocolToString, l7EndpointToString, parseEndpoints, parseL34Endpoint, parseL3Endpoint, parseL4Endpoint, parseL7Endpoint, requireInputL3Endpoint, requireInputL4Endpoint, sshPrivateKeyToKeyPair, tlsCertificateMediator, updateEndpoints, updateEndpointsWithFqdn };
|
1448
|
+
//# sourceMappingURL=chunk-WDYIUWYZ.js.map
|
1449
|
+
//# sourceMappingURL=chunk-WDYIUWYZ.js.map
|