@highstate/common 0.9.16 → 0.9.18

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.
@@ -1,12 +1,12 @@
1
- import { toPromise, output, ComponentResource, interpolate, normalize, secret, getOrCreateSecret, asset } from '@highstate/pulumi';
2
- import { uniqueBy, capitalize, groupBy } from 'remeda';
1
+ import { toPromise, output, ComponentResource, interpolate, normalize, secret, ensureSecretValue, asset } from '@highstate/pulumi';
2
+ import { uniqueBy, flat, capitalize, groupBy } from 'remeda';
3
+ import { homedir, tmpdir } from 'node:os';
3
4
  import { local, remote } from '@pulumi/command';
4
5
  import '@highstate/library';
5
- import { randomBytes } from '@noble/hashes/utils';
6
+ import { randomBytes, bytesToHex } from '@noble/hashes/utils';
6
7
  import { secureMask } from 'micro-key-producer/password.js';
7
8
  import getKeys, { PrivateExport } from 'micro-key-producer/ssh.js';
8
9
  import { randomBytes as randomBytes$1 } from 'micro-key-producer/utils.js';
9
- import { tmpdir } from 'node:os';
10
10
  import { mkdtemp, writeFile, cp, rm, stat, rename, mkdir } from 'node:fs/promises';
11
11
  import { join, dirname, basename, extname } from 'node:path';
12
12
  import { createReadStream } from 'node:fs';
@@ -123,7 +123,7 @@ async function requireInputL4Endpoint(rawEndpoint, inputEndpoint) {
123
123
  }
124
124
  throw new Error("No endpoint provided");
125
125
  }
126
- function l3ToL4Endpoint(l3Endpoint, port, protocol = "tcp") {
126
+ function l3EndpointToL4(l3Endpoint, port, protocol = "tcp") {
127
127
  return {
128
128
  ...parseL3Endpoint(l3Endpoint),
129
129
  port,
@@ -216,74 +216,124 @@ function createCommand(command) {
216
216
  }
217
217
  return command;
218
218
  }
219
+ function wrapWithWorkDir(dir) {
220
+ if (!dir) {
221
+ return (command) => output(command);
222
+ }
223
+ return (command) => interpolate`cd "${dir}" && ${command}`;
224
+ }
225
+ function wrapWithWaitFor(timeout = 300, interval = 5) {
226
+ return (command) => (
227
+ // TOD: escape the command
228
+ interpolate`timeout ${timeout} bash -c 'while ! ${createCommand(command)}; do sleep ${interval}; done'`
229
+ );
230
+ }
219
231
  var Command = class _Command extends ComponentResource {
220
- command;
221
232
  stdout;
222
233
  stderr;
223
234
  constructor(name, args, opts) {
224
235
  super("highstate:common:Command", name, args, opts);
225
- this.command = output(args).apply((args2) => {
226
- if (args2.host === "local") {
227
- return new local.Command(
228
- name,
229
- {
230
- create: createCommand(args2.create),
231
- update: args2.update ? createCommand(args2.update) : void 0,
232
- delete: args2.delete ? createCommand(args2.delete) : void 0,
233
- logging: args2.logging,
234
- triggers: args2.triggers,
235
- dir: args2.cwd
236
- },
237
- { ...opts, parent: this }
238
- );
239
- }
240
- if (!args2.host.ssh) {
241
- throw new Error(`The host "${args2.host.hostname}" has no SSH credentials`);
242
- }
243
- return new remote.Command(
244
- name,
245
- {
246
- connection: getServerConnection(args2.host.ssh),
247
- create: createCommand(args2.create),
248
- update: args2.update ? createCommand(args2.update) : void 0,
249
- delete: args2.delete ? createCommand(args2.delete) : void 0,
250
- logging: args2.logging,
251
- triggers: args2.triggers
252
- },
253
- { ...opts, parent: this }
254
- );
255
- });
256
- this.stdout = this.command.stdout;
257
- this.stderr = this.command.stderr;
236
+ const command = args.host === "local" ? new local.Command(
237
+ name,
238
+ {
239
+ create: output(args.create).apply(createCommand),
240
+ update: args.update ? output(args.update).apply(createCommand) : void 0,
241
+ delete: args.delete ? output(args.delete).apply(createCommand) : void 0,
242
+ logging: args.logging,
243
+ triggers: args.triggers ? output(args.triggers).apply(flat) : void 0,
244
+ dir: args.cwd ?? homedir(),
245
+ environment: args.environment,
246
+ stdin: args.stdin
247
+ },
248
+ { ...opts, parent: this }
249
+ ) : new remote.Command(
250
+ name,
251
+ {
252
+ connection: output(args.host).apply((server) => {
253
+ if ("host" in server) {
254
+ return output(server);
255
+ }
256
+ if (!server.ssh) {
257
+ throw new Error(`The server "${server.hostname}" has no SSH credentials`);
258
+ }
259
+ return getServerConnection(server.ssh);
260
+ }),
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,
264
+ logging: args.logging,
265
+ triggers: args.triggers ? output(args.triggers).apply(flat) : void 0,
266
+ stdin: args.stdin,
267
+ environment: args.environment
268
+ },
269
+ { ...opts, parent: this }
270
+ );
271
+ this.stdout = command.stdout;
272
+ this.stderr = command.stderr;
258
273
  }
274
+ /**
275
+ * Waits for the command to complete and returns its output.
276
+ * The standard output will be returned.
277
+ */
278
+ async wait() {
279
+ return await toPromise(this.stdout);
280
+ }
281
+ /**
282
+ * Creates a command that writes the given content to a file on the host.
283
+ * The file will be created if it does not exist, and overwritten if it does.
284
+ *
285
+ * Use for small text files like configuration files.
286
+ */
259
287
  static createTextFile(name, options, opts) {
260
- return output(options).apply((options2) => {
261
- const escapedContent = options2.content.replace(/"/g, '\\"');
262
- const command = new _Command(
263
- name,
264
- {
265
- host: options2.host,
266
- create: interpolate`mkdir -p $(dirname ${options2.path}) && echo "${escapedContent}" > ${options2.path}`,
267
- delete: interpolate`rm -rf ${options2.path}`
268
- },
269
- opts
270
- );
271
- return command;
272
- });
288
+ return new _Command(
289
+ name,
290
+ {
291
+ host: options.host,
292
+ create: interpolate`mkdir -p $(dirname "${options.path}") && cat > ${options.path}`,
293
+ delete: interpolate`rm -rf ${options.path}`,
294
+ stdin: options.content
295
+ },
296
+ opts
297
+ );
273
298
  }
299
+ /**
300
+ * Creates a command that waits for a file to be created and then reads its content.
301
+ * This is useful for waiting for a file to be generated by another process.
302
+ *
303
+ * Use for small text files like configuration files.
304
+ */
274
305
  static receiveTextFile(name, options, opts) {
275
- return output(options).apply((options2) => {
276
- const command = new _Command(
277
- name,
278
- {
279
- host: options2.host,
280
- create: interpolate`while ! test -f ${options2.path}; do sleep 1; done; cat ${options2.path}`,
281
- logging: "stderr"
282
- },
283
- opts
284
- );
285
- return command;
286
- });
306
+ return new _Command(
307
+ name,
308
+ {
309
+ host: options.host,
310
+ create: interpolate`while ! test -f "${options.path}"; do sleep 1; done; cat "${options.path}"`,
311
+ logging: "stderr"
312
+ },
313
+ opts
314
+ );
315
+ }
316
+ /**
317
+ * Creates a command that waits for a condition to be met.
318
+ * The command will run until the condition is met or the timeout is reached.
319
+ *
320
+ * The condition is considered met if the command returns a zero exit code.
321
+ *
322
+ * @param name The name of the command resource.
323
+ * @param args The arguments for the command, including the condition to check.
324
+ * @param opts Optional resource options.
325
+ */
326
+ static waitFor(name, args, opts) {
327
+ return new _Command(
328
+ name,
329
+ {
330
+ ...args,
331
+ create: output(args.create).apply(wrapWithWaitFor(args.timeout, args.interval)),
332
+ update: args.update ? output(args.update).apply(wrapWithWaitFor(args.timeout, args.interval)) : void 0,
333
+ delete: args.delete ? output(args.delete).apply(wrapWithWaitFor(args.timeout, args.interval)) : void 0
334
+ },
335
+ opts
336
+ );
287
337
  }
288
338
  };
289
339
  function getTypeByEndpoint(endpoint) {
@@ -435,6 +485,16 @@ async function updateEndpointsWithFqdn(endpoints, fqdn, fqdnEndpointFilter, patc
435
485
  function generatePassword() {
436
486
  return secureMask.apply(randomBytes(32)).password;
437
487
  }
488
+ function generateKey(format = "hex") {
489
+ const bytes = randomBytes(32);
490
+ if (format === "raw") {
491
+ return bytes;
492
+ }
493
+ if (format === "base64") {
494
+ return Buffer.from(bytes).toString("base64");
495
+ }
496
+ return bytesToHex(bytes);
497
+ }
438
498
 
439
499
  // assets/images.json
440
500
  var terminal_ssh = {
@@ -494,11 +554,11 @@ function createSshTerminal(credentials) {
494
554
  };
495
555
  });
496
556
  }
497
- function generatePrivateKey() {
557
+ function generateSshPrivateKey() {
498
558
  const seed = randomBytes$1(32);
499
559
  return getKeys(seed).privateKey;
500
560
  }
501
- function privateKeyToKeyPair(privateKeyString) {
561
+ function sshPrivateKeyToKeyPair(privateKeyString) {
502
562
  return output(privateKeyString).apply((privateKeyString2) => {
503
563
  const privateKeyStruct = PrivateExport.decode(privateKeyString2);
504
564
  const privKey = privateKeyStruct.keys[0].privKey.privKey;
@@ -511,60 +571,88 @@ function privateKeyToKeyPair(privateKeyString) {
511
571
  });
512
572
  });
513
573
  }
514
- function getOrCreateSshKeyPair(inputs, secrets) {
515
- if (inputs.sshKeyPair) {
516
- return output(inputs.sshKeyPair);
574
+ function ensureSshKeyPair(privateKey, existingKeyPair) {
575
+ if (existingKeyPair) {
576
+ return output(existingKeyPair);
517
577
  }
518
- const privateKey = getOrCreateSecret(secrets, "sshPrivateKey", generatePrivateKey);
519
- return privateKey.apply(privateKeyToKeyPair);
578
+ return ensureSecretValue(privateKey, generateSshPrivateKey).value.apply(sshPrivateKeyToKeyPair);
520
579
  }
521
- function createServerEntity(fallbackHostname, endpoint, sshPort = 22, sshUser = "root", sshPassword, sshPrivateKey, hasSsh = true) {
580
+ async function createServerEntity({
581
+ name,
582
+ fallbackHostname,
583
+ endpoints,
584
+ sshEndpoint,
585
+ sshPort = 22,
586
+ sshUser = "root",
587
+ sshPassword,
588
+ sshPrivateKey,
589
+ hasSsh = true,
590
+ pingInterval,
591
+ pingTimeout,
592
+ waitForPing,
593
+ waitForSsh,
594
+ sshCheckInterval,
595
+ sshCheckTimeout
596
+ }) {
597
+ if (endpoints.length === 0) {
598
+ throw new Error("At least one L3 endpoint is required to create a server entity");
599
+ }
600
+ fallbackHostname ??= name;
601
+ waitForSsh ??= hasSsh;
602
+ waitForPing ??= !waitForSsh;
603
+ if (waitForPing) {
604
+ await Command.waitFor(`${name}.ping`, {
605
+ host: "local",
606
+ create: `ping -c 1 ${l3EndpointToString(endpoints[0])}`,
607
+ timeout: pingTimeout ?? 300,
608
+ interval: pingInterval ?? 5,
609
+ triggers: [Date.now()]
610
+ }).wait();
611
+ }
612
+ if (!hasSsh) {
613
+ return {
614
+ hostname: name,
615
+ endpoints
616
+ };
617
+ }
618
+ sshEndpoint ??= l3EndpointToL4(endpoints[0], sshPort);
619
+ if (waitForSsh) {
620
+ await Command.waitFor(`${name}.ssh`, {
621
+ host: "local",
622
+ create: `nc -zv ${l3EndpointToString(sshEndpoint)} ${sshPort}`,
623
+ timeout: sshCheckTimeout ?? 300,
624
+ interval: sshCheckInterval ?? 5,
625
+ triggers: [Date.now()]
626
+ }).wait();
627
+ }
522
628
  const connection = output({
523
- host: l3EndpointToString(endpoint),
524
- port: sshPort,
629
+ host: l3EndpointToString(sshEndpoint),
630
+ port: sshEndpoint.port,
525
631
  user: sshUser,
526
632
  password: sshPassword,
527
633
  privateKey: sshPrivateKey,
528
634
  dialErrorLimit: 3
529
635
  });
530
- if (!hasSsh) {
531
- return output({
532
- hostname: fallbackHostname,
533
- endpoints: [endpoint]
534
- });
535
- }
536
- const command = new local.Command("check-ssh", {
537
- create: `nc -zv ${l3EndpointToString(endpoint)} ${sshPort} && echo "up" || echo "down"`,
636
+ const hostnameResult = new remote.Command("hostname", {
637
+ connection,
638
+ create: "hostname",
538
639
  triggers: [Date.now()]
539
640
  });
540
- return command.stdout.apply((result) => {
541
- if (result === "down") {
542
- return output({
543
- hostname: fallbackHostname,
544
- endpoints: [endpoint]
545
- });
641
+ const hostKeyResult = new remote.Command("host-key", {
642
+ connection,
643
+ create: "cat /etc/ssh/ssh_host_ed25519_key.pub",
644
+ triggers: [Date.now()]
645
+ });
646
+ return await toPromise({
647
+ endpoints,
648
+ hostname: hostnameResult.stdout.apply((x) => x.trim()),
649
+ ssh: {
650
+ endpoints: [sshEndpoint],
651
+ user: sshUser,
652
+ hostKey: hostKeyResult.stdout.apply((x) => x.trim()),
653
+ password: sshPassword,
654
+ keyPair: sshPrivateKey ? sshPrivateKeyToKeyPair(sshPrivateKey) : void 0
546
655
  }
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
656
  });
569
657
  }
570
658
  function assetFromFile(file) {
@@ -673,21 +761,20 @@ var MaterializedFile = class _MaterializedFile {
673
761
  break;
674
762
  }
675
763
  case "remote": {
676
- const response = await load(l7EndpointToString(this.entity.content.endpoint));
764
+ const response = await fetch(l7EndpointToString(this.entity.content.endpoint));
677
765
  if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`);
678
766
  const arrayBuffer = await response.arrayBuffer();
679
767
  await writeFile(this._path, Buffer.from(arrayBuffer), { mode: this.entity.meta.mode });
680
768
  break;
681
769
  }
682
770
  case "artifact": {
683
- const artifactData = this.entity.content[HighstateSignature.Artifact];
684
771
  const artifactPath = process.env.HIGHSTATE_ARTIFACT_READ_PATH;
685
772
  if (!artifactPath) {
686
773
  throw new Error(
687
774
  "HIGHSTATE_ARTIFACT_READ_PATH environment variable is not set but required for artifact content"
688
775
  );
689
776
  }
690
- const tgzPath = join(artifactPath, `${artifactData.hash}.tgz`);
777
+ const tgzPath = join(artifactPath, `${this.entity.content.hash}.tgz`);
691
778
  const readStream = createReadStream(tgzPath);
692
779
  await unarchiveFromStream(readStream, dirname(this._path), "tar");
693
780
  break;
@@ -751,10 +838,9 @@ var MaterializedFile = class _MaterializedFile {
751
838
  meta: newMeta,
752
839
  content: {
753
840
  type: "artifact",
754
- [HighstateSignature.Artifact]: {
755
- hash: hashValue,
756
- meta: await toPromise(this.artifactMeta)
757
- }
841
+ [HighstateSignature.Artifact]: true,
842
+ hash: hashValue,
843
+ meta: await toPromise(this.artifactMeta)
758
844
  }
759
845
  };
760
846
  } finally {
@@ -853,7 +939,7 @@ var MaterializedFolder = class _MaterializedFolder {
853
939
  break;
854
940
  }
855
941
  case "remote": {
856
- const response = await load(l7EndpointToString(this.entity.content.endpoint));
942
+ const response = await fetch(l7EndpointToString(this.entity.content.endpoint));
857
943
  if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`);
858
944
  if (!response.body) throw new Error("Response body is empty");
859
945
  const url = new URL(l7EndpointToString(this.entity.content.endpoint));
@@ -886,14 +972,13 @@ var MaterializedFolder = class _MaterializedFolder {
886
972
  break;
887
973
  }
888
974
  case "artifact": {
889
- const artifactData = this.entity.content[HighstateSignature.Artifact];
890
975
  const artifactPath = process.env.HIGHSTATE_ARTIFACT_READ_PATH;
891
976
  if (!artifactPath) {
892
977
  throw new Error(
893
978
  "HIGHSTATE_ARTIFACT_READ_PATH environment variable is not set but required for artifact content"
894
979
  );
895
980
  }
896
- const tgzPath = join(artifactPath, `${artifactData.hash}.tgz`);
981
+ const tgzPath = join(artifactPath, `${this.entity.content.hash}.tgz`);
897
982
  const readStream = createReadStream(tgzPath);
898
983
  await unarchiveFromStream(readStream, dirname(this._path), "tar");
899
984
  break;
@@ -972,11 +1057,10 @@ var MaterializedFolder = class _MaterializedFolder {
972
1057
  return {
973
1058
  meta: newMeta,
974
1059
  content: {
1060
+ [HighstateSignature.Artifact]: true,
975
1061
  type: "artifact",
976
- [HighstateSignature.Artifact]: {
977
- hash: hashValue,
978
- meta: await toPromise(this.artifactMeta)
979
- }
1062
+ hash: hashValue,
1063
+ meta: await toPromise(this.artifactMeta)
980
1064
  }
981
1065
  };
982
1066
  } finally {
@@ -1033,7 +1117,7 @@ async function fetchFileSize(endpoint) {
1033
1117
  );
1034
1118
  }
1035
1119
  const url = l7EndpointToString(endpoint);
1036
- const response = await load(url, { method: "HEAD" });
1120
+ const response = await fetch(url, { method: "HEAD" });
1037
1121
  if (!response.ok) {
1038
1122
  throw new Error(`Failed to fetch file size: ${response.statusText}`);
1039
1123
  }
@@ -1052,6 +1136,6 @@ function getNameByEndpoint(endpoint) {
1052
1136
  return parsedEndpoint.resource ? basename(parsedEndpoint.resource) : "";
1053
1137
  }
1054
1138
 
1055
- export { Command, DnsRecord, DnsRecordSet, MaterializedFile, MaterializedFolder, archiveFromFolder, assetFromFile, createServerEntity, createSshTerminal, fetchFileSize, filterEndpoints, generatePassword, generatePrivateKey, getNameByEndpoint, getOrCreateSshKeyPair, getServerConnection, l34EndpointToString, l3EndpointToCidr, l3EndpointToString, l3ToL4Endpoint, l4EndpointToString, l4EndpointWithProtocolToString, l7EndpointToString, parseL34Endpoint, parseL3Endpoint, parseL4Endpoint, parseL7Endpoint, privateKeyToKeyPair, requireInputL3Endpoint, requireInputL4Endpoint, updateEndpoints, updateEndpointsWithFqdn };
1056
- //# sourceMappingURL=chunk-HZBJ6LLS.js.map
1057
- //# sourceMappingURL=chunk-HZBJ6LLS.js.map
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