@reventlessdev/reventless-aws 3.0.0-alpha.345 → 3.0.0-alpha.347
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 +54 -0
- package/package.json +19 -12
- package/rescript.json +2 -1
- package/run-bake-manifest.mjs +3 -0
- package/run-deploy-app.mjs +3 -0
- package/run-provision-accounts.mjs +3 -0
- package/run-provision-admin.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 +339 -0
- package/scripts/ProvisionAccounts.res.mjs +351 -0
- package/scripts/ProvisionAdmin.res +275 -0
- package/scripts/ProvisionAdmin.res.mjs +289 -0
- package/scripts/ProvisionCognito.res +149 -0
- package/scripts/ProvisionCognito.res.mjs +98 -0
- package/scripts/ProvisionIdentity.res +76 -11
- package/scripts/ProvisionIdentity.res.mjs +72 -7
- package/scripts/ProvisionProvider.res +104 -0
- package/scripts/ProvisionProvider.res.mjs +123 -0
- package/scripts/PulumiCli.res +43 -0
- package/scripts/PulumiCli.res.mjs +106 -0
- package/src/Platform.res +51 -4
- package/src/Platform.res.mjs +32 -14
- package/src/Platform_Stack.res +119 -9
- package/src/Platform_Stack.res.mjs +42 -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/Auth/Auth_LoginIdentifier.res +66 -0
- package/src/adapter/Auth/Auth_LoginIdentifier.res.mjs +70 -0
- package/src/adapter/Auth/Auth_SignUpMode.res +78 -0
- package/src/adapter/Auth/Auth_SignUpMode.res.mjs +56 -0
- 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/AutomationSliceEntryPoint_Ops.res +5 -0
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +5 -1
- 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_Adapter.res +56 -72
- package/src/components/Api/AppSync_Adapter.res.mjs +26 -60
- package/src/components/Api/AppSync_SdlDecorate.res +82 -1
- package/src/components/Api/AppSync_SdlDecorate.res.mjs +45 -0
- 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/AppSync_AdapterTest.res +83 -0
- package/tests/AppSync_AdapterTest.res.mjs +69 -0
- package/tests/Auth_LoginIdentifierTest.res +51 -0
- package/tests/Auth_LoginIdentifierTest.res.mjs +51 -0
- package/tests/Auth_SignUpModeTest.res +56 -0
- package/tests/Auth_SignUpModeTest.res.mjs +53 -0
- 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 +168 -0
- package/tests/ProvisionAccountsTest.res.mjs +184 -0
- package/tests/ProvisionAdminTest.res +55 -0
- package/tests/ProvisionAdminTest.res.mjs +66 -0
- package/tests/ProvisionIdentityTest.res +89 -3
- package/tests/ProvisionIdentityTest.res.mjs +66 -1
- package/tests/Util_OwnerScopeEnvTest.res +25 -0
- package/tests/Util_OwnerScopeEnvTest.res.mjs +13 -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
|
@@ -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 */
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// What can be decided before an AWS call is made: which pool and which manifest
|
|
4
|
+
// the run is about, and which usernames a pool could never authenticate.
|
|
5
|
+
//
|
|
6
|
+
// The applying itself is not covered here — it is four SDK calls in a loop. What
|
|
7
|
+
// is covered is the preflight, because its whole purpose is to fail *before*
|
|
8
|
+
// half a cast exists, and the argument parsing, because getting it wrong
|
|
9
|
+
// provisions the right accounts into the wrong deployment.
|
|
10
|
+
//
|
|
11
|
+
// The manifest's own rules — what a second run does to a password already in the
|
|
12
|
+
// file — live with the format, in `spec`'s `AccountsManifestTest`.
|
|
13
|
+
|
|
14
|
+
module Provision = ProvisionAccounts
|
|
15
|
+
|
|
16
|
+
describe("ProvisionAccounts.parseArgs", () => {
|
|
17
|
+
testSync("no arguments leaves every choice unanswered", () =>
|
|
18
|
+
expect(Provision.parseArgs([]))->toEqual(
|
|
19
|
+
Ok({Provision.providerId: None, stack: None, file: None, help: false}),
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
testSync("a pool and a manifest parse", () =>
|
|
24
|
+
expect(
|
|
25
|
+
Provision.parseArgs(["--provider-id", "eu-west-1_AbCdEfGhI", "--file", "cast.yaml"]),
|
|
26
|
+
)->toEqual(
|
|
27
|
+
Ok({
|
|
28
|
+
Provision.providerId: Some("eu-west-1_AbCdEfGhI"),
|
|
29
|
+
stack: None,
|
|
30
|
+
file: Some("cast.yaml"),
|
|
31
|
+
help: false,
|
|
32
|
+
}),
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
testSync("--stack picks the deployment to read the pool id from", () =>
|
|
37
|
+
expect(Provision.parseArgs(["--stack", "beta"]))->toEqual(
|
|
38
|
+
Ok({Provision.providerId: None, stack: Some("beta"), file: None, help: false}),
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
// The reason unknown flags refuse rather than being skipped, and it lands
|
|
43
|
+
// harder here than on the first administrator: a typo'd `--provider-id` falls
|
|
44
|
+
// through to the environment and creates the whole cast in a different pool
|
|
45
|
+
// than the operator named.
|
|
46
|
+
testSync("an unknown flag refuses rather than being ignored", () =>
|
|
47
|
+
expect(Provision.parseArgs(["--pool", "eu-west-1_AbCdEfGhI"]))->toEqual(
|
|
48
|
+
Error(`unknown argument "--pool"`),
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
testSync("a flag with no value refuses", () =>
|
|
53
|
+
expect(Provision.parseArgs(["--provider-id"]))->toEqual(Error("--provider-id needs a value"))
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
testSync("-h asks for the usage", () =>
|
|
57
|
+
expect(Provision.parseArgs(["-h"]))->toEqual(
|
|
58
|
+
Ok({Provision.providerId: None, stack: None, file: None, help: true}),
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Which pool a run writes to, which is the one decision that is expensive to get
|
|
64
|
+
// wrong: provisioning a cast into the wrong deployment succeeds, and leaves
|
|
65
|
+
// working accounts somewhere nobody is looking.
|
|
66
|
+
//
|
|
67
|
+
// The stack fallback is not exercised here — it shells out to `pulumi`, and a
|
|
68
|
+
// test that did would depend on a logged-in CLI and a deployed stack. What is
|
|
69
|
+
// covered is the precedence above it, and that every answer says where it came
|
|
70
|
+
// from, which is what makes a wrong-pool run visible rather than silent.
|
|
71
|
+
describe("ProvisionProvider.resolve", () => {
|
|
72
|
+
let withoutEnv = fn => {
|
|
73
|
+
let saved = NodeProcess.env->Dict.get(ProvisionProvider.envKey)
|
|
74
|
+
NodeProcess.env->Dict.delete(ProvisionProvider.envKey)
|
|
75
|
+
let outcome = fn()
|
|
76
|
+
switch saved {
|
|
77
|
+
| Some(value) => NodeProcess.env->Dict.set(ProvisionProvider.envKey, value)
|
|
78
|
+
| None => ()
|
|
79
|
+
}
|
|
80
|
+
outcome
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
testSync("an explicit id wins, and says so", () =>
|
|
84
|
+
expect(
|
|
85
|
+
withoutEnv(
|
|
86
|
+
() => ProvisionProvider.resolve(~given=Some("eu-west-1_Explicit"), ~stack=Some("alpha")),
|
|
87
|
+
),
|
|
88
|
+
)->toEqual(Ok(("eu-west-1_Explicit", ProvisionProvider.Flag)))
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
// The flag beats the variable rather than the other way round: a CI shell that
|
|
92
|
+
// exports one for every stack must still be overridable for a single run.
|
|
93
|
+
testSync("an explicit id beats the environment", () => {
|
|
94
|
+
NodeProcess.env->Dict.set(ProvisionProvider.envKey, "eu-west-1_FromEnv")
|
|
95
|
+
let resolved = ProvisionProvider.resolve(~given=Some("eu-west-1_Explicit"), ~stack=None)
|
|
96
|
+
NodeProcess.env->Dict.delete(ProvisionProvider.envKey)
|
|
97
|
+
expect(resolved)->toEqual(Ok(("eu-west-1_Explicit", ProvisionProvider.Flag)))
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
testSync("the environment answers when no flag does", () => {
|
|
101
|
+
NodeProcess.env->Dict.set(ProvisionProvider.envKey, "eu-west-1_FromEnv")
|
|
102
|
+
let resolved = ProvisionProvider.resolve(~given=None, ~stack=None)
|
|
103
|
+
NodeProcess.env->Dict.delete(ProvisionProvider.envKey)
|
|
104
|
+
expect(resolved)->toEqual(Ok(("eu-west-1_FromEnv", ProvisionProvider.Environment)))
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// An empty variable is not an answer. Exporting it unset is the ordinary shape
|
|
108
|
+
// of a CI script whose earlier step did not run, and reading "" as the pool id
|
|
109
|
+
// would fail much later with a message about Cognito rather than about setup.
|
|
110
|
+
testSync("an empty environment variable is not an answer", () =>
|
|
111
|
+
expect({
|
|
112
|
+
NodeProcess.env->Dict.set(ProvisionProvider.envKey, " ")
|
|
113
|
+
let resolved = ProvisionProvider.resolve(~given=None, ~stack=Some("no-such-stack-here"))
|
|
114
|
+
NodeProcess.env->Dict.delete(ProvisionProvider.envKey)
|
|
115
|
+
resolved->Result.isError
|
|
116
|
+
})->toBe(true)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
testSync("each source describes itself", () => {
|
|
120
|
+
expect(ProvisionProvider.describe(Flag))->toBe("--provider-id")
|
|
121
|
+
expect(ProvisionProvider.describe(Environment))->toBe("REVENTLESS_IDENTITY_PROVIDER_ID")
|
|
122
|
+
expect(ProvisionProvider.describe(Stack("beta")))->toBe("stack beta")
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// A manifest usually names plain usernames — `shopper`, `merch` — and a pool
|
|
127
|
+
// created with `UsernameAttributes: [email]` will not take them. The accounts it
|
|
128
|
+
// makes instead are correctly created and cannot authenticate, which is a symptom
|
|
129
|
+
// that arrives at a login screen far from the run that caused it.
|
|
130
|
+
describe("ProvisionAccounts.looksLikeEmail", () => {
|
|
131
|
+
testSync("an address is one", () =>
|
|
132
|
+
expect(Provision.looksLikeEmail("me@example.com"))->toBe(true)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
testSync("a demo username is not", () => expect(Provision.looksLikeEmail("shopper"))->toBe(false))
|
|
136
|
+
|
|
137
|
+
// Not a validator — Cognito owns that. It answers one question: would this pool
|
|
138
|
+
// accept this string as a sign-in name. A local part with no domain would not.
|
|
139
|
+
testSync("an @ with no domain dot is not", () =>
|
|
140
|
+
expect(Provision.looksLikeEmail("admin@localhost"))->toBe(false)
|
|
141
|
+
)
|
|
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
|
+
})
|