@reventlessdev/reventless-seed-aws 1.0.0-alpha.28 → 1.0.0-alpha.29
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.
- package/CHANGELOG.md +7 -0
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/compiler-info.json +1 -1
- package/lib/bs/src/ReventlessSeedAws.ast +0 -0
- package/lib/bs/src/ReventlessSeedAws.cmi +0 -0
- package/lib/bs/src/ReventlessSeedAws.cmj +0 -0
- package/lib/bs/src/ReventlessSeedAws.cmt +0 -0
- package/lib/bs/src/ReventlessSeedAws.res +111 -59
- package/lib/bs/src/ReventlessSeedAws.res.mjs +23 -1
- package/lib/bs/src/ReventlessSeedAws_Quiesce.ast +0 -0
- package/lib/bs/src/ReventlessSeedAws_Quiesce.cmi +0 -0
- package/lib/bs/src/ReventlessSeedAws_Quiesce.cmt +0 -0
- package/lib/bs/src/ReventlessSeedAws_Quiesce.res +11 -9
- package/lib/bs/src/ReventlessSeedAws_Reconcile.ast +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.cmi +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.cmt +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reconcile.res +26 -20
- package/lib/bs/src/ReventlessSeedAws_Reset.ast +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.cmi +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.cmt +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.res +51 -46
- package/lib/bs/tests/EndpointResolutionTest.cmi +0 -0
- package/lib/bs/tests/EndpointResolutionTest.cmt +0 -0
- package/lib/bs/tests/ObjectStoreReconcileTest.ast +0 -0
- package/lib/bs/tests/ObjectStoreReconcileTest.cmi +0 -0
- package/lib/bs/tests/ObjectStoreReconcileTest.cmt +0 -0
- package/lib/bs/tests/ObjectStoreReconcileTest.res +4 -5
- package/lib/bs/tests/ObjectStoreResetTest.ast +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.cmi +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.cmt +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.res +27 -6
- package/lib/bs/tests/QuiesceResetTest.ast +0 -0
- package/lib/bs/tests/QuiesceResetTest.cmi +0 -0
- package/lib/bs/tests/QuiesceResetTest.cmt +0 -0
- package/lib/bs/tests/QuiesceResetTest.res +34 -22
- package/lib/ocaml/.compiler.log +2 -2
- package/lib/ocaml/EndpointResolutionTest.cmi +0 -0
- package/lib/ocaml/EndpointResolutionTest.cmt +0 -0
- package/lib/ocaml/ObjectStoreReconcileTest.ast +0 -0
- package/lib/ocaml/ObjectStoreReconcileTest.cmi +0 -0
- package/lib/ocaml/ObjectStoreReconcileTest.cmt +0 -0
- package/lib/ocaml/ObjectStoreReconcileTest.res +4 -5
- package/lib/ocaml/ObjectStoreResetTest.ast +0 -0
- package/lib/ocaml/ObjectStoreResetTest.cmi +0 -0
- package/lib/ocaml/ObjectStoreResetTest.cmt +0 -0
- package/lib/ocaml/ObjectStoreResetTest.res +27 -6
- package/lib/ocaml/QuiesceResetTest.ast +0 -0
- package/lib/ocaml/QuiesceResetTest.cmi +0 -0
- package/lib/ocaml/QuiesceResetTest.cmt +0 -0
- package/lib/ocaml/QuiesceResetTest.res +34 -22
- package/lib/ocaml/ReventlessSeedAws.ast +0 -0
- package/lib/ocaml/ReventlessSeedAws.cmi +0 -0
- package/lib/ocaml/ReventlessSeedAws.cmj +0 -0
- package/lib/ocaml/ReventlessSeedAws.cmt +0 -0
- package/lib/ocaml/ReventlessSeedAws.res +111 -59
- package/lib/ocaml/ReventlessSeedAws_Quiesce.ast +0 -0
- package/lib/ocaml/ReventlessSeedAws_Quiesce.cmi +0 -0
- package/lib/ocaml/ReventlessSeedAws_Quiesce.cmt +0 -0
- package/lib/ocaml/ReventlessSeedAws_Quiesce.res +11 -9
- package/lib/ocaml/ReventlessSeedAws_Reconcile.ast +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reconcile.cmi +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reconcile.cmt +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reconcile.res +26 -20
- package/lib/ocaml/ReventlessSeedAws_Reset.ast +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.cmi +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.cmt +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.res +51 -46
- package/package.json +4 -4
- package/src/ReventlessSeedAws.res +111 -59
- package/src/ReventlessSeedAws.res.mjs +23 -1
- package/src/ReventlessSeedAws_Quiesce.res +11 -9
- package/src/ReventlessSeedAws_Reconcile.res +26 -20
- package/src/ReventlessSeedAws_Reset.res +51 -46
- package/tests/ObjectStoreReconcileTest.res +4 -5
- package/tests/ObjectStoreResetTest.res +27 -6
- package/tests/QuiesceResetTest.res +34 -22
|
@@ -50,8 +50,6 @@ module S3 = AwsSdk.S3
|
|
|
50
50
|
module Lambda = AwsSdk.Lambda
|
|
51
51
|
module Quiesce = ReventlessSeedAws_Quiesce
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
53
|
// The Pulumi project name, which the framework stamps as `reventless:platform` on
|
|
56
54
|
// every resource (`Plugin.res`: `platformName = getProjectName()`). Discovery
|
|
57
55
|
// MUST scope on this as well as the stack: `reventless:environment` carries only
|
|
@@ -72,7 +70,7 @@ let projectName = (~projectDir: string): string => {
|
|
|
72
70
|
trimmed
|
|
73
71
|
->String.slice(~start=String.length("name:"), ~end=String.length(trimmed))
|
|
74
72
|
->String.trim
|
|
75
|
-
->String.replaceRegExp(
|
|
73
|
+
->String.replaceRegExp(/^[\"']|[\"']$/g, "")
|
|
76
74
|
| None => throw(Seed.Failed(`could not find a \`name:\` field in ${path}.`))
|
|
77
75
|
}
|
|
78
76
|
}
|
|
@@ -106,7 +104,7 @@ let chunk = (arr: array<'a>, size: int): array<array<'a>> => {
|
|
|
106
104
|
|
|
107
105
|
// Fail-closed name allowlist. A denylist ("everything except prod") fails open
|
|
108
106
|
// the day a new prod-like stack is added and forgotten; this fails closed.
|
|
109
|
-
let nameAllowlist =
|
|
107
|
+
let nameAllowlist = /^(alpha|dev|pr-.+)$/
|
|
110
108
|
|
|
111
109
|
// The stack's fully-resolved Pulumi config, read once. Reading it whole (rather
|
|
112
110
|
// than `pulumi config get <key>`, which exits non-zero for BOTH a missing key
|
|
@@ -114,21 +112,19 @@ let nameAllowlist = %re("/^(alpha|dev|pr-.+)$/")
|
|
|
114
112
|
// unreadable config is a pulumi/backend problem; an absent flag is a missing
|
|
115
113
|
// opt-in. `pulumi config --json` maps each key to `{value, secret}`.
|
|
116
114
|
let readStackConfig = (~projectDir, ~backend, ~stack): result<dict<JSON.t>, string> =>
|
|
117
|
-
switch (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
switch try Some(
|
|
116
|
+
ReventlessSeedAws.pulumi(~projectDir, ~backend, ["config", "--json", "--stack", stack]),
|
|
117
|
+
) catch {
|
|
118
|
+
| _ => None
|
|
119
|
+
} {
|
|
122
120
|
| None =>
|
|
123
121
|
Error(
|
|
124
122
|
`could not read the Pulumi config for stack "${stack}" — is pulumi logged in to the right backend, and is the stack deployed?`,
|
|
125
123
|
)
|
|
126
124
|
| Some(raw) =>
|
|
127
|
-
switch (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
) {
|
|
125
|
+
switch try Some(JSON.parseOrThrow(raw)) catch {
|
|
126
|
+
| _ => None
|
|
127
|
+
} {
|
|
132
128
|
| Some(Object(obj)) => Ok(obj)
|
|
133
129
|
| _ => Error(`could not parse \`pulumi config --json\` output for stack "${stack}".`)
|
|
134
130
|
}
|
|
@@ -304,8 +300,7 @@ let parseObjectStores = (json: option<JSON.t>): result<array<objectStore>, strin
|
|
|
304
300
|
Ok(Array.concat(stores, [{qualified, plugin, store, bucketName, keyPrefix}]))
|
|
305
301
|
| _ =>
|
|
306
302
|
Error(
|
|
307
|
-
`the platform stack's \`objectStores\` output has a malformed entry for "${qualified}" — ` ++
|
|
308
|
-
`expected a {plugin}.{store} key carrying bucketName and keyPrefix.`,
|
|
303
|
+
`the platform stack's \`objectStores\` output has a malformed entry for "${qualified}" — ` ++ `expected a {plugin}.{store} key carrying bucketName and keyPrefix.`,
|
|
309
304
|
)
|
|
310
305
|
}
|
|
311
306
|
}
|
|
@@ -329,8 +324,7 @@ let validateStores = (stores: array<objectStore>): result<unit, string> =>
|
|
|
329
324
|
switch stores->Array.find(s => s.keyPrefix == "" || s.store->String.includes("/")) {
|
|
330
325
|
| Some(s) =>
|
|
331
326
|
Error(
|
|
332
|
-
`store "${s.qualified}" has an unusable key prefix ("${s.keyPrefix}") — ` ++
|
|
333
|
-
`a store name may not be empty or contain "/".`,
|
|
327
|
+
`store "${s.qualified}" has an unusable key prefix ("${s.keyPrefix}") — ` ++ `a store name may not be empty or contain "/".`,
|
|
334
328
|
)
|
|
335
329
|
| None =>
|
|
336
330
|
switch stores->Array.findMap(a =>
|
|
@@ -341,8 +335,7 @@ let validateStores = (stores: array<objectStore>): result<unit, string> =>
|
|
|
341
335
|
Some(
|
|
342
336
|
`stores "${a.qualified}" and "${b.qualified}" both live at ` ++
|
|
343
337
|
`${a.bucketName}/${a.keyPrefix}/ — a prefix-scoped wipe cannot tell their objects ` ++
|
|
344
|
-
`apart. Rename one store, or qualify the \`@storageRef\` annotation if they were ` ++
|
|
345
|
-
`meant to be one shared store.`,
|
|
338
|
+
`apart. Rename one store, or qualify the \`@storageRef\` annotation if they were ` ++ `meant to be one shared store.`,
|
|
346
339
|
)
|
|
347
340
|
} else if b.keyPrefix->String.startsWith(a.keyPrefix ++ "/") {
|
|
348
341
|
Some(
|
|
@@ -386,8 +379,8 @@ let countBucket = async (bucket: string, ~prefix: option<string>=?): int => {
|
|
|
386
379
|
let out = await S3.ListObjectVersionsCommand.send(
|
|
387
380
|
S3.ListObjectVersionsCommand.make({
|
|
388
381
|
bucket,
|
|
389
|
-
|
|
390
|
-
|
|
382
|
+
?prefix,
|
|
383
|
+
?keyMarker,
|
|
391
384
|
versionIdMarker: ?versionMarker,
|
|
392
385
|
}),
|
|
393
386
|
)
|
|
@@ -415,7 +408,13 @@ let rec sendBatch = async (
|
|
|
415
408
|
): unit =>
|
|
416
409
|
if requests->Array.length > 0 {
|
|
417
410
|
if attempt > 8 {
|
|
418
|
-
throw(
|
|
411
|
+
throw(
|
|
412
|
+
Seed.Failed(
|
|
413
|
+
`table ${table}: ${requests
|
|
414
|
+
->Array.length
|
|
415
|
+
->Int.toString} item(s) still unprocessed after 8 retries.`,
|
|
416
|
+
),
|
|
417
|
+
)
|
|
419
418
|
}
|
|
420
419
|
let out = await Ddb.DocumentClient.BatchWriteCommand.send(
|
|
421
420
|
Ddb.DocumentClient.BatchWriteCommand.make({
|
|
@@ -456,7 +455,9 @@ let truncateTable = async (table: string): unit => {
|
|
|
456
455
|
->Option.getOr([])
|
|
457
456
|
->Array.map(item => {
|
|
458
457
|
let key =
|
|
459
|
-
keyAttrs
|
|
458
|
+
keyAttrs
|
|
459
|
+
->Array.filterMap(attr => item->field(attr)->Option.map(v => (attr, v)))
|
|
460
|
+
->Dict.fromArray
|
|
460
461
|
({deleteRequest: {key: key}}: Ddb.DocumentClient.BatchWriteCommand.writeRequest)
|
|
461
462
|
})
|
|
462
463
|
let batches = chunk(requests, Ddb.DocumentClient.BatchWriteCommand.maxBatchSize)
|
|
@@ -481,15 +482,15 @@ let emptyBucket = async (bucket: string, ~prefix: option<string>=?): unit => {
|
|
|
481
482
|
let out = await S3.ListObjectVersionsCommand.send(
|
|
482
483
|
S3.ListObjectVersionsCommand.make({
|
|
483
484
|
bucket,
|
|
484
|
-
|
|
485
|
-
|
|
485
|
+
?prefix,
|
|
486
|
+
?keyMarker,
|
|
486
487
|
versionIdMarker: ?versionMarker,
|
|
487
488
|
}),
|
|
488
489
|
)
|
|
489
490
|
let ids =
|
|
490
|
-
Array.concat(out.versions->Option.getOr([]), out.deleteMarkers->Option.getOr([]))->Array.map(
|
|
491
|
-
|
|
492
|
-
))
|
|
491
|
+
Array.concat(out.versions->Option.getOr([]), out.deleteMarkers->Option.getOr([]))->Array.map((
|
|
492
|
+
v
|
|
493
|
+
): S3.DeleteObjectsCommand.objectIdentifier => {key: v.key, versionId: v.versionId})
|
|
493
494
|
if ids->Array.length > 0 {
|
|
494
495
|
let res = await S3.DeleteObjectsCommand.send(
|
|
495
496
|
S3.DeleteObjectsCommand.make({
|
|
@@ -501,7 +502,7 @@ let emptyBucket = async (bucket: string, ~prefix: option<string>=?): unit => {
|
|
|
501
502
|
| Some(errs) if errs->Array.length > 0 =>
|
|
502
503
|
throw(
|
|
503
504
|
Seed.Failed(
|
|
504
|
-
`failed to delete ${
|
|
505
|
+
`failed to delete ${errs->Array.length->Int.toString} object(s) from ${bucket}: ${errs
|
|
505
506
|
->Array.get(0)
|
|
506
507
|
->Option.flatMap(e => e.message)
|
|
507
508
|
->Option.getOr("unknown")}`,
|
|
@@ -583,6 +584,7 @@ let chooseScope = async (~targets: array<target>): array<target> => {
|
|
|
583
584
|
let options = []
|
|
584
585
|
if domain->Array.length > 0 {
|
|
585
586
|
options->Array.push((`domain — ${labelsOf(domain)}`, domain))
|
|
587
|
+
|
|
586
588
|
// Single-plugin entries only when there is more than one, else they just
|
|
587
589
|
// duplicate the `domain` entry.
|
|
588
590
|
if domain->Array.length > 1 {
|
|
@@ -610,7 +612,9 @@ let gateTarget = (~target: target, ~backend, ~stack): string => {
|
|
|
610
612
|
| Ok(c) => c
|
|
611
613
|
| Error(message) => throw(Seed.Failed(message))
|
|
612
614
|
}
|
|
613
|
-
switch configValue(cfg, "reventless:wipeable")->Option.map(v =>
|
|
615
|
+
switch configValue(cfg, "reventless:wipeable")->Option.map(v =>
|
|
616
|
+
v->String.trim->String.toLowerCase
|
|
617
|
+
) {
|
|
614
618
|
| Some("true") => ()
|
|
615
619
|
| _ =>
|
|
616
620
|
throw(
|
|
@@ -666,13 +670,11 @@ let refillMessage = (~refilled: array<(string, int)>, ~heldCount: int): string =
|
|
|
666
670
|
->Array.join("")
|
|
667
671
|
let diagnosis = if heldCount > 0 {
|
|
668
672
|
`\n\n The wipe deleted these and something wrote them back. All ${heldCount->Int.toString} runtime(s) ` ++
|
|
669
|
-
`in scope were held at zero concurrency for the whole wipe, so the writer was an invocation already ` ++
|
|
670
|
-
`in flight when the hold took effect. Re-running clears it.`
|
|
673
|
+
`in scope were held at zero concurrency for the whole wipe, so the writer was an invocation already ` ++ `in flight when the hold took effect. Re-running clears it.`
|
|
671
674
|
} else {
|
|
672
675
|
`\n\n The wipe deleted these and a running runtime wrote them back. The stack was NOT held ` ++
|
|
673
676
|
`(SEED_RESET_NO_QUIESCE), so nothing stopped it, and re-running will not help: the writer keeps the ` ++
|
|
674
|
-
`state in memory and restores it on every invocation, identical each time. Re-run without ` ++
|
|
675
|
-
`SEED_RESET_NO_QUIESCE so the runtimes are held and recycled.`
|
|
677
|
+
`state in memory and restores it on every invocation, identical each time. Re-run without ` ++ `SEED_RESET_NO_QUIESCE so the runtimes are held and recycled.`
|
|
676
678
|
}
|
|
677
679
|
`${total->Int.toString} item(s)/object(s) came back after the wipe:${lines}${diagnosis}`
|
|
678
680
|
}
|
|
@@ -700,6 +702,7 @@ let reportAll = (resolvedList: array<resolved>, ~stack, ~region): int => {
|
|
|
700
702
|
if r.bucketCounts->Array.length == 0 {
|
|
701
703
|
Console.log(" (none)")
|
|
702
704
|
}
|
|
705
|
+
|
|
703
706
|
// Declared stores get their own section rather than being folded in with the
|
|
704
707
|
// plain buckets: the unit is a prefix inside a bucket that other plugins also
|
|
705
708
|
// write to, and the operator needs to see which is which before confirming.
|
|
@@ -770,8 +773,7 @@ let run = (~stack=?, ~backend=?, ~targets: array<target>, ()): unit => {
|
|
|
770
773
|
// this resolution exists to fix.
|
|
771
774
|
Console.log("")
|
|
772
775
|
Console.log(
|
|
773
|
-
"Note: no `platform` target is declared, so declared object stores could not be " ++
|
|
774
|
-
"resolved — any uploaded objects will be left in place.",
|
|
776
|
+
"Note: no `platform` target is declared, so declared object stores could not be " ++ "resolved — any uploaded objects will be left in place.",
|
|
775
777
|
)
|
|
776
778
|
[]
|
|
777
779
|
| Some(pt) =>
|
|
@@ -910,8 +912,7 @@ let run = (~stack=?, ~backend=?, ~targets: array<target>, ()): unit => {
|
|
|
910
912
|
let interactive = NodeProcess.stdin->NodeProcess.isTTY->Option.getOr(false)
|
|
911
913
|
let confirmed = if interactive {
|
|
912
914
|
let typed = await Seed.Prompt.ask(
|
|
913
|
-
`About to permanently empty ${total->Int.toString} item(s)/object(s) across the selected scope of "${stack}". ` ++
|
|
914
|
-
`Type the stack name to confirm, or press Enter to keep this a dry run: `,
|
|
915
|
+
`About to permanently empty ${total->Int.toString} item(s)/object(s) across the selected scope of "${stack}". ` ++ `Type the stack name to confirm, or press Enter to keep this a dry run: `,
|
|
915
916
|
)
|
|
916
917
|
typed->String.trim == stack
|
|
917
918
|
} else {
|
|
@@ -932,9 +933,10 @@ let run = (~stack=?, ~backend=?, ~targets: array<target>, ()): unit => {
|
|
|
932
933
|
// durable while the runtimes that own the data are running — see
|
|
933
934
|
// ReventlessSeedAws_Quiesce for the mechanism and for why emptying the
|
|
934
935
|
// upstream stores first does not substitute for this.
|
|
935
|
-
let functionNames =
|
|
936
|
-
Array.
|
|
937
|
-
|
|
936
|
+
let functionNames =
|
|
937
|
+
resolvedList->Array.reduce([], (acc, r) =>
|
|
938
|
+
Array.concat(acc, r.functions->Array.filter(f => !(acc->Array.includes(f))))
|
|
939
|
+
)
|
|
938
940
|
let quiesce = !noQuiesce() && functionNames->Array.length > 0
|
|
939
941
|
let lambdaClient = Lambda.client(~region, ())
|
|
940
942
|
let held = if quiesce {
|
|
@@ -946,8 +948,7 @@ let run = (~stack=?, ~backend=?, ~targets: array<target>, ()): unit => {
|
|
|
946
948
|
if functionNames->Array.length > 0 {
|
|
947
949
|
Console.log("")
|
|
948
950
|
Console.log(
|
|
949
|
-
"Note: SEED_RESET_NO_QUIESCE is set — the stack's runtimes keep running through the wipe. " ++
|
|
950
|
-
"A runtime that holds state across invocations can write it straight back over an emptied store.",
|
|
951
|
+
"Note: SEED_RESET_NO_QUIESCE is set — the stack's runtimes keep running through the wipe. " ++ "A runtime that holds state across invocations can write it straight back over an emptied store.",
|
|
951
952
|
)
|
|
952
953
|
}
|
|
953
954
|
[]
|
|
@@ -1078,7 +1079,9 @@ let run = (~stack=?, ~backend=?, ~targets: array<target>, ()): unit => {
|
|
|
1078
1079
|
}
|
|
1079
1080
|
|
|
1080
1081
|
Console.log("")
|
|
1081
|
-
Console.log(
|
|
1082
|
+
Console.log(
|
|
1083
|
+
`Reset complete — the selected scope of "${stack}" reads empty and is re-seedable.`,
|
|
1084
|
+
)
|
|
1082
1085
|
NodeProcess.exit(0)
|
|
1083
1086
|
} catch {
|
|
1084
1087
|
| Seed.Failed(message) =>
|
|
@@ -1090,7 +1093,9 @@ let run = (~stack=?, ~backend=?, ~targets: array<target>, ()): unit => {
|
|
|
1090
1093
|
Seed.Prompt.close()
|
|
1091
1094
|
Console.error("")
|
|
1092
1095
|
Console.error("Reset aborted with an unexpected error:")
|
|
1093
|
-
Console.error(
|
|
1096
|
+
Console.error(
|
|
1097
|
+
exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown"),
|
|
1098
|
+
)
|
|
1094
1099
|
NodeProcess.exit(1)
|
|
1095
1100
|
}
|
|
1096
1101
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-seed-aws",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.29",
|
|
4
4
|
"description": "AWS connect + guarded reset for the Reventless seed harness — stack discovery, Cognito login, and a tag-scoped, allowlisted store wipe",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.
|
|
8
|
-
"@reventlessdev/
|
|
9
|
-
"@reventlessdev/
|
|
7
|
+
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.17",
|
|
8
|
+
"@reventlessdev/reventless-seed": "1.0.0-alpha.21",
|
|
9
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.11"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"rescript": "12.3.0",
|
|
@@ -14,7 +14,6 @@ open ReventlessSeed
|
|
|
14
14
|
|
|
15
15
|
// ── Node bindings ─────────────────────────────────────────────────────────────
|
|
16
16
|
|
|
17
|
-
|
|
18
17
|
type fetchInit = {method: string, headers: dict<string>, body?: string}
|
|
19
18
|
type response
|
|
20
19
|
@val external fetch: (string, fetchInit) => promise<response> = "fetch"
|
|
@@ -77,12 +76,15 @@ let deployedStacks = (~projectDir: string, ~backend: option<string>): array<stri
|
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
let stackOutputs = (~projectDir: string, ~backend: option<string>, stack: string): JSON.t => {
|
|
80
|
-
let raw = try pulumi(
|
|
79
|
+
let raw = try pulumi(
|
|
80
|
+
~projectDir,
|
|
81
|
+
~backend,
|
|
82
|
+
["stack", "output", "--stack", stack, "--json"],
|
|
83
|
+
) catch {
|
|
81
84
|
| _ =>
|
|
82
85
|
throw(
|
|
83
86
|
Seed.Failed(
|
|
84
|
-
`pulumi stack output --stack ${stack} failed — is pulumi installed, logged in, ` ++
|
|
85
|
-
"and the stack deployed?",
|
|
87
|
+
`pulumi stack output --stack ${stack} failed — is pulumi installed, logged in, ` ++ "and the stack deployed?",
|
|
86
88
|
),
|
|
87
89
|
)
|
|
88
90
|
}
|
|
@@ -97,7 +99,11 @@ let backendNote = (~backend: option<string>): string =>
|
|
|
97
99
|
| None => ""
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
let resolveStack = async (
|
|
102
|
+
let resolveStack = async (
|
|
103
|
+
~projectDir: string,
|
|
104
|
+
~backend: option<string>,
|
|
105
|
+
~stack: option<string>,
|
|
106
|
+
): string =>
|
|
101
107
|
switch stack {
|
|
102
108
|
| Some(s) => s
|
|
103
109
|
| None =>
|
|
@@ -111,8 +117,7 @@ let resolveStack = async (~projectDir: string, ~backend: option<string>, ~stack:
|
|
|
111
117
|
`no deployed Pulumi stacks for this project (\`pulumi stack ls\` is empty in ${projectDir})${backendNote(
|
|
112
118
|
~backend,
|
|
113
119
|
)}. ` ++
|
|
114
|
-
"A Pulumi.<stack>.yaml config file alone is not a deployed stack — run `pulumi up` " ++
|
|
115
|
-
"first, log in to the backend that holds the stack, or set SEED_STACK to target one.",
|
|
120
|
+
"A Pulumi.<stack>.yaml config file alone is not a deployed stack — run `pulumi up` " ++ "first, log in to the backend that holds the stack, or set SEED_STACK to target one.",
|
|
116
121
|
),
|
|
117
122
|
)
|
|
118
123
|
}
|
|
@@ -123,13 +128,13 @@ let resolveStack = async (~projectDir: string, ~backend: option<string>, ~stack:
|
|
|
123
128
|
// ── Endpoints ─────────────────────────────────────────────────────────────────
|
|
124
129
|
|
|
125
130
|
let fetchConfig = async (hostShellUrl: string): JSON.t => {
|
|
126
|
-
let base = hostShellUrl->String.replaceRegExp(
|
|
131
|
+
let base = hostShellUrl->String.replaceRegExp(/\/+$/g, "")
|
|
127
132
|
let url = `${base}/config.json`
|
|
128
133
|
let res = try await fetch(url, {method: "GET", headers: Dict.make()}) catch {
|
|
129
134
|
| _ => throw(Seed.Failed(`cannot reach ${url}`))
|
|
130
135
|
}
|
|
131
136
|
if !(res->responseOk) {
|
|
132
|
-
throw(Seed.Failed(`GET ${url} → HTTP ${
|
|
137
|
+
throw(Seed.Failed(`GET ${url} → HTTP ${res->responseStatus->Int.toString}`))
|
|
133
138
|
}
|
|
134
139
|
await res->responseJson
|
|
135
140
|
}
|
|
@@ -232,64 +237,110 @@ let resolveEndpoints = async (
|
|
|
232
237
|
|
|
233
238
|
// ── Cognito login ─────────────────────────────────────────────────────────────
|
|
234
239
|
|
|
235
|
-
let cognito = (~region: string, ~clientId: string) =>
|
|
236
|
-
~username: string,
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
]),
|
|
240
|
+
let cognito = (~region: string, ~clientId: string) =>
|
|
241
|
+
async (~username: string, ~password: string): string => {
|
|
242
|
+
let body = JSON.stringify(
|
|
243
|
+
JSON.Encode.object(
|
|
244
|
+
Dict.fromArray([
|
|
245
|
+
("AuthFlow", JSON.Encode.string("USER_PASSWORD_AUTH")),
|
|
246
|
+
("ClientId", JSON.Encode.string(clientId)),
|
|
247
|
+
(
|
|
248
|
+
"AuthParameters",
|
|
249
|
+
JSON.Encode.object(
|
|
250
|
+
Dict.fromArray([
|
|
251
|
+
("USERNAME", JSON.Encode.string(username)),
|
|
252
|
+
("PASSWORD", JSON.Encode.string(password)),
|
|
253
|
+
]),
|
|
254
|
+
),
|
|
251
255
|
),
|
|
252
|
-
),
|
|
253
|
-
]),
|
|
254
|
-
),
|
|
255
|
-
)
|
|
256
|
-
let headers = Dict.fromArray([
|
|
257
|
-
("content-type", "application/x-amz-json-1.1"),
|
|
258
|
-
("x-amz-target", "AWSCognitoIdentityProviderService.InitiateAuth"),
|
|
259
|
-
])
|
|
260
|
-
let res = try await fetch(`https://cognito-idp.${region}.amazonaws.com/`, {
|
|
261
|
-
method: "POST",
|
|
262
|
-
headers,
|
|
263
|
-
body,
|
|
264
|
-
}) catch {
|
|
265
|
-
| _ => throw(Seed.Failed(`cannot reach Cognito in ${region}`))
|
|
266
|
-
}
|
|
267
|
-
let json = await res->responseJson
|
|
268
|
-
if !(res->responseOk) {
|
|
269
|
-
let detail =
|
|
270
|
-
json->field("message")->Option.flatMap(asString)->Option.getOr(JSON.stringify(json))
|
|
271
|
-
throw(
|
|
272
|
-
Seed.Failed(
|
|
273
|
-
`Cognito InitiateAuth failed (HTTP ${(res->responseStatus)->Int.toString}): ${detail}`,
|
|
256
|
+
]),
|
|
274
257
|
),
|
|
275
258
|
)
|
|
259
|
+
let headers = Dict.fromArray([
|
|
260
|
+
("content-type", "application/x-amz-json-1.1"),
|
|
261
|
+
("x-amz-target", "AWSCognitoIdentityProviderService.InitiateAuth"),
|
|
262
|
+
])
|
|
263
|
+
let res = try await fetch(
|
|
264
|
+
`https://cognito-idp.${region}.amazonaws.com/`,
|
|
265
|
+
{
|
|
266
|
+
method: "POST",
|
|
267
|
+
headers,
|
|
268
|
+
body,
|
|
269
|
+
},
|
|
270
|
+
) catch {
|
|
271
|
+
| _ => throw(Seed.Failed(`cannot reach Cognito in ${region}`))
|
|
272
|
+
}
|
|
273
|
+
let json = await res->responseJson
|
|
274
|
+
if !(res->responseOk) {
|
|
275
|
+
let detail =
|
|
276
|
+
json->field("message")->Option.flatMap(asString)->Option.getOr(JSON.stringify(json))
|
|
277
|
+
throw(
|
|
278
|
+
Seed.Failed(
|
|
279
|
+
`Cognito InitiateAuth failed (HTTP ${res->responseStatus->Int.toString}): ${detail}`,
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
switch json->field("ChallengeName")->Option.flatMap(asString) {
|
|
284
|
+
| Some(challenge) =>
|
|
285
|
+
throw(
|
|
286
|
+
Seed.Failed(
|
|
287
|
+
`Cognito returned challenge ${challenge} — set a permanent password first ` ++ "(aws cognito-idp admin-set-user-password … --permanent).",
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
| None => ()
|
|
291
|
+
}
|
|
292
|
+
switch json
|
|
293
|
+
->field("AuthenticationResult")
|
|
294
|
+
->Option.flatMap(ar => ar->field("IdToken"))
|
|
295
|
+
->Option.flatMap(asString) {
|
|
296
|
+
| Some(token) => token
|
|
297
|
+
| None => throw(Seed.Failed(`Cognito response carried no IdToken: ${JSON.stringify(json)}`))
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Active role ───────────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The Cognito path's active-role door: `Platform_SetActiveRole` on the domain
|
|
305
|
+
* API, which is where the seed already sends everything else.
|
|
306
|
+
*
|
|
307
|
+
* It sits on the domain base document at `AllowAuthenticated`, deliberately not
|
|
308
|
+
* behind the Admin gate — the whole point is that a caller can drop into a
|
|
309
|
+
* narrower role — so a token already narrowed to `Shopper` can still call this
|
|
310
|
+
* to widen back out. There is no chicken-and-egg, and no `sub` argument: the
|
|
311
|
+
* mutation addresses the authorizer-verified caller's own row and nobody else's.
|
|
312
|
+
*
|
|
313
|
+
* Returns `None` always: the choice is a stored row, read by the pool's
|
|
314
|
+
* pre-token-generation trigger on the NEXT login, so the current bearer is
|
|
315
|
+
* unchanged by it and the caller must re-authenticate to see any effect.
|
|
316
|
+
*
|
|
317
|
+
* Split-API only. A unified-mode deployment's single API does not carry the
|
|
318
|
+
* field, so a refusal naming it is reported as "this deployment offers no role
|
|
319
|
+
* switch" rather than as a seeding failure.
|
|
320
|
+
*/
|
|
321
|
+
let setActiveRole: Seed.Connect.roleSwitch = async (~client, ~role) => {
|
|
322
|
+
let argument = switch role {
|
|
323
|
+
| None => "null"
|
|
324
|
+
| Some(r) => JSON.stringify(JSON.Encode.string(r))
|
|
276
325
|
}
|
|
277
|
-
|
|
278
|
-
|
|
326
|
+
try {
|
|
327
|
+
let _ = await Seed.Client.gql(
|
|
328
|
+
client,
|
|
329
|
+
~query=`mutation { Platform_SetActiveRole(activeRole: ${argument}) { activeRole availableRoles } }`,
|
|
330
|
+
~label="Platform_SetActiveRole",
|
|
331
|
+
)
|
|
332
|
+
None
|
|
333
|
+
} catch {
|
|
334
|
+
| Seed.Failed(message) =>
|
|
279
335
|
throw(
|
|
280
336
|
Seed.Failed(
|
|
281
|
-
|
|
282
|
-
|
|
337
|
+
message->String.includes("Platform_SetActiveRole")
|
|
338
|
+
? `this deployment offers no role switch — \`Platform_SetActiveRole\` is a split-API ` ++
|
|
339
|
+
`field, and a unified-mode API does not carry it. Change the role in the host shell ` ++
|
|
340
|
+
`instead.\n ${message}`
|
|
341
|
+
: message,
|
|
283
342
|
),
|
|
284
343
|
)
|
|
285
|
-
| None => ()
|
|
286
|
-
}
|
|
287
|
-
switch json
|
|
288
|
-
->field("AuthenticationResult")
|
|
289
|
-
->Option.flatMap(ar => ar->field("IdToken"))
|
|
290
|
-
->Option.flatMap(asString) {
|
|
291
|
-
| Some(token) => token
|
|
292
|
-
| None => throw(Seed.Failed(`Cognito response carried no IdToken: ${JSON.stringify(json)}`))
|
|
293
344
|
}
|
|
294
345
|
}
|
|
295
346
|
|
|
@@ -324,5 +375,6 @@ let connect = (~projectDir: string=".", ~stack=?, ~backend=?, ()): (
|
|
|
324
375
|
~label=stackName,
|
|
325
376
|
~endpoint=eps.graphql,
|
|
326
377
|
~login=cognito(~region=eps.cognitoRegion, ~clientId=eps.cognitoClientId),
|
|
378
|
+
~roleSwitch=setActiveRole,
|
|
327
379
|
)
|
|
328
380
|
}
|
|
@@ -4,6 +4,8 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
|
4
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
5
|
import * as Nodechild_process from "node:child_process";
|
|
6
6
|
import * as Seed$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed.res.mjs";
|
|
7
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
8
|
+
import * as Seed_Client$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed_Client.res.mjs";
|
|
7
9
|
import * as Seed_Prompt$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed_Prompt.res.mjs";
|
|
8
10
|
import * as Seed_Connect$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed_Connect.res.mjs";
|
|
9
11
|
|
|
@@ -279,6 +281,25 @@ function cognito(region, clientId) {
|
|
|
279
281
|
};
|
|
280
282
|
}
|
|
281
283
|
|
|
284
|
+
async function setActiveRole(client, role) {
|
|
285
|
+
let argument = role !== undefined ? JSON.stringify(role) : "null";
|
|
286
|
+
try {
|
|
287
|
+
await Seed_Client$ReventlessSeed.gql(client, `mutation { Platform_SetActiveRole(activeRole: ` + argument + `) { activeRole availableRoles } }`, "Platform_SetActiveRole");
|
|
288
|
+
return;
|
|
289
|
+
} catch (raw_message) {
|
|
290
|
+
let message = Primitive_exceptions.internalToException(raw_message);
|
|
291
|
+
if (message.RE_EXN_ID === Seed$ReventlessSeed.Failed) {
|
|
292
|
+
let message$1 = message._1;
|
|
293
|
+
throw {
|
|
294
|
+
RE_EXN_ID: Seed$ReventlessSeed.Failed,
|
|
295
|
+
_1: message$1.includes("Platform_SetActiveRole") ? `this deployment offers no role switch — \`Platform_SetActiveRole\` is a split-API field, and a unified-mode API does not carry it. Change the role in the host shell instead.\n ` + message$1 : message$1,
|
|
296
|
+
Error: new Error()
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
throw message;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
282
303
|
function connect($staropt$star, stack, backend, param) {
|
|
283
304
|
return async () => {
|
|
284
305
|
let projectDir = $staropt$star !== undefined ? $staropt$star : ".";
|
|
@@ -286,7 +307,7 @@ function connect($staropt$star, stack, backend, param) {
|
|
|
286
307
|
let backend$1 = url !== undefined ? url : backend;
|
|
287
308
|
let stackName = await resolveStack(projectDir, backend$1, stack);
|
|
288
309
|
let eps = await resolveEndpoints(projectDir, backend$1, stackName);
|
|
289
|
-
return await Seed_Connect$ReventlessSeed.make(stackName, eps.graphql, cognito(eps.cognitoRegion, eps.cognitoClientId), undefined);
|
|
310
|
+
return await Seed_Connect$ReventlessSeed.make(stackName, eps.graphql, cognito(eps.cognitoRegion, eps.cognitoClientId), setActiveRole, undefined);
|
|
290
311
|
};
|
|
291
312
|
}
|
|
292
313
|
|
|
@@ -304,6 +325,7 @@ export {
|
|
|
304
325
|
endpointsFrom,
|
|
305
326
|
resolveEndpoints,
|
|
306
327
|
cognito,
|
|
328
|
+
setActiveRole,
|
|
307
329
|
connect,
|
|
308
330
|
}
|
|
309
331
|
/* node:child_process Not a pure module */
|
|
@@ -153,8 +153,9 @@ let restoreConcurrency = async (~client, ~h: held): option<string> =>
|
|
|
153
153
|
| () => None
|
|
154
154
|
| exception exn =>
|
|
155
155
|
Some(
|
|
156
|
-
`LEFT SWITCHED OFF: could not restore concurrency on ${h.functionName} (${errorText(
|
|
157
|
-
|
|
156
|
+
`LEFT SWITCHED OFF: could not restore concurrency on ${h.functionName} (${errorText(
|
|
157
|
+
exn,
|
|
158
|
+
)}). ` ++ `It is still reserved at 0 and will not run until that is undone.`,
|
|
158
159
|
)
|
|
159
160
|
}
|
|
160
161
|
|
|
@@ -181,7 +182,9 @@ let release = async (~client, ~held: array<held>): array<string> => {
|
|
|
181
182
|
| () => None
|
|
182
183
|
| exception exn =>
|
|
183
184
|
Some(
|
|
184
|
-
`could not restore the environment of ${h.functionName} (${errorText(
|
|
185
|
+
`could not restore the environment of ${h.functionName} (${errorText(
|
|
186
|
+
exn,
|
|
187
|
+
)}) — it may still ` ++
|
|
185
188
|
`carry ${markerKey}, which is inert and is removed by the next deploy.`,
|
|
186
189
|
)
|
|
187
190
|
}
|
|
@@ -207,8 +210,9 @@ let recycle = async (~client, ~held: array<held>): array<string> => {
|
|
|
207
210
|
| () => None
|
|
208
211
|
| exception exn =>
|
|
209
212
|
Some(
|
|
210
|
-
`could not recycle ${h.functionName} (${errorText(
|
|
211
|
-
|
|
213
|
+
`could not recycle ${h.functionName} (${errorText(
|
|
214
|
+
exn,
|
|
215
|
+
)}) — a warm container may still hold ` ++ `pre-wipe state and write it back. Redeploy or update that function to clear it.`,
|
|
212
216
|
)
|
|
213
217
|
}
|
|
214
218
|
})
|
|
@@ -283,12 +287,10 @@ let hold = async (~client, ~functionNames: array<string>): array<held> => {
|
|
|
283
287
|
let advice = if reason->isThrottle {
|
|
284
288
|
`The Lambda control plane throttled the hold. That budget is per account and region and is ` ++
|
|
285
289
|
`shared with anything else using it — a deploy, an inspector sync, another stack in the same ` ++
|
|
286
|
-
`region — so this is transient and nothing is wrong with the credentials. Wait for whatever ` ++
|
|
287
|
-
`else is running to finish and re-run.`
|
|
290
|
+
`region — so this is transient and nothing is wrong with the credentials. Wait for whatever ` ++ `else is running to finish and re-run.`
|
|
288
291
|
} else {
|
|
289
292
|
`The reset holds every runtime in scope for the length of the wipe, which needs ` ++
|
|
290
|
-
`lambda:GetFunctionConcurrency, lambda:GetFunctionConfiguration, lambda:PutFunctionConcurrency, ` ++
|
|
291
|
-
`lambda:DeleteFunctionConcurrency and lambda:UpdateFunctionConfiguration.`
|
|
293
|
+
`lambda:GetFunctionConcurrency, lambda:GetFunctionConfiguration, lambda:PutFunctionConcurrency, ` ++ `lambda:DeleteFunctionConcurrency and lambda:UpdateFunctionConfiguration.`
|
|
292
294
|
}
|
|
293
295
|
throw(
|
|
294
296
|
ReventlessSeed.Seed.Failed(
|