@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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/***
|
|
2
|
+
An app's `deploy-manifest.yaml`: the folder holding the platform stack, and the
|
|
3
|
+
folders holding the plugin stacks, in the order they deploy.
|
|
4
|
+
|
|
5
|
+
The reusable deploy workflow reads it with `yq`; the commands in this folder read
|
|
6
|
+
it here. Paths in the file are relative to the file's own folder.
|
|
7
|
+
|
|
8
|
+
`stack-defaults` on an entry are the settings `deploy-app` gives a stack it
|
|
9
|
+
creates for that folder — what a new stack of this app cannot deploy without.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
@module("yaml") external parseYaml: string => JSON.t = "parse"
|
|
13
|
+
|
|
14
|
+
@schema
|
|
15
|
+
type platform = {
|
|
16
|
+
path: string,
|
|
17
|
+
name?: string,
|
|
18
|
+
@as("stack-defaults") stackDefaults?: dict<string>,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@schema
|
|
22
|
+
type plugin = {
|
|
23
|
+
name: string,
|
|
24
|
+
path: string,
|
|
25
|
+
@as("depends-on") dependsOn?: array<string>,
|
|
26
|
+
@as("stack-defaults") stackDefaults?: dict<string>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@schema
|
|
30
|
+
type t = {
|
|
31
|
+
region?: string,
|
|
32
|
+
platform: platform,
|
|
33
|
+
plugins?: array<plugin>,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One stack folder, with its path made absolute. */
|
|
37
|
+
type project = {name: string, dir: string, stackDefaults: dict<string>}
|
|
38
|
+
|
|
39
|
+
type resolved = {
|
|
40
|
+
file: string,
|
|
41
|
+
region: option<string>,
|
|
42
|
+
platform: project,
|
|
43
|
+
plugins: array<project>,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let defaultFile = "deploy-manifest.yaml"
|
|
47
|
+
|
|
48
|
+
let parseString = (text: string): result<t, string> =>
|
|
49
|
+
try Ok(S.parseOrThrow(parseYaml(text), ~to=schema)) catch {
|
|
50
|
+
| JsExn(err) => Error(JsExn.message(err)->Option.getOr("not a deploy manifest"))
|
|
51
|
+
| _ => Error("not a deploy manifest")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let resolve = (manifest: t, ~file: string): resolved => {
|
|
55
|
+
let base = NodePath.dirname(NodePath.resolve([file]))
|
|
56
|
+
{
|
|
57
|
+
file,
|
|
58
|
+
region: manifest.region,
|
|
59
|
+
platform: {
|
|
60
|
+
name: manifest.platform.name->Option.getOr("platform"),
|
|
61
|
+
dir: NodePath.resolve([base, manifest.platform.path]),
|
|
62
|
+
stackDefaults: manifest.platform.stackDefaults->Option.getOr(Dict.make()),
|
|
63
|
+
},
|
|
64
|
+
plugins: manifest.plugins
|
|
65
|
+
->Option.getOr([])
|
|
66
|
+
->Array.map(p => {
|
|
67
|
+
name: p.name,
|
|
68
|
+
dir: NodePath.resolve([base, p.path]),
|
|
69
|
+
stackDefaults: p.stackDefaults->Option.getOr(Dict.make()),
|
|
70
|
+
}),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let load = (file: string): result<resolved, string> =>
|
|
75
|
+
switch try Ok(NodeFs.readFileSync(file)) catch {
|
|
76
|
+
| _ => Error(`cannot read ${file}`)
|
|
77
|
+
} {
|
|
78
|
+
| Error(_) as e => e
|
|
79
|
+
| Ok(text) =>
|
|
80
|
+
switch parseString(text) {
|
|
81
|
+
| Error(message) => Error(`${file}: ${message}`)
|
|
82
|
+
| Ok(manifest) => Ok(manifest->resolve(~file))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Sury from "sury";
|
|
5
|
+
import * as Yaml from "yaml";
|
|
6
|
+
import * as Nodefs from "node:fs";
|
|
7
|
+
import * as Nodepath from "node:path";
|
|
8
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
9
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
10
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
11
|
+
|
|
12
|
+
let platformSchema = Sury.$schema(s => ({
|
|
13
|
+
path: s.m(Sury.string),
|
|
14
|
+
name: s.m(Sury.$option(Sury.string)),
|
|
15
|
+
"stack-defaults": s.m(Sury.$option(Sury.dict(Sury.string)))
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
let pluginSchema = Sury.$schema(s => ({
|
|
19
|
+
name: s.m(Sury.string),
|
|
20
|
+
path: s.m(Sury.string),
|
|
21
|
+
"depends-on": s.m(Sury.$option(Sury.array(Sury.string))),
|
|
22
|
+
"stack-defaults": s.m(Sury.$option(Sury.dict(Sury.string)))
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
let schema = Sury.$schema(s => ({
|
|
26
|
+
region: s.m(Sury.$option(Sury.string)),
|
|
27
|
+
platform: s.m(platformSchema),
|
|
28
|
+
plugins: s.m(Sury.$option(Sury.array(pluginSchema)))
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
function parseString(text) {
|
|
32
|
+
try {
|
|
33
|
+
return {
|
|
34
|
+
TAG: "Ok",
|
|
35
|
+
_0: S.parseOrThrow(Yaml.parse(text), schema)
|
|
36
|
+
};
|
|
37
|
+
} catch (raw_err) {
|
|
38
|
+
let err = Primitive_exceptions.internalToException(raw_err);
|
|
39
|
+
if (err.RE_EXN_ID === "JsExn") {
|
|
40
|
+
return {
|
|
41
|
+
TAG: "Error",
|
|
42
|
+
_0: Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "not a deploy manifest")
|
|
43
|
+
};
|
|
44
|
+
} else {
|
|
45
|
+
return {
|
|
46
|
+
TAG: "Error",
|
|
47
|
+
_0: "not a deploy manifest"
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolve(manifest, file) {
|
|
54
|
+
let base = Nodepath.dirname(Nodepath.resolve(file));
|
|
55
|
+
return {
|
|
56
|
+
file: file,
|
|
57
|
+
region: manifest.region,
|
|
58
|
+
platform: {
|
|
59
|
+
name: Stdlib_Option.getOr(manifest.platform.name, "platform"),
|
|
60
|
+
dir: Nodepath.resolve(base, manifest.platform.path),
|
|
61
|
+
stackDefaults: Stdlib_Option.getOr(manifest.platform["stack-defaults"], {})
|
|
62
|
+
},
|
|
63
|
+
plugins: Stdlib_Option.getOr(manifest.plugins, []).map(p => ({
|
|
64
|
+
name: p.name,
|
|
65
|
+
dir: Nodepath.resolve(base, p.path),
|
|
66
|
+
stackDefaults: Stdlib_Option.getOr(p["stack-defaults"], {})
|
|
67
|
+
}))
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function load(file) {
|
|
72
|
+
let e;
|
|
73
|
+
try {
|
|
74
|
+
e = {
|
|
75
|
+
TAG: "Ok",
|
|
76
|
+
_0: Nodefs.readFileSync(file, "utf8")
|
|
77
|
+
};
|
|
78
|
+
} catch (exn) {
|
|
79
|
+
e = {
|
|
80
|
+
TAG: "Error",
|
|
81
|
+
_0: `cannot read ` + file
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (e.TAG !== "Ok") {
|
|
85
|
+
return e;
|
|
86
|
+
}
|
|
87
|
+
let message = parseString(e._0);
|
|
88
|
+
if (message.TAG === "Ok") {
|
|
89
|
+
return {
|
|
90
|
+
TAG: "Ok",
|
|
91
|
+
_0: resolve(message._0, file)
|
|
92
|
+
};
|
|
93
|
+
} else {
|
|
94
|
+
return {
|
|
95
|
+
TAG: "Error",
|
|
96
|
+
_0: file + `: ` + message._0
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let defaultFile = "deploy-manifest.yaml";
|
|
102
|
+
|
|
103
|
+
export {
|
|
104
|
+
platformSchema,
|
|
105
|
+
pluginSchema,
|
|
106
|
+
schema,
|
|
107
|
+
defaultFile,
|
|
108
|
+
parseString,
|
|
109
|
+
resolve,
|
|
110
|
+
load,
|
|
111
|
+
}
|
|
112
|
+
/* platformSchema Not a pure module */
|
|
@@ -121,8 +121,8 @@ question is whether the pool will accept *those*. A pool created with
|
|
|
121
121
|
`UsernameAttributes: [email]` will not, and the accounts it makes instead are
|
|
122
122
|
correctly created and unable to authenticate.
|
|
123
123
|
|
|
124
|
-
Checked for every entry before anything is created, so a manifest that
|
|
125
|
-
costs nothing rather than half a cast.
|
|
124
|
+
Checked for every entry before anything is created or written, so a manifest that
|
|
125
|
+
cannot work costs nothing rather than half a cast.
|
|
126
126
|
*/
|
|
127
127
|
let checkPoolAcceptsUsernames = async (~providerId: string, ~usernames: array<string>): result<
|
|
128
128
|
unit,
|
|
@@ -239,6 +239,58 @@ let manifestNote = (~file: string) =>
|
|
|
239
239
|
It is gitignored on every platform; keep it that way. These are bootstrap
|
|
240
240
|
credentials for a deployment, not secrets to reuse anywhere real.`
|
|
241
241
|
|
|
242
|
+
/**
|
|
243
|
+
The manifest at `file`, applied to the pool `providerId`.
|
|
244
|
+
|
|
245
|
+
🚨 **The pool is checked before anything is written.** Preparing mints passwords
|
|
246
|
+
into the file; doing that first left a refused run with a manifest offering
|
|
247
|
+
accounts that exist nowhere, which the seed then tried to sign in as. `checkPool`
|
|
248
|
+
is a parameter so a test can refuse without a pool.
|
|
249
|
+
*/
|
|
250
|
+
let provision = async (
|
|
251
|
+
~file: string,
|
|
252
|
+
~providerId: string,
|
|
253
|
+
~checkPool=checkPoolAcceptsUsernames,
|
|
254
|
+
): result<unit, string> =>
|
|
255
|
+
switch Manifest.parseFile(file) {
|
|
256
|
+
| Error(message) => Error(`${file}: ${message}`)
|
|
257
|
+
| Ok([]) => Error(`${file} declares no accounts`)
|
|
258
|
+
| Ok(declared) =>
|
|
259
|
+
switch await checkPool(~providerId, ~usernames=declared->Array.map(e => e.username)) {
|
|
260
|
+
| Error(_) as e => e
|
|
261
|
+
| Ok() =>
|
|
262
|
+
switch Manifest.prepare(~path=file) {
|
|
263
|
+
| Error(message) => Error(`${file}: ${message}`)
|
|
264
|
+
| Ok(prepared) =>
|
|
265
|
+
let generated = prepared->Array.filter(p => p.passwordGenerated)->Array.length
|
|
266
|
+
Console.log(
|
|
267
|
+
`manifest ${file} (${prepared
|
|
268
|
+
->Array.length
|
|
269
|
+
->Int.toString} accounts, ${generated->Int.toString} password(s) generated)`,
|
|
270
|
+
)
|
|
271
|
+
let entries = prepared->Array.map(p => p.entry)
|
|
272
|
+
await ensureGroups(~providerId, ~entries)
|
|
273
|
+
let fills = []
|
|
274
|
+
for index in 0 to entries->Array.length - 1 {
|
|
275
|
+
let entry = entries->Array.getUnsafe(index)
|
|
276
|
+
let userId = await applyEntry(~providerId, ~entry)
|
|
277
|
+
fills->Array.push({Manifest.index, password: None, userId})
|
|
278
|
+
}
|
|
279
|
+
switch Manifest.fillFile(~path=file, ~fills) {
|
|
280
|
+
| Error(message) => Error(`${file}: ${message}`)
|
|
281
|
+
| Ok(report) =>
|
|
282
|
+
let corrected = report->Array.filter(r => r.userIdWritten)->Array.length
|
|
283
|
+
Console.log(
|
|
284
|
+
`manifest ${file} (${corrected->Int.toString} id(s) written back)${manifestNote(
|
|
285
|
+
~file,
|
|
286
|
+
)}`,
|
|
287
|
+
)
|
|
288
|
+
Ok()
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
242
294
|
let run = async (): result<unit, string> =>
|
|
243
295
|
// argv[0] is node, argv[1] this script.
|
|
244
296
|
switch parseArgs(NodeProcess.argv->Array.slice(~start=2, ~end=NodeProcess.argv->Array.length)) {
|
|
@@ -255,50 +307,12 @@ let run = async (): result<unit, string> =>
|
|
|
255
307
|
| SeededFrom(_, template) => Console.log(`manifest ${file} (new, copied from ${template})`)
|
|
256
308
|
| Declared(_) => ()
|
|
257
309
|
}
|
|
258
|
-
switch
|
|
259
|
-
| Error(message) =>
|
|
260
|
-
|
|
261
|
-
| Ok(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
`manifest ${file} (${prepared
|
|
265
|
-
->Array.length
|
|
266
|
-
->Int.toString} accounts, ${generated->Int.toString} password(s) generated)`,
|
|
267
|
-
)
|
|
268
|
-
let entries = prepared->Array.map(p => p.entry)
|
|
269
|
-
switch ProvisionProvider.resolve(~given=args.providerId, ~stack=args.stack) {
|
|
270
|
-
| Error(message) =>
|
|
271
|
-
Error(
|
|
272
|
-
`${message}. To fill in the manifest without creating anything, run prepare-accounts`,
|
|
273
|
-
)
|
|
274
|
-
| Ok((providerId, source)) =>
|
|
275
|
-
Console.log(`provider ${providerId} (from ${source->ProvisionProvider.describe})`)
|
|
276
|
-
switch await checkPoolAcceptsUsernames(
|
|
277
|
-
~providerId,
|
|
278
|
-
~usernames=entries->Array.map(e => e.username),
|
|
279
|
-
) {
|
|
280
|
-
| Error(_) as e => e
|
|
281
|
-
| Ok() =>
|
|
282
|
-
await ensureGroups(~providerId, ~entries)
|
|
283
|
-
let fills = []
|
|
284
|
-
for index in 0 to entries->Array.length - 1 {
|
|
285
|
-
let entry = entries->Array.getUnsafe(index)
|
|
286
|
-
let userId = await applyEntry(~providerId, ~entry)
|
|
287
|
-
fills->Array.push({Manifest.index, password: None, userId})
|
|
288
|
-
}
|
|
289
|
-
switch Manifest.fillFile(~path=file, ~fills) {
|
|
290
|
-
| Error(message) => Error(`${file}: ${message}`)
|
|
291
|
-
| Ok(report) =>
|
|
292
|
-
let corrected = report->Array.filter(r => r.userIdWritten)->Array.length
|
|
293
|
-
Console.log(
|
|
294
|
-
`manifest ${file} (${corrected->Int.toString} id(s) written back)${manifestNote(
|
|
295
|
-
~file,
|
|
296
|
-
)}`,
|
|
297
|
-
)
|
|
298
|
-
Ok()
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
310
|
+
switch ProvisionProvider.resolve(~given=args.providerId, ~stack=args.stack) {
|
|
311
|
+
| Error(message) =>
|
|
312
|
+
Error(`${message}. To fill in the manifest without creating anything, run prepare-accounts`)
|
|
313
|
+
| Ok((providerId, source)) =>
|
|
314
|
+
Console.log(`provider ${providerId} (from ${source->ProvisionProvider.describe})`)
|
|
315
|
+
await provision(~file, ~providerId)
|
|
302
316
|
}
|
|
303
317
|
}
|
|
304
318
|
}
|
|
@@ -220,59 +220,37 @@ function manifestNote(file) {
|
|
|
220
220
|
credentials for a deployment, not secrets to reuse anywhere real.`;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
async function
|
|
224
|
-
let
|
|
225
|
-
|
|
226
|
-
return e;
|
|
227
|
-
}
|
|
228
|
-
let args = e._0;
|
|
229
|
-
if (args.help) {
|
|
230
|
-
console.log(usage);
|
|
231
|
-
return {
|
|
232
|
-
TAG: "Ok",
|
|
233
|
-
_0: undefined
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
let e$1 = AccountsManifest$Reventless.locate(args.file, undefined);
|
|
237
|
-
if (e$1.TAG !== "Ok") {
|
|
238
|
-
return e$1;
|
|
239
|
-
}
|
|
240
|
-
let located = e$1._0;
|
|
241
|
-
let file = AccountsManifest$Reventless.pathOf(located);
|
|
242
|
-
if (located.TAG !== "Declared") {
|
|
243
|
-
console.log(`manifest ` + file + ` (new, copied from ` + located._1 + `)`);
|
|
244
|
-
}
|
|
245
|
-
let message = AccountsManifest$Reventless.prepare(file);
|
|
223
|
+
async function provision(file, providerId, checkPoolOpt) {
|
|
224
|
+
let checkPool = checkPoolOpt !== undefined ? checkPoolOpt : checkPoolAcceptsUsernames;
|
|
225
|
+
let message = AccountsManifest$Reventless.parseFile(file);
|
|
246
226
|
if (message.TAG !== "Ok") {
|
|
247
227
|
return {
|
|
248
228
|
TAG: "Error",
|
|
249
229
|
_0: file + `: ` + message._0
|
|
250
230
|
};
|
|
251
231
|
}
|
|
252
|
-
let
|
|
253
|
-
if (
|
|
232
|
+
let declared = message._0;
|
|
233
|
+
if (declared.length === 0) {
|
|
254
234
|
return {
|
|
255
235
|
TAG: "Error",
|
|
256
236
|
_0: file + ` declares no accounts`
|
|
257
237
|
};
|
|
258
238
|
}
|
|
259
|
-
let
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
239
|
+
let e = await checkPool(providerId, declared.map(e => e.username));
|
|
240
|
+
if (e.TAG !== "Ok") {
|
|
241
|
+
return e;
|
|
242
|
+
}
|
|
243
|
+
let message$1 = AccountsManifest$Reventless.prepare(file);
|
|
263
244
|
if (message$1.TAG !== "Ok") {
|
|
264
245
|
return {
|
|
265
246
|
TAG: "Error",
|
|
266
|
-
_0:
|
|
247
|
+
_0: file + `: ` + message$1._0
|
|
267
248
|
};
|
|
268
249
|
}
|
|
269
|
-
let
|
|
270
|
-
let
|
|
271
|
-
console.log(`
|
|
272
|
-
let
|
|
273
|
-
if (e$2.TAG !== "Ok") {
|
|
274
|
-
return e$2;
|
|
275
|
-
}
|
|
250
|
+
let prepared = message$1._0;
|
|
251
|
+
let generated = prepared.filter(p => p.passwordGenerated).length;
|
|
252
|
+
console.log(`manifest ` + file + ` (` + prepared.length.toString() + ` accounts, ` + generated.toString() + ` password(s) generated)`);
|
|
253
|
+
let entries = prepared.map(p => p.entry);
|
|
276
254
|
await ensureGroups(providerId, entries);
|
|
277
255
|
let fills = [];
|
|
278
256
|
for (let index = 0, index_finish = entries.length; index < index_finish; ++index) {
|
|
@@ -299,6 +277,41 @@ async function run() {
|
|
|
299
277
|
};
|
|
300
278
|
}
|
|
301
279
|
|
|
280
|
+
async function run() {
|
|
281
|
+
let e = parseArgs(process.argv.slice(2, process.argv.length));
|
|
282
|
+
if (e.TAG !== "Ok") {
|
|
283
|
+
return e;
|
|
284
|
+
}
|
|
285
|
+
let args = e._0;
|
|
286
|
+
if (args.help) {
|
|
287
|
+
console.log(usage);
|
|
288
|
+
return {
|
|
289
|
+
TAG: "Ok",
|
|
290
|
+
_0: undefined
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
let e$1 = AccountsManifest$Reventless.locate(args.file, undefined);
|
|
294
|
+
if (e$1.TAG !== "Ok") {
|
|
295
|
+
return e$1;
|
|
296
|
+
}
|
|
297
|
+
let located = e$1._0;
|
|
298
|
+
let file = AccountsManifest$Reventless.pathOf(located);
|
|
299
|
+
if (located.TAG !== "Declared") {
|
|
300
|
+
console.log(`manifest ` + file + ` (new, copied from ` + located._1 + `)`);
|
|
301
|
+
}
|
|
302
|
+
let message = ProvisionProvider$ReventlessAws.resolve(args.providerId, args.stack);
|
|
303
|
+
if (message.TAG !== "Ok") {
|
|
304
|
+
return {
|
|
305
|
+
TAG: "Error",
|
|
306
|
+
_0: message._0 + `. To fill in the manifest without creating anything, run prepare-accounts`
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
let match = message._0;
|
|
310
|
+
let providerId = match[0];
|
|
311
|
+
console.log(`provider ` + providerId + ` (from ` + ProvisionProvider$ReventlessAws.describe(match[1]) + `)`);
|
|
312
|
+
return await provision(file, providerId, undefined);
|
|
313
|
+
}
|
|
314
|
+
|
|
302
315
|
async function main() {
|
|
303
316
|
let message;
|
|
304
317
|
try {
|
|
@@ -331,6 +344,7 @@ export {
|
|
|
331
344
|
ensureGroups,
|
|
332
345
|
applyEntry,
|
|
333
346
|
manifestNote,
|
|
347
|
+
provision,
|
|
334
348
|
run,
|
|
335
349
|
main,
|
|
336
350
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/***
|
|
2
|
+
Reading deployed stacks through the `pulumi` CLI, so a command follows whatever
|
|
3
|
+
backend the CLI is logged into.
|
|
4
|
+
|
|
5
|
+
stderr goes to the caller's terminal: a Pulumi failure is legible where it
|
|
6
|
+
happens rather than re-reported here as a vaguer message.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
let run = (~cwd: string, args: array<string>): string =>
|
|
10
|
+
NodeChildProcess.execFileSync(
|
|
11
|
+
"pulumi",
|
|
12
|
+
args->Array.concat(["--cwd", cwd, "--non-interactive"]),
|
|
13
|
+
{encoding: "utf8", stdio: ["ignore", "pipe", "inherit"], maxBuffer: 64 * 1024 * 1024},
|
|
14
|
+
)->String.trim
|
|
15
|
+
|
|
16
|
+
/** Whether the project in `dir` keeps settings for `stack` — how the deploy
|
|
17
|
+
workflow tells a stack that was never deployed from one that was. */
|
|
18
|
+
let hasStackFile = (~dir: string, ~stack: string): bool =>
|
|
19
|
+
NodeFs.existsSync(NodePath.join([dir, `Pulumi.${stack}.yaml`]))
|
|
20
|
+
|
|
21
|
+
let stackOutputs = (~dir: string, ~stack: string): result<dict<JSON.t>, string> =>
|
|
22
|
+
switch run(~cwd=dir, ["stack", "output", "--json", "--stack", stack]) {
|
|
23
|
+
| text =>
|
|
24
|
+
switch text->JSON.parseOrThrow->JSON.Decode.object {
|
|
25
|
+
| Some(outputs) => Ok(outputs)
|
|
26
|
+
| None => Error(`stack "${stack}" in ${dir} returned outputs that are not an object`)
|
|
27
|
+
| exception _ => Error(`stack "${stack}" in ${dir} returned outputs that are not JSON`)
|
|
28
|
+
}
|
|
29
|
+
| exception _ => Error(`could not read the outputs of stack "${stack}" in ${dir}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let selectedStack = (~dir: string): option<string> =>
|
|
33
|
+
switch run(~cwd=dir, ["stack", "--show-name"]) {
|
|
34
|
+
| "" => None
|
|
35
|
+
| name => Some(name)
|
|
36
|
+
| exception _ => None
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let stringOutput = (outputs: dict<JSON.t>, name: string): option<string> =>
|
|
40
|
+
switch outputs->Dict.get(name)->Option.flatMap(JSON.Decode.string) {
|
|
41
|
+
| Some("") | None => None
|
|
42
|
+
| value => value
|
|
43
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Nodefs from "node:fs";
|
|
4
|
+
import * as Nodepath from "node:path";
|
|
5
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Nodechild_process from "node:child_process";
|
|
8
|
+
|
|
9
|
+
function run(cwd, args) {
|
|
10
|
+
return Nodechild_process.execFileSync("pulumi", args.concat([
|
|
11
|
+
"--cwd",
|
|
12
|
+
cwd,
|
|
13
|
+
"--non-interactive"
|
|
14
|
+
]), {
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
stdio: [
|
|
17
|
+
"ignore",
|
|
18
|
+
"pipe",
|
|
19
|
+
"inherit"
|
|
20
|
+
],
|
|
21
|
+
maxBuffer: 67108864
|
|
22
|
+
}).trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hasStackFile(dir, stack) {
|
|
26
|
+
return Nodefs.existsSync(Nodepath.join(dir, `Pulumi.` + stack + `.yaml`));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function stackOutputs(dir, stack) {
|
|
30
|
+
let exit = 0;
|
|
31
|
+
let text;
|
|
32
|
+
try {
|
|
33
|
+
text = run(dir, [
|
|
34
|
+
"stack",
|
|
35
|
+
"output",
|
|
36
|
+
"--json",
|
|
37
|
+
"--stack",
|
|
38
|
+
stack
|
|
39
|
+
]);
|
|
40
|
+
exit = 1;
|
|
41
|
+
} catch (exn) {
|
|
42
|
+
return {
|
|
43
|
+
TAG: "Error",
|
|
44
|
+
_0: `could not read the outputs of stack "` + stack + `" in ` + dir
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (exit === 1) {
|
|
48
|
+
let exit$1 = 0;
|
|
49
|
+
let outputs;
|
|
50
|
+
try {
|
|
51
|
+
outputs = Stdlib_JSON.Decode.object(JSON.parse(text));
|
|
52
|
+
exit$1 = 2;
|
|
53
|
+
} catch (exn$1) {
|
|
54
|
+
return {
|
|
55
|
+
TAG: "Error",
|
|
56
|
+
_0: `stack "` + stack + `" in ` + dir + ` returned outputs that are not JSON`
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (exit$1 === 2) {
|
|
60
|
+
if (outputs !== undefined) {
|
|
61
|
+
return {
|
|
62
|
+
TAG: "Ok",
|
|
63
|
+
_0: outputs
|
|
64
|
+
};
|
|
65
|
+
} else {
|
|
66
|
+
return {
|
|
67
|
+
TAG: "Error",
|
|
68
|
+
_0: `stack "` + stack + `" in ` + dir + ` returned outputs that are not an object`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function selectedStack(dir) {
|
|
76
|
+
let name;
|
|
77
|
+
try {
|
|
78
|
+
name = run(dir, [
|
|
79
|
+
"stack",
|
|
80
|
+
"--show-name"
|
|
81
|
+
]);
|
|
82
|
+
} catch (exn) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (name === "") {
|
|
86
|
+
return;
|
|
87
|
+
} else {
|
|
88
|
+
return name;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function stringOutput(outputs, name) {
|
|
93
|
+
let value = Stdlib_Option.flatMap(outputs[name], Stdlib_JSON.Decode.string);
|
|
94
|
+
if (value !== undefined && value !== "") {
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export {
|
|
100
|
+
run,
|
|
101
|
+
hasStackFile,
|
|
102
|
+
stackOutputs,
|
|
103
|
+
selectedStack,
|
|
104
|
+
stringOutput,
|
|
105
|
+
}
|
|
106
|
+
/* node:fs Not a pure module */
|
package/src/Platform.res
CHANGED
|
@@ -1492,7 +1492,7 @@ module MakeWithConfig = (
|
|
|
1492
1492
|
// GETs from it to resolve an offloaded `structure`. Kept out of the declared
|
|
1493
1493
|
// `@storageRef` stores and their pending-claim/expiry machinery: offload
|
|
1494
1494
|
// objects are written durably up front and are never "pending".
|
|
1495
|
-
let offloadProtection = Util_StoreLayout.
|
|
1495
|
+
let offloadProtection = Util_StoreLayout.protectionOfDeployedStack()
|
|
1496
1496
|
let offloadBucket = PulumiAws.S3.Bucket.make(
|
|
1497
1497
|
~name="reventless-offload",
|
|
1498
1498
|
~args={
|
|
@@ -1518,6 +1518,14 @@ module MakeWithConfig = (
|
|
|
1518
1518
|
},
|
|
1519
1519
|
)
|
|
1520
1520
|
Pulumi.Pulumi.export("offloadBucket", offloadBucketName)
|
|
1521
|
+
// What a plugin stack's `platform:stack` names, fully qualified so it holds on
|
|
1522
|
+
// every backend. `deploy-app` reads it to point the plugin stacks it creates.
|
|
1523
|
+
Pulumi.Pulumi.export(
|
|
1524
|
+
"platformStack",
|
|
1525
|
+
Pulumi.Output.make(
|
|
1526
|
+
`${Pulumi.Pulumi.getOrganization()}/${Pulumi.Pulumi.getProjectName()}/${Pulumi.Pulumi.getStackName()}`,
|
|
1527
|
+
),
|
|
1528
|
+
)
|
|
1521
1529
|
|
|
1522
1530
|
// Mount the Platform_ComponentDefinitions Lambda resolver on the Platform API
|
|
1523
1531
|
// (split mode) or Domain API (unified mode — platformApi == domainApi above).
|
|
@@ -1774,7 +1782,7 @@ module MakeWithConfig = (
|
|
|
1774
1782
|
~stack=stackName,
|
|
1775
1783
|
~prodStacks=Util_HostUiDomain.resolveProdStacks(),
|
|
1776
1784
|
)
|
|
1777
|
-
let storeProtection = Util_StoreLayout.
|
|
1785
|
+
let storeProtection = Util_StoreLayout.protectionOfDeployedStack()
|
|
1778
1786
|
|
|
1779
1787
|
// Dedup by `(plugin, store)` — that pair is a store's identity, and many
|
|
1780
1788
|
// fields legitimately name one store.
|
|
@@ -2529,6 +2537,15 @@ module MakeWithConfig = (
|
|
|
2529
2537
|
let startServers = () => ()
|
|
2530
2538
|
|
|
2531
2539
|
let deployPlugin = (~plugin: module(PluginMaker), ~apiTarget=Domain) => {
|
|
2540
|
+
// Without the setting the plugin used to deploy as a platform of its own,
|
|
2541
|
+
// with no error, and never register with the real one.
|
|
2542
|
+
if platformStackRef->Option.isNone {
|
|
2543
|
+
JsError.throwWithMessage(
|
|
2544
|
+
`This plugin stack has no platform:stack setting, so it cannot find its platform. ` ++
|
|
2545
|
+
`Set it to the platform's stack: pulumi config set platform:stack ` ++
|
|
2546
|
+
`${Pulumi.Pulumi.getOrganization()}/<platform project>/${Pulumi.Pulumi.getStackName()}`,
|
|
2547
|
+
)
|
|
2548
|
+
}
|
|
2532
2549
|
log.info(
|
|
2533
2550
|
~comp="Platform:deployPlugin",
|
|
2534
2551
|
`target=${switch apiTarget {
|