@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
@@ -1,22 +1,27 @@
1
- import {
2
- generatePrivateKey,
3
- privateKeyToKeyPair
4
- } from "../../../chunk-NISDP46H.js";
1
+ import { ensureSshKeyPair } from '../../../chunk-YYNV3MVT.js';
2
+ import { ssh } from '@highstate/library';
3
+ import { forUnit } from '@highstate/pulumi';
5
4
 
6
- // src/units/ssh/key-pair/index.ts
7
- import { ssh } from "@highstate/library";
8
- import { forUnit, getOrCreateSecret } from "@highstate/pulumi";
9
- var { secrets, outputs } = forUnit(ssh.keyPair);
10
- var privateKey = getOrCreateSecret(secrets, "privateKey", generatePrivateKey);
11
- var keyPair = privateKeyToKeyPair(privateKey);
5
+ var { name, secrets, outputs } = forUnit(ssh.keyPair);
6
+ var keyPair = ensureSshKeyPair(secrets.privateKey);
12
7
  var key_pair_default = outputs({
13
- keyPair: privateKeyToKeyPair(privateKey),
14
- $status: {
8
+ keyPair,
9
+ publicKeyFile: {
10
+ meta: {
11
+ name: `${name}.pub`,
12
+ mode: 420
13
+ },
14
+ content: {
15
+ type: "embedded",
16
+ value: keyPair.publicKey
17
+ }
18
+ },
19
+ $statusFields: {
15
20
  fingerprint: keyPair.fingerprint,
16
21
  publicKey: keyPair.publicKey
17
22
  }
18
23
  });
19
- export {
20
- key_pair_default as default
21
- };
24
+
25
+ export { key_pair_default as default };
26
+ //# sourceMappingURL=index.js.map
22
27
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/units/ssh/key-pair/index.ts"],"sourcesContent":["import { ssh } from \"@highstate/library\"\nimport { forUnit, getOrCreateSecret } from \"@highstate/pulumi\"\nimport { generatePrivateKey, privateKeyToKeyPair } from \"../../../shared\"\n\nconst { secrets, outputs } = forUnit(ssh.keyPair)\n\nconst privateKey = getOrCreateSecret(secrets, \"privateKey\", generatePrivateKey)\nconst keyPair = privateKeyToKeyPair(privateKey)\n\nexport default outputs({\n keyPair: privateKeyToKeyPair(privateKey),\n $status: {\n fingerprint: keyPair.fingerprint,\n publicKey: keyPair.publicKey,\n },\n})\n"],"mappings":";;;;;;AAAA,SAAS,WAAW;AACpB,SAAS,SAAS,yBAAyB;AAG3C,IAAM,EAAE,SAAS,QAAQ,IAAI,QAAQ,IAAI,OAAO;AAEhD,IAAM,aAAa,kBAAkB,SAAS,cAAc,kBAAkB;AAC9E,IAAM,UAAU,oBAAoB,UAAU;AAE9C,IAAO,mBAAQ,QAAQ;AAAA,EACrB,SAAS,oBAAoB,UAAU;AAAA,EACvC,SAAS;AAAA,IACP,aAAa,QAAQ;AAAA,IACrB,WAAW,QAAQ;AAAA,EACrB;AACF,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../../../src/units/ssh/key-pair/index.ts"],"names":[],"mappings":";;;;AAIA,IAAM,EAAE,IAAA,EAAM,OAAA,EAAS,SAAQ,GAAI,OAAA,CAAQ,IAAI,OAAO,CAAA;AAEtD,IAAM,OAAA,GAAU,gBAAA,CAAiB,OAAA,CAAQ,UAAU,CAAA;AAEnD,IAAO,mBAAQ,OAAA,CAAQ;AAAA,EACrB,OAAA;AAAA,EACA,aAAA,EAAe;AAAA,IACb,IAAA,EAAM;AAAA,MACJ,IAAA,EAAM,GAAG,IAAI,CAAA,IAAA,CAAA;AAAA,MACb,IAAA,EAAM;AAAA,KACR;AAAA,IACA,OAAA,EAAS;AAAA,MACP,IAAA,EAAM,UAAA;AAAA,MACN,OAAO,OAAA,CAAQ;AAAA;AACjB,GACF;AAAA,EACA,aAAA,EAAe;AAAA,IACb,aAAa,OAAA,CAAQ,WAAA;AAAA,IACrB,WAAW,OAAA,CAAQ;AAAA;AAEvB,CAAC","file":"index.js","sourcesContent":["import { ssh } from \"@highstate/library\"\nimport { forUnit } from \"@highstate/pulumi\"\nimport { ensureSshKeyPair } from \"../../../shared\"\n\nconst { name, secrets, outputs } = forUnit(ssh.keyPair)\n\nconst keyPair = ensureSshKeyPair(secrets.privateKey)\n\nexport default outputs({\n keyPair,\n publicKeyFile: {\n meta: {\n name: `${name}.pub`,\n mode: 0o644,\n },\n content: {\n type: \"embedded\",\n value: keyPair.publicKey,\n },\n },\n $statusFields: {\n fingerprint: keyPair.fingerprint,\n publicKey: keyPair.publicKey,\n },\n})\n"]}
package/package.json CHANGED
@@ -1,11 +1,19 @@
1
1
  {
2
2
  "name": "@highstate/common",
3
- "version": "0.9.15",
3
+ "version": "0.9.18",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
7
7
  "src"
8
8
  ],
9
+ "highstate": {
10
+ "sourceHash": {
11
+ ".": {
12
+ "mode": "manual",
13
+ "version": "1"
14
+ }
15
+ }
16
+ },
9
17
  "exports": {
10
18
  ".": {
11
19
  "types": "./src/index.ts",
@@ -28,15 +36,21 @@
28
36
  "update-images": "../../scripts/update-images.sh ./assets/images.json"
29
37
  },
30
38
  "dependencies": {
31
- "@highstate/library": "^0.9.15",
32
- "@highstate/pulumi": "^0.9.15",
39
+ "@highstate/contract": "^0.9.18",
40
+ "@highstate/library": "^0.9.18",
41
+ "@highstate/pulumi": "^0.9.18",
33
42
  "@noble/hashes": "^1.7.1",
34
43
  "@pulumi/command": "^1.0.2",
35
44
  "micro-key-producer": "^0.7.3",
36
- "remeda": "^2.21.0"
45
+ "minimatch": "^10.0.3",
46
+ "remeda": "^2.21.0",
47
+ "tar": "^7.4.3",
48
+ "unzipper": "^0.12.3"
37
49
  },
38
50
  "devDependencies": {
39
- "@highstate/cli": "^0.9.15"
51
+ "@highstate/cli": "^0.9.18",
52
+ "@types/tar": "^6.1.13",
53
+ "@types/unzipper": "^0.10.11"
40
54
  },
41
- "gitHead": "f61b9905d4cd50511b03331411f42595403ebc06"
55
+ "gitHead": "9ebcd7da56b00b8ca08bf52cc8438f527338cd64"
42
56
  }
@@ -1,16 +1,26 @@
1
+ import { homedir } from "node:os"
1
2
  import { local, remote, type types } from "@pulumi/command"
2
3
  import {
3
4
  ComponentResource,
4
5
  interpolate,
5
6
  output,
7
+ toPromise,
6
8
  type ComponentResourceOptions,
7
9
  type Input,
10
+ type InputMap,
8
11
  type InputOrArray,
9
12
  type Output,
10
13
  } from "@highstate/pulumi"
11
14
  import { common, ssh } from "@highstate/library"
15
+ import { flat } from "remeda"
12
16
  import { l3EndpointToString } from "./network"
13
17
 
18
+ /**
19
+ * Creates a connection object for the given SSH credentials.
20
+ *
21
+ * @param ssh The SSH credentials.
22
+ * @returns An output connection object for Pulumi remote commands.
23
+ */
14
24
  export function getServerConnection(
15
25
  ssh: Input<ssh.Credentials>,
16
26
  ): Output<types.input.remote.ConnectionArgs> {
@@ -25,108 +35,282 @@ export function getServerConnection(
25
35
  }))
26
36
  }
27
37
 
28
- export type CommandHost = "local" | common.Server
38
+ export type CommandHost = "local" | Input<common.Server> | Input<types.input.remote.ConnectionArgs>
39
+ export type CommandRunMode = "auto" | "prefer-host"
29
40
 
30
41
  export type CommandArgs = {
31
- host: Input<CommandHost>
32
- create: Input<string>
33
- update?: Input<string>
34
- delete?: Input<string>
35
- logging?: Input<remote.Logging>
42
+ /**
43
+ * The host to run the command on.
44
+ *
45
+ * Can be "local" for local execution or a server object for remote execution.
46
+ */
47
+ host: CommandHost
48
+
49
+ /**
50
+ * The command to run when the resource is created.
51
+ *
52
+ * If an array is provided, it will be joined with spaces.
53
+ */
54
+ create: InputOrArray<string>
55
+
56
+ /**
57
+ * The command to run when the resource is updated or one of the triggers changes.
58
+ *
59
+ * If not set, the `create` command will be used.
60
+ */
61
+ update?: InputOrArray<string>
62
+
63
+ /**
64
+ * The command to run when the resource is deleted.
65
+ */
66
+ delete?: InputOrArray<string>
67
+
68
+ /**
69
+ * The stdin content to pass to the command.
70
+ */
71
+ stdin?: Input<string>
72
+
73
+ /**
74
+ * The logging level for the command.
75
+ */
76
+ logging?: Input<local.Logging>
77
+
78
+ /**
79
+ * The triggers for the command.
80
+ *
81
+ * They will be captured in the command's state and will trigger the command to run again
82
+ * if they change.
83
+ */
36
84
  triggers?: InputOrArray<unknown>
85
+
86
+ /**
87
+ * The working directory for the command.
88
+ *
89
+ * If not set, the command will run in the user's home directory (for both local and remote hosts).
90
+ */
91
+ cwd?: Input<string>
92
+
93
+ /**
94
+ * The environment variables to set for the command.
95
+ */
96
+ environment?: Input<InputMap<string>>
97
+
98
+ /**
99
+ * The run mode for the command.
100
+ *
101
+ * - `auto` (default): if the `image` is set, it will always run in a container, never on the host;
102
+ * otherwise, it will run on the host.
103
+ *
104
+ * - `prefer-host`: it will try to run on the host if the executable is available;
105
+ * otherwise, it will run in a container or throw an error if the `image` is not set.
106
+ */
107
+ runMode?: CommandRunMode
108
+
109
+ /**
110
+ * The container image to use to run the command.
111
+ */
112
+ image?: Input<string>
113
+
114
+ /**
115
+ * The paths to mount if the command runs in a container.
116
+ *
117
+ * They will be mounted to the same paths in the container.
118
+ */
119
+ mounts?: InputOrArray<string>
37
120
  }
38
121
 
39
122
  export type TextFileArgs = {
123
+ /**
124
+ * The host to run the command on.
125
+ */
40
126
  host: CommandHost
127
+
128
+ /**
129
+ * The absolute path to the file on the host.
130
+ */
41
131
  path: Input<string>
132
+
133
+ /**
134
+ * The content to write to the file.
135
+ */
42
136
  content: Input<string>
43
137
  }
44
138
 
45
- export class Command extends ComponentResource {
46
- public readonly command: Output<local.Command | remote.Command>
139
+ export type WaitForArgs = CommandArgs & {
140
+ /**
141
+ * The timeout in seconds to wait for the command to complete.
142
+ *
143
+ * Defaults to 5 minutes (300 seconds).
144
+ */
145
+ timeout?: Input<number>
47
146
 
147
+ /**
148
+ * The interval in seconds to wait between checks.
149
+ *
150
+ * Defaults to 5 seconds.
151
+ */
152
+ interval?: Input<number>
153
+ }
154
+
155
+ function createCommand(command: string | string[]): string {
156
+ if (Array.isArray(command)) {
157
+ return command.join(" ")
158
+ }
159
+
160
+ return command
161
+ }
162
+
163
+ function wrapWithWorkDir(dir?: Input<string>) {
164
+ if (!dir) {
165
+ return (command: string) => output(command)
166
+ }
167
+
168
+ return (command: string) => interpolate`cd "${dir}" && ${command}`
169
+ }
170
+
171
+ function wrapWithWaitFor(timeout: Input<number> = 300, interval: Input<number> = 5) {
172
+ return (command: string | string[]) =>
173
+ // TOD: escape the command
174
+ interpolate`timeout ${timeout} bash -c 'while ! ${createCommand(command)}; do sleep ${interval}; done'`
175
+ }
176
+
177
+ export class Command extends ComponentResource {
48
178
  public readonly stdout: Output<string>
49
179
  public readonly stderr: Output<string>
50
180
 
51
181
  constructor(name: string, args: CommandArgs, opts?: ComponentResourceOptions) {
52
182
  super("highstate:common:Command", name, args, opts)
53
183
 
54
- this.command = output(args).apply(args => {
55
- if (args.host === "local") {
56
- return new local.Command(
57
- name,
58
- {
59
- create: args.create,
60
- update: args.update,
61
- delete: args.delete,
62
- logging: args.logging,
63
- triggers: args.triggers,
64
- },
65
- { ...opts, parent: this },
66
- )
67
- }
68
-
69
- if (!args.host.ssh) {
70
- throw new Error(`The host "${args.host.hostname}" has no SSH credentials`)
71
- }
72
-
73
- return new remote.Command(
74
- name,
75
- {
76
- connection: getServerConnection(args.host.ssh),
77
- create: args.create,
78
- update: args.update,
79
- delete: args.delete,
80
- logging: args.logging,
81
- triggers: args.triggers,
82
- },
83
- { ...opts, parent: this },
84
- )
85
- })
86
-
87
- this.stdout = this.command.stdout
88
- this.stderr = this.command.stderr
184
+ const command =
185
+ args.host === "local"
186
+ ? new local.Command(
187
+ name,
188
+ {
189
+ create: output(args.create).apply(createCommand),
190
+ update: args.update ? output(args.update).apply(createCommand) : undefined,
191
+ delete: args.delete ? output(args.delete).apply(createCommand) : undefined,
192
+ logging: args.logging,
193
+ triggers: args.triggers ? output(args.triggers).apply(flat) : undefined,
194
+ dir: args.cwd ?? homedir(),
195
+ environment: args.environment as local.CommandArgs["environment"],
196
+ stdin: args.stdin,
197
+ },
198
+ { ...opts, parent: this },
199
+ )
200
+ : new remote.Command(
201
+ name,
202
+ {
203
+ connection: output(args.host).apply(server => {
204
+ if ("host" in server) {
205
+ return output(server)
206
+ }
207
+
208
+ if (!server.ssh) {
209
+ throw new Error(`The server "${server.hostname}" has no SSH credentials`)
210
+ }
211
+
212
+ return getServerConnection(server.ssh)
213
+ }),
214
+
215
+ create: output(args.create).apply(createCommand).apply(wrapWithWorkDir(args.cwd)),
216
+
217
+ update: args.update
218
+ ? output(args.update).apply(createCommand).apply(wrapWithWorkDir(args.cwd))
219
+ : undefined,
220
+
221
+ delete: args.delete
222
+ ? output(args.delete).apply(createCommand).apply(wrapWithWorkDir(args.cwd))
223
+ : undefined,
224
+
225
+ logging: args.logging,
226
+ triggers: args.triggers ? output(args.triggers).apply(flat) : undefined,
227
+ stdin: args.stdin,
228
+ environment: args.environment as remote.CommandArgs["environment"],
229
+ },
230
+ { ...opts, parent: this },
231
+ )
232
+
233
+ this.stdout = command.stdout
234
+ this.stderr = command.stderr
89
235
  }
90
236
 
237
+ /**
238
+ * Waits for the command to complete and returns its output.
239
+ * The standard output will be returned.
240
+ */
241
+ async wait(): Promise<string> {
242
+ return await toPromise(this.stdout)
243
+ }
244
+
245
+ /**
246
+ * Creates a command that writes the given content to a file on the host.
247
+ * The file will be created if it does not exist, and overwritten if it does.
248
+ *
249
+ * Use for small text files like configuration files.
250
+ */
91
251
  static createTextFile(
92
252
  name: string,
93
253
  options: TextFileArgs,
94
254
  opts?: ComponentResourceOptions,
95
- ): Output<Command> {
96
- return output(options).apply(options => {
97
- const escapedContent = options.content.replace(/"/g, '\\"')
98
-
99
- const command = new Command(
100
- name,
101
- {
102
- host: options.host,
103
- create: interpolate`mkdir -p $(dirname ${options.path}) && echo "${escapedContent}" > ${options.path}`,
104
- delete: interpolate`rm -rf ${options.path}`,
105
- },
106
- opts,
107
- )
108
-
109
- return command
110
- })
255
+ ): Command {
256
+ return new Command(
257
+ name,
258
+ {
259
+ host: options.host,
260
+ create: interpolate`mkdir -p $(dirname "${options.path}") && cat > ${options.path}`,
261
+ delete: interpolate`rm -rf ${options.path}`,
262
+ stdin: options.content,
263
+ },
264
+ opts,
265
+ )
111
266
  }
112
267
 
268
+ /**
269
+ * Creates a command that waits for a file to be created and then reads its content.
270
+ * This is useful for waiting for a file to be generated by another process.
271
+ *
272
+ * Use for small text files like configuration files.
273
+ */
113
274
  static receiveTextFile(
114
275
  name: string,
115
276
  options: Omit<TextFileArgs, "content">,
116
277
  opts?: ComponentResourceOptions,
117
- ): Output<Command> {
118
- return output(options).apply(options => {
119
- const command = new Command(
120
- name,
121
- {
122
- host: options.host,
123
- create: interpolate`while ! test -f ${options.path}; do sleep 1; done; cat ${options.path}`,
124
- logging: "stderr",
125
- },
126
- opts,
127
- )
128
-
129
- return command
130
- })
278
+ ): Command {
279
+ return new Command(
280
+ name,
281
+ {
282
+ host: options.host,
283
+ create: interpolate`while ! test -f "${options.path}"; do sleep 1; done; cat "${options.path}"`,
284
+ logging: "stderr",
285
+ },
286
+ opts,
287
+ )
288
+ }
289
+
290
+ /**
291
+ * Creates a command that waits for a condition to be met.
292
+ * The command will run until the condition is met or the timeout is reached.
293
+ *
294
+ * The condition is considered met if the command returns a zero exit code.
295
+ *
296
+ * @param name The name of the command resource.
297
+ * @param args The arguments for the command, including the condition to check.
298
+ * @param opts Optional resource options.
299
+ */
300
+ static waitFor(name: string, args: WaitForArgs, opts?: ComponentResourceOptions): Command {
301
+ return new Command(
302
+ name,
303
+ {
304
+ ...args,
305
+ create: output(args.create).apply(wrapWithWaitFor(args.timeout, args.interval)),
306
+ update: args.update
307
+ ? output(args.update).apply(wrapWithWaitFor(args.timeout, args.interval))
308
+ : undefined,
309
+ delete: args.delete
310
+ ? output(args.delete).apply(wrapWithWaitFor(args.timeout, args.interval))
311
+ : undefined,
312
+ },
313
+ opts,
314
+ )
131
315
  }
132
316
  }