@highstate/common 0.9.18 → 0.9.20
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-YYNV3MVT.js → chunk-WDYIUWYZ.js} +484 -176
- 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 +9 -9
- package/dist/units/existing-server/index.js.map +1 -1
- package/dist/units/network/l3-endpoint/index.js +1 -1
- package/dist/units/network/l4-endpoint/index.js +1 -1
- package/dist/units/script/index.js +1 -1
- package/dist/units/server-dns/index.js +1 -1
- package/dist/units/server-patch/index.js +1 -1
- package/dist/units/ssh/key-pair/index.js +4 -3
- 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 +81 -14
- package/src/shared/dns.ts +150 -90
- package/src/shared/files.ts +23 -18
- 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 +39 -25
- package/src/shared/passwords.ts +3 -3
- package/src/shared/ssh.ts +109 -124
- 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 +9 -15
- package/src/units/ssh/key-pair/index.ts +4 -3
- package/dist/chunk-YYNV3MVT.js.map +0 -1
@@ -1,22 +1,23 @@
|
|
1
|
-
import { toPromise, output,
|
2
|
-
import { uniqueBy, flat,
|
1
|
+
import { ComponentResource, Resource, toPromise, output, interpolate, normalizeInputsAndMap, fileFromString, secret, asset, normalizeInputs } from '@highstate/pulumi';
|
2
|
+
import { uniqueBy, flat, groupBy } from 'remeda';
|
3
3
|
import { homedir, tmpdir } from 'node:os';
|
4
4
|
import { local, remote } from '@pulumi/command';
|
5
|
-
import '@
|
5
|
+
import { sha256 } from '@noble/hashes/sha2';
|
6
|
+
import { z, getOrCreate, stripNullish, HighstateSignature } from '@highstate/contract';
|
6
7
|
import { randomBytes, bytesToHex } from '@noble/hashes/utils';
|
7
8
|
import { secureMask } from 'micro-key-producer/password.js';
|
8
9
|
import getKeys, { PrivateExport } from 'micro-key-producer/ssh.js';
|
9
10
|
import { randomBytes as randomBytes$1 } from 'micro-key-producer/utils.js';
|
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) {
|
@@ -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) {
|
@@ -222,17 +224,46 @@ function wrapWithWorkDir(dir) {
|
|
222
224
|
}
|
223
225
|
return (command) => interpolate`cd "${dir}" && ${command}`;
|
224
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
|
+
}
|
225
239
|
function wrapWithWaitFor(timeout = 300, interval = 5) {
|
226
240
|
return (command) => (
|
227
241
|
// TOD: escape the command
|
228
242
|
interpolate`timeout ${timeout} bash -c 'while ! ${createCommand(command)}; do sleep ${interval}; done'`
|
229
243
|
);
|
230
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
|
+
}
|
231
258
|
var Command = class _Command extends ComponentResource {
|
232
259
|
stdout;
|
233
260
|
stderr;
|
234
261
|
constructor(name, args, opts) {
|
235
262
|
super("highstate:common:Command", name, args, opts);
|
263
|
+
const environment = applyUpdateTriggers(
|
264
|
+
args.environment,
|
265
|
+
args.updateTriggers
|
266
|
+
);
|
236
267
|
const command = args.host === "local" ? new local.Command(
|
237
268
|
name,
|
238
269
|
{
|
@@ -242,7 +273,7 @@ var Command = class _Command extends ComponentResource {
|
|
242
273
|
logging: args.logging,
|
243
274
|
triggers: args.triggers ? output(args.triggers).apply(flat) : void 0,
|
244
275
|
dir: args.cwd ?? homedir(),
|
245
|
-
environment
|
276
|
+
environment,
|
246
277
|
stdin: args.stdin
|
247
278
|
},
|
248
279
|
{ ...opts, parent: this }
|
@@ -250,26 +281,29 @@ var Command = class _Command extends ComponentResource {
|
|
250
281
|
name,
|
251
282
|
{
|
252
283
|
connection: output(args.host).apply((server) => {
|
253
|
-
if ("host" in server) {
|
254
|
-
return output(server);
|
255
|
-
}
|
256
284
|
if (!server.ssh) {
|
257
285
|
throw new Error(`The server "${server.hostname}" has no SSH credentials`);
|
258
286
|
}
|
259
287
|
return getServerConnection(server.ssh);
|
260
288
|
}),
|
261
|
-
create: output(args.create).apply(createCommand).apply(wrapWithWorkDir(args.cwd)),
|
262
|
-
update: args.update ? output(args.update).apply(createCommand).apply(wrapWithWorkDir(args.cwd)) : void 0,
|
263
|
-
delete: args.delete ? output(args.delete).apply(createCommand).apply(wrapWithWorkDir(args.cwd)) : void 0,
|
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,
|
264
292
|
logging: args.logging,
|
265
293
|
triggers: args.triggers ? output(args.triggers).apply(flat) : void 0,
|
266
294
|
stdin: args.stdin,
|
267
|
-
|
295
|
+
addPreviousOutputInEnv: false
|
296
|
+
// TODO: does not work if server do not define AcceptEnv
|
297
|
+
// environment,
|
268
298
|
},
|
269
299
|
{ ...opts, parent: this }
|
270
300
|
);
|
271
301
|
this.stdout = command.stdout;
|
272
302
|
this.stderr = command.stderr;
|
303
|
+
this.registerOutputs({
|
304
|
+
stdout: this.stdout,
|
305
|
+
stderr: this.stderr
|
306
|
+
});
|
273
307
|
}
|
274
308
|
/**
|
275
309
|
* Waits for the command to complete and returns its output.
|
@@ -336,6 +370,82 @@ var Command = class _Command extends ComponentResource {
|
|
336
370
|
);
|
337
371
|
}
|
338
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.`
|
422
|
+
);
|
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));
|
442
|
+
}
|
443
|
+
};
|
444
|
+
var dnsRecordMediator = new ImplementationMediator(
|
445
|
+
"dns-record",
|
446
|
+
z.object({ name: z.string(), args: z.custom() }),
|
447
|
+
z.instanceof(ComponentResource)
|
448
|
+
);
|
339
449
|
function getTypeByEndpoint(endpoint) {
|
340
450
|
switch (endpoint.type) {
|
341
451
|
case "ipv4":
|
@@ -352,53 +462,73 @@ var DnsRecord = class extends ComponentResource {
|
|
352
462
|
*/
|
353
463
|
dnsRecord;
|
354
464
|
/**
|
355
|
-
* The
|
465
|
+
* The commands to be executed after the DNS record is created/updated.
|
356
466
|
*
|
357
|
-
*
|
467
|
+
* These commands will wait for the DNS record to be resolved to the specified value.
|
358
468
|
*/
|
359
469
|
waitCommands;
|
360
470
|
constructor(name, args, opts) {
|
361
471
|
super("highstate:common:DnsRecord", name, args, opts);
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
);
|
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
|
+
});
|
376
486
|
});
|
377
|
-
this.waitCommands = output(
|
378
|
-
|
379
|
-
|
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) => {
|
380
515
|
const hostname = host === "local" ? "local" : host.hostname;
|
381
516
|
return new Command(
|
382
|
-
`${name}-
|
517
|
+
`${name}.wait-for-dns.${hostname}`,
|
383
518
|
{
|
384
519
|
host,
|
385
|
-
create:
|
386
|
-
|
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
|
+
]
|
387
526
|
},
|
388
527
|
{ parent: this }
|
389
528
|
);
|
390
529
|
});
|
391
530
|
});
|
392
531
|
}
|
393
|
-
static create(name, args, opts) {
|
394
|
-
return output(args).apply(async (args2) => {
|
395
|
-
const providerType = args2.provider.type;
|
396
|
-
const implName = `${capitalize(providerType)}DnsRecord`;
|
397
|
-
const implModule = await import(`@highstate/${providerType}`);
|
398
|
-
const implClass = implModule[implName];
|
399
|
-
return new implClass(name, args2, opts);
|
400
|
-
});
|
401
|
-
}
|
402
532
|
};
|
403
533
|
var DnsRecordSet = class _DnsRecordSet extends ComponentResource {
|
404
534
|
/**
|
@@ -406,34 +536,63 @@ var DnsRecordSet = class _DnsRecordSet extends ComponentResource {
|
|
406
536
|
*/
|
407
537
|
dnsRecords;
|
408
538
|
/**
|
409
|
-
* The
|
539
|
+
* The flat list of all wait commands for the DNS records.
|
410
540
|
*/
|
411
541
|
waitCommands;
|
412
|
-
constructor(name,
|
413
|
-
super("highstate:common:DnsRecordSet", name,
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
)
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
return output(
|
424
|
-
args2.providers.filter((provider) => recordName.endsWith(provider.domain)).flatMap((provider) => {
|
425
|
-
return values.map((value) => {
|
426
|
-
const l3Endpoint = parseL3Endpoint(value);
|
427
|
-
return DnsRecord.create(
|
428
|
-
`${provider.type}-from-${recordName}-to-${l3EndpointToString(l3Endpoint)}`,
|
429
|
-
{ name: recordName, ...args2, value: l3Endpoint, provider },
|
430
|
-
opts
|
431
|
-
);
|
432
|
-
});
|
433
|
-
})
|
434
|
-
);
|
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;
|
435
553
|
});
|
436
|
-
|
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
|
+
);
|
437
596
|
}
|
438
597
|
};
|
439
598
|
async function updateEndpointsWithFqdn(endpoints, fqdn, fqdnEndpointFilter, patchMode, dnsProviders) {
|
@@ -445,7 +604,7 @@ async function updateEndpointsWithFqdn(endpoints, fqdn, fqdnEndpointFilter, patc
|
|
445
604
|
};
|
446
605
|
}
|
447
606
|
const filteredEndpoints = filterEndpoints(resolvedEndpoints, fqdnEndpointFilter);
|
448
|
-
const dnsRecordSet = DnsRecordSet
|
607
|
+
const dnsRecordSet = new DnsRecordSet(fqdn, {
|
449
608
|
providers: dnsProviders,
|
450
609
|
values: filteredEndpoints,
|
451
610
|
waitAt: "local"
|
@@ -502,61 +661,46 @@ var terminal_ssh = {
|
|
502
661
|
};
|
503
662
|
|
504
663
|
// src/shared/ssh.ts
|
505
|
-
function createSshTerminal(credentials) {
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
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
|
+
})
|
519
698
|
}
|
520
|
-
return {
|
521
|
-
name: "ssh",
|
522
|
-
meta: {
|
523
|
-
title: "Shell",
|
524
|
-
description: "Connect to the server via SSH",
|
525
|
-
icon: "gg:remote"
|
526
|
-
},
|
527
|
-
spec: {
|
528
|
-
image: terminal_ssh.image,
|
529
|
-
command,
|
530
|
-
files: {
|
531
|
-
"/password": credentials2.password,
|
532
|
-
"/private_key": credentials2.keyPair?.privateKey && {
|
533
|
-
content: {
|
534
|
-
type: "embedded",
|
535
|
-
value: credentials2.keyPair?.privateKey
|
536
|
-
},
|
537
|
-
meta: {
|
538
|
-
name: "private_key",
|
539
|
-
mode: 384
|
540
|
-
}
|
541
|
-
},
|
542
|
-
"/known_hosts": {
|
543
|
-
content: {
|
544
|
-
type: "embedded",
|
545
|
-
value: `${l3EndpointToString(endpoint)} ${credentials2.hostKey}`
|
546
|
-
},
|
547
|
-
meta: {
|
548
|
-
name: "known_hosts",
|
549
|
-
mode: 420
|
550
|
-
}
|
551
|
-
}
|
552
|
-
}
|
553
|
-
}
|
554
|
-
};
|
555
699
|
});
|
556
700
|
}
|
557
701
|
function generateSshPrivateKey() {
|
558
702
|
const seed = randomBytes$1(32);
|
559
|
-
return getKeys(seed).privateKey;
|
703
|
+
return secret(getKeys(seed).privateKey);
|
560
704
|
}
|
561
705
|
function sshPrivateKeyToKeyPair(privateKeyString) {
|
562
706
|
return output(privateKeyString).apply((privateKeyString2) => {
|
@@ -571,22 +715,22 @@ function sshPrivateKeyToKeyPair(privateKeyString) {
|
|
571
715
|
});
|
572
716
|
});
|
573
717
|
}
|
574
|
-
function
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
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
|
+
};
|
579
725
|
}
|
580
726
|
async function createServerEntity({
|
581
727
|
name,
|
582
728
|
fallbackHostname,
|
583
729
|
endpoints,
|
584
|
-
|
585
|
-
sshPort = 22,
|
586
|
-
sshUser = "root",
|
730
|
+
sshArgs = { enabled: true, port: 22, user: "root" },
|
587
731
|
sshPassword,
|
588
732
|
sshPrivateKey,
|
589
|
-
|
733
|
+
sshKeyPair,
|
590
734
|
pingInterval,
|
591
735
|
pingTimeout,
|
592
736
|
waitForPing,
|
@@ -598,7 +742,7 @@ async function createServerEntity({
|
|
598
742
|
throw new Error("At least one L3 endpoint is required to create a server entity");
|
599
743
|
}
|
600
744
|
fallbackHostname ??= name;
|
601
|
-
waitForSsh ??=
|
745
|
+
waitForSsh ??= sshArgs.enabled;
|
602
746
|
waitForPing ??= !waitForSsh;
|
603
747
|
if (waitForPing) {
|
604
748
|
await Command.waitFor(`${name}.ping`, {
|
@@ -609,28 +753,28 @@ async function createServerEntity({
|
|
609
753
|
triggers: [Date.now()]
|
610
754
|
}).wait();
|
611
755
|
}
|
612
|
-
if (!
|
613
|
-
return {
|
756
|
+
if (!sshArgs.enabled) {
|
757
|
+
return output({
|
614
758
|
hostname: name,
|
615
759
|
endpoints
|
616
|
-
};
|
760
|
+
});
|
617
761
|
}
|
618
|
-
|
762
|
+
const sshHost = sshArgs?.host ?? l3EndpointToString(endpoints[0]);
|
619
763
|
if (waitForSsh) {
|
620
764
|
await Command.waitFor(`${name}.ssh`, {
|
621
765
|
host: "local",
|
622
|
-
create: `nc -zv ${
|
766
|
+
create: `nc -zv ${sshHost} ${sshArgs.port}`,
|
623
767
|
timeout: sshCheckTimeout ?? 300,
|
624
768
|
interval: sshCheckInterval ?? 5,
|
625
769
|
triggers: [Date.now()]
|
626
770
|
}).wait();
|
627
771
|
}
|
628
772
|
const connection = output({
|
629
|
-
host:
|
630
|
-
port:
|
631
|
-
user:
|
773
|
+
host: sshHost,
|
774
|
+
port: sshArgs.port,
|
775
|
+
user: sshArgs.user,
|
632
776
|
password: sshPassword,
|
633
|
-
privateKey: sshPrivateKey,
|
777
|
+
privateKey: sshKeyPair ? output(sshKeyPair).privateKey : sshPrivateKey,
|
634
778
|
dialErrorLimit: 3
|
635
779
|
});
|
636
780
|
const hostnameResult = new remote.Command("hostname", {
|
@@ -643,15 +787,15 @@ async function createServerEntity({
|
|
643
787
|
create: "cat /etc/ssh/ssh_host_ed25519_key.pub",
|
644
788
|
triggers: [Date.now()]
|
645
789
|
});
|
646
|
-
return
|
790
|
+
return output({
|
647
791
|
endpoints,
|
648
792
|
hostname: hostnameResult.stdout.apply((x) => x.trim()),
|
649
793
|
ssh: {
|
650
|
-
endpoints: [
|
651
|
-
user:
|
794
|
+
endpoints: [l3EndpointToL4(sshHost, sshArgs.port ?? 22)],
|
795
|
+
user: sshArgs.user ?? "root",
|
652
796
|
hostKey: hostKeyResult.stdout.apply((x) => x.trim()),
|
653
|
-
password:
|
654
|
-
keyPair: sshPrivateKey ? sshPrivateKeyToKeyPair(sshPrivateKey) : void 0
|
797
|
+
password: connection.password,
|
798
|
+
keyPair: sshKeyPair ? sshKeyPair : sshPrivateKey ? sshPrivateKeyToKeyPair(sshPrivateKey) : void 0
|
655
799
|
}
|
656
800
|
});
|
657
801
|
}
|
@@ -667,7 +811,7 @@ function assetFromFile(file) {
|
|
667
811
|
"Artifact-based files cannot be converted to Pulumi assets directly. Use MaterializedFile instead."
|
668
812
|
);
|
669
813
|
}
|
670
|
-
if (file.
|
814
|
+
if (file.content.isBinary) {
|
671
815
|
throw new Error(
|
672
816
|
"Cannot create asset from inline binary file content. Please open an issue if you need this feature."
|
673
817
|
);
|
@@ -734,11 +878,14 @@ var MaterializedFile = class _MaterializedFile {
|
|
734
878
|
constructor(entity, parent) {
|
735
879
|
this.entity = entity;
|
736
880
|
this.parent = parent;
|
881
|
+
this.artifactMeta = {
|
882
|
+
title: `Materialized file "${entity.meta.name}"`
|
883
|
+
};
|
737
884
|
}
|
738
885
|
_tmpPath;
|
739
886
|
_path;
|
740
887
|
_disposed = false;
|
741
|
-
artifactMeta
|
888
|
+
artifactMeta;
|
742
889
|
get path() {
|
743
890
|
return this._path;
|
744
891
|
}
|
@@ -752,7 +899,7 @@ var MaterializedFile = class _MaterializedFile {
|
|
752
899
|
}
|
753
900
|
switch (this.entity.content.type) {
|
754
901
|
case "embedded": {
|
755
|
-
const content = this.entity.
|
902
|
+
const content = this.entity.content.isBinary ? Buffer.from(this.entity.content.value, "base64") : this.entity.content.value;
|
756
903
|
await writeFile(this._path, content, { mode: this.entity.meta.mode });
|
757
904
|
break;
|
758
905
|
}
|
@@ -830,9 +977,7 @@ var MaterializedFile = class _MaterializedFile {
|
|
830
977
|
name: this.entity.meta.name,
|
831
978
|
mode: fileStats.mode & 511,
|
832
979
|
// extract only permission bits
|
833
|
-
size: fileStats.size
|
834
|
-
isBinary: this.entity.meta.isBinary
|
835
|
-
// keep original binary flag as we can't reliably detect this from filesystem
|
980
|
+
size: fileStats.size
|
836
981
|
};
|
837
982
|
return {
|
838
983
|
meta: newMeta,
|
@@ -863,8 +1008,7 @@ var MaterializedFile = class _MaterializedFile {
|
|
863
1008
|
meta: {
|
864
1009
|
name,
|
865
1010
|
mode,
|
866
|
-
size: 0
|
867
|
-
isBinary: false
|
1011
|
+
size: 0
|
868
1012
|
},
|
869
1013
|
content: {
|
870
1014
|
type: "embedded",
|
@@ -895,12 +1039,15 @@ var MaterializedFolder = class _MaterializedFolder {
|
|
895
1039
|
constructor(entity, parent) {
|
896
1040
|
this.entity = entity;
|
897
1041
|
this.parent = parent;
|
1042
|
+
this.artifactMeta = {
|
1043
|
+
title: `Materialized folder "${entity.meta.name}"`
|
1044
|
+
};
|
898
1045
|
}
|
899
1046
|
_tmpPath;
|
900
1047
|
_path;
|
901
1048
|
_disposed = false;
|
902
1049
|
_disposables = [];
|
903
|
-
artifactMeta
|
1050
|
+
artifactMeta;
|
904
1051
|
get path() {
|
905
1052
|
return this._path;
|
906
1053
|
}
|
@@ -1126,7 +1273,7 @@ async function fetchFileSize(endpoint) {
|
|
1126
1273
|
throw new Error("Content-Length header is missing in the response");
|
1127
1274
|
}
|
1128
1275
|
const size = parseInt(contentLength, 10);
|
1129
|
-
if (isNaN(size)) {
|
1276
|
+
if (Number.isNaN(size)) {
|
1130
1277
|
throw new Error(`Invalid Content-Length value: ${contentLength}`);
|
1131
1278
|
}
|
1132
1279
|
return size;
|
@@ -1135,7 +1282,168 @@ function getNameByEndpoint(endpoint) {
|
|
1135
1282
|
const parsedEndpoint = parseL7Endpoint(endpoint);
|
1136
1283
|
return parsedEndpoint.resource ? basename(parsedEndpoint.resource) : "";
|
1137
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
|
+
};
|
1138
1446
|
|
1139
|
-
export { Command, DnsRecord, DnsRecordSet, MaterializedFile, MaterializedFolder, archiveFromFolder, assetFromFile, createServerEntity, createSshTerminal,
|
1140
|
-
//# sourceMappingURL=chunk-
|
1141
|
-
//# 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
|