@reventlessdev/reventless-aws 3.0.0-alpha.346 → 3.0.0-alpha.348

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 (80) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/package.json +17 -13
  3. package/rescript.json +2 -1
  4. package/run-bake-manifest.mjs +3 -0
  5. package/run-deploy-app.mjs +3 -0
  6. package/scripts/BakeManifest.res +395 -0
  7. package/scripts/BakeManifest.res.mjs +487 -0
  8. package/scripts/DeployApp.res +708 -0
  9. package/scripts/DeployApp.res.mjs +1015 -0
  10. package/scripts/DeployManifest.res +84 -0
  11. package/scripts/DeployManifest.res.mjs +112 -0
  12. package/scripts/ProvisionAccounts.res +60 -46
  13. package/scripts/ProvisionAccounts.res.mjs +51 -37
  14. package/scripts/PulumiCli.res +43 -0
  15. package/scripts/PulumiCli.res.mjs +106 -0
  16. package/src/Platform.res +19 -2
  17. package/src/Platform.res.mjs +12 -4
  18. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +1 -5
  19. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +1 -1
  20. package/src/adapter/Api/Platform_UIFragments_Lambda.res +1 -5
  21. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +1 -2
  22. package/src/adapter/Auth/Auth_ActiveRoleStore.res +1 -5
  23. package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +1 -2
  24. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +1 -5
  25. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +1 -2
  26. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +52 -31
  27. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs +75 -44
  28. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +1 -5
  29. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +1 -2
  30. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res +1 -5
  31. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res.mjs +1 -2
  32. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +5 -4
  33. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +1 -1
  34. package/src/adapter/Postgres/PgMigration_Builder.res +1 -1
  35. package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -2
  36. package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res +2 -0
  37. package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res.mjs +2 -2
  38. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +15 -6
  39. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +51 -30
  40. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +31 -14
  41. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +1 -5
  42. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +1 -1
  43. package/src/adapter/StateTopic/StateTopic_AppSync.res +1 -5
  44. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +1 -1
  45. package/src/adapter/Task/TaskBucket_S3.res +1 -3
  46. package/src/adapter/Task/TaskBucket_S3.res.mjs +1 -1
  47. package/src/adapter/Upload/Upload_Claim_S3.res +1 -5
  48. package/src/adapter/Upload/Upload_Claim_S3.res.mjs +1 -1
  49. package/src/adapter/Upload/Upload_Presign_S3.res +1 -5
  50. package/src/adapter/Upload/Upload_Presign_S3.res.mjs +1 -2
  51. package/src/capability/Capability_Geocoding_AwsLocation.res +5 -1
  52. package/src/capability/Capability_Geocoding_AwsLocation.res.mjs +7 -1
  53. package/src/components/Api/AppSync_SdlDecorate.res +4 -2
  54. package/src/components/Api/AppSync_SdlDecorate.res.mjs +1 -1
  55. package/src/plugin/cloner/ClonerRunner_Fargate.res +1 -5
  56. package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +1 -1
  57. package/src/plugin/stack/Plugin_Stack.res +4 -0
  58. package/src/plugin/stack/Plugin_Stack.res.mjs +2 -0
  59. package/src/util/Util_DeadLetterQueue.res +1 -5
  60. package/src/util/Util_DeadLetterQueue.res.mjs +1 -2
  61. package/src/util/Util_StoreLayout.res +15 -2
  62. package/src/util/Util_StoreLayout.res.mjs +11 -3
  63. package/tests/BakeManifestTest.res +204 -0
  64. package/tests/BakeManifestTest.res.mjs +259 -0
  65. package/tests/DcbColdStartPartitionTest.res +32 -0
  66. package/tests/DcbColdStartPartitionTest.res.mjs +33 -0
  67. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +88 -0
  68. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +112 -0
  69. package/tests/DcbUnresolvedPartitionSlice.res +28 -0
  70. package/tests/DcbUnresolvedPartitionSlice.res.mjs +38 -0
  71. package/tests/DeployAppTest.res +90 -0
  72. package/tests/DeployAppTest.res.mjs +133 -0
  73. package/tests/LambdaLayerLookupTest.res +104 -0
  74. package/tests/LambdaLayerLookupTest.res.mjs +117 -0
  75. package/tests/ProvisionAccountsTest.res +26 -0
  76. package/tests/ProvisionAccountsTest.res.mjs +31 -0
  77. package/tests/Util_StoreLayoutTest.res +6 -0
  78. package/tests/Util_StoreLayoutTest.res.mjs +7 -4
  79. package/tests/dcbColdStartPartition.mjs +66 -0
  80. package/tests/setup/layerArn.cjs +5 -0
@@ -0,0 +1,84 @@
1
+ /***
2
+ An app's `deploy-manifest.yaml`: the folder holding the platform stack, and the
3
+ folders holding the plugin stacks, in the order they deploy.
4
+
5
+ The reusable deploy workflow reads it with `yq`; the commands in this folder read
6
+ it here. Paths in the file are relative to the file's own folder.
7
+
8
+ `stack-defaults` on an entry are the settings `deploy-app` gives a stack it
9
+ creates for that folder — what a new stack of this app cannot deploy without.
10
+ */
11
+
12
+ @module("yaml") external parseYaml: string => JSON.t = "parse"
13
+
14
+ @schema
15
+ type platform = {
16
+ path: string,
17
+ name?: string,
18
+ @as("stack-defaults") stackDefaults?: dict<string>,
19
+ }
20
+
21
+ @schema
22
+ type plugin = {
23
+ name: string,
24
+ path: string,
25
+ @as("depends-on") dependsOn?: array<string>,
26
+ @as("stack-defaults") stackDefaults?: dict<string>,
27
+ }
28
+
29
+ @schema
30
+ type t = {
31
+ region?: string,
32
+ platform: platform,
33
+ plugins?: array<plugin>,
34
+ }
35
+
36
+ /** One stack folder, with its path made absolute. */
37
+ type project = {name: string, dir: string, stackDefaults: dict<string>}
38
+
39
+ type resolved = {
40
+ file: string,
41
+ region: option<string>,
42
+ platform: project,
43
+ plugins: array<project>,
44
+ }
45
+
46
+ let defaultFile = "deploy-manifest.yaml"
47
+
48
+ let parseString = (text: string): result<t, string> =>
49
+ try Ok(S.parseOrThrow(parseYaml(text), ~to=schema)) catch {
50
+ | JsExn(err) => Error(JsExn.message(err)->Option.getOr("not a deploy manifest"))
51
+ | _ => Error("not a deploy manifest")
52
+ }
53
+
54
+ let resolve = (manifest: t, ~file: string): resolved => {
55
+ let base = NodePath.dirname(NodePath.resolve([file]))
56
+ {
57
+ file,
58
+ region: manifest.region,
59
+ platform: {
60
+ name: manifest.platform.name->Option.getOr("platform"),
61
+ dir: NodePath.resolve([base, manifest.platform.path]),
62
+ stackDefaults: manifest.platform.stackDefaults->Option.getOr(Dict.make()),
63
+ },
64
+ plugins: manifest.plugins
65
+ ->Option.getOr([])
66
+ ->Array.map(p => {
67
+ name: p.name,
68
+ dir: NodePath.resolve([base, p.path]),
69
+ stackDefaults: p.stackDefaults->Option.getOr(Dict.make()),
70
+ }),
71
+ }
72
+ }
73
+
74
+ let load = (file: string): result<resolved, string> =>
75
+ switch try Ok(NodeFs.readFileSync(file)) catch {
76
+ | _ => Error(`cannot read ${file}`)
77
+ } {
78
+ | Error(_) as e => e
79
+ | Ok(text) =>
80
+ switch parseString(text) {
81
+ | Error(message) => Error(`${file}: ${message}`)
82
+ | Ok(manifest) => Ok(manifest->resolve(~file))
83
+ }
84
+ }
@@ -0,0 +1,112 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Sury from "sury";
5
+ import * as Yaml from "yaml";
6
+ import * as Nodefs from "node:fs";
7
+ import * as Nodepath from "node:path";
8
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
9
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
10
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
11
+
12
+ let platformSchema = Sury.$schema(s => ({
13
+ path: s.m(Sury.string),
14
+ name: s.m(Sury.$option(Sury.string)),
15
+ "stack-defaults": s.m(Sury.$option(Sury.dict(Sury.string)))
16
+ }));
17
+
18
+ let pluginSchema = Sury.$schema(s => ({
19
+ name: s.m(Sury.string),
20
+ path: s.m(Sury.string),
21
+ "depends-on": s.m(Sury.$option(Sury.array(Sury.string))),
22
+ "stack-defaults": s.m(Sury.$option(Sury.dict(Sury.string)))
23
+ }));
24
+
25
+ let schema = Sury.$schema(s => ({
26
+ region: s.m(Sury.$option(Sury.string)),
27
+ platform: s.m(platformSchema),
28
+ plugins: s.m(Sury.$option(Sury.array(pluginSchema)))
29
+ }));
30
+
31
+ function parseString(text) {
32
+ try {
33
+ return {
34
+ TAG: "Ok",
35
+ _0: S.parseOrThrow(Yaml.parse(text), schema)
36
+ };
37
+ } catch (raw_err) {
38
+ let err = Primitive_exceptions.internalToException(raw_err);
39
+ if (err.RE_EXN_ID === "JsExn") {
40
+ return {
41
+ TAG: "Error",
42
+ _0: Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "not a deploy manifest")
43
+ };
44
+ } else {
45
+ return {
46
+ TAG: "Error",
47
+ _0: "not a deploy manifest"
48
+ };
49
+ }
50
+ }
51
+ }
52
+
53
+ function resolve(manifest, file) {
54
+ let base = Nodepath.dirname(Nodepath.resolve(file));
55
+ return {
56
+ file: file,
57
+ region: manifest.region,
58
+ platform: {
59
+ name: Stdlib_Option.getOr(manifest.platform.name, "platform"),
60
+ dir: Nodepath.resolve(base, manifest.platform.path),
61
+ stackDefaults: Stdlib_Option.getOr(manifest.platform["stack-defaults"], {})
62
+ },
63
+ plugins: Stdlib_Option.getOr(manifest.plugins, []).map(p => ({
64
+ name: p.name,
65
+ dir: Nodepath.resolve(base, p.path),
66
+ stackDefaults: Stdlib_Option.getOr(p["stack-defaults"], {})
67
+ }))
68
+ };
69
+ }
70
+
71
+ function load(file) {
72
+ let e;
73
+ try {
74
+ e = {
75
+ TAG: "Ok",
76
+ _0: Nodefs.readFileSync(file, "utf8")
77
+ };
78
+ } catch (exn) {
79
+ e = {
80
+ TAG: "Error",
81
+ _0: `cannot read ` + file
82
+ };
83
+ }
84
+ if (e.TAG !== "Ok") {
85
+ return e;
86
+ }
87
+ let message = parseString(e._0);
88
+ if (message.TAG === "Ok") {
89
+ return {
90
+ TAG: "Ok",
91
+ _0: resolve(message._0, file)
92
+ };
93
+ } else {
94
+ return {
95
+ TAG: "Error",
96
+ _0: file + `: ` + message._0
97
+ };
98
+ }
99
+ }
100
+
101
+ let defaultFile = "deploy-manifest.yaml";
102
+
103
+ export {
104
+ platformSchema,
105
+ pluginSchema,
106
+ schema,
107
+ defaultFile,
108
+ parseString,
109
+ resolve,
110
+ load,
111
+ }
112
+ /* platformSchema Not a pure module */
@@ -121,8 +121,8 @@ question is whether the pool will accept *those*. A pool created with
121
121
  `UsernameAttributes: [email]` will not, and the accounts it makes instead are
122
122
  correctly created and unable to authenticate.
123
123
 
124
- Checked for every entry before anything is created, so a manifest that cannot work
125
- costs nothing rather than half a cast.
124
+ Checked for every entry before anything is created or written, so a manifest that
125
+ cannot work costs nothing rather than half a cast.
126
126
  */
127
127
  let checkPoolAcceptsUsernames = async (~providerId: string, ~usernames: array<string>): result<
128
128
  unit,
@@ -239,6 +239,58 @@ let manifestNote = (~file: string) =>
239
239
  It is gitignored on every platform; keep it that way. These are bootstrap
240
240
  credentials for a deployment, not secrets to reuse anywhere real.`
241
241
 
242
+ /**
243
+ The manifest at `file`, applied to the pool `providerId`.
244
+
245
+ 🚨 **The pool is checked before anything is written.** Preparing mints passwords
246
+ into the file; doing that first left a refused run with a manifest offering
247
+ accounts that exist nowhere, which the seed then tried to sign in as. `checkPool`
248
+ is a parameter so a test can refuse without a pool.
249
+ */
250
+ let provision = async (
251
+ ~file: string,
252
+ ~providerId: string,
253
+ ~checkPool=checkPoolAcceptsUsernames,
254
+ ): result<unit, string> =>
255
+ switch Manifest.parseFile(file) {
256
+ | Error(message) => Error(`${file}: ${message}`)
257
+ | Ok([]) => Error(`${file} declares no accounts`)
258
+ | Ok(declared) =>
259
+ switch await checkPool(~providerId, ~usernames=declared->Array.map(e => e.username)) {
260
+ | Error(_) as e => e
261
+ | Ok() =>
262
+ switch Manifest.prepare(~path=file) {
263
+ | Error(message) => Error(`${file}: ${message}`)
264
+ | Ok(prepared) =>
265
+ let generated = prepared->Array.filter(p => p.passwordGenerated)->Array.length
266
+ Console.log(
267
+ `manifest ${file} (${prepared
268
+ ->Array.length
269
+ ->Int.toString} accounts, ${generated->Int.toString} password(s) generated)`,
270
+ )
271
+ let entries = prepared->Array.map(p => p.entry)
272
+ await ensureGroups(~providerId, ~entries)
273
+ let fills = []
274
+ for index in 0 to entries->Array.length - 1 {
275
+ let entry = entries->Array.getUnsafe(index)
276
+ let userId = await applyEntry(~providerId, ~entry)
277
+ fills->Array.push({Manifest.index, password: None, userId})
278
+ }
279
+ switch Manifest.fillFile(~path=file, ~fills) {
280
+ | Error(message) => Error(`${file}: ${message}`)
281
+ | Ok(report) =>
282
+ let corrected = report->Array.filter(r => r.userIdWritten)->Array.length
283
+ Console.log(
284
+ `manifest ${file} (${corrected->Int.toString} id(s) written back)${manifestNote(
285
+ ~file,
286
+ )}`,
287
+ )
288
+ Ok()
289
+ }
290
+ }
291
+ }
292
+ }
293
+
242
294
  let run = async (): result<unit, string> =>
243
295
  // argv[0] is node, argv[1] this script.
244
296
  switch parseArgs(NodeProcess.argv->Array.slice(~start=2, ~end=NodeProcess.argv->Array.length)) {
@@ -255,50 +307,12 @@ let run = async (): result<unit, string> =>
255
307
  | SeededFrom(_, template) => Console.log(`manifest ${file} (new, copied from ${template})`)
256
308
  | Declared(_) => ()
257
309
  }
258
- switch Manifest.prepare(~path=file) {
259
- | Error(message) => Error(`${file}: ${message}`)
260
- | Ok([]) => Error(`${file} declares no accounts`)
261
- | Ok(prepared) =>
262
- let generated = prepared->Array.filter(p => p.passwordGenerated)->Array.length
263
- Console.log(
264
- `manifest ${file} (${prepared
265
- ->Array.length
266
- ->Int.toString} accounts, ${generated->Int.toString} password(s) generated)`,
267
- )
268
- let entries = prepared->Array.map(p => p.entry)
269
- switch ProvisionProvider.resolve(~given=args.providerId, ~stack=args.stack) {
270
- | Error(message) =>
271
- Error(
272
- `${message}. To fill in the manifest without creating anything, run prepare-accounts`,
273
- )
274
- | Ok((providerId, source)) =>
275
- Console.log(`provider ${providerId} (from ${source->ProvisionProvider.describe})`)
276
- switch await checkPoolAcceptsUsernames(
277
- ~providerId,
278
- ~usernames=entries->Array.map(e => e.username),
279
- ) {
280
- | Error(_) as e => e
281
- | Ok() =>
282
- await ensureGroups(~providerId, ~entries)
283
- let fills = []
284
- for index in 0 to entries->Array.length - 1 {
285
- let entry = entries->Array.getUnsafe(index)
286
- let userId = await applyEntry(~providerId, ~entry)
287
- fills->Array.push({Manifest.index, password: None, userId})
288
- }
289
- switch Manifest.fillFile(~path=file, ~fills) {
290
- | Error(message) => Error(`${file}: ${message}`)
291
- | Ok(report) =>
292
- let corrected = report->Array.filter(r => r.userIdWritten)->Array.length
293
- Console.log(
294
- `manifest ${file} (${corrected->Int.toString} id(s) written back)${manifestNote(
295
- ~file,
296
- )}`,
297
- )
298
- Ok()
299
- }
300
- }
301
- }
310
+ switch ProvisionProvider.resolve(~given=args.providerId, ~stack=args.stack) {
311
+ | Error(message) =>
312
+ Error(`${message}. To fill in the manifest without creating anything, run prepare-accounts`)
313
+ | Ok((providerId, source)) =>
314
+ Console.log(`provider ${providerId} (from ${source->ProvisionProvider.describe})`)
315
+ await provision(~file, ~providerId)
302
316
  }
303
317
  }
304
318
  }
@@ -220,59 +220,37 @@ function manifestNote(file) {
220
220
  credentials for a deployment, not secrets to reuse anywhere real.`;
221
221
  }
222
222
 
223
- async function run() {
224
- let e = parseArgs(process.argv.slice(2, process.argv.length));
225
- if (e.TAG !== "Ok") {
226
- return e;
227
- }
228
- let args = e._0;
229
- if (args.help) {
230
- console.log(usage);
231
- return {
232
- TAG: "Ok",
233
- _0: undefined
234
- };
235
- }
236
- let e$1 = AccountsManifest$Reventless.locate(args.file, undefined);
237
- if (e$1.TAG !== "Ok") {
238
- return e$1;
239
- }
240
- let located = e$1._0;
241
- let file = AccountsManifest$Reventless.pathOf(located);
242
- if (located.TAG !== "Declared") {
243
- console.log(`manifest ` + file + ` (new, copied from ` + located._1 + `)`);
244
- }
245
- let message = AccountsManifest$Reventless.prepare(file);
223
+ async function provision(file, providerId, checkPoolOpt) {
224
+ let checkPool = checkPoolOpt !== undefined ? checkPoolOpt : checkPoolAcceptsUsernames;
225
+ let message = AccountsManifest$Reventless.parseFile(file);
246
226
  if (message.TAG !== "Ok") {
247
227
  return {
248
228
  TAG: "Error",
249
229
  _0: file + `: ` + message._0
250
230
  };
251
231
  }
252
- let prepared = message._0;
253
- if (prepared.length === 0) {
232
+ let declared = message._0;
233
+ if (declared.length === 0) {
254
234
  return {
255
235
  TAG: "Error",
256
236
  _0: file + ` declares no accounts`
257
237
  };
258
238
  }
259
- let generated = prepared.filter(p => p.passwordGenerated).length;
260
- console.log(`manifest ` + file + ` (` + prepared.length.toString() + ` accounts, ` + generated.toString() + ` password(s) generated)`);
261
- let entries = prepared.map(p => p.entry);
262
- let message$1 = ProvisionProvider$ReventlessAws.resolve(args.providerId, args.stack);
239
+ let e = await checkPool(providerId, declared.map(e => e.username));
240
+ if (e.TAG !== "Ok") {
241
+ return e;
242
+ }
243
+ let message$1 = AccountsManifest$Reventless.prepare(file);
263
244
  if (message$1.TAG !== "Ok") {
264
245
  return {
265
246
  TAG: "Error",
266
- _0: message$1._0 + `. To fill in the manifest without creating anything, run prepare-accounts`
247
+ _0: file + `: ` + message$1._0
267
248
  };
268
249
  }
269
- let match = message$1._0;
270
- let providerId = match[0];
271
- console.log(`provider ` + providerId + ` (from ` + ProvisionProvider$ReventlessAws.describe(match[1]) + `)`);
272
- let e$2 = await checkPoolAcceptsUsernames(providerId, entries.map(e => e.username));
273
- if (e$2.TAG !== "Ok") {
274
- return e$2;
275
- }
250
+ let prepared = message$1._0;
251
+ let generated = prepared.filter(p => p.passwordGenerated).length;
252
+ console.log(`manifest ` + file + ` (` + prepared.length.toString() + ` accounts, ` + generated.toString() + ` password(s) generated)`);
253
+ let entries = prepared.map(p => p.entry);
276
254
  await ensureGroups(providerId, entries);
277
255
  let fills = [];
278
256
  for (let index = 0, index_finish = entries.length; index < index_finish; ++index) {
@@ -299,6 +277,41 @@ async function run() {
299
277
  };
300
278
  }
301
279
 
280
+ async function run() {
281
+ let e = parseArgs(process.argv.slice(2, process.argv.length));
282
+ if (e.TAG !== "Ok") {
283
+ return e;
284
+ }
285
+ let args = e._0;
286
+ if (args.help) {
287
+ console.log(usage);
288
+ return {
289
+ TAG: "Ok",
290
+ _0: undefined
291
+ };
292
+ }
293
+ let e$1 = AccountsManifest$Reventless.locate(args.file, undefined);
294
+ if (e$1.TAG !== "Ok") {
295
+ return e$1;
296
+ }
297
+ let located = e$1._0;
298
+ let file = AccountsManifest$Reventless.pathOf(located);
299
+ if (located.TAG !== "Declared") {
300
+ console.log(`manifest ` + file + ` (new, copied from ` + located._1 + `)`);
301
+ }
302
+ let message = ProvisionProvider$ReventlessAws.resolve(args.providerId, args.stack);
303
+ if (message.TAG !== "Ok") {
304
+ return {
305
+ TAG: "Error",
306
+ _0: message._0 + `. To fill in the manifest without creating anything, run prepare-accounts`
307
+ };
308
+ }
309
+ let match = message._0;
310
+ let providerId = match[0];
311
+ console.log(`provider ` + providerId + ` (from ` + ProvisionProvider$ReventlessAws.describe(match[1]) + `)`);
312
+ return await provision(file, providerId, undefined);
313
+ }
314
+
302
315
  async function main() {
303
316
  let message;
304
317
  try {
@@ -331,6 +344,7 @@ export {
331
344
  ensureGroups,
332
345
  applyEntry,
333
346
  manifestNote,
347
+ provision,
334
348
  run,
335
349
  main,
336
350
  }
@@ -0,0 +1,43 @@
1
+ /***
2
+ Reading deployed stacks through the `pulumi` CLI, so a command follows whatever
3
+ backend the CLI is logged into.
4
+
5
+ stderr goes to the caller's terminal: a Pulumi failure is legible where it
6
+ happens rather than re-reported here as a vaguer message.
7
+ */
8
+
9
+ let run = (~cwd: string, args: array<string>): string =>
10
+ NodeChildProcess.execFileSync(
11
+ "pulumi",
12
+ args->Array.concat(["--cwd", cwd, "--non-interactive"]),
13
+ {encoding: "utf8", stdio: ["ignore", "pipe", "inherit"], maxBuffer: 64 * 1024 * 1024},
14
+ )->String.trim
15
+
16
+ /** Whether the project in `dir` keeps settings for `stack` — how the deploy
17
+ workflow tells a stack that was never deployed from one that was. */
18
+ let hasStackFile = (~dir: string, ~stack: string): bool =>
19
+ NodeFs.existsSync(NodePath.join([dir, `Pulumi.${stack}.yaml`]))
20
+
21
+ let stackOutputs = (~dir: string, ~stack: string): result<dict<JSON.t>, string> =>
22
+ switch run(~cwd=dir, ["stack", "output", "--json", "--stack", stack]) {
23
+ | text =>
24
+ switch text->JSON.parseOrThrow->JSON.Decode.object {
25
+ | Some(outputs) => Ok(outputs)
26
+ | None => Error(`stack "${stack}" in ${dir} returned outputs that are not an object`)
27
+ | exception _ => Error(`stack "${stack}" in ${dir} returned outputs that are not JSON`)
28
+ }
29
+ | exception _ => Error(`could not read the outputs of stack "${stack}" in ${dir}`)
30
+ }
31
+
32
+ let selectedStack = (~dir: string): option<string> =>
33
+ switch run(~cwd=dir, ["stack", "--show-name"]) {
34
+ | "" => None
35
+ | name => Some(name)
36
+ | exception _ => None
37
+ }
38
+
39
+ let stringOutput = (outputs: dict<JSON.t>, name: string): option<string> =>
40
+ switch outputs->Dict.get(name)->Option.flatMap(JSON.Decode.string) {
41
+ | Some("") | None => None
42
+ | value => value
43
+ }
@@ -0,0 +1,106 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodefs from "node:fs";
4
+ import * as Nodepath from "node:path";
5
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Nodechild_process from "node:child_process";
8
+
9
+ function run(cwd, args) {
10
+ return Nodechild_process.execFileSync("pulumi", args.concat([
11
+ "--cwd",
12
+ cwd,
13
+ "--non-interactive"
14
+ ]), {
15
+ encoding: "utf8",
16
+ stdio: [
17
+ "ignore",
18
+ "pipe",
19
+ "inherit"
20
+ ],
21
+ maxBuffer: 67108864
22
+ }).trim();
23
+ }
24
+
25
+ function hasStackFile(dir, stack) {
26
+ return Nodefs.existsSync(Nodepath.join(dir, `Pulumi.` + stack + `.yaml`));
27
+ }
28
+
29
+ function stackOutputs(dir, stack) {
30
+ let exit = 0;
31
+ let text;
32
+ try {
33
+ text = run(dir, [
34
+ "stack",
35
+ "output",
36
+ "--json",
37
+ "--stack",
38
+ stack
39
+ ]);
40
+ exit = 1;
41
+ } catch (exn) {
42
+ return {
43
+ TAG: "Error",
44
+ _0: `could not read the outputs of stack "` + stack + `" in ` + dir
45
+ };
46
+ }
47
+ if (exit === 1) {
48
+ let exit$1 = 0;
49
+ let outputs;
50
+ try {
51
+ outputs = Stdlib_JSON.Decode.object(JSON.parse(text));
52
+ exit$1 = 2;
53
+ } catch (exn$1) {
54
+ return {
55
+ TAG: "Error",
56
+ _0: `stack "` + stack + `" in ` + dir + ` returned outputs that are not JSON`
57
+ };
58
+ }
59
+ if (exit$1 === 2) {
60
+ if (outputs !== undefined) {
61
+ return {
62
+ TAG: "Ok",
63
+ _0: outputs
64
+ };
65
+ } else {
66
+ return {
67
+ TAG: "Error",
68
+ _0: `stack "` + stack + `" in ` + dir + ` returned outputs that are not an object`
69
+ };
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ function selectedStack(dir) {
76
+ let name;
77
+ try {
78
+ name = run(dir, [
79
+ "stack",
80
+ "--show-name"
81
+ ]);
82
+ } catch (exn) {
83
+ return;
84
+ }
85
+ if (name === "") {
86
+ return;
87
+ } else {
88
+ return name;
89
+ }
90
+ }
91
+
92
+ function stringOutput(outputs, name) {
93
+ let value = Stdlib_Option.flatMap(outputs[name], Stdlib_JSON.Decode.string);
94
+ if (value !== undefined && value !== "") {
95
+ return value;
96
+ }
97
+ }
98
+
99
+ export {
100
+ run,
101
+ hasStackFile,
102
+ stackOutputs,
103
+ selectedStack,
104
+ stringOutput,
105
+ }
106
+ /* node:fs Not a pure module */
package/src/Platform.res CHANGED
@@ -1492,7 +1492,7 @@ module MakeWithConfig = (
1492
1492
  // GETs from it to resolve an offloaded `structure`. Kept out of the declared
1493
1493
  // `@storageRef` stores and their pending-claim/expiry machinery: offload
1494
1494
  // objects are written durably up front and are never "pending".
1495
- let offloadProtection = Util_StoreLayout.protectionFor(~stack=Pulumi.Pulumi.getStackName())
1495
+ let offloadProtection = Util_StoreLayout.protectionOfDeployedStack()
1496
1496
  let offloadBucket = PulumiAws.S3.Bucket.make(
1497
1497
  ~name="reventless-offload",
1498
1498
  ~args={
@@ -1518,6 +1518,14 @@ module MakeWithConfig = (
1518
1518
  },
1519
1519
  )
1520
1520
  Pulumi.Pulumi.export("offloadBucket", offloadBucketName)
1521
+ // What a plugin stack's `platform:stack` names, fully qualified so it holds on
1522
+ // every backend. `deploy-app` reads it to point the plugin stacks it creates.
1523
+ Pulumi.Pulumi.export(
1524
+ "platformStack",
1525
+ Pulumi.Output.make(
1526
+ `${Pulumi.Pulumi.getOrganization()}/${Pulumi.Pulumi.getProjectName()}/${Pulumi.Pulumi.getStackName()}`,
1527
+ ),
1528
+ )
1521
1529
 
1522
1530
  // Mount the Platform_ComponentDefinitions Lambda resolver on the Platform API
1523
1531
  // (split mode) or Domain API (unified mode — platformApi == domainApi above).
@@ -1774,7 +1782,7 @@ module MakeWithConfig = (
1774
1782
  ~stack=stackName,
1775
1783
  ~prodStacks=Util_HostUiDomain.resolveProdStacks(),
1776
1784
  )
1777
- let storeProtection = Util_StoreLayout.protectionFor(~stack=stackName)
1785
+ let storeProtection = Util_StoreLayout.protectionOfDeployedStack()
1778
1786
 
1779
1787
  // Dedup by `(plugin, store)` — that pair is a store's identity, and many
1780
1788
  // fields legitimately name one store.
@@ -2529,6 +2537,15 @@ module MakeWithConfig = (
2529
2537
  let startServers = () => ()
2530
2538
 
2531
2539
  let deployPlugin = (~plugin: module(PluginMaker), ~apiTarget=Domain) => {
2540
+ // Without the setting the plugin used to deploy as a platform of its own,
2541
+ // with no error, and never register with the real one.
2542
+ if platformStackRef->Option.isNone {
2543
+ JsError.throwWithMessage(
2544
+ `This plugin stack has no platform:stack setting, so it cannot find its platform. ` ++
2545
+ `Set it to the platform's stack: pulumi config set platform:stack ` ++
2546
+ `${Pulumi.Pulumi.getOrganization()}/<platform project>/${Pulumi.Pulumi.getStackName()}`,
2547
+ )
2548
+ }
2532
2549
  log.info(
2533
2550
  ~comp="Platform:deployPlugin",
2534
2551
  `target=${switch apiTarget {