@reventlessdev/reventless-aws 3.0.0-alpha.370 → 3.0.0-alpha.372
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 +18 -0
- package/package.json +12 -11
- package/run-clear-pending-creates.mjs +3 -0
- package/scripts/ClearPendingCreates.res +142 -0
- package/scripts/ClearPendingCreates.res.mjs +187 -0
- package/scripts/DeployApp.res +19 -1
- package/scripts/DeployApp.res.mjs +18 -2
- package/scripts/DeployManifest.res +7 -1
- package/scripts/DeployManifest.res.mjs +7 -3
- package/src/Platform.res +31 -11
- package/src/Platform.res.mjs +24 -2
- package/src/Platform_ProvidedGroups.res +97 -0
- package/src/Platform_ProvidedGroups.res.mjs +103 -0
- package/tests/BakeManifestTest.res +15 -2
- package/tests/BakeManifestTest.res.mjs +8 -3
- package/tests/ClearPendingCreatesTest.res +93 -0
- package/tests/ClearPendingCreatesTest.res.mjs +99 -0
- package/tests/DeployAppTest.res +11 -0
- package/tests/DeployAppTest.res.mjs +22 -1
- package/tests/Platform_ProvidedGroupsTest.res +52 -0
- package/tests/Platform_ProvidedGroupsTest.res.mjs +98 -0
package/src/Platform.res.mjs
CHANGED
|
@@ -80,6 +80,7 @@ import * as EventTopicPublisher_SNS$ReventlessAws from "./adapter/EventTopic/Eve
|
|
|
80
80
|
import * as ExtensionPointMapping$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/ExtensionPointMapping.res.mjs";
|
|
81
81
|
import * as PgQueryResolver_Builder$ReventlessAws from "./adapter/QueryDb/PgQueryResolver_Builder.res.mjs";
|
|
82
82
|
import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
|
|
83
|
+
import * as Platform_ProvidedGroups$ReventlessAws from "./Platform_ProvidedGroups.res.mjs";
|
|
83
84
|
import * as UiFragments_Projection$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateView/UiFragments_Projection.res.mjs";
|
|
84
85
|
import * as Aggregate_Builder_Single$ReventlessAws from "./components/Aggregate_Builder_Single.res.mjs";
|
|
85
86
|
import * as ReadModel_Builder_Single$ReventlessAws from "./components/ReadModel_Builder_Single.res.mjs";
|
|
@@ -152,6 +153,24 @@ let messagingEmailProviderRef = {
|
|
|
152
153
|
contents: Pulumi.output("")
|
|
153
154
|
};
|
|
154
155
|
|
|
156
|
+
let platformStackRefCache = {
|
|
157
|
+
contents: undefined
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
function platformStackReference() {
|
|
161
|
+
let stackRef = platformStackRefCache.contents;
|
|
162
|
+
if (stackRef !== undefined) {
|
|
163
|
+
return Primitive_option.valFromOption(stackRef);
|
|
164
|
+
}
|
|
165
|
+
let stackRef$1 = Stdlib_Option.map(new Pulumi.Config("platform").get("stack"), stack => new Pulumi.StackReference(stack));
|
|
166
|
+
platformStackRefCache.contents = Primitive_option.some(stackRef$1);
|
|
167
|
+
return stackRef$1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function loadProvidedGroups() {
|
|
171
|
+
return Platform_ProvidedGroups$ReventlessAws.load(platformStackReference());
|
|
172
|
+
}
|
|
173
|
+
|
|
155
174
|
function MakeWithConfig(Config) {
|
|
156
175
|
OwnerScope$Reventless.defaultElevatedRoles([Role$Reventless.admin]);
|
|
157
176
|
Role$Reventless.provideGroupsFrom(() => [Role$Reventless.adminGroup()].concat(AccountsManifest$Reventless.declaredGroups()));
|
|
@@ -183,7 +202,7 @@ function MakeWithConfig(Config) {
|
|
|
183
202
|
let currentDeployTarget = {
|
|
184
203
|
contents: "Domain"
|
|
185
204
|
};
|
|
186
|
-
let platformStackRef =
|
|
205
|
+
let platformStackRef = platformStackReference();
|
|
187
206
|
let claimStores = () => {
|
|
188
207
|
if (platformStackRef !== undefined) {
|
|
189
208
|
return Primitive_option.valFromOption(platformStackRef).getOutput("objectStores").apply(objectStores => Stdlib_Array.filterMap(Object.entries(Stdlib_Option.getOr(Stdlib_Option.flatMap(objectStores, Stdlib_JSON.Decode.object), {})), param => {
|
|
@@ -1599,7 +1618,7 @@ function Make($star) {
|
|
|
1599
1618
|
let currentDeployTarget = {
|
|
1600
1619
|
contents: "Domain"
|
|
1601
1620
|
};
|
|
1602
|
-
let platformStackRef =
|
|
1621
|
+
let platformStackRef = platformStackReference();
|
|
1603
1622
|
let claimStores = () => {
|
|
1604
1623
|
if (platformStackRef !== undefined) {
|
|
1605
1624
|
return Primitive_option.valFromOption(platformStackRef).getOutput("objectStores").apply(objectStores => Stdlib_Array.filterMap(Object.entries(Stdlib_Option.getOr(Stdlib_Option.flatMap(objectStores, Stdlib_JSON.Decode.object), {})), param => {
|
|
@@ -2984,6 +3003,9 @@ export {
|
|
|
2984
3003
|
messagingEmailProviderRef,
|
|
2985
3004
|
roleGroups,
|
|
2986
3005
|
providedGroups,
|
|
3006
|
+
platformStackRefCache,
|
|
3007
|
+
platformStackReference,
|
|
3008
|
+
loadProvidedGroups,
|
|
2987
3009
|
groupOf,
|
|
2988
3010
|
adminGroup,
|
|
2989
3011
|
MakeWithConfig,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/***
|
|
2
|
+
The user pool's groups, read before a plugin is built so the role check compares
|
|
3
|
+
its roles with the pool, not a manifest (`Role.provideListedGroups`). A group the
|
|
4
|
+
pool lacks refuses every caller, which nothing at runtime can tell from a caller
|
|
5
|
+
without the role. Works in a preview: `ListGroups` is a read, and the pool id is
|
|
6
|
+
read outside any `Output.apply`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module Cognito = AwsSdk.CognitoIdentityServiceProvider
|
|
10
|
+
|
|
11
|
+
type pool = {id: string, region: option<string>}
|
|
12
|
+
|
|
13
|
+
type send = Cognito.ListGroupsCommand.input => promise<Cognito.ListGroupsCommand.output>
|
|
14
|
+
|
|
15
|
+
/** Every group of the pool, following `NextToken` across pages. */
|
|
16
|
+
let listAll = async (~send: send, ~userPoolId: string): array<string> => {
|
|
17
|
+
let rec page = async (nextToken, acc) => {
|
|
18
|
+
let out = await send({userPoolId, limit: 60, ?nextToken})
|
|
19
|
+
let names = out.groups->Option.getOr([])->Array.filterMap(g => g.groupName)
|
|
20
|
+
let acc = acc->Array.concat(names)
|
|
21
|
+
switch out.nextToken {
|
|
22
|
+
| Some(_) as next => await page(next, acc)
|
|
23
|
+
| None => acc
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
await page(None, [])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A plugin stack's pool, from the platform stack's exports. The new key before
|
|
30
|
+
the old one, and each directly before under `default`, where ESM programs
|
|
31
|
+
nest their exports. */
|
|
32
|
+
let poolFromExports = (~get: string => option<JSON.t>): option<pool> => {
|
|
33
|
+
let nested = get("default")->Option.flatMap(JSON.Decode.object)
|
|
34
|
+
let read = key =>
|
|
35
|
+
get(key)
|
|
36
|
+
->Option.orElse(nested->Option.flatMap(d => d->Dict.get(key)))
|
|
37
|
+
->Option.flatMap(JSON.Decode.string)
|
|
38
|
+
read("identityProviderId")
|
|
39
|
+
->Option.orElse(read("cognitoUserPoolId"))
|
|
40
|
+
->Option.map(id => {
|
|
41
|
+
id,
|
|
42
|
+
region: read("identityProviderRegion")->Option.orElse(read("cognitoRegion")),
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
The pool this program deploys against, or `None` where it cannot be listed yet:
|
|
48
|
+
a pool this program creates does not exist before its first deploy.
|
|
49
|
+
|
|
50
|
+
A plugin stack reads its platform stack's exports, the same ones its resolvers
|
|
51
|
+
use. A program that is its own platform reads a supplied pool's id from config.
|
|
52
|
+
*/
|
|
53
|
+
let resolvePool = async (~stackRef: option<Pulumi.StackReference.t>): option<pool> =>
|
|
54
|
+
switch stackRef {
|
|
55
|
+
| Some(stackRef) =>
|
|
56
|
+
let keys = [
|
|
57
|
+
"identityProviderId",
|
|
58
|
+
"cognitoUserPoolId",
|
|
59
|
+
"identityProviderRegion",
|
|
60
|
+
"cognitoRegion",
|
|
61
|
+
"default",
|
|
62
|
+
]
|
|
63
|
+
let values = await Promise.all(
|
|
64
|
+
keys->Array.map(key => stackRef->Pulumi.StackReference.getOutputValue(key)),
|
|
65
|
+
)
|
|
66
|
+
let exports = Dict.fromArray(
|
|
67
|
+
keys->Array.mapWithIndex((key, i) => (key, values->Array.getUnsafe(i))),
|
|
68
|
+
)
|
|
69
|
+
poolFromExports(~get=key => exports->Dict.get(key)->Option.flatMap(v => v))
|
|
70
|
+
| None =>
|
|
71
|
+
Platform_Stack._identityProviderId(
|
|
72
|
+
~cfg=Pulumi.Config.make(Some("platform")),
|
|
73
|
+
)->Option.map(id => {
|
|
74
|
+
id,
|
|
75
|
+
region: Pulumi.Config.make(Some("aws"))->Pulumi.Config.get("region"),
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let sendTo = (~region: option<string>): send => {
|
|
80
|
+
let client = Cognito.Raw.client(~options={region: ?region})
|
|
81
|
+
input => Cognito.ListGroupsCommand.make(input)->Cognito.ListGroupsCommand.Raw.send(client, _)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** List the pool's groups and record them for the role check. Leaves the check
|
|
85
|
+
on its other sources where there is no pool to ask yet. */
|
|
86
|
+
let load = async (~stackRef: option<Pulumi.StackReference.t>): unit =>
|
|
87
|
+
switch await resolvePool(~stackRef) {
|
|
88
|
+
| None => ()
|
|
89
|
+
| Some({id, region}) =>
|
|
90
|
+
let groups = try await listAll(~send=sendTo(~region), ~userPoolId=id) catch {
|
|
91
|
+
| exn if exn->Util_AwsError.hasCode(~code="AccessDeniedException") =>
|
|
92
|
+
JsError.throwWithMessage(
|
|
93
|
+
`Cannot list the groups of the user pool ${id}: the deploying principal needs ` ++ `cognito-idp:ListGroups on it, so the deploy can check that every role a plugin needs has a group.`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
Reventless.Role.provideListedGroups(~source=`the user pool ${id}`, groups)
|
|
97
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Pulumi from "@pulumi/pulumi";
|
|
7
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
8
|
+
import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
|
|
9
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
10
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
11
|
+
import * as Util_AwsError$ReventlessAws from "./util/Util_AwsError.res.mjs";
|
|
12
|
+
import * as Platform_Stack$ReventlessAws from "./Platform_Stack.res.mjs";
|
|
13
|
+
import * as ClientCognitoIdentityProvider from "@aws-sdk/client-cognito-identity-provider";
|
|
14
|
+
|
|
15
|
+
async function listAll(send, userPoolId) {
|
|
16
|
+
let page = async (nextToken, acc) => {
|
|
17
|
+
let out = await send({
|
|
18
|
+
UserPoolId: userPoolId,
|
|
19
|
+
Limit: 60,
|
|
20
|
+
NextToken: nextToken
|
|
21
|
+
});
|
|
22
|
+
let names = Stdlib_Array.filterMap(Stdlib_Option.getOr(out.Groups, []), g => g.GroupName);
|
|
23
|
+
let acc$1 = acc.concat(names);
|
|
24
|
+
let next = out.NextToken;
|
|
25
|
+
if (next !== undefined) {
|
|
26
|
+
return await page(next, acc$1);
|
|
27
|
+
} else {
|
|
28
|
+
return acc$1;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
return await page(undefined, []);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function poolFromExports(get) {
|
|
35
|
+
let nested = Stdlib_Option.flatMap(get("default"), Stdlib_JSON.Decode.object);
|
|
36
|
+
let read = key => Stdlib_Option.flatMap(Stdlib_Option.orElse(get(key), Stdlib_Option.flatMap(nested, d => d[key])), Stdlib_JSON.Decode.string);
|
|
37
|
+
return Stdlib_Option.map(Stdlib_Option.orElse(read("identityProviderId"), read("cognitoUserPoolId")), id => ({
|
|
38
|
+
id: id,
|
|
39
|
+
region: Stdlib_Option.orElse(read("identityProviderRegion"), read("cognitoRegion"))
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function resolvePool(stackRef) {
|
|
44
|
+
if (stackRef === undefined) {
|
|
45
|
+
return Stdlib_Option.map(Platform_Stack$ReventlessAws._identityProviderId(new Pulumi.Config("platform")), id => ({
|
|
46
|
+
id: id,
|
|
47
|
+
region: new Pulumi.Config("aws").get("region")
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
let stackRef$1 = Primitive_option.valFromOption(stackRef);
|
|
51
|
+
let keys = [
|
|
52
|
+
"identityProviderId",
|
|
53
|
+
"cognitoUserPoolId",
|
|
54
|
+
"identityProviderRegion",
|
|
55
|
+
"cognitoRegion",
|
|
56
|
+
"default"
|
|
57
|
+
];
|
|
58
|
+
let values = await Promise.all(keys.map(key => stackRef$1.getOutputValue(key)));
|
|
59
|
+
let $$exports = Object.fromEntries(keys.map((key, i) => [
|
|
60
|
+
key,
|
|
61
|
+
values[i]
|
|
62
|
+
]));
|
|
63
|
+
return poolFromExports(key => Stdlib_Option.flatMap($$exports[key], v => v));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sendTo(region) {
|
|
67
|
+
let client = new ClientCognitoIdentityProvider.CognitoIdentityProviderClient({
|
|
68
|
+
region: region
|
|
69
|
+
});
|
|
70
|
+
return input => client.send(new ClientCognitoIdentityProvider.ListGroupsCommand(input));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function load(stackRef) {
|
|
74
|
+
let match = await resolvePool(stackRef);
|
|
75
|
+
if (match === undefined) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
let id = match.id;
|
|
79
|
+
let groups;
|
|
80
|
+
try {
|
|
81
|
+
groups = await listAll(sendTo(match.region), id);
|
|
82
|
+
} catch (raw_exn) {
|
|
83
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
84
|
+
if (Util_AwsError$ReventlessAws.hasCode(exn, "AccessDeniedException")) {
|
|
85
|
+
groups = Stdlib_JsError.throwWithMessage(`Cannot list the groups of the user pool ` + id + `: the deploying principal needs cognito-idp:ListGroups on it, so the deploy can check that every role a plugin needs has a group.`);
|
|
86
|
+
} else {
|
|
87
|
+
throw exn;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return Role$Reventless.provideListedGroups(`the user pool ` + id, groups);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let Cognito;
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
Cognito,
|
|
97
|
+
listAll,
|
|
98
|
+
poolFromExports,
|
|
99
|
+
resolvePool,
|
|
100
|
+
sendTo,
|
|
101
|
+
load,
|
|
102
|
+
}
|
|
103
|
+
/* @pulumi/pulumi Not a pure module */
|
|
@@ -21,6 +21,8 @@ plugins:
|
|
|
21
21
|
depends-on: []
|
|
22
22
|
- name: ordering
|
|
23
23
|
path: ordering-aws
|
|
24
|
+
region: eu-central-1
|
|
25
|
+
review: false
|
|
24
26
|
`
|
|
25
27
|
|
|
26
28
|
testSync("resolves every folder against the manifest's own folder", () => {
|
|
@@ -36,10 +38,21 @@ plugins:
|
|
|
36
38
|
name: "shop-platform",
|
|
37
39
|
dir: "/repo/shop/platform-aws",
|
|
38
40
|
stackDefaults: Dict.fromArray([("platform:messagingEmailProvider", "log")]),
|
|
41
|
+
region: None,
|
|
39
42
|
},
|
|
40
43
|
plugins: [
|
|
41
|
-
{
|
|
42
|
-
|
|
44
|
+
{
|
|
45
|
+
name: "catalog",
|
|
46
|
+
dir: "/repo/shop/catalog-aws",
|
|
47
|
+
stackDefaults: Dict.make(),
|
|
48
|
+
region: None,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "ordering",
|
|
52
|
+
dir: "/repo/shop/ordering-aws",
|
|
53
|
+
stackDefaults: Dict.make(),
|
|
54
|
+
region: Some("eu-central-1"),
|
|
55
|
+
},
|
|
43
56
|
],
|
|
44
57
|
}),
|
|
45
58
|
)
|
|
@@ -25,6 +25,8 @@ plugins:
|
|
|
25
25
|
depends-on: []
|
|
26
26
|
- name: ordering
|
|
27
27
|
path: ordering-aws
|
|
28
|
+
region: eu-central-1
|
|
29
|
+
review: false
|
|
28
30
|
`), m => DeployManifest$ReventlessAws.resolve(m, "/repo/shop/deploy-manifest.yaml"));
|
|
29
31
|
globalThis.expect(resolved).toEqual({
|
|
30
32
|
TAG: "Ok",
|
|
@@ -37,18 +39,21 @@ plugins:
|
|
|
37
39
|
stackDefaults: Object.fromEntries([[
|
|
38
40
|
"platform:messagingEmailProvider",
|
|
39
41
|
"log"
|
|
40
|
-
]])
|
|
42
|
+
]]),
|
|
43
|
+
region: undefined
|
|
41
44
|
},
|
|
42
45
|
plugins: [
|
|
43
46
|
{
|
|
44
47
|
name: "catalog",
|
|
45
48
|
dir: "/repo/shop/catalog-aws",
|
|
46
|
-
stackDefaults: {}
|
|
49
|
+
stackDefaults: {},
|
|
50
|
+
region: undefined
|
|
47
51
|
},
|
|
48
52
|
{
|
|
49
53
|
name: "ordering",
|
|
50
54
|
dir: "/repo/shop/ordering-aws",
|
|
51
|
-
stackDefaults: {}
|
|
55
|
+
stackDefaults: {},
|
|
56
|
+
region: "eu-central-1"
|
|
52
57
|
}
|
|
53
58
|
]
|
|
54
59
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// Which pending operations the deploy workflow's recovery step drops and which
|
|
4
|
+
// stop it. The pulumi calls around them are checked against a real backend by
|
|
5
|
+
// hand; what is tested here is the decision.
|
|
6
|
+
|
|
7
|
+
module Clear = ClearPendingCreates
|
|
8
|
+
|
|
9
|
+
let export = (operations: string) =>
|
|
10
|
+
JSON.parseOrThrow(
|
|
11
|
+
`{"version": 3, "deployment": {"resources": [], "pending_operations": ${operations}}}`,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
let operation = (~urn, ~type_) =>
|
|
15
|
+
`{"resource": {"urn": "${urn}", "type": "${type_}"}, "type": "creating"}`
|
|
16
|
+
|
|
17
|
+
let validation = operation(
|
|
18
|
+
~urn="urn:pulumi:alpha::app::aws:acm/certificateValidation:CertificateValidation::cert",
|
|
19
|
+
~type_="aws:acm/certificateValidation:CertificateValidation",
|
|
20
|
+
)
|
|
21
|
+
let lambda = operation(
|
|
22
|
+
~urn="urn:pulumi:alpha::app::aws:lambda/function:Function::handler",
|
|
23
|
+
~type_="aws:lambda/function:Function",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
let refused = (state: JSON.t) =>
|
|
27
|
+
state
|
|
28
|
+
->Clear.pendingOperations
|
|
29
|
+
->Array.filter(p => !Clear.isSafe(p))
|
|
30
|
+
->Array.map(p => p.resourceType)
|
|
31
|
+
|
|
32
|
+
describe("ClearPendingCreates.pendingOperations", () => {
|
|
33
|
+
testSync("a stack with none, or with no deployment yet, has none", () => {
|
|
34
|
+
expect(Clear.pendingOperations(export("[]")))->toEqual([])
|
|
35
|
+
expect(
|
|
36
|
+
Clear.pendingOperations(JSON.parseOrThrow(`{"version": 3, "deployment": {}}`)),
|
|
37
|
+
)->toEqual([])
|
|
38
|
+
expect(Clear.pendingOperations(JSON.parseOrThrow(`{"version": 3}`)))->toEqual([])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
testSync("each operation with its resource and kind", () =>
|
|
42
|
+
expect(Clear.pendingOperations(export(`[${lambda}]`)))->toEqual([
|
|
43
|
+
{
|
|
44
|
+
Clear.urn: "urn:pulumi:alpha::app::aws:lambda/function:Function::handler",
|
|
45
|
+
resourceType: "aws:lambda/function:Function",
|
|
46
|
+
operation: "creating",
|
|
47
|
+
},
|
|
48
|
+
])
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe("ClearPendingCreates.isSafe", () => {
|
|
53
|
+
testSync("a certificate validation is dropped", () =>
|
|
54
|
+
expect(refused(export(`[${validation}]`)))->toEqual([])
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
testSync("a resource that may exist in AWS stops the deploy", () =>
|
|
58
|
+
expect(refused(export(`[${lambda}]`)))->toEqual(["aws:lambda/function:Function"])
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
testSync("in a mix, only the unsafe one is refused", () =>
|
|
62
|
+
expect(refused(export(`[${validation}, ${lambda}]`)))->toEqual(["aws:lambda/function:Function"])
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
testSync("the type must match whole, not as a prefix", () =>
|
|
66
|
+
expect(
|
|
67
|
+
refused(
|
|
68
|
+
export(
|
|
69
|
+
`[${operation(~urn="u", ~type_="aws:acm/certificateValidation:CertificateValidationX")}]`,
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
)->toEqual(["aws:acm/certificateValidation:CertificateValidationX"])
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe("ClearPendingCreates.parseArgs", () => {
|
|
77
|
+
testSync("the stack, and the project directory", () =>
|
|
78
|
+
expect(Clear.parseArgs(["--stack", "alpha", "--cwd", "platform-aws"]))->toEqual(
|
|
79
|
+
Ok({Clear.stack: "alpha", cwd: "platform-aws"}),
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
testSync("the project directory defaults to the working directory", () =>
|
|
84
|
+
expect(Clear.parseArgs(["--stack", "alpha"])->Result.map(a => a.cwd))->toEqual(
|
|
85
|
+
Ok(NodeProcess.cwd()),
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
test("no stack is a usage mistake", async () => {
|
|
90
|
+
let observed = await Reventless.CliArgs.observe(Clear.cli, [])
|
|
91
|
+
expect((observed.exitCode, observed.reachedMain))->toEqual((Some(2), false))
|
|
92
|
+
})
|
|
93
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
4
|
+
import * as CliArgs$Reventless from "@reventlessdev/reventless-spec/src/CliArgs.res.mjs";
|
|
5
|
+
import * as ClearPendingCreates$ReventlessAws from "../scripts/ClearPendingCreates.res.mjs";
|
|
6
|
+
|
|
7
|
+
function $$export(operations) {
|
|
8
|
+
return JSON.parse(`{"version": 3, "deployment": {"resources": [], "pending_operations": ` + operations + `}}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function operation(urn, type_) {
|
|
12
|
+
return `{"resource": {"urn": "` + urn + `", "type": "` + type_ + `"}, "type": "creating"}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let validation = `{"resource": {"urn": "` + "urn:pulumi:alpha::app::aws:acm/certificateValidation:CertificateValidation::cert" + `", "type": "` + "aws:acm/certificateValidation:CertificateValidation" + `"}, "type": "creating"}`;
|
|
16
|
+
|
|
17
|
+
let lambda = `{"resource": {"urn": "` + "urn:pulumi:alpha::app::aws:lambda/function:Function::handler" + `", "type": "` + "aws:lambda/function:Function" + `"}, "type": "creating"}`;
|
|
18
|
+
|
|
19
|
+
function refused(state) {
|
|
20
|
+
return ClearPendingCreates$ReventlessAws.pendingOperations(state).filter(p => !ClearPendingCreates$ReventlessAws.isSafe(p)).map(p => p.resourceType);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
globalThis.describe("ClearPendingCreates.pendingOperations", () => {
|
|
24
|
+
globalThis.test("a stack with none, or with no deployment yet, has none", () => {
|
|
25
|
+
globalThis.expect(ClearPendingCreates$ReventlessAws.pendingOperations($$export("[]"))).toEqual([]);
|
|
26
|
+
globalThis.expect(ClearPendingCreates$ReventlessAws.pendingOperations(JSON.parse(`{"version": 3, "deployment": {}}`))).toEqual([]);
|
|
27
|
+
globalThis.expect(ClearPendingCreates$ReventlessAws.pendingOperations(JSON.parse(`{"version": 3}`))).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
globalThis.test("each operation with its resource and kind", () => {
|
|
30
|
+
globalThis.expect(ClearPendingCreates$ReventlessAws.pendingOperations($$export(`[` + lambda + `]`))).toEqual([{
|
|
31
|
+
urn: "urn:pulumi:alpha::app::aws:lambda/function:Function::handler",
|
|
32
|
+
resourceType: "aws:lambda/function:Function",
|
|
33
|
+
operation: "creating"
|
|
34
|
+
}]);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
globalThis.describe("ClearPendingCreates.isSafe", () => {
|
|
39
|
+
globalThis.test("a certificate validation is dropped", () => {
|
|
40
|
+
globalThis.expect(refused($$export(`[` + validation + `]`))).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
globalThis.test("a resource that may exist in AWS stops the deploy", () => {
|
|
43
|
+
globalThis.expect(refused($$export(`[` + lambda + `]`))).toEqual(["aws:lambda/function:Function"]);
|
|
44
|
+
});
|
|
45
|
+
globalThis.test("in a mix, only the unsafe one is refused", () => {
|
|
46
|
+
globalThis.expect(refused($$export(`[` + validation + `, ` + lambda + `]`))).toEqual(["aws:lambda/function:Function"]);
|
|
47
|
+
});
|
|
48
|
+
globalThis.test("the type must match whole, not as a prefix", () => {
|
|
49
|
+
globalThis.expect(refused($$export(`[` + (`{"resource": {"urn": "` + "u" + `", "type": "` + "aws:acm/certificateValidation:CertificateValidationX" + `"}, "type": "creating"}`) + `]`))).toEqual(["aws:acm/certificateValidation:CertificateValidationX"]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
globalThis.describe("ClearPendingCreates.parseArgs", () => {
|
|
54
|
+
globalThis.test("the stack, and the project directory", () => {
|
|
55
|
+
globalThis.expect(ClearPendingCreates$ReventlessAws.parseArgs([
|
|
56
|
+
"--stack",
|
|
57
|
+
"alpha",
|
|
58
|
+
"--cwd",
|
|
59
|
+
"platform-aws"
|
|
60
|
+
])).toEqual({
|
|
61
|
+
TAG: "Ok",
|
|
62
|
+
_0: {
|
|
63
|
+
stack: "alpha",
|
|
64
|
+
cwd: "platform-aws"
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
globalThis.test("the project directory defaults to the working directory", () => {
|
|
69
|
+
globalThis.expect(Stdlib_Result.map(ClearPendingCreates$ReventlessAws.parseArgs([
|
|
70
|
+
"--stack",
|
|
71
|
+
"alpha"
|
|
72
|
+
]), a => a.cwd)).toEqual({
|
|
73
|
+
TAG: "Ok",
|
|
74
|
+
_0: process.cwd()
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
globalThis.test("no stack is a usage mistake", async () => {
|
|
78
|
+
let observed = await CliArgs$Reventless.observe(ClearPendingCreates$ReventlessAws.cli, []);
|
|
79
|
+
globalThis.expect([
|
|
80
|
+
observed.exitCode,
|
|
81
|
+
observed.reachedMain
|
|
82
|
+
]).toEqual([
|
|
83
|
+
2,
|
|
84
|
+
false
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
let Clear;
|
|
90
|
+
|
|
91
|
+
export {
|
|
92
|
+
Clear,
|
|
93
|
+
$$export,
|
|
94
|
+
operation,
|
|
95
|
+
validation,
|
|
96
|
+
lambda,
|
|
97
|
+
refused,
|
|
98
|
+
}
|
|
99
|
+
/* Not a pure module */
|
package/tests/DeployAppTest.res
CHANGED
|
@@ -55,8 +55,19 @@ describe("DeployApp stacks and layer", () => {
|
|
|
55
55
|
projectName: "shop-platform-aws",
|
|
56
56
|
program: "/app/platform-aws/src/Main.res.mjs",
|
|
57
57
|
stackDefaults: Dict.fromArray([("aws:region", "us-east-1"), ("platform:x", "y")]),
|
|
58
|
+
region: None,
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
testSync("a stack the manifest puts in another region is refused", () => {
|
|
62
|
+
let ingest = {...project, label: "ingest", region: Some("eu-central-1")}
|
|
63
|
+
expect(
|
|
64
|
+
DeployApp.regionProblems(~projects=[project, ingest], ~region="eu-west-1")->Array.length,
|
|
65
|
+
)->toBe(1)
|
|
66
|
+
expect(
|
|
67
|
+
DeployApp.regionProblems(~projects=[project, ingest], ~region="eu-central-1"),
|
|
68
|
+
)->toEqual([])
|
|
69
|
+
})
|
|
70
|
+
|
|
60
71
|
// The region and the try-out flag come last, so an app default cannot turn a
|
|
61
72
|
// try-out into a stack `down` refuses to remove.
|
|
62
73
|
testSync("a new stack gets the app's defaults, then the region and the try-out flag", () =>
|
|
@@ -79,8 +79,29 @@ globalThis.describe("DeployApp stacks and layer", () => {
|
|
|
79
79
|
dir: "/app/platform-aws",
|
|
80
80
|
projectName: "shop-platform-aws",
|
|
81
81
|
program: "/app/platform-aws/src/Main.res.mjs",
|
|
82
|
-
stackDefaults: project_stackDefaults
|
|
82
|
+
stackDefaults: project_stackDefaults,
|
|
83
|
+
region: undefined
|
|
83
84
|
};
|
|
85
|
+
globalThis.test("a stack the manifest puts in another region is refused", () => {
|
|
86
|
+
let ingest_stackDefaults = project_stackDefaults;
|
|
87
|
+
let ingest_region = "eu-central-1";
|
|
88
|
+
let ingest = {
|
|
89
|
+
label: "ingest",
|
|
90
|
+
dir: "/app/platform-aws",
|
|
91
|
+
projectName: "shop-platform-aws",
|
|
92
|
+
program: "/app/platform-aws/src/Main.res.mjs",
|
|
93
|
+
stackDefaults: ingest_stackDefaults,
|
|
94
|
+
region: ingest_region
|
|
95
|
+
};
|
|
96
|
+
globalThis.expect(DeployApp$ReventlessAws.regionProblems([
|
|
97
|
+
project,
|
|
98
|
+
ingest
|
|
99
|
+
], "eu-west-1").length).toBe(1);
|
|
100
|
+
globalThis.expect(DeployApp$ReventlessAws.regionProblems([
|
|
101
|
+
project,
|
|
102
|
+
ingest
|
|
103
|
+
], "eu-central-1")).toEqual([]);
|
|
104
|
+
});
|
|
84
105
|
globalThis.test("a new stack gets the app's defaults, then the region and the try-out flag", () => {
|
|
85
106
|
globalThis.expect(DeployApp$ReventlessAws.newStackConfig(project, "eu-west-1")).toEqual([
|
|
86
107
|
[
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// What the role check compares a plugin's roles with on AWS: the groups the user
|
|
4
|
+
// pool reports, found through the platform stack's exports.
|
|
5
|
+
|
|
6
|
+
module Cognito = AwsSdk.CognitoIdentityServiceProvider
|
|
7
|
+
|
|
8
|
+
let group = (name): Cognito.ListGroupsCommand.group => {groupName: name}
|
|
9
|
+
|
|
10
|
+
describe("Platform_ProvidedGroups.listAll", () => {
|
|
11
|
+
test("follows NextToken until the last page", async () => {
|
|
12
|
+
let seen = []
|
|
13
|
+
let send = async (input: Cognito.ListGroupsCommand.input): Cognito.ListGroupsCommand.output => {
|
|
14
|
+
seen->Array.push(input.nextToken)
|
|
15
|
+
switch input.nextToken {
|
|
16
|
+
| None => {groups: [group("Admin"), group("Shopper")], nextToken: "p2"}
|
|
17
|
+
| Some(_) => {groups: [group("Merchandiser")]}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
let groups = await Platform_ProvidedGroups.listAll(~send, ~userPoolId="p1")
|
|
21
|
+
expect((groups, seen))->toEqual((["Admin", "Shopper", "Merchandiser"], [None, Some("p2")]))
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe("Platform_ProvidedGroups.poolFromExports", () => {
|
|
26
|
+
let from = (pairs: array<(string, JSON.t)>) => {
|
|
27
|
+
let exports = Dict.fromArray(pairs)
|
|
28
|
+
Platform_ProvidedGroups.poolFromExports(~get=key => exports->Dict.get(key))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
testSync("reads the pool id and region", () =>
|
|
32
|
+
expect(
|
|
33
|
+
from([
|
|
34
|
+
("identityProviderId", JSON.Encode.string("p1")),
|
|
35
|
+
("identityProviderRegion", JSON.Encode.string("eu-west-1")),
|
|
36
|
+
]),
|
|
37
|
+
)->toEqual(Some({Platform_ProvidedGroups.id: "p1", region: Some("eu-west-1")}))
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
testSync("reads them nested under default, as an ESM platform exports them", () =>
|
|
41
|
+
expect(
|
|
42
|
+
from([
|
|
43
|
+
(
|
|
44
|
+
"default",
|
|
45
|
+
JSON.Encode.object(Dict.fromArray([("cognitoUserPoolId", JSON.Encode.string("p-old"))])),
|
|
46
|
+
),
|
|
47
|
+
]),
|
|
48
|
+
)->toEqual(Some({Platform_ProvidedGroups.id: "p-old", region: None}))
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
testSync("no pool id is no pool to list", () => expect(from([]))->toEqual(None))
|
|
52
|
+
})
|