@highstate/common 0.9.15 → 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.
Files changed (35) hide show
  1. package/dist/chunk-YYNV3MVT.js +1141 -0
  2. package/dist/chunk-YYNV3MVT.js.map +1 -0
  3. package/dist/highstate.manifest.json +9 -9
  4. package/dist/index.js +2 -50
  5. package/dist/index.js.map +1 -1
  6. package/dist/units/dns/record-set/index.js +4 -6
  7. package/dist/units/dns/record-set/index.js.map +1 -1
  8. package/dist/units/existing-server/index.js +16 -22
  9. package/dist/units/existing-server/index.js.map +1 -1
  10. package/dist/units/network/l3-endpoint/index.js +6 -9
  11. package/dist/units/network/l3-endpoint/index.js.map +1 -1
  12. package/dist/units/network/l4-endpoint/index.js +6 -9
  13. package/dist/units/network/l4-endpoint/index.js.map +1 -1
  14. package/dist/units/script/index.js +6 -9
  15. package/dist/units/script/index.js.map +1 -1
  16. package/dist/units/server-dns/index.js +7 -11
  17. package/dist/units/server-dns/index.js.map +1 -1
  18. package/dist/units/server-patch/index.js +7 -11
  19. package/dist/units/server-patch/index.js.map +1 -1
  20. package/dist/units/ssh/key-pair/index.js +20 -15
  21. package/dist/units/ssh/key-pair/index.js.map +1 -1
  22. package/package.json +20 -6
  23. package/src/shared/command.ts +257 -73
  24. package/src/shared/files.ts +725 -0
  25. package/src/shared/index.ts +1 -0
  26. package/src/shared/network.ts +90 -3
  27. package/src/shared/passwords.ts +38 -2
  28. package/src/shared/ssh.ts +249 -81
  29. package/src/units/existing-server/index.ts +12 -11
  30. package/src/units/remote-folder/index.ts +0 -0
  31. package/src/units/server-dns/index.ts +1 -1
  32. package/src/units/server-patch/index.ts +1 -1
  33. package/src/units/ssh/key-pair/index.ts +16 -7
  34. package/dist/chunk-NISDP46H.js +0 -546
  35. package/dist/chunk-NISDP46H.js.map +0 -1
@@ -3,3 +3,4 @@ export * from "./dns"
3
3
  export * from "./passwords"
4
4
  export * from "./ssh"
5
5
  export * from "./network"
6
+ export * from "./files"
@@ -19,6 +19,13 @@ export type InputL3Endpoint = network.L3Endpoint | string
19
19
  */
20
20
  export type InputL4Endpoint = network.L4Endpoint | string
21
21
 
22
+ /**
23
+ * The L7 endpoint for some service.
24
+ *
25
+ * The format is: `appProtocol://endpoint[:port][/resource]`
26
+ */
27
+ export type InputL7Endpoint = network.L7Endpoint | string
28
+
22
29
  /**
23
30
  * Stringifies a L3 endpoint object into a string.
24
31
  *
@@ -40,7 +47,6 @@ export function l3EndpointToString(l3Endpoint: network.L3Endpoint): string {
40
47
  * Stringifies a L4 endpoint object into a string.
41
48
  *
42
49
  * @param l4Endpoint The L4 endpoint object to stringify.
43
- *
44
50
  * @returns The string representation of the L4 endpoint.
45
51
  */
46
52
  export function l4EndpointToString(l4Endpoint: network.L4Endpoint): string {
@@ -51,6 +57,37 @@ export function l4EndpointToString(l4Endpoint: network.L4Endpoint): string {
51
57
  return `${l3EndpointToString(l4Endpoint)}:${l4Endpoint.port}`
52
58
  }
53
59
 
60
+ /**
61
+ * Stringifies a L4 endpoint object into a string with protocol.
62
+ *
63
+ * @param l4Endpoint The L4 endpoint object to stringify.
64
+ * @returns The string representation of the L4 endpoint with protocol.
65
+ */
66
+ export function l4EndpointWithProtocolToString(l4Endpoint: network.L4Endpoint): string {
67
+ const protocol = `${l4Endpoint.protocol}://`
68
+
69
+ return `${protocol}${l4EndpointToString(l4Endpoint)}`
70
+ }
71
+
72
+ /**
73
+ * Stringifies a L7 endpoint object into a string.
74
+ *
75
+ * The format is: `appProtocol://endpoint[:port][/resource]`
76
+ * @param l7Endpoint The L7 endpoint object to stringify.
77
+ * @returns The string representation of the L7 endpoint.
78
+ */
79
+ export function l7EndpointToString(l7Endpoint: network.L7Endpoint): string {
80
+ const protocol = `${l7Endpoint.appProtocol}://`
81
+
82
+ let endpoint = l4EndpointToString(l7Endpoint)
83
+
84
+ if (l7Endpoint.resource) {
85
+ endpoint += `/${l7Endpoint.resource}`
86
+ }
87
+
88
+ return `${protocol}${endpoint}`
89
+ }
90
+
54
91
  /**
55
92
  * Stringifies a L3 or L4 endpoint object into a string.
56
93
  *
@@ -68,6 +105,9 @@ export function l34EndpointToString(l34Endpoint: network.L34Endpoint): string {
68
105
  const L34_ENDPOINT_RE =
69
106
  /^(?:(?<protocol>[a-z]+):\/\/)?(?:(?:\[?(?<ipv6>[0-9A-Fa-f:]+)\]?)|(?<ipv4>(?:\d{1,3}\.){3}\d{1,3})|(?<hostname>[a-zA-Z0-9-*]+(?:\.[a-zA-Z0-9-*]+)*))(?::(?<port>\d{1,5}))?$/
70
107
 
108
+ const L7_ENDPOINT_RE =
109
+ /^(?<appProtocol>[a-z]+):\/\/(?:(?:\[?(?<ipv6>[0-9A-Fa-f:]+)\]?)|(?<ipv4>(?:\d{1,3}\.){3}\d{1,3})|(?<hostname>[a-zA-Z0-9-*]+(?:\.[a-zA-Z0-9-*]+)*))(?::(?<port>\d{1,5}))?(?:\/(?<resource>.*))?$/
110
+
71
111
  /**
72
112
  * Parses a L3 or L4 endpoint from a string.
73
113
  *
@@ -208,14 +248,14 @@ export async function requireInputL4Endpoint(
208
248
  }
209
249
 
210
250
  /**
211
- * Convers L3 endpoint to L4 endpoint by adding a port and protocol.
251
+ * Converts L3 endpoint to L4 endpoint by adding a port and protocol.
212
252
  *
213
253
  * @param l3Endpoint The L3 endpoint to convert.
214
254
  * @param port The port to add to the L3 endpoint.
215
255
  * @param protocol The protocol to add to the L3 endpoint. Defaults to "tcp".
216
256
  * @returns The L4 endpoint with the port and protocol added.
217
257
  */
218
- export function l3ToL4Endpoint(
258
+ export function l3EndpointToL4(
219
259
  l3Endpoint: InputL3Endpoint,
220
260
  port: number,
221
261
  protocol: network.L4Protocol = "tcp",
@@ -275,6 +315,53 @@ export function l3EndpointToCidr(l3Endpoint: network.L3Endpoint): string {
275
315
  }
276
316
  }
277
317
 
318
+ const udpAppProtocols = ["dns", "dhcp"]
319
+
320
+ /**
321
+ * Parses a L7 endpoint from a string.
322
+ *
323
+ * The format is: `appProtocol://endpoint[:port][/resource]`
324
+ *
325
+ * @param l7Endpoint The L7 endpoint string to parse.
326
+ * @returns The parsed L7 endpoint object.
327
+ */
328
+ export function parseL7Endpoint(l7Endpoint: InputL7Endpoint): network.L7Endpoint {
329
+ if (typeof l7Endpoint === "object") {
330
+ return l7Endpoint
331
+ }
332
+
333
+ const match = l7Endpoint.match(L7_ENDPOINT_RE)
334
+ if (!match) {
335
+ throw new Error(`Invalid L7 endpoint: "${l7Endpoint}"`)
336
+ }
337
+
338
+ const { appProtocol, ipv6, ipv4, hostname, port, resource } = match.groups!
339
+
340
+ let visibility: network.EndpointVisibility = "public"
341
+
342
+ if (ipv4 && IPV4_PRIVATE_REGEX.test(ipv4)) {
343
+ visibility = "external"
344
+ } else if (ipv6 && IPV6_PRIVATE_REGEX.test(ipv6)) {
345
+ visibility = "external"
346
+ }
347
+
348
+ return {
349
+ type: ipv6 ? "ipv6" : ipv4 ? "ipv4" : "hostname",
350
+ visibility,
351
+ address: ipv6 || ipv4,
352
+ hostname: hostname,
353
+
354
+ // Default port for L7 endpoints (TODO: add more specific defaults for common protocols)
355
+ port: port ? parseInt(port, 10) : 443,
356
+
357
+ // L7 endpoints typically use TCP, but can also use UDP for specific protocols
358
+ protocol: udpAppProtocols.includes(appProtocol) ? "udp" : "tcp",
359
+
360
+ appProtocol,
361
+ resource: resource || "",
362
+ } as network.L7Endpoint
363
+ }
364
+
278
365
  /**
279
366
  * Updates the endpoints based on the given mode.
280
367
  *
@@ -1,6 +1,42 @@
1
- import { randomBytes } from "@noble/hashes/utils"
1
+ import { bytesToHex, randomBytes } from "@noble/hashes/utils"
2
2
  import { secureMask } from "micro-key-producer/password.js"
3
3
 
4
- export function generatePassword() {
4
+ /**
5
+ * Generates a secure random password strong enough for online use.
6
+ *
7
+ * It uses "Safari Keychain Secure Password" format.
8
+ *
9
+ * The approximate entropy is 71 bits (https://support.apple.com/guide/security/automatic-strong-passwords-secc84c811c4/web).
10
+ */
11
+ export function generatePassword(): string {
5
12
  return secureMask.apply(randomBytes(32)).password
6
13
  }
14
+
15
+ type KeyFormatMap = {
16
+ raw: Uint8Array
17
+ hex: string
18
+ base64: string
19
+ }
20
+
21
+ /**
22
+ * Generates a secure random key strong enough for online use such as encryption.
23
+ *
24
+ * The strong entropy is 256 bits.
25
+ *
26
+ * @param format The format of the generated key. By default, it is "hex".
27
+ */
28
+ export function generateKey<TFormat extends keyof KeyFormatMap>(
29
+ format: TFormat = "hex" as TFormat,
30
+ ): KeyFormatMap[TFormat] {
31
+ const bytes = randomBytes(32)
32
+
33
+ if (format === "raw") {
34
+ return bytes as KeyFormatMap[TFormat]
35
+ }
36
+
37
+ if (format === "base64") {
38
+ return Buffer.from(bytes).toString("base64") as KeyFormatMap[TFormat]
39
+ }
40
+
41
+ return bytesToHex(bytes) as KeyFormatMap[TFormat]
42
+ }
package/src/shared/ssh.ts CHANGED
@@ -1,17 +1,20 @@
1
1
  import type { common, network, ssh } from "@highstate/library"
2
2
  import {
3
- getOrCreateSecret,
3
+ ensureSecretValue,
4
4
  output,
5
5
  Output,
6
6
  secret,
7
+ toPromise,
7
8
  type Input,
8
9
  type InstanceTerminal,
10
+ type UnitSecret,
9
11
  } from "@highstate/pulumi"
10
12
  import getKeys, { PrivateExport } from "micro-key-producer/ssh.js"
11
13
  import { randomBytes } from "micro-key-producer/utils.js"
12
- import { local, remote } from "@pulumi/command"
14
+ import { remote } from "@pulumi/command"
13
15
  import * as images from "../../assets/images.json"
14
- import { l3EndpointToString, l3ToL4Endpoint } from "./network"
16
+ import { l3EndpointToString, l3EndpointToL4 } from "./network"
17
+ import { Command } from "./command"
15
18
 
16
19
  export function createSshTerminal(
17
20
  credentials: Input<ssh.Credentials | undefined>,
@@ -40,134 +43,299 @@ export function createSshTerminal(
40
43
 
41
44
  return {
42
45
  name: "ssh",
43
- title: "Shell",
44
- description: "Connect to the server via SSH",
45
- icon: "gg:remote",
46
46
 
47
- image: images["terminal-ssh"].image,
48
- command,
47
+ meta: {
48
+ title: "Shell",
49
+ description: "Connect to the server via SSH",
50
+ icon: "gg:remote",
51
+ },
49
52
 
50
- files: {
51
- "/password": credentials.password,
53
+ spec: {
54
+ image: images["terminal-ssh"].image,
55
+ command,
52
56
 
53
- "/private_key": {
54
- content: credentials.keyPair?.privateKey,
55
- mode: 0o600,
56
- },
57
+ files: {
58
+ "/password": credentials.password,
57
59
 
58
- "/known_hosts": {
59
- content: `${l3EndpointToString(endpoint)} ${credentials.hostKey}`,
60
- mode: 0o644,
60
+ "/private_key": credentials.keyPair?.privateKey && {
61
+ content: {
62
+ type: "embedded",
63
+ value: credentials.keyPair?.privateKey,
64
+ },
65
+ meta: {
66
+ name: "private_key",
67
+ mode: 0o600,
68
+ },
69
+ },
70
+
71
+ "/known_hosts": {
72
+ content: {
73
+ type: "embedded",
74
+ value: `${l3EndpointToString(endpoint)} ${credentials.hostKey}`,
75
+ },
76
+ meta: {
77
+ name: "known_hosts",
78
+ mode: 0o644,
79
+ },
80
+ },
61
81
  },
62
82
  },
63
83
  } satisfies InstanceTerminal
64
84
  })
65
85
  }
66
86
 
67
- export function generatePrivateKey(): string {
87
+ /**
88
+ * Generates a secure random SSH private key.
89
+ * The key is generated using the Ed25519 algorithm.
90
+ *
91
+ * @returns The generated SSH private key in PEM format.
92
+ */
93
+ export function generateSshPrivateKey(): string {
68
94
  const seed = randomBytes(32)
69
95
 
70
96
  return getKeys(seed).privateKey
71
97
  }
72
98
 
73
- export function privateKeyToKeyPair(privateKeyString: Input<string>): Output<ssh.KeyPair> {
99
+ /**
100
+ * Converts a private SSH key string to a KeyPair object.
101
+ *
102
+ * @param privateKeyString The private key string to convert.
103
+ * @returns An Output of the KeyPair object.
104
+ */
105
+ export function sshPrivateKeyToKeyPair(privateKeyString: Input<string>): Output<ssh.KeyPair> {
74
106
  return output(privateKeyString).apply(privateKeyString => {
75
107
  const privateKeyStruct = PrivateExport.decode(privateKeyString)
76
108
 
77
109
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
78
110
  const privKey = privateKeyStruct.keys[0].privKey.privKey as Uint8Array
79
111
 
80
- const { fingerprint, publicKey, privateKey } = getKeys(privKey.slice(0, 32))
112
+ const { fingerprint, publicKey } = getKeys(privKey.slice(0, 32))
81
113
 
82
114
  return output({
83
115
  type: "ed25519" as const,
84
116
  fingerprint,
85
117
  publicKey,
86
- privateKey: secret(privateKey),
118
+ privateKey: secret(privateKeyString),
87
119
  })
88
120
  })
89
121
  }
90
122
 
91
- export type SshKeyPairInputs = {
92
- sshKeyPair?: Input<ssh.KeyPair>
123
+ /**
124
+ * Ensures that the provided SSH key is available and generates a new key pair if not.
125
+ * If an existing key pair is provided, it will be used instead of generating a new one.
126
+ *
127
+ * @param privateKey The secret containing the private key.
128
+ * @param existingKeyPair An optional existing key pair to use if available.
129
+ * @returns An Output of the KeyPair object.
130
+ */
131
+ export function ensureSshKeyPair(
132
+ privateKey: UnitSecret<string | undefined>,
133
+ existingKeyPair?: Input<ssh.KeyPair>,
134
+ ): Output<ssh.KeyPair> {
135
+ if (existingKeyPair) {
136
+ return output(existingKeyPair)
137
+ }
138
+
139
+ return ensureSecretValue(privateKey, generateSshPrivateKey).value.apply(sshPrivateKeyToKeyPair)
93
140
  }
94
141
 
95
- export type SshKeyPairSecrets = {
142
+ export type ServerOptions = {
143
+ /**
144
+ * The local name of the server to namespace resources.
145
+ */
146
+ name: string
147
+
148
+ /**
149
+ * The fallback hostname to use if the server cannot be determined.
150
+ *
151
+ * If not provided, the `name` will be used as the fallback hostname.
152
+ */
153
+ fallbackHostname?: string
154
+
155
+ /**
156
+ * The L3 endpoints of the server.
157
+ */
158
+ endpoints: network.L3Endpoint[]
159
+
160
+ /**
161
+ * The L4 endpoint of the server.
162
+ *
163
+ * If not provided, the first endpoint will be used + `sshPort`.
164
+ */
165
+ sshEndpoint?: network.L4Endpoint
166
+
167
+ /**
168
+ * The SSH port to use for the server.
169
+ *
170
+ * Will be ignored if `sshEndpoint` is provided.
171
+ */
172
+ sshPort?: number
173
+
174
+ /**
175
+ * The SSH user to use for the server.
176
+ */
177
+ sshUser?: string
178
+
179
+ /**
180
+ * The SSH password to use for the server.
181
+ */
182
+ sshPassword?: Input<string | undefined>
183
+
184
+ /**
185
+ * The SSH private key to use for the server.
186
+ */
96
187
  sshPrivateKey?: Input<string>
97
- }
98
188
 
99
- export function getOrCreateSshKeyPair(
100
- inputs: SshKeyPairInputs,
101
- secrets: Output<SshKeyPairSecrets>,
102
- ): Output<ssh.KeyPair> {
103
- if (inputs.sshKeyPair) {
104
- return output(inputs.sshKeyPair)
105
- }
189
+ /**
190
+ * Whether the server is expected to have SSH access.
191
+ * If false, the server will be created without SSH credentials.
192
+ *
193
+ * By default, this is true.
194
+ */
195
+ hasSsh?: boolean
196
+
197
+ /**
198
+ * Whether to wait for the server to respond to a ping command before returning.
199
+ *
200
+ * If true, the command will wait for a successful ping response before proceeding.
201
+ *
202
+ * By default, this is equal to `!waitForSsh`, so when `waitForSsh` is true, no extra ping is performed.
203
+ */
204
+ waitForPing?: boolean
205
+
206
+ /**
207
+ * The interval in seconds to wait between ping attempts.
208
+ *
209
+ * Only used if `waitForPing` is true.
210
+ * By default, it will wait 5 seconds between attempts.
211
+ */
212
+ pingInterval?: number
213
+
214
+ /**
215
+ * The timeout in seconds to wait for the server to respond to a ping command.
216
+ *
217
+ * Only used if `waitForPing` is true.
218
+ * By default, it will wait 5 minutes (300 seconds) before timing out.
219
+ */
220
+ pingTimeout?: number
221
+
222
+ /**
223
+ * Whether to wait for the SSH service to be available before returning.
224
+ *
225
+ * If true, the command will wait for the SSH service to respond before proceeding.
226
+ *
227
+ * By default, this is true if `hasSsh` is true.
228
+ */
229
+ waitForSsh?: boolean
106
230
 
107
- const privateKey = getOrCreateSecret(secrets, "sshPrivateKey", generatePrivateKey)
231
+ /**
232
+ * The interval in seconds to wait between SSH connection attempts.
233
+ *
234
+ * Only used if `waitForSsh` is true.
235
+ * By default, it will wait 5 seconds between attempts.
236
+ */
237
+ sshCheckInterval?: number
108
238
 
109
- return privateKey.apply(privateKeyToKeyPair)
239
+ /**
240
+ * The timeout in seconds to wait for the SSH service to respond.
241
+ *
242
+ * Only used if `waitForSsh` is true.
243
+ * By default, it will wait 5 minutes (300 seconds) before timing out.
244
+ */
245
+ sshCheckTimeout?: number
110
246
  }
111
247
 
112
- export function createServerEntity(
113
- fallbackHostname: string,
114
- endpoint: network.L3Endpoint,
248
+ /**
249
+ * Creates a server entity with the provided options.
250
+ * It will create a command to check the SSH service and return the server entity.
251
+ *
252
+ * @param options The options for creating the server entity.
253
+ * @returns A promise that resolves to the created server entity.
254
+ */
255
+ export async function createServerEntity({
256
+ name,
257
+ fallbackHostname,
258
+ endpoints,
259
+ sshEndpoint,
115
260
  sshPort = 22,
116
261
  sshUser = "root",
117
- sshPassword?: Input<string | undefined>,
118
- sshPrivateKey?: Input<string>,
262
+ sshPassword,
263
+ sshPrivateKey,
119
264
  hasSsh = true,
120
- ): Output<common.Server> {
265
+ pingInterval,
266
+ pingTimeout,
267
+ waitForPing,
268
+ waitForSsh,
269
+ sshCheckInterval,
270
+ sshCheckTimeout,
271
+ }: ServerOptions): Promise<common.Server> {
272
+ if (endpoints.length === 0) {
273
+ throw new Error("At least one L3 endpoint is required to create a server entity")
274
+ }
275
+
276
+ fallbackHostname ??= name
277
+ waitForSsh ??= hasSsh
278
+ waitForPing ??= !waitForSsh
279
+
280
+ if (waitForPing) {
281
+ await Command.waitFor(`${name}.ping`, {
282
+ host: "local",
283
+ create: `ping -c 1 ${l3EndpointToString(endpoints[0])}`,
284
+ timeout: pingTimeout ?? 300,
285
+ interval: pingInterval ?? 5,
286
+ triggers: [Date.now()],
287
+ }).wait()
288
+ }
289
+
290
+ if (!hasSsh) {
291
+ return {
292
+ hostname: name,
293
+ endpoints,
294
+ }
295
+ }
296
+
297
+ sshEndpoint ??= l3EndpointToL4(endpoints[0], sshPort)
298
+
299
+ if (waitForSsh) {
300
+ await Command.waitFor(`${name}.ssh`, {
301
+ host: "local",
302
+ create: `nc -zv ${l3EndpointToString(sshEndpoint)} ${sshPort}`,
303
+ timeout: sshCheckTimeout ?? 300,
304
+ interval: sshCheckInterval ?? 5,
305
+ triggers: [Date.now()],
306
+ }).wait()
307
+ }
308
+
121
309
  const connection = output({
122
- host: l3EndpointToString(endpoint),
123
- port: sshPort,
310
+ host: l3EndpointToString(sshEndpoint),
311
+ port: sshEndpoint.port,
124
312
  user: sshUser,
125
313
  password: sshPassword,
126
314
  privateKey: sshPrivateKey,
127
315
  dialErrorLimit: 3,
128
316
  })
129
317
 
130
- if (!hasSsh) {
131
- return output({
132
- hostname: fallbackHostname,
133
- endpoints: [endpoint],
134
- })
135
- }
136
-
137
- const command = new local.Command("check-ssh", {
138
- create: `nc -zv ${l3EndpointToString(endpoint)} ${sshPort} && echo "up" || echo "down"`,
318
+ const hostnameResult = new remote.Command("hostname", {
319
+ connection,
320
+ create: "hostname",
321
+ triggers: [Date.now()],
139
322
  })
140
323
 
141
- return command.stdout.apply(result => {
142
- if (result === "down") {
143
- return output({
144
- hostname: fallbackHostname,
145
- endpoints: [endpoint],
146
- })
147
- }
148
-
149
- const hostnameResult = new remote.Command("hostname", {
150
- connection,
151
- create: "hostname",
152
- triggers: [Date.now()],
153
- })
154
-
155
- const hostKeyResult = new remote.Command("host-key", {
156
- connection,
157
- create: "cat /etc/ssh/ssh_host_ed25519_key.pub",
158
- triggers: [Date.now()],
159
- })
324
+ const hostKeyResult = new remote.Command("host-key", {
325
+ connection,
326
+ create: "cat /etc/ssh/ssh_host_ed25519_key.pub",
327
+ triggers: [Date.now()],
328
+ })
160
329
 
161
- return output({
162
- endpoints: [endpoint],
163
- hostname: hostnameResult.stdout.apply(x => x.trim()),
164
- ssh: {
165
- endpoints: [l3ToL4Endpoint(endpoint, sshPort)],
166
- user: sshUser,
167
- hostKey: hostKeyResult.stdout.apply(x => x.trim()),
168
- password: sshPassword,
169
- keyPair: sshPrivateKey ? privateKeyToKeyPair(sshPrivateKey) : undefined,
170
- },
171
- })
330
+ return await toPromise({
331
+ endpoints,
332
+ hostname: hostnameResult.stdout.apply(x => x.trim()),
333
+ ssh: {
334
+ endpoints: [sshEndpoint],
335
+ user: sshUser,
336
+ hostKey: hostKeyResult.stdout.apply(x => x.trim()),
337
+ password: sshPassword,
338
+ keyPair: sshPrivateKey ? sshPrivateKeyToKeyPair(sshPrivateKey) : undefined,
339
+ },
172
340
  })
173
341
  }
@@ -1,8 +1,9 @@
1
1
  import { common } from "@highstate/library"
2
- import { forUnit, toPromise } from "@highstate/pulumi"
2
+ import { forUnit } from "@highstate/pulumi"
3
3
  import {
4
4
  createServerEntity,
5
5
  createSshTerminal,
6
+ ensureSshKeyPair,
6
7
  l3EndpointToString,
7
8
  requireInputL3Endpoint,
8
9
  } from "../../shared"
@@ -10,24 +11,24 @@ import {
10
11
  const { name, args, inputs, secrets, outputs } = forUnit(common.existingServer)
11
12
 
12
13
  const endpoint = await requireInputL3Endpoint(args.endpoint, inputs.endpoint)
13
- const privateKey = await toPromise(inputs.sshKeyPair?.privateKey ?? secrets.sshPrivateKey)
14
+ const keyPair = ensureSshKeyPair(secrets.sshPrivateKey, inputs.sshKeyPair)
14
15
 
15
- const server = createServerEntity(
16
+ const server = await createServerEntity({
16
17
  name,
17
- endpoint,
18
- args.sshPort,
19
- args.sshUser,
20
- secrets.sshPassword,
21
- privateKey,
22
- )
18
+ endpoints: [endpoint],
19
+ sshPort: args.sshPort,
20
+ sshUser: args.sshUser,
21
+ sshPassword: secrets.sshPassword.value,
22
+ sshPrivateKey: keyPair.privateKey,
23
+ })
23
24
 
24
25
  export default outputs({
25
26
  server,
26
27
  endpoints: server.endpoints,
27
28
 
28
- $status: {
29
+ $statusFields: {
29
30
  hostname: server.hostname,
30
- endpoints: server.endpoints.apply(endpoints => endpoints.map(l3EndpointToString)),
31
+ endpoints: server.endpoints.map(l3EndpointToString),
31
32
  },
32
33
 
33
34
  $terminals: [createSshTerminal(server.ssh)],
File without changes
@@ -20,7 +20,7 @@ export default outputs({
20
20
 
21
21
  endpoints,
22
22
 
23
- $status: {
23
+ $statusFields: {
24
24
  endpoints: endpoints.map(l3EndpointToString),
25
25
  },
26
26
  })
@@ -19,7 +19,7 @@ export default outputs({
19
19
 
20
20
  endpoints,
21
21
 
22
- $status: {
22
+ $statusFields: {
23
23
  endpoints: endpoints.map(l3EndpointToString),
24
24
  },
25
25
  })
@@ -1,15 +1,24 @@
1
1
  import { ssh } from "@highstate/library"
2
- import { forUnit, getOrCreateSecret } from "@highstate/pulumi"
3
- import { generatePrivateKey, privateKeyToKeyPair } from "../../../shared"
2
+ import { forUnit } from "@highstate/pulumi"
3
+ import { ensureSshKeyPair } from "../../../shared"
4
4
 
5
- const { secrets, outputs } = forUnit(ssh.keyPair)
5
+ const { name, secrets, outputs } = forUnit(ssh.keyPair)
6
6
 
7
- const privateKey = getOrCreateSecret(secrets, "privateKey", generatePrivateKey)
8
- const keyPair = privateKeyToKeyPair(privateKey)
7
+ const keyPair = ensureSshKeyPair(secrets.privateKey)
9
8
 
10
9
  export default outputs({
11
- keyPair: privateKeyToKeyPair(privateKey),
12
- $status: {
10
+ keyPair,
11
+ publicKeyFile: {
12
+ meta: {
13
+ name: `${name}.pub`,
14
+ mode: 0o644,
15
+ },
16
+ content: {
17
+ type: "embedded",
18
+ value: keyPair.publicKey,
19
+ },
20
+ },
21
+ $statusFields: {
13
22
  fingerprint: keyPair.fingerprint,
14
23
  publicKey: keyPair.publicKey,
15
24
  },