@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.
- package/CHANGELOG.md +34 -0
- package/package.json +17 -13
- package/rescript.json +2 -1
- package/run-bake-manifest.mjs +3 -0
- package/run-deploy-app.mjs +3 -0
- package/scripts/BakeManifest.res +395 -0
- package/scripts/BakeManifest.res.mjs +487 -0
- package/scripts/DeployApp.res +708 -0
- package/scripts/DeployApp.res.mjs +1015 -0
- package/scripts/DeployManifest.res +84 -0
- package/scripts/DeployManifest.res.mjs +112 -0
- package/scripts/ProvisionAccounts.res +60 -46
- package/scripts/ProvisionAccounts.res.mjs +51 -37
- package/scripts/PulumiCli.res +43 -0
- package/scripts/PulumiCli.res.mjs +106 -0
- package/src/Platform.res +19 -2
- package/src/Platform.res.mjs +12 -4
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +1 -5
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +1 -1
- package/src/adapter/Api/Platform_UIFragments_Lambda.res +1 -5
- package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +1 -2
- package/src/adapter/Auth/Auth_ActiveRoleStore.res +1 -5
- package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +1 -2
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +1 -5
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +1 -2
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +52 -31
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs +75 -44
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +1 -5
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +1 -2
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res +1 -5
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res.mjs +1 -2
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +5 -4
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +1 -1
- package/src/adapter/Postgres/PgMigration_Builder.res +1 -1
- package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -2
- package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res +2 -0
- package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res.mjs +2 -2
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +15 -6
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +51 -30
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +31 -14
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +1 -5
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +1 -1
- package/src/adapter/StateTopic/StateTopic_AppSync.res +1 -5
- package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +1 -1
- package/src/adapter/Task/TaskBucket_S3.res +1 -3
- package/src/adapter/Task/TaskBucket_S3.res.mjs +1 -1
- package/src/adapter/Upload/Upload_Claim_S3.res +1 -5
- package/src/adapter/Upload/Upload_Claim_S3.res.mjs +1 -1
- package/src/adapter/Upload/Upload_Presign_S3.res +1 -5
- package/src/adapter/Upload/Upload_Presign_S3.res.mjs +1 -2
- package/src/capability/Capability_Geocoding_AwsLocation.res +5 -1
- package/src/capability/Capability_Geocoding_AwsLocation.res.mjs +7 -1
- package/src/components/Api/AppSync_SdlDecorate.res +4 -2
- package/src/components/Api/AppSync_SdlDecorate.res.mjs +1 -1
- package/src/plugin/cloner/ClonerRunner_Fargate.res +1 -5
- package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +1 -1
- package/src/plugin/stack/Plugin_Stack.res +4 -0
- package/src/plugin/stack/Plugin_Stack.res.mjs +2 -0
- package/src/util/Util_DeadLetterQueue.res +1 -5
- package/src/util/Util_DeadLetterQueue.res.mjs +1 -2
- package/src/util/Util_StoreLayout.res +15 -2
- package/src/util/Util_StoreLayout.res.mjs +11 -3
- package/tests/BakeManifestTest.res +204 -0
- package/tests/BakeManifestTest.res.mjs +259 -0
- package/tests/DcbColdStartPartitionTest.res +32 -0
- package/tests/DcbColdStartPartitionTest.res.mjs +33 -0
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +88 -0
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +112 -0
- package/tests/DcbUnresolvedPartitionSlice.res +28 -0
- package/tests/DcbUnresolvedPartitionSlice.res.mjs +38 -0
- package/tests/DeployAppTest.res +90 -0
- package/tests/DeployAppTest.res.mjs +133 -0
- package/tests/LambdaLayerLookupTest.res +104 -0
- package/tests/LambdaLayerLookupTest.res.mjs +117 -0
- package/tests/ProvisionAccountsTest.res +26 -0
- package/tests/ProvisionAccountsTest.res.mjs +31 -0
- package/tests/Util_StoreLayoutTest.res +6 -0
- package/tests/Util_StoreLayoutTest.res.mjs +7 -4
- package/tests/dcbColdStartPartition.mjs +66 -0
- package/tests/setup/layerArn.cjs +5 -0
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
4
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
5
6
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
7
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
8
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
7
9
|
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
8
10
|
import * as DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws from "../src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs";
|
|
9
11
|
|
|
@@ -314,6 +316,116 @@ globalThis.describe("Runtime.buildScanFilter — excludes fence sentinels (Issue
|
|
|
314
316
|
});
|
|
315
317
|
});
|
|
316
318
|
|
|
319
|
+
globalThis.describe("Runtime.derivePartitionKey — each event type under its slice's key", () => {
|
|
320
|
+
let partitionTag = {
|
|
321
|
+
TAG: "ByEventType",
|
|
322
|
+
_0: Object.fromEntries([
|
|
323
|
+
[
|
|
324
|
+
"OrderPlaced",
|
|
325
|
+
"orderId"
|
|
326
|
+
],
|
|
327
|
+
[
|
|
328
|
+
"AddressLinked",
|
|
329
|
+
"customerId"
|
|
330
|
+
]
|
|
331
|
+
])
|
|
332
|
+
};
|
|
333
|
+
let thrownMessage = f => {
|
|
334
|
+
try {
|
|
335
|
+
f();
|
|
336
|
+
return "";
|
|
337
|
+
} catch (raw_err) {
|
|
338
|
+
let err = Primitive_exceptions.internalToException(raw_err);
|
|
339
|
+
if (err.RE_EXN_ID === "JsExn") {
|
|
340
|
+
return Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "");
|
|
341
|
+
}
|
|
342
|
+
throw err;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
globalThis.test("files an event under its own type's key, not its first tag", () => {
|
|
346
|
+
globalThis.expect(DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.derivePartitionKey(partitionTag, "AddressLinked", [
|
|
347
|
+
{
|
|
348
|
+
key: "addressId",
|
|
349
|
+
value: "a1"
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
key: "customerId",
|
|
353
|
+
value: "c1"
|
|
354
|
+
}
|
|
355
|
+
])).toBe("customerId:c1");
|
|
356
|
+
});
|
|
357
|
+
globalThis.test("another type in the same log keeps its own key", () => {
|
|
358
|
+
globalThis.expect(DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.derivePartitionKey(partitionTag, "OrderPlaced", [
|
|
359
|
+
{
|
|
360
|
+
key: "customerId",
|
|
361
|
+
value: "c1"
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
key: "orderId",
|
|
365
|
+
value: "o1"
|
|
366
|
+
}
|
|
367
|
+
])).toBe("orderId:o1");
|
|
368
|
+
});
|
|
369
|
+
globalThis.test("an event type no slice writes throws instead of guessing", () => {
|
|
370
|
+
globalThis.expect(thrownMessage(() => DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.derivePartitionKey(partitionTag, "Unknown", [{
|
|
371
|
+
key: "customerId",
|
|
372
|
+
value: "c1"
|
|
373
|
+
}])).includes("Unknown has no partition key")).toBe(true);
|
|
374
|
+
});
|
|
375
|
+
globalThis.test("an event missing its partition tag throws instead of guessing", () => {
|
|
376
|
+
globalThis.expect(thrownMessage(() => DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.derivePartitionKey(partitionTag, "AddressLinked", [{
|
|
377
|
+
key: "addressId",
|
|
378
|
+
value: "a1"
|
|
379
|
+
}])).includes("carries no customerId tag")).toBe(true);
|
|
380
|
+
});
|
|
381
|
+
globalThis.test("a conditional append puts and fences the event under that key", () => {
|
|
382
|
+
let linked_data = {};
|
|
383
|
+
let linked_tags = [
|
|
384
|
+
{
|
|
385
|
+
key: "addressId",
|
|
386
|
+
value: "a1"
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
key: "customerId",
|
|
390
|
+
value: "c1"
|
|
391
|
+
}
|
|
392
|
+
];
|
|
393
|
+
let linked_meta = testMeta();
|
|
394
|
+
let linked = {
|
|
395
|
+
eventType: "AddressLinked",
|
|
396
|
+
data: linked_data,
|
|
397
|
+
tags: linked_tags,
|
|
398
|
+
meta: linked_meta
|
|
399
|
+
};
|
|
400
|
+
let cond_query = [{
|
|
401
|
+
eventTypes: ["AddressLinked"],
|
|
402
|
+
tags: [{
|
|
403
|
+
key: "customerId",
|
|
404
|
+
value: "c1"
|
|
405
|
+
}]
|
|
406
|
+
}];
|
|
407
|
+
let cond_after = "50";
|
|
408
|
+
let cond = {
|
|
409
|
+
query: cond_query,
|
|
410
|
+
after: cond_after
|
|
411
|
+
};
|
|
412
|
+
let ids = Stdlib_Array.filterMap(Stdlib_Array.filterMap(DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.buildConditionalTransactItems(table, [linked], cond, "100", partitionTag, undefined), it => {
|
|
413
|
+
let match = it.Put;
|
|
414
|
+
let match$1 = it.Update;
|
|
415
|
+
if (match !== undefined) {
|
|
416
|
+
return Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(match.Item), i => i["id"]);
|
|
417
|
+
} else if (match$1 !== undefined) {
|
|
418
|
+
return match$1.Key["id"];
|
|
419
|
+
} else {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
}), Stdlib_JSON.Decode.string);
|
|
423
|
+
globalThis.expect(ids.includes("customerId:c1")).toBe(true);
|
|
424
|
+
globalThis.expect(ids.includes("fence#customerId:c1")).toBe(true);
|
|
425
|
+
globalThis.expect(ids.some(id => id.includes("addressId"))).toBe(false);
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
317
429
|
globalThis.describe("Runtime.buildEventPuts", () => {
|
|
318
430
|
let event = (eventType, tags) => ({
|
|
319
431
|
eventType: eventType,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// A slice whose partition cannot be inferred, for `DcbColdStartPartitionTest`.
|
|
2
|
+
// It writes two ids and reads both off an event nothing in its boundary writes,
|
|
3
|
+
// so neither can be told apart from a reference.
|
|
4
|
+
//
|
|
5
|
+
// Explicit `@s.matches(...)` form: reventless-ppx is not wired into this package.
|
|
6
|
+
|
|
7
|
+
@schema
|
|
8
|
+
type consumedEvent = GadgetImported({gadgetId: string, supplierId: string})
|
|
9
|
+
|
|
10
|
+
@schema
|
|
11
|
+
type command =
|
|
12
|
+
| LinkGadget({
|
|
13
|
+
gadgetId: @s.matches(Reventless.DcbTag.string) string,
|
|
14
|
+
supplierId: @s.matches(Reventless.DcbTag.string) string,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
@schema
|
|
18
|
+
type error = AlreadyLinked
|
|
19
|
+
|
|
20
|
+
@schema
|
|
21
|
+
type event =
|
|
22
|
+
| GadgetLinked({
|
|
23
|
+
gadgetId: @s.matches(Reventless.DcbTag.string) string,
|
|
24
|
+
supplierId: @s.matches(Reventless.DcbTag.string) string,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
let name = "LinkGadget"
|
|
28
|
+
let moduleUrl = "cold-start-test://LinkGadget"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
5
|
+
|
|
6
|
+
let consumedEventSchema = Sury.$schema(s => ({
|
|
7
|
+
TAG: "GadgetImported",
|
|
8
|
+
gadgetId: s.m(Sury.string),
|
|
9
|
+
supplierId: s.m(Sury.string)
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
let commandSchema = Sury.$schema(s => ({
|
|
13
|
+
TAG: "LinkGadget",
|
|
14
|
+
gadgetId: s.m(DcbTag$Reventless.string),
|
|
15
|
+
supplierId: s.m(DcbTag$Reventless.string)
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
let errorSchema = Sury.literal("AlreadyLinked");
|
|
19
|
+
|
|
20
|
+
let eventSchema = Sury.$schema(s => ({
|
|
21
|
+
TAG: "GadgetLinked",
|
|
22
|
+
gadgetId: s.m(DcbTag$Reventless.string),
|
|
23
|
+
supplierId: s.m(DcbTag$Reventless.string)
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
let name = "LinkGadget";
|
|
27
|
+
|
|
28
|
+
let moduleUrl = "cold-start-test://LinkGadget";
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
consumedEventSchema,
|
|
32
|
+
commandSchema,
|
|
33
|
+
errorSchema,
|
|
34
|
+
eventSchema,
|
|
35
|
+
name,
|
|
36
|
+
moduleUrl,
|
|
37
|
+
}
|
|
38
|
+
/* consumedEventSchema Not a pure module */
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// What `deploy-app` decides without AWS or Pulumi: its arguments, the checks
|
|
4
|
+
// that stop it before anything is created, and what a new stack starts with.
|
|
5
|
+
|
|
6
|
+
describe("DeployApp.parseArgs", () => {
|
|
7
|
+
testSync("up with the defaults", () =>
|
|
8
|
+
expect(DeployApp.parseArgs(["up"]))->toEqual(
|
|
9
|
+
Ok({DeployApp.command: Some(Up), manifest: None, stack: "dev", help: false}),
|
|
10
|
+
)
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
testSync("down with a manifest and a stack", () =>
|
|
14
|
+
expect(DeployApp.parseArgs(["down", "--manifest", "../m.yaml", "--stack", "try"]))->toEqual(
|
|
15
|
+
Ok({DeployApp.command: Some(Down), manifest: Some("../m.yaml"), stack: "try", help: false}),
|
|
16
|
+
)
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
testSync("a second command is refused", () =>
|
|
20
|
+
expect(DeployApp.parseArgs(["up", "down"]))->toEqual(Error(`unknown argument "down"`))
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe("DeployApp checks", () => {
|
|
25
|
+
testSync("Node 22 and later pass", () => {
|
|
26
|
+
expect(DeployApp.checkNode("22.17.1"))->toEqual(None)
|
|
27
|
+
expect(DeployApp.checkNode("24.0.0"))->toEqual(None)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
testSync("an older Node says which version is needed", () =>
|
|
31
|
+
expect(DeployApp.checkNode("20.11.0")->Option.getOr(""))->toContain("Node 22 or later")
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
testSync("Pulumi Cloud needs no passphrase", () =>
|
|
35
|
+
expect(
|
|
36
|
+
DeployApp.backendNeedsPassphrase(~url=Some("https://app.pulumi.com/me"), ~env=Dict.make()),
|
|
37
|
+
)->toBe(false)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
testSync("a local backend needs one, unless it is set", () => {
|
|
41
|
+
expect(DeployApp.backendNeedsPassphrase(~url=Some("file://~"), ~env=Dict.make()))->toBe(true)
|
|
42
|
+
expect(
|
|
43
|
+
DeployApp.backendNeedsPassphrase(
|
|
44
|
+
~url=Some("s3://bucket"),
|
|
45
|
+
~env=Dict.fromArray([("PULUMI_CONFIG_PASSPHRASE", "")]),
|
|
46
|
+
),
|
|
47
|
+
)->toBe(false)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe("DeployApp stacks and layer", () => {
|
|
52
|
+
let project = {
|
|
53
|
+
DeployApp.label: "platform",
|
|
54
|
+
dir: "/app/platform-aws",
|
|
55
|
+
projectName: "shop-platform-aws",
|
|
56
|
+
program: "/app/platform-aws/src/Main.res.mjs",
|
|
57
|
+
stackDefaults: Dict.fromArray([("aws:region", "us-east-1"), ("platform:x", "y")]),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The region and the try-out flag come last, so an app default cannot turn a
|
|
61
|
+
// try-out into a stack `down` refuses to remove.
|
|
62
|
+
testSync("a new stack gets the app's defaults, then the region and the try-out flag", () =>
|
|
63
|
+
expect(DeployApp.newStackConfig(~project, ~region="eu-west-1"))->toEqual([
|
|
64
|
+
("aws:region", "us-east-1"),
|
|
65
|
+
("platform:x", "y"),
|
|
66
|
+
("aws:region", "eu-west-1"),
|
|
67
|
+
("aws-native:region", "eu-west-1"),
|
|
68
|
+
("reventless:disposable", "true"),
|
|
69
|
+
])
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
testSync("the release download is named by the package and version", () =>
|
|
73
|
+
expect(DeployApp.releaseUrl("3.0.0-alpha.346"))->toBe(
|
|
74
|
+
"https://github.com/ReventlessDev/reventless-core/releases/download/%40reventlessdev%2Freventless-aws%403.0.0-alpha.346/reventless-layer.zip",
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
testSync("a layer is marked with the release it was published for", () =>
|
|
79
|
+
expect(DeployApp.layerDescription("3.0.0"))->toBe("reventless-aws@3.0.0")
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
testSync("sign-ins line up under each other", () =>
|
|
83
|
+
expect(
|
|
84
|
+
DeployApp.signInLines([
|
|
85
|
+
{username: "admin@example.com", password: "", groups: ["Admin"]},
|
|
86
|
+
{username: "a@example.com", password: "", groups: []},
|
|
87
|
+
]),
|
|
88
|
+
)->toEqual([" admin@example.com [Admin]", " a@example.com []"])
|
|
89
|
+
)
|
|
90
|
+
})
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as DeployApp$ReventlessAws from "../scripts/DeployApp.res.mjs";
|
|
5
|
+
|
|
6
|
+
globalThis.describe("DeployApp.parseArgs", () => {
|
|
7
|
+
globalThis.test("up with the defaults", () => {
|
|
8
|
+
globalThis.expect(DeployApp$ReventlessAws.parseArgs(["up"])).toEqual({
|
|
9
|
+
TAG: "Ok",
|
|
10
|
+
_0: {
|
|
11
|
+
command: "Up",
|
|
12
|
+
manifest: undefined,
|
|
13
|
+
stack: "dev",
|
|
14
|
+
help: false
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
globalThis.test("down with a manifest and a stack", () => {
|
|
19
|
+
globalThis.expect(DeployApp$ReventlessAws.parseArgs([
|
|
20
|
+
"down",
|
|
21
|
+
"--manifest",
|
|
22
|
+
"../m.yaml",
|
|
23
|
+
"--stack",
|
|
24
|
+
"try"
|
|
25
|
+
])).toEqual({
|
|
26
|
+
TAG: "Ok",
|
|
27
|
+
_0: {
|
|
28
|
+
command: "Down",
|
|
29
|
+
manifest: "../m.yaml",
|
|
30
|
+
stack: "try",
|
|
31
|
+
help: false
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
globalThis.test("a second command is refused", () => {
|
|
36
|
+
globalThis.expect(DeployApp$ReventlessAws.parseArgs([
|
|
37
|
+
"up",
|
|
38
|
+
"down"
|
|
39
|
+
])).toEqual({
|
|
40
|
+
TAG: "Error",
|
|
41
|
+
_0: `unknown argument "down"`
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
globalThis.describe("DeployApp checks", () => {
|
|
47
|
+
globalThis.test("Node 22 and later pass", () => {
|
|
48
|
+
globalThis.expect(DeployApp$ReventlessAws.checkNode("22.17.1")).toEqual(undefined);
|
|
49
|
+
globalThis.expect(DeployApp$ReventlessAws.checkNode("24.0.0")).toEqual(undefined);
|
|
50
|
+
});
|
|
51
|
+
globalThis.test("an older Node says which version is needed", () => {
|
|
52
|
+
globalThis.expect(Stdlib_Option.getOr(DeployApp$ReventlessAws.checkNode("20.11.0"), "")).toContain("Node 22 or later");
|
|
53
|
+
});
|
|
54
|
+
globalThis.test("Pulumi Cloud needs no passphrase", () => {
|
|
55
|
+
globalThis.expect(DeployApp$ReventlessAws.backendNeedsPassphrase("https://app.pulumi.com/me", {})).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
globalThis.test("a local backend needs one, unless it is set", () => {
|
|
58
|
+
globalThis.expect(DeployApp$ReventlessAws.backendNeedsPassphrase("file://~", {})).toBe(true);
|
|
59
|
+
globalThis.expect(DeployApp$ReventlessAws.backendNeedsPassphrase("s3://bucket", Object.fromEntries([[
|
|
60
|
+
"PULUMI_CONFIG_PASSPHRASE",
|
|
61
|
+
""
|
|
62
|
+
]]))).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
globalThis.describe("DeployApp stacks and layer", () => {
|
|
67
|
+
let project_stackDefaults = Object.fromEntries([
|
|
68
|
+
[
|
|
69
|
+
"aws:region",
|
|
70
|
+
"us-east-1"
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
"platform:x",
|
|
74
|
+
"y"
|
|
75
|
+
]
|
|
76
|
+
]);
|
|
77
|
+
let project = {
|
|
78
|
+
label: "platform",
|
|
79
|
+
dir: "/app/platform-aws",
|
|
80
|
+
projectName: "shop-platform-aws",
|
|
81
|
+
program: "/app/platform-aws/src/Main.res.mjs",
|
|
82
|
+
stackDefaults: project_stackDefaults
|
|
83
|
+
};
|
|
84
|
+
globalThis.test("a new stack gets the app's defaults, then the region and the try-out flag", () => {
|
|
85
|
+
globalThis.expect(DeployApp$ReventlessAws.newStackConfig(project, "eu-west-1")).toEqual([
|
|
86
|
+
[
|
|
87
|
+
"aws:region",
|
|
88
|
+
"us-east-1"
|
|
89
|
+
],
|
|
90
|
+
[
|
|
91
|
+
"platform:x",
|
|
92
|
+
"y"
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
"aws:region",
|
|
96
|
+
"eu-west-1"
|
|
97
|
+
],
|
|
98
|
+
[
|
|
99
|
+
"aws-native:region",
|
|
100
|
+
"eu-west-1"
|
|
101
|
+
],
|
|
102
|
+
[
|
|
103
|
+
"reventless:disposable",
|
|
104
|
+
"true"
|
|
105
|
+
]
|
|
106
|
+
]);
|
|
107
|
+
});
|
|
108
|
+
globalThis.test("the release download is named by the package and version", () => {
|
|
109
|
+
globalThis.expect(DeployApp$ReventlessAws.releaseUrl("3.0.0-alpha.346")).toBe("https://github.com/ReventlessDev/reventless-core/releases/download/%40reventlessdev%2Freventless-aws%403.0.0-alpha.346/reventless-layer.zip");
|
|
110
|
+
});
|
|
111
|
+
globalThis.test("a layer is marked with the release it was published for", () => {
|
|
112
|
+
globalThis.expect(DeployApp$ReventlessAws.layerDescription("3.0.0")).toBe("reventless-aws@3.0.0");
|
|
113
|
+
});
|
|
114
|
+
globalThis.test("sign-ins line up under each other", () => {
|
|
115
|
+
globalThis.expect(DeployApp$ReventlessAws.signInLines([
|
|
116
|
+
{
|
|
117
|
+
username: "admin@example.com",
|
|
118
|
+
password: "",
|
|
119
|
+
groups: ["Admin"]
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
username: "a@example.com",
|
|
123
|
+
password: "",
|
|
124
|
+
groups: []
|
|
125
|
+
}
|
|
126
|
+
])).toEqual([
|
|
127
|
+
" admin@example.com [Admin]",
|
|
128
|
+
" a@example.com []"
|
|
129
|
+
]);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/* Not a pure module */
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
module Lambda = PulumiAws.Lambda
|
|
4
|
+
|
|
5
|
+
// A deploy with no layer used to succeed and leave every function failing with
|
|
6
|
+
// "Cannot find package". These pin what the lookup tells apart and what the
|
|
7
|
+
// stopped deploy says.
|
|
8
|
+
let parameter = "/reventless/layer-arn/dev"
|
|
9
|
+
|
|
10
|
+
let cliResult = (~status=?, ~stdout="", ~stderr="", ~error=?) => {
|
|
11
|
+
NodeChildProcess.status: status->Nullable.fromOption,
|
|
12
|
+
stdout: Nullable.make(stdout),
|
|
13
|
+
stderr: Nullable.make(stderr),
|
|
14
|
+
error: error->Nullable.fromOption,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("Lambda.classifySsmLookup", () => {
|
|
18
|
+
let classify = (~region=Some("eu-west-1"), result) =>
|
|
19
|
+
Lambda.classifySsmLookup(~parameter, ~region, result)
|
|
20
|
+
|
|
21
|
+
testSync("a stored value is the layer", () =>
|
|
22
|
+
expect(
|
|
23
|
+
classify(cliResult(~status=0, ~stdout="arn:aws:lambda:eu-west-1:1:layer:reventless:7\n")),
|
|
24
|
+
)->toEqual(Lambda.Found("arn:aws:lambda:eu-west-1:1:layer:reventless:7"))
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
testSync("ParameterNotFound means there is no layer", () =>
|
|
28
|
+
expect(
|
|
29
|
+
classify(
|
|
30
|
+
cliResult(
|
|
31
|
+
~status=254,
|
|
32
|
+
~stderr="\nAn error occurred (ParameterNotFound) when calling the GetParameter operation: \n",
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
)->toEqual(Lambda.NotFound({parameter, region: Some("eu-west-1")}))
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
testSync("a refused call is not mistaken for a missing layer", () =>
|
|
39
|
+
expect(
|
|
40
|
+
classify(
|
|
41
|
+
cliResult(
|
|
42
|
+
~status=254,
|
|
43
|
+
~stderr="\nAn error occurred (AccessDeniedException) when calling the GetParameter operation: no\n",
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
)->toEqual(
|
|
47
|
+
Lambda.CouldNotLook({
|
|
48
|
+
parameter,
|
|
49
|
+
region: Some("eu-west-1"),
|
|
50
|
+
reason: "An error occurred (AccessDeniedException) when calling the GetParameter operation: no",
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
testSync("a CLI that cannot be started is not mistaken for a missing layer", () => {
|
|
56
|
+
let error = JsError.make("spawnSync aws ENOENT")->JsError.toJsExn
|
|
57
|
+
let reason = switch classify(
|
|
58
|
+
~region=None,
|
|
59
|
+
{...cliResult(~error), stdout: Nullable.null, stderr: Nullable.null},
|
|
60
|
+
) {
|
|
61
|
+
| CouldNotLook({reason}) => Some(reason)
|
|
62
|
+
| _ => None
|
|
63
|
+
}
|
|
64
|
+
expect(reason)->toEqual(Some("the AWS CLI could not be run (spawnSync aws ENOENT)"))
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe("Lambda.missingLayerMessage", () => {
|
|
69
|
+
testSync("names the parameter and the region it looked in", () => {
|
|
70
|
+
let message =
|
|
71
|
+
Lambda.NotFound({parameter, region: Some("eu-west-1")})
|
|
72
|
+
->Lambda.missingLayerMessage
|
|
73
|
+
->Option.getOr("")
|
|
74
|
+
expect(message)->toContain(parameter)
|
|
75
|
+
expect(message)->toContain("region eu-west-1")
|
|
76
|
+
expect(message)->toContain("REVENTLESS_LAYER_ARN")
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
testSync("says when the CLI's default region was asked", () =>
|
|
80
|
+
expect(
|
|
81
|
+
Lambda.NotFound({parameter, region: None})->Lambda.missingLayerMessage->Option.getOr(""),
|
|
82
|
+
)->toContain("the AWS CLI's default region")
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
testSync("carries the reason a lookup could not be made", () =>
|
|
86
|
+
expect(
|
|
87
|
+
Lambda.CouldNotLook({parameter, region: None, reason: "no credentials"})
|
|
88
|
+
->Lambda.missingLayerMessage
|
|
89
|
+
->Option.getOr(""),
|
|
90
|
+
)->toContain("no credentials")
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
testSync("is silent when the layer was found", () =>
|
|
94
|
+
expect(Lambda.Found("arn")->Lambda.missingLayerMessage)->toEqual(None)
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe("Lambda.reventlessLayerArn", () => {
|
|
99
|
+
testSync("REVENTLESS_LAYER_ARN wins without asking AWS", () =>
|
|
100
|
+
expect(Lambda.reventlessLayerArn())->toBe(
|
|
101
|
+
"arn:aws:lambda:eu-central-1:000000000000:layer:reventless-test:1",
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as Stdlib_Nullable from "@rescript/runtime/lib/es6/Stdlib_Nullable.js";
|
|
5
|
+
import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
|
|
6
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
7
|
+
|
|
8
|
+
let parameter = "/reventless/layer-arn/dev";
|
|
9
|
+
|
|
10
|
+
function cliResult(status, stdoutOpt, stderrOpt, error) {
|
|
11
|
+
let stdout = stdoutOpt !== undefined ? stdoutOpt : "";
|
|
12
|
+
let stderr = stderrOpt !== undefined ? stderrOpt : "";
|
|
13
|
+
return {
|
|
14
|
+
status: Stdlib_Nullable.fromOption(status),
|
|
15
|
+
stdout: stdout,
|
|
16
|
+
stderr: stderr,
|
|
17
|
+
error: Stdlib_Nullable.fromOption(error)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
globalThis.describe("Lambda.classifySsmLookup", () => {
|
|
22
|
+
let classify = (regionOpt, result) => {
|
|
23
|
+
let region = regionOpt !== undefined ? Primitive_option.valFromOption(regionOpt) : "eu-west-1";
|
|
24
|
+
return Lambda$PulumiAws.classifySsmLookup(parameter, region, result);
|
|
25
|
+
};
|
|
26
|
+
globalThis.test("a stored value is the layer", () => {
|
|
27
|
+
globalThis.expect(classify(undefined, cliResult(0, "arn:aws:lambda:eu-west-1:1:layer:reventless:7\n", undefined, undefined))).toEqual({
|
|
28
|
+
TAG: "Found",
|
|
29
|
+
_0: "arn:aws:lambda:eu-west-1:1:layer:reventless:7"
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
globalThis.test("ParameterNotFound means there is no layer", () => {
|
|
33
|
+
globalThis.expect(classify(undefined, cliResult(254, undefined, "\nAn error occurred (ParameterNotFound) when calling the GetParameter operation: \n", undefined))).toEqual({
|
|
34
|
+
TAG: "NotFound",
|
|
35
|
+
parameter: parameter,
|
|
36
|
+
region: "eu-west-1"
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
globalThis.test("a refused call is not mistaken for a missing layer", () => {
|
|
40
|
+
globalThis.expect(classify(undefined, cliResult(254, undefined, "\nAn error occurred (AccessDeniedException) when calling the GetParameter operation: no\n", undefined))).toEqual({
|
|
41
|
+
TAG: "CouldNotLook",
|
|
42
|
+
parameter: parameter,
|
|
43
|
+
region: "eu-west-1",
|
|
44
|
+
reason: "An error occurred (AccessDeniedException) when calling the GetParameter operation: no"
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
globalThis.test("a CLI that cannot be started is not mistaken for a missing layer", () => {
|
|
48
|
+
let error = new Error("spawnSync aws ENOENT");
|
|
49
|
+
let init = cliResult(undefined, undefined, undefined, Primitive_option.some(error));
|
|
50
|
+
let match = classify(Primitive_option.some(undefined), {
|
|
51
|
+
status: init.status,
|
|
52
|
+
stdout: null,
|
|
53
|
+
stderr: null,
|
|
54
|
+
error: init.error
|
|
55
|
+
});
|
|
56
|
+
let reason;
|
|
57
|
+
switch (match.TAG) {
|
|
58
|
+
case "Found" :
|
|
59
|
+
case "NotFound" :
|
|
60
|
+
reason = undefined;
|
|
61
|
+
break;
|
|
62
|
+
case "CouldNotLook" :
|
|
63
|
+
reason = match.reason;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
globalThis.expect(reason).toEqual("the AWS CLI could not be run (spawnSync aws ENOENT)");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
globalThis.describe("Lambda.missingLayerMessage", () => {
|
|
71
|
+
globalThis.test("names the parameter and the region it looked in", () => {
|
|
72
|
+
let message = Stdlib_Option.getOr(Lambda$PulumiAws.missingLayerMessage({
|
|
73
|
+
TAG: "NotFound",
|
|
74
|
+
parameter: parameter,
|
|
75
|
+
region: "eu-west-1"
|
|
76
|
+
}), "");
|
|
77
|
+
globalThis.expect(message).toContain(parameter);
|
|
78
|
+
globalThis.expect(message).toContain("region eu-west-1");
|
|
79
|
+
globalThis.expect(message).toContain("REVENTLESS_LAYER_ARN");
|
|
80
|
+
});
|
|
81
|
+
globalThis.test("says when the CLI's default region was asked", () => {
|
|
82
|
+
globalThis.expect(Stdlib_Option.getOr(Lambda$PulumiAws.missingLayerMessage({
|
|
83
|
+
TAG: "NotFound",
|
|
84
|
+
parameter: parameter,
|
|
85
|
+
region: undefined
|
|
86
|
+
}), "")).toContain("the AWS CLI's default region");
|
|
87
|
+
});
|
|
88
|
+
globalThis.test("carries the reason a lookup could not be made", () => {
|
|
89
|
+
globalThis.expect(Stdlib_Option.getOr(Lambda$PulumiAws.missingLayerMessage({
|
|
90
|
+
TAG: "CouldNotLook",
|
|
91
|
+
parameter: parameter,
|
|
92
|
+
region: undefined,
|
|
93
|
+
reason: "no credentials"
|
|
94
|
+
}), "")).toContain("no credentials");
|
|
95
|
+
});
|
|
96
|
+
globalThis.test("is silent when the layer was found", () => {
|
|
97
|
+
globalThis.expect(Lambda$PulumiAws.missingLayerMessage({
|
|
98
|
+
TAG: "Found",
|
|
99
|
+
_0: "arn"
|
|
100
|
+
})).toEqual(undefined);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
globalThis.describe("Lambda.reventlessLayerArn", () => {
|
|
105
|
+
globalThis.test("REVENTLESS_LAYER_ARN wins without asking AWS", () => {
|
|
106
|
+
globalThis.expect(Lambda$PulumiAws.reventlessLayerArn()).toBe("arn:aws:lambda:eu-central-1:000000000000:layer:reventless-test:1");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
let Lambda;
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
Lambda,
|
|
114
|
+
parameter,
|
|
115
|
+
cliResult,
|
|
116
|
+
}
|
|
117
|
+
/* Not a pure module */
|
|
@@ -140,3 +140,29 @@ describe("ProvisionAccounts.looksLikeEmail", () => {
|
|
|
140
140
|
expect(Provision.looksLikeEmail("admin@localhost"))->toBe(false)
|
|
141
141
|
)
|
|
142
142
|
})
|
|
143
|
+
|
|
144
|
+
// A refused pool used to leave generated passwords in the file, and the seed then
|
|
145
|
+
// offered accounts that exist nowhere.
|
|
146
|
+
describe("ProvisionAccounts.provision", () => {
|
|
147
|
+
let manifest = `# the cast
|
|
148
|
+
- username: shopper
|
|
149
|
+
password: ""
|
|
150
|
+
groups: [Shopper]
|
|
151
|
+
`
|
|
152
|
+
|
|
153
|
+
test("a refused pool leaves the manifest untouched", async () => {
|
|
154
|
+
let dir = NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "provision-accounts-"]))
|
|
155
|
+
let file = NodePath.join([dir, "users.yaml"])
|
|
156
|
+
NodeFs.writeFileSync(file, manifest)
|
|
157
|
+
let refuse = async (~providerId as _, ~usernames as _) => Error("pool signs in on email")
|
|
158
|
+
let outcome = await Provision.provision(
|
|
159
|
+
~file,
|
|
160
|
+
~providerId="eu-west-1_Refuses",
|
|
161
|
+
~checkPool=refuse,
|
|
162
|
+
)
|
|
163
|
+
let after = NodeFs.readFileSync(file)
|
|
164
|
+
NodeFs.rmSync(dir, {recursive: true, force: true})
|
|
165
|
+
expect(outcome)->toEqual(Error("pool signs in on email"))
|
|
166
|
+
expect(after)->toBe(manifest)
|
|
167
|
+
})
|
|
168
|
+
})
|