@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.
Files changed (43) hide show
  1. package/dist/{chunk-YYNV3MVT.js → chunk-WDYIUWYZ.js} +484 -176
  2. package/dist/chunk-WDYIUWYZ.js.map +1 -0
  3. package/dist/highstate.manifest.json +12 -8
  4. package/dist/index.js +1 -1
  5. package/dist/units/access-point/index.js +16 -0
  6. package/dist/units/access-point/index.js.map +1 -0
  7. package/dist/units/databases/existing-mariadb/index.js +17 -0
  8. package/dist/units/databases/existing-mariadb/index.js.map +1 -0
  9. package/dist/units/databases/existing-mongodb/index.js +17 -0
  10. package/dist/units/databases/existing-mongodb/index.js.map +1 -0
  11. package/dist/units/databases/existing-postgresql/index.js +17 -0
  12. package/dist/units/databases/existing-postgresql/index.js.map +1 -0
  13. package/dist/units/dns/record-set/index.js +22 -11
  14. package/dist/units/dns/record-set/index.js.map +1 -1
  15. package/dist/units/existing-server/index.js +9 -9
  16. package/dist/units/existing-server/index.js.map +1 -1
  17. package/dist/units/network/l3-endpoint/index.js +1 -1
  18. package/dist/units/network/l4-endpoint/index.js +1 -1
  19. package/dist/units/script/index.js +1 -1
  20. package/dist/units/server-dns/index.js +1 -1
  21. package/dist/units/server-patch/index.js +1 -1
  22. package/dist/units/ssh/key-pair/index.js +4 -3
  23. package/dist/units/ssh/key-pair/index.js.map +1 -1
  24. package/package.json +61 -8
  25. package/src/shared/access-point.ts +110 -0
  26. package/src/shared/command.ts +81 -14
  27. package/src/shared/dns.ts +150 -90
  28. package/src/shared/files.ts +23 -18
  29. package/src/shared/gateway.ts +117 -0
  30. package/src/shared/impl-ref.ts +123 -0
  31. package/src/shared/index.ts +4 -0
  32. package/src/shared/network.ts +39 -25
  33. package/src/shared/passwords.ts +3 -3
  34. package/src/shared/ssh.ts +109 -124
  35. package/src/shared/tls.ts +123 -0
  36. package/src/units/access-point/index.ts +12 -0
  37. package/src/units/databases/existing-mariadb/index.ts +14 -0
  38. package/src/units/databases/existing-mongodb/index.ts +14 -0
  39. package/src/units/databases/existing-postgresql/index.ts +14 -0
  40. package/src/units/dns/record-set/index.ts +21 -11
  41. package/src/units/existing-server/index.ts +9 -15
  42. package/src/units/ssh/key-pair/index.ts +4 -3
  43. package/dist/chunk-YYNV3MVT.js.map +0 -1
@@ -1,22 +1,23 @@
1
- import { toPromise, output, ComponentResource, interpolate, normalize, secret, ensureSecretValue, asset } from '@highstate/pulumi';
2
- import { uniqueBy, flat, capitalize, groupBy } from 'remeda';
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 '@highstate/library';
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 { createHash } from 'node:crypto';
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 && types.length) {
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 resolvedInputEndpoints = await toPromise(inputEndpoints);
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
- (endpoint) => l34EndpointToString(endpoint)
194
+ l34EndpointToString
200
195
  );
201
196
  }
202
- function getServerConnection(ssh2) {
203
- return output(ssh2).apply((ssh3) => ({
204
- host: l3EndpointToString(ssh3.endpoints[0]),
205
- port: ssh3.endpoints[0].port,
206
- user: ssh3.user,
207
- password: ssh3.password,
208
- privateKey: ssh3.keyPair?.privateKey,
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: ssh3.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: args.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
- environment: args.environment
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 wait commands to be executed after the DNS record is created/updated.
465
+ * The commands to be executed after the DNS record is created/updated.
356
466
  *
357
- * Use this field as a dependency for other resources.
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
- this.dnsRecord = output(args).apply((args2) => {
363
- const l3Endpoint = parseL3Endpoint(args2.value);
364
- const type = args2.type ?? getTypeByEndpoint(l3Endpoint);
365
- return output(
366
- this.create(
367
- name,
368
- {
369
- ...args2,
370
- type,
371
- value: l3EndpointToString(l3Endpoint)
372
- },
373
- { ...opts, parent: this }
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(args).apply((args2) => {
378
- const waitAt = args2.waitAt ? Array.isArray(args2.waitAt) ? args2.waitAt : [args2.waitAt] : [];
379
- return waitAt.map((host) => {
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}-wait-${hostname}`,
517
+ `${name}.wait-for-dns.${hostname}`,
383
518
  {
384
519
  host,
385
- create: `while ! getent hosts ${args2.name} >/dev/null; do echo "Waiting for DNS record ${args2.name} to be created"; sleep 5; done`,
386
- triggers: [args2.type, args2.ttl, args2.priority, args2.proxied]
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 wait commands to be executed after the DNS records are created/updated.
539
+ * The flat list of all wait commands for the DNS records.
410
540
  */
411
541
  waitCommands;
412
- constructor(name, records, opts) {
413
- super("highstate:common:DnsRecordSet", name, records, opts);
414
- this.dnsRecords = records;
415
- this.waitCommands = records.apply(
416
- (records2) => records2.flatMap((record) => record.waitCommands)
417
- );
418
- }
419
- static create(name, args, opts) {
420
- const records = output(args).apply((args2) => {
421
- const recordName = args2.name ?? name;
422
- const values = normalize(args2.value, args2.values);
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
- return new _DnsRecordSet(name, records, opts);
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.create(fqdn, {
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
- return output(credentials).apply((credentials2) => {
507
- if (!credentials2) {
508
- return void 0;
509
- }
510
- const command = ["ssh", "-tt", "-o", "UserKnownHostsFile=/known_hosts"];
511
- const endpoint = credentials2.endpoints[0];
512
- command.push("-p", endpoint.port.toString());
513
- if (credentials2.keyPair) {
514
- command.push("-i", "/private_key");
515
- }
516
- command.push(`${credentials2.user}@${l3EndpointToString(endpoint)}`);
517
- if (credentials2.password) {
518
- command.unshift("sshpass", "-f", "/password");
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 ensureSshKeyPair(privateKey, existingKeyPair) {
575
- if (existingKeyPair) {
576
- return output(existingKeyPair);
577
- }
578
- return ensureSecretValue(privateKey, generateSshPrivateKey).value.apply(sshPrivateKeyToKeyPair);
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
- sshEndpoint,
585
- sshPort = 22,
586
- sshUser = "root",
730
+ sshArgs = { enabled: true, port: 22, user: "root" },
587
731
  sshPassword,
588
732
  sshPrivateKey,
589
- hasSsh = true,
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 ??= hasSsh;
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 (!hasSsh) {
613
- return {
756
+ if (!sshArgs.enabled) {
757
+ return output({
614
758
  hostname: name,
615
759
  endpoints
616
- };
760
+ });
617
761
  }
618
- sshEndpoint ??= l3EndpointToL4(endpoints[0], sshPort);
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 ${l3EndpointToString(sshEndpoint)} ${sshPort}`,
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: l3EndpointToString(sshEndpoint),
630
- port: sshEndpoint.port,
631
- user: sshUser,
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 await toPromise({
790
+ return output({
647
791
  endpoints,
648
792
  hostname: hostnameResult.stdout.apply((x) => x.trim()),
649
793
  ssh: {
650
- endpoints: [sshEndpoint],
651
- user: sshUser,
794
+ endpoints: [l3EndpointToL4(sshHost, sshArgs.port ?? 22)],
795
+ user: sshArgs.user ?? "root",
652
796
  hostKey: hostKeyResult.stdout.apply((x) => x.trim()),
653
- password: sshPassword,
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.meta.isBinary) {
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.meta.isBinary ? Buffer.from(this.entity.content.value, "base64") : this.entity.content.value;
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, ensureSshKeyPair, fetchFileSize, filterEndpoints, generateKey, generatePassword, generateSshPrivateKey, getNameByEndpoint, getServerConnection, l34EndpointToString, l3EndpointToCidr, l3EndpointToL4, l3EndpointToString, l4EndpointToString, l4EndpointWithProtocolToString, l7EndpointToString, parseL34Endpoint, parseL3Endpoint, parseL4Endpoint, parseL7Endpoint, requireInputL3Endpoint, requireInputL4Endpoint, sshPrivateKeyToKeyPair, updateEndpoints, updateEndpointsWithFqdn };
1140
- //# sourceMappingURL=chunk-YYNV3MVT.js.map
1141
- //# sourceMappingURL=chunk-YYNV3MVT.js.map
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