@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
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -45,7 +45,9 @@ describe("ReventlessSeedAws_Reconcile.collectRefs", () => {
|
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
testSync("finds a ref nested inside a record", () =>
|
|
48
|
-
expect(collect(obj([("variant", obj([("photo", JSON.Encode.string(refIn))]))])))->toEqual([
|
|
48
|
+
expect(collect(obj([("variant", obj([("photo", JSON.Encode.string(refIn))]))])))->toEqual([
|
|
49
|
+
refIn,
|
|
50
|
+
])
|
|
49
51
|
)
|
|
50
52
|
|
|
51
53
|
// Store isolation: one bucket holds several stores under distinct prefixes, so
|
|
@@ -75,10 +77,7 @@ describe("ReventlessSeedAws_Reconcile.collectRefs", () => {
|
|
|
75
77
|
)
|
|
76
78
|
|
|
77
79
|
testSync("collects each distinct ref once", () => {
|
|
78
|
-
let json = obj([
|
|
79
|
-
("a", JSON.Encode.string(refIn)),
|
|
80
|
-
("b", JSON.Encode.string(refIn)),
|
|
81
|
-
])
|
|
80
|
+
let json = obj([("a", JSON.Encode.string(refIn)), ("b", JSON.Encode.string(refIn))])
|
|
82
81
|
expect(collect(json)->Array.length)->toBe(1)
|
|
83
82
|
})
|
|
84
83
|
})
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -28,7 +28,9 @@ let store = (~qualified, ~bucket, ~prefix): Reset.objectStore => {
|
|
|
28
28
|
describe("parseObjectStores", () => {
|
|
29
29
|
testSync("splits the qualified key into plugin and store", () =>
|
|
30
30
|
switch Reset.parseObjectStores(
|
|
31
|
-
output([
|
|
31
|
+
output([
|
|
32
|
+
("Catalog.productImages", storeEntry(~bucket="alpha-stores", ~prefix="productImages")),
|
|
33
|
+
]),
|
|
32
34
|
) {
|
|
33
35
|
| Ok([s]) =>
|
|
34
36
|
expect((s.plugin, s.store, s.bucketName, s.keyPrefix))->toEqual((
|
|
@@ -61,7 +63,9 @@ describe("parseObjectStores", () => {
|
|
|
61
63
|
// A store the reset cannot read is a store it would silently leave behind, so
|
|
62
64
|
// a malformed entry fails the run rather than being skipped.
|
|
63
65
|
testSync("a malformed entry is an error, not a skip", () =>
|
|
64
|
-
switch Reset.parseObjectStores(
|
|
66
|
+
switch Reset.parseObjectStores(
|
|
67
|
+
output([("Catalog.productImages", obj([("keyPrefix", str("x"))]))]),
|
|
68
|
+
) {
|
|
65
69
|
| Ok(_) => fail("expected a malformed entry to be refused")
|
|
66
70
|
| Error(message) => expect(message->String.includes("Catalog.productImages"))->toBe(true)
|
|
67
71
|
}
|
|
@@ -108,7 +112,11 @@ describe("validateStores", () => {
|
|
|
108
112
|
testSync("an enclosing prefix is refused", () =>
|
|
109
113
|
switch Reset.validateStores([
|
|
110
114
|
store(~qualified="Legacy.catalog", ~bucket="alpha-stores", ~prefix="Catalog"),
|
|
111
|
-
store(
|
|
115
|
+
store(
|
|
116
|
+
~qualified="Catalog.productImages",
|
|
117
|
+
~bucket="alpha-stores",
|
|
118
|
+
~prefix="Catalog/productImages",
|
|
119
|
+
),
|
|
112
120
|
]) {
|
|
113
121
|
| Ok() => fail("expected an enclosing prefix to be refused")
|
|
114
122
|
| Error(message) => expect(message->String.includes("encloses"))->toBe(true)
|
|
@@ -120,8 +128,16 @@ describe("validateStores", () => {
|
|
|
120
128
|
testSync("one prefix in two different buckets is fine", () =>
|
|
121
129
|
expect(
|
|
122
130
|
Reset.validateStores([
|
|
123
|
-
store(
|
|
124
|
-
|
|
131
|
+
store(
|
|
132
|
+
~qualified="Catalog.productImages",
|
|
133
|
+
~bucket="catalog-productImages",
|
|
134
|
+
~prefix="productImages",
|
|
135
|
+
),
|
|
136
|
+
store(
|
|
137
|
+
~qualified="Ordering.productImages",
|
|
138
|
+
~bucket="ordering-productImages",
|
|
139
|
+
~prefix="productImages",
|
|
140
|
+
),
|
|
125
141
|
]),
|
|
126
142
|
)->toEqual(Ok())
|
|
127
143
|
)
|
|
@@ -160,7 +176,12 @@ describe("pluginOf", () => {
|
|
|
160
176
|
// never has to guess the relation between the two.
|
|
161
177
|
testSync("prefers the declared plugin name over the label", () =>
|
|
162
178
|
expect(
|
|
163
|
-
Reset.pluginOf({
|
|
179
|
+
Reset.pluginOf({
|
|
180
|
+
projectDir: "../catalog-aws",
|
|
181
|
+
label: "catalog",
|
|
182
|
+
group: Domain,
|
|
183
|
+
plugin: "Catalog",
|
|
184
|
+
}),
|
|
164
185
|
)->toBe("Catalog")
|
|
165
186
|
)
|
|
166
187
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -20,29 +20,33 @@ module Quiesce = ReventlessSeedAws_Quiesce
|
|
|
20
20
|
|
|
21
21
|
describe("classify", () => {
|
|
22
22
|
testSync("reads the function name out of a Lambda ARN", () =>
|
|
23
|
-
expect(
|
|
24
|
-
|
|
23
|
+
expect(
|
|
24
|
+
Reset.classify("arn:aws:lambda:eu-west-1:123456789012:function:AllAutomationSlices-28b116c"),
|
|
25
|
+
)->toEqual(Reset.Function("AllAutomationSlices-28b116c"))
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
// The tagging API returns unqualified ARNs, but a qualified one must still
|
|
28
29
|
// yield the function name — every Lambda control-plane call the hold makes
|
|
29
30
|
// takes a name, and "Name:1" is not one.
|
|
30
31
|
testSync("drops a version or alias qualifier", () =>
|
|
31
|
-
expect(
|
|
32
|
-
|
|
32
|
+
expect(
|
|
33
|
+
Reset.classify("arn:aws:lambda:eu-west-1:123456789012:function:AllReadModels-0287438:42"),
|
|
34
|
+
)->toEqual(Reset.Function("AllReadModels-0287438"))
|
|
33
35
|
)
|
|
34
36
|
|
|
35
37
|
testSync("still classifies tables and buckets", () => {
|
|
36
|
-
expect(
|
|
37
|
-
|
|
38
|
+
expect(
|
|
39
|
+
Reset.classify("arn:aws:dynamodb:eu-west-1:123456789012:table/Products-0ba6849"),
|
|
40
|
+
)->toEqual(Reset.Table("Products-0ba6849"))
|
|
38
41
|
expect(Reset.classify("arn:aws:s3:::alpha-product-images"))->toEqual(
|
|
39
42
|
Reset.Bucket("alpha-product-images"),
|
|
40
43
|
)
|
|
41
44
|
})
|
|
42
45
|
|
|
43
46
|
testSync("ignores anything else the tag scope returns", () =>
|
|
44
|
-
expect(
|
|
45
|
-
|
|
47
|
+
expect(
|
|
48
|
+
Reset.classify("arn:aws:sqs:eu-west-1:123456789012:OrderingDcbCmdTopic-4b3658c"),
|
|
49
|
+
)->toEqual(Reset.Other)
|
|
46
50
|
)
|
|
47
51
|
})
|
|
48
52
|
|
|
@@ -52,22 +56,28 @@ describe("mapBounded", () => {
|
|
|
52
56
|
// shuffled array would restore the wrong concurrency to the wrong function.
|
|
53
57
|
test("keeps results in input order under out-of-order completion", async () => {
|
|
54
58
|
let delays = [40, 0, 30, 10, 20, 5]
|
|
55
|
-
let out = await delays->Quiesce.mapBounded(
|
|
56
|
-
|
|
57
|
-
ms
|
|
58
|
-
|
|
59
|
+
let out = await delays->Quiesce.mapBounded(
|
|
60
|
+
~limit=3,
|
|
61
|
+
async ms => {
|
|
62
|
+
await ReventlessSeed.Seed.Client.sleep(ms)
|
|
63
|
+
ms
|
|
64
|
+
},
|
|
65
|
+
)
|
|
59
66
|
expect(out)->toEqual(delays)
|
|
60
67
|
})
|
|
61
68
|
|
|
62
69
|
test("never runs more than the limit at once", async () => {
|
|
63
70
|
let inFlight = ref(0)
|
|
64
71
|
let peak = ref(0)
|
|
65
|
-
let _ = await Array.make(~length=12, 0)->Quiesce.mapBounded(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
let _ = await Array.make(~length=12, 0)->Quiesce.mapBounded(
|
|
73
|
+
~limit=4,
|
|
74
|
+
async _ => {
|
|
75
|
+
inFlight := inFlight.contents + 1
|
|
76
|
+
peak := Math.Int.max(peak.contents, inFlight.contents)
|
|
77
|
+
await ReventlessSeed.Seed.Client.sleep(5)
|
|
78
|
+
inFlight := inFlight.contents - 1
|
|
79
|
+
},
|
|
80
|
+
)
|
|
71
81
|
expect(peak.contents)->toBe(4)
|
|
72
82
|
})
|
|
73
83
|
|
|
@@ -148,10 +158,12 @@ describe("noQuiesce", () => {
|
|
|
148
158
|
})
|
|
149
159
|
|
|
150
160
|
testSync("accepts the spellings an operator would actually type", () => {
|
|
151
|
-
["1", "true", "TRUE", " yes "]->Array.forEach(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
161
|
+
["1", "true", "TRUE", " yes "]->Array.forEach(
|
|
162
|
+
v => {
|
|
163
|
+
set(v)
|
|
164
|
+
expect(Reset.noQuiesce())->toBe(true)
|
|
165
|
+
},
|
|
166
|
+
)
|
|
155
167
|
set("")
|
|
156
168
|
})
|
|
157
169
|
})
|
package/lib/ocaml/.compiler.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#Start(
|
|
2
|
-
#Done(
|
|
1
|
+
#Start(1788992686668)
|
|
2
|
+
#Done(1788992686721)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -45,7 +45,9 @@ describe("ReventlessSeedAws_Reconcile.collectRefs", () => {
|
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
testSync("finds a ref nested inside a record", () =>
|
|
48
|
-
expect(collect(obj([("variant", obj([("photo", JSON.Encode.string(refIn))]))])))->toEqual([
|
|
48
|
+
expect(collect(obj([("variant", obj([("photo", JSON.Encode.string(refIn))]))])))->toEqual([
|
|
49
|
+
refIn,
|
|
50
|
+
])
|
|
49
51
|
)
|
|
50
52
|
|
|
51
53
|
// Store isolation: one bucket holds several stores under distinct prefixes, so
|
|
@@ -75,10 +77,7 @@ describe("ReventlessSeedAws_Reconcile.collectRefs", () => {
|
|
|
75
77
|
)
|
|
76
78
|
|
|
77
79
|
testSync("collects each distinct ref once", () => {
|
|
78
|
-
let json = obj([
|
|
79
|
-
("a", JSON.Encode.string(refIn)),
|
|
80
|
-
("b", JSON.Encode.string(refIn)),
|
|
81
|
-
])
|
|
80
|
+
let json = obj([("a", JSON.Encode.string(refIn)), ("b", JSON.Encode.string(refIn))])
|
|
82
81
|
expect(collect(json)->Array.length)->toBe(1)
|
|
83
82
|
})
|
|
84
83
|
})
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -28,7 +28,9 @@ let store = (~qualified, ~bucket, ~prefix): Reset.objectStore => {
|
|
|
28
28
|
describe("parseObjectStores", () => {
|
|
29
29
|
testSync("splits the qualified key into plugin and store", () =>
|
|
30
30
|
switch Reset.parseObjectStores(
|
|
31
|
-
output([
|
|
31
|
+
output([
|
|
32
|
+
("Catalog.productImages", storeEntry(~bucket="alpha-stores", ~prefix="productImages")),
|
|
33
|
+
]),
|
|
32
34
|
) {
|
|
33
35
|
| Ok([s]) =>
|
|
34
36
|
expect((s.plugin, s.store, s.bucketName, s.keyPrefix))->toEqual((
|
|
@@ -61,7 +63,9 @@ describe("parseObjectStores", () => {
|
|
|
61
63
|
// A store the reset cannot read is a store it would silently leave behind, so
|
|
62
64
|
// a malformed entry fails the run rather than being skipped.
|
|
63
65
|
testSync("a malformed entry is an error, not a skip", () =>
|
|
64
|
-
switch Reset.parseObjectStores(
|
|
66
|
+
switch Reset.parseObjectStores(
|
|
67
|
+
output([("Catalog.productImages", obj([("keyPrefix", str("x"))]))]),
|
|
68
|
+
) {
|
|
65
69
|
| Ok(_) => fail("expected a malformed entry to be refused")
|
|
66
70
|
| Error(message) => expect(message->String.includes("Catalog.productImages"))->toBe(true)
|
|
67
71
|
}
|
|
@@ -108,7 +112,11 @@ describe("validateStores", () => {
|
|
|
108
112
|
testSync("an enclosing prefix is refused", () =>
|
|
109
113
|
switch Reset.validateStores([
|
|
110
114
|
store(~qualified="Legacy.catalog", ~bucket="alpha-stores", ~prefix="Catalog"),
|
|
111
|
-
store(
|
|
115
|
+
store(
|
|
116
|
+
~qualified="Catalog.productImages",
|
|
117
|
+
~bucket="alpha-stores",
|
|
118
|
+
~prefix="Catalog/productImages",
|
|
119
|
+
),
|
|
112
120
|
]) {
|
|
113
121
|
| Ok() => fail("expected an enclosing prefix to be refused")
|
|
114
122
|
| Error(message) => expect(message->String.includes("encloses"))->toBe(true)
|
|
@@ -120,8 +128,16 @@ describe("validateStores", () => {
|
|
|
120
128
|
testSync("one prefix in two different buckets is fine", () =>
|
|
121
129
|
expect(
|
|
122
130
|
Reset.validateStores([
|
|
123
|
-
store(
|
|
124
|
-
|
|
131
|
+
store(
|
|
132
|
+
~qualified="Catalog.productImages",
|
|
133
|
+
~bucket="catalog-productImages",
|
|
134
|
+
~prefix="productImages",
|
|
135
|
+
),
|
|
136
|
+
store(
|
|
137
|
+
~qualified="Ordering.productImages",
|
|
138
|
+
~bucket="ordering-productImages",
|
|
139
|
+
~prefix="productImages",
|
|
140
|
+
),
|
|
125
141
|
]),
|
|
126
142
|
)->toEqual(Ok())
|
|
127
143
|
)
|
|
@@ -160,7 +176,12 @@ describe("pluginOf", () => {
|
|
|
160
176
|
// never has to guess the relation between the two.
|
|
161
177
|
testSync("prefers the declared plugin name over the label", () =>
|
|
162
178
|
expect(
|
|
163
|
-
Reset.pluginOf({
|
|
179
|
+
Reset.pluginOf({
|
|
180
|
+
projectDir: "../catalog-aws",
|
|
181
|
+
label: "catalog",
|
|
182
|
+
group: Domain,
|
|
183
|
+
plugin: "Catalog",
|
|
184
|
+
}),
|
|
164
185
|
)->toBe("Catalog")
|
|
165
186
|
)
|
|
166
187
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|