@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/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.372 (2026-10-05)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **deploy:** the pending-creates recovery runs from reventless-aws, and works ([b9e749e](https://github.com/ReventlessDev/reventless-core/commit/b9e749eb09354a559505fa6facc7f58e38afd2a9))
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **deploy:** a plugin names its own region, and can stay out of review environments ([7edd318](https://github.com/ReventlessDev/reventless-core/commit/7edd3187f51f74e57c331b1cb417b17de8067b58))
|
|
14
|
+
* **deploy:** the deploy workflow names its state backend ([a370346](https://github.com/ReventlessDev/reventless-core/commit/a3703461bae9dc22c098315a09bbabcf0c5816b1))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# 3.0.0-alpha.371 (2026-10-01)
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* check a plugin's roles against the user pool's groups ([ddb3c42](https://github.com/ReventlessDev/reventless-core/commit/ddb3c422b7226a19800e781abd5f03af18867007))
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
# 3.0.0-alpha.370 (2026-10-01)
|
|
7
25
|
|
|
8
26
|
* feat!: roles a plugin declares, groups a platform provides ([43daace](https://github.com/ReventlessDev/reventless-core/commit/43daace6ca588afe22562d079af1e6f1da81942e))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.372",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"provision-accounts": "./run-provision-accounts.mjs",
|
|
10
10
|
"bake-manifest": "./run-bake-manifest.mjs",
|
|
11
11
|
"deploy-app": "./run-deploy-app.mjs",
|
|
12
|
-
"resolve-environment": "./run-resolve-environment.mjs"
|
|
12
|
+
"resolve-environment": "./run-resolve-environment.mjs",
|
|
13
|
+
"clear-pending-creates": "./run-clear-pending-creates.mjs"
|
|
13
14
|
},
|
|
14
15
|
"dependencies": {
|
|
15
16
|
"@aws-sdk/client-appsync": "3.970.0",
|
|
@@ -21,19 +22,19 @@
|
|
|
21
22
|
"sury": "11.0.0",
|
|
22
23
|
"uuid": "^13.0.0",
|
|
23
24
|
"yaml": "^2.8.3",
|
|
24
|
-
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.
|
|
25
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.34",
|
|
25
|
+
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.28",
|
|
26
26
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.11",
|
|
27
27
|
"@reventlessdev/rescript-node": "2.0.0-alpha.20",
|
|
28
|
-
"@reventlessdev/rescript-
|
|
28
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.34",
|
|
29
|
+
"@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.23",
|
|
30
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
|
|
29
31
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
|
|
30
|
-
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.22",
|
|
31
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.286",
|
|
32
32
|
"@reventlessdev/rescript-web": "1.0.0-alpha.7",
|
|
33
|
-
"@reventlessdev/reventless-
|
|
34
|
-
"@reventlessdev/reventless-
|
|
35
|
-
"@reventlessdev/reventless-
|
|
36
|
-
"@reventlessdev/reventless-
|
|
33
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.287",
|
|
34
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.185",
|
|
35
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.151",
|
|
36
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.39",
|
|
37
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.157"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"rescript": "12.3.1",
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/***
|
|
2
|
+
Before `pulumi up`, recovers a stack an interrupted deploy left behind: releases a
|
|
3
|
+
stale lock, drops the pending creates known to have nothing in AWS behind them,
|
|
4
|
+
and refreshes. Any other pending operation stops it: `--clear-pending-creates`
|
|
5
|
+
drops them all, and would orphan a function or a role that does exist.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Resource types with nothing in AWS behind them, so a pending create of one is
|
|
9
|
+
safe to drop. Add one only when that holds; a refusal costs one look, an
|
|
10
|
+
orphan stays until someone finds it. */
|
|
11
|
+
let safeTypes = [
|
|
12
|
+
// ACM's wait for DNS validation, which gives up after 1h15m when the record is late.
|
|
13
|
+
"aws:acm/certificateValidation:CertificateValidation",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
type pending = {urn: string, resourceType: string, operation: string}
|
|
17
|
+
|
|
18
|
+
let pendingOperations = (export: JSON.t): array<pending> => {
|
|
19
|
+
let field = (json, name) => json->JSON.Decode.object->Option.flatMap(o => o->Dict.get(name))
|
|
20
|
+
let text = (json, name) => json->field(name)->Option.flatMap(JSON.Decode.string)->Option.getOr("")
|
|
21
|
+
export
|
|
22
|
+
->field("deployment")
|
|
23
|
+
->Option.flatMap(deployment => deployment->field("pending_operations"))
|
|
24
|
+
->Option.flatMap(JSON.Decode.array)
|
|
25
|
+
->Option.getOr([])
|
|
26
|
+
->Array.map(operation => {
|
|
27
|
+
let resource = operation->field("resource")->Option.getOr(JSON.Null)
|
|
28
|
+
{
|
|
29
|
+
urn: resource->text("urn"),
|
|
30
|
+
resourceType: resource->text("type"),
|
|
31
|
+
operation: operation->text("type"),
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let isSafe = (pending: pending) => safeTypes->Array.includes(pending.resourceType)
|
|
37
|
+
|
|
38
|
+
type args = {stack: string, cwd: string}
|
|
39
|
+
|
|
40
|
+
let parseArgs = (argv: array<string>): result<args, string> =>
|
|
41
|
+
Reventless.CliArgs.parse(~strings=["stack", "cwd"], argv)
|
|
42
|
+
->Result.flatMap(Reventless.CliArgs.noPositionals)
|
|
43
|
+
->Result.flatMap(a =>
|
|
44
|
+
switch a->Reventless.CliArgs.string("stack") {
|
|
45
|
+
| Some(stack) =>
|
|
46
|
+
Ok({stack, cwd: a->Reventless.CliArgs.string("cwd")->Option.getOr(NodeProcess.cwd())})
|
|
47
|
+
| None => Error("--stack names the stack to recover")
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
let usage = `
|
|
52
|
+
Usage: clear-pending-creates --stack <name> [--cwd <dir>]
|
|
53
|
+
|
|
54
|
+
Recovers a stack before \`pulumi up\`: cancels a stale update, drops the pending
|
|
55
|
+
creates of types with nothing in AWS behind them, and refreshes. Any other pending
|
|
56
|
+
operation fails the command, for someone to resolve by hand.
|
|
57
|
+
|
|
58
|
+
--stack <name> The stack.
|
|
59
|
+
--cwd <dir> The Pulumi project. Defaults to the working directory.
|
|
60
|
+
`
|
|
61
|
+
|
|
62
|
+
let pulumi = (args: args, command: array<string>) =>
|
|
63
|
+
NodeChildProcess.spawnSync(
|
|
64
|
+
"pulumi",
|
|
65
|
+
command->Array.concat(["--stack", args.stack, "--cwd", args.cwd, "--non-interactive"]),
|
|
66
|
+
{encoding: "utf8", maxBuffer: 256 * 1024 * 1024},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
let succeeded = (result: NodeChildProcess.spawnSyncResult) =>
|
|
70
|
+
result.status->Nullable.toOption == Some(0)
|
|
71
|
+
|
|
72
|
+
let stderrOf = (result: NodeChildProcess.spawnSyncResult) =>
|
|
73
|
+
result.stderr->Nullable.toOption->Option.getOr("")->String.trim
|
|
74
|
+
|
|
75
|
+
let run = (args: args): result<unit, array<string>> => {
|
|
76
|
+
let cancel = pulumi(args, ["cancel", "--yes"])
|
|
77
|
+
switch stderrOf(cancel) {
|
|
78
|
+
| message
|
|
79
|
+
if !succeeded(cancel) &&
|
|
80
|
+
message != "" &&
|
|
81
|
+
!(message->String.toLowerCase->String.includes("no update in progress")) =>
|
|
82
|
+
Console.error(`::warning::pulumi cancel: ${message}`)
|
|
83
|
+
| _ => ()
|
|
84
|
+
}
|
|
85
|
+
let exported = pulumi(args, ["stack", "export"])
|
|
86
|
+
let state = switch exported.stdout->Nullable.toOption {
|
|
87
|
+
| Some(stdout) if succeeded(exported) =>
|
|
88
|
+
try Ok(JSON.parseOrThrow(stdout)) catch {
|
|
89
|
+
| _ => Error([`the export of stack ${args.stack} is not JSON`])
|
|
90
|
+
}
|
|
91
|
+
| _ => Error([`pulumi stack export failed: ${stderrOf(exported)}`])
|
|
92
|
+
}
|
|
93
|
+
state->Result.flatMap(state => {
|
|
94
|
+
let pending = pendingOperations(state)
|
|
95
|
+
let refused = pending->Array.filter(p => !isSafe(p))
|
|
96
|
+
if pending == [] {
|
|
97
|
+
Console.log(`No pending operations on stack ${args.stack}.`)
|
|
98
|
+
} else {
|
|
99
|
+
Console.log(
|
|
100
|
+
`${pending->Array.length->Int.toString} pending operation(s) on stack ${args.stack}.`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
if refused != [] {
|
|
104
|
+
Error(
|
|
105
|
+
refused->Array.map(p =>
|
|
106
|
+
`Pending ${p.operation} of ${p.urn} (${p.resourceType}): not cleared, because something may exist in AWS behind it. Resolve it by hand, or, if the type has nothing behind it, add it to ClearPendingCreates.safeTypes.`
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
} else {
|
|
110
|
+
// Every pending operation left is safe, so the refresh may drop them all.
|
|
111
|
+
// `pulumi state delete` cannot: a pending create has no resource in the state.
|
|
112
|
+
pending->Array.forEach(p =>
|
|
113
|
+
Console.log(`::warning::Clearing pending ${p.operation} of ${p.urn} (${p.resourceType})`)
|
|
114
|
+
)
|
|
115
|
+
let clear = pending == [] ? [] : ["--clear-pending-creates"]
|
|
116
|
+
// A failed refresh is usually a stale provider serialization, which the
|
|
117
|
+
// `pulumi up` after this heals.
|
|
118
|
+
let refreshed = pulumi(args, ["refresh", "--yes", "--skip-preview"]->Array.concat(clear))
|
|
119
|
+
if !succeeded(refreshed) {
|
|
120
|
+
Console.error(
|
|
121
|
+
`::warning::pulumi refresh had errors; pulumi up runs anyway. ${stderrOf(refreshed)}`,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
Ok()
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let cli: Reventless.CliArgs.cli<args> = {bin: "clear-pending-creates", usage, parse: parseArgs}
|
|
130
|
+
|
|
131
|
+
let main = () =>
|
|
132
|
+
Reventless.CliArgs.run(cli, async args =>
|
|
133
|
+
switch run(args) {
|
|
134
|
+
| Ok() => ()
|
|
135
|
+
| Error(messages) =>
|
|
136
|
+
messages->Array.forEach(message => Console.error(`::error::${message}`))
|
|
137
|
+
NodeProcess.exit(1)
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
// No top-level call: `../run-clear-pending-creates.mjs` invokes [main], so a test
|
|
142
|
+
// can import this module.
|
|
@@ -0,0 +1,187 @@
|
|
|
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_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
6
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
7
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
8
|
+
import * as CliArgs$Reventless from "@reventlessdev/reventless-spec/src/CliArgs.res.mjs";
|
|
9
|
+
import * as Nodechild_process from "node:child_process";
|
|
10
|
+
|
|
11
|
+
let safeTypes = ["aws:acm/certificateValidation:CertificateValidation"];
|
|
12
|
+
|
|
13
|
+
function pendingOperations($$export) {
|
|
14
|
+
let field = (json, name) => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => o[name]);
|
|
15
|
+
let text = (json, name) => Stdlib_Option.getOr(Stdlib_Option.flatMap(field(json, name), Stdlib_JSON.Decode.string), "");
|
|
16
|
+
return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(field($$export, "deployment"), deployment => field(deployment, "pending_operations")), Stdlib_JSON.Decode.array), []).map(operation => {
|
|
17
|
+
let resource = Stdlib_Option.getOr(field(operation, "resource"), null);
|
|
18
|
+
return {
|
|
19
|
+
urn: text(resource, "urn"),
|
|
20
|
+
resourceType: text(resource, "type"),
|
|
21
|
+
operation: text(operation, "type")
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isSafe(pending) {
|
|
27
|
+
return safeTypes.includes(pending.resourceType);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
return Stdlib_Result.flatMap(Stdlib_Result.flatMap(CliArgs$Reventless.parse([
|
|
32
|
+
"stack",
|
|
33
|
+
"cwd"
|
|
34
|
+
], undefined, undefined, undefined, argv), CliArgs$Reventless.noPositionals), a => {
|
|
35
|
+
let stack = CliArgs$Reventless.string(a, "stack");
|
|
36
|
+
if (stack !== undefined) {
|
|
37
|
+
return {
|
|
38
|
+
TAG: "Ok",
|
|
39
|
+
_0: {
|
|
40
|
+
stack: stack,
|
|
41
|
+
cwd: Stdlib_Option.getOr(CliArgs$Reventless.string(a, "cwd"), process.cwd())
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
} else {
|
|
45
|
+
return {
|
|
46
|
+
TAG: "Error",
|
|
47
|
+
_0: "--stack names the stack to recover"
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let usage = `
|
|
54
|
+
Usage: clear-pending-creates --stack <name> [--cwd <dir>]
|
|
55
|
+
|
|
56
|
+
Recovers a stack before \`pulumi up\`: cancels a stale update, drops the pending
|
|
57
|
+
creates of types with nothing in AWS behind them, and refreshes. Any other pending
|
|
58
|
+
operation fails the command, for someone to resolve by hand.
|
|
59
|
+
|
|
60
|
+
--stack <name> The stack.
|
|
61
|
+
--cwd <dir> The Pulumi project. Defaults to the working directory.
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
function pulumi(args, command) {
|
|
65
|
+
return Nodechild_process.spawnSync("pulumi", command.concat([
|
|
66
|
+
"--stack",
|
|
67
|
+
args.stack,
|
|
68
|
+
"--cwd",
|
|
69
|
+
args.cwd,
|
|
70
|
+
"--non-interactive"
|
|
71
|
+
]), {
|
|
72
|
+
encoding: "utf8",
|
|
73
|
+
maxBuffer: 268435456
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function succeeded(result) {
|
|
78
|
+
return Primitive_object.equal(Primitive_option.fromNullable(result.status), 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function stderrOf(result) {
|
|
82
|
+
return Stdlib_Option.getOr(Primitive_option.fromNullable(result.stderr), "").trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function run(args) {
|
|
86
|
+
let cancel = pulumi(args, [
|
|
87
|
+
"cancel",
|
|
88
|
+
"--yes"
|
|
89
|
+
]);
|
|
90
|
+
let message = stderrOf(cancel);
|
|
91
|
+
if (!succeeded(cancel) && message !== "" && !message.toLowerCase().includes("no update in progress")) {
|
|
92
|
+
console.error(`::warning::pulumi cancel: ` + message);
|
|
93
|
+
}
|
|
94
|
+
let exported = pulumi(args, [
|
|
95
|
+
"stack",
|
|
96
|
+
"export"
|
|
97
|
+
]);
|
|
98
|
+
let stdout = exported.stdout;
|
|
99
|
+
let state;
|
|
100
|
+
let exit = 0;
|
|
101
|
+
if ((stdout == null) || !succeeded(exported)) {
|
|
102
|
+
exit = 1;
|
|
103
|
+
} else {
|
|
104
|
+
try {
|
|
105
|
+
state = {
|
|
106
|
+
TAG: "Ok",
|
|
107
|
+
_0: JSON.parse(stdout)
|
|
108
|
+
};
|
|
109
|
+
} catch (exn) {
|
|
110
|
+
state = {
|
|
111
|
+
TAG: "Error",
|
|
112
|
+
_0: [`the export of stack ` + args.stack + ` is not JSON`]
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (exit === 1) {
|
|
117
|
+
state = {
|
|
118
|
+
TAG: "Error",
|
|
119
|
+
_0: [`pulumi stack export failed: ` + stderrOf(exported)]
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return Stdlib_Result.flatMap(state, state => {
|
|
123
|
+
let pending = pendingOperations(state);
|
|
124
|
+
let refused = pending.filter(p => !safeTypes.includes(p.resourceType));
|
|
125
|
+
if (Primitive_object.equal(pending, [])) {
|
|
126
|
+
console.log(`No pending operations on stack ` + args.stack + `.`);
|
|
127
|
+
} else {
|
|
128
|
+
console.log(pending.length.toString() + ` pending operation(s) on stack ` + args.stack + `.`);
|
|
129
|
+
}
|
|
130
|
+
if (Primitive_object.notequal(refused, [])) {
|
|
131
|
+
return {
|
|
132
|
+
TAG: "Error",
|
|
133
|
+
_0: refused.map(p => `Pending ` + p.operation + ` of ` + p.urn + ` (` + p.resourceType + `): not cleared, because something may exist in AWS behind it. Resolve it by hand, or, if the type has nothing behind it, add it to ClearPendingCreates.safeTypes.`)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
pending.forEach(p => {
|
|
137
|
+
console.log(`::warning::Clearing pending ` + p.operation + ` of ` + p.urn + ` (` + p.resourceType + `)`);
|
|
138
|
+
});
|
|
139
|
+
let clear = Primitive_object.equal(pending, []) ? [] : ["--clear-pending-creates"];
|
|
140
|
+
let refreshed = pulumi(args, [
|
|
141
|
+
"refresh",
|
|
142
|
+
"--yes",
|
|
143
|
+
"--skip-preview"
|
|
144
|
+
].concat(clear));
|
|
145
|
+
if (!succeeded(refreshed)) {
|
|
146
|
+
console.error(`::warning::pulumi refresh had errors; pulumi up runs anyway. ` + stderrOf(refreshed));
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
TAG: "Ok",
|
|
150
|
+
_0: undefined
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let cli = {
|
|
156
|
+
bin: "clear-pending-creates",
|
|
157
|
+
usage: usage,
|
|
158
|
+
parse: parseArgs
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
function main() {
|
|
162
|
+
return CliArgs$Reventless.run(cli, undefined, undefined, async args => {
|
|
163
|
+
let messages = run(args);
|
|
164
|
+
if (messages.TAG === "Ok") {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
messages._0.forEach(message => {
|
|
168
|
+
console.error(`::error::` + message);
|
|
169
|
+
});
|
|
170
|
+
process.exit(1);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export {
|
|
175
|
+
safeTypes,
|
|
176
|
+
pendingOperations,
|
|
177
|
+
isSafe,
|
|
178
|
+
parseArgs,
|
|
179
|
+
usage,
|
|
180
|
+
pulumi,
|
|
181
|
+
succeeded,
|
|
182
|
+
stderrOf,
|
|
183
|
+
run,
|
|
184
|
+
cli,
|
|
185
|
+
main,
|
|
186
|
+
}
|
|
187
|
+
/* CliArgs-Reventless Not a pure module */
|
package/scripts/DeployApp.res
CHANGED
|
@@ -86,6 +86,7 @@ type project = {
|
|
|
86
86
|
projectName: string,
|
|
87
87
|
program: string,
|
|
88
88
|
stackDefaults: dict<string>,
|
|
89
|
+
region: option<string>,
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
@module("yaml") external parseYaml: string => JSON.t = "parse"
|
|
@@ -111,6 +112,7 @@ let projectOf = (p: DeployManifest.project): result<project, string> => {
|
|
|
111
112
|
projectName,
|
|
112
113
|
program: NodePath.join([p.dir, field("main")->Option.getOr("index.js")]),
|
|
113
114
|
stackDefaults: p.stackDefaults,
|
|
115
|
+
region: p.region,
|
|
114
116
|
})
|
|
115
117
|
}
|
|
116
118
|
}
|
|
@@ -150,6 +152,19 @@ let backendNeedsPassphrase = (~url: option<string>, ~env: dict<string>): bool =>
|
|
|
150
152
|
| _ => false
|
|
151
153
|
}
|
|
152
154
|
|
|
155
|
+
/** A manifest that puts a stack in another region than the one `up` deploys
|
|
156
|
+
every stack to. */
|
|
157
|
+
let regionProblems = (~projects: array<project>, ~region: string): array<string> =>
|
|
158
|
+
projects->Array.filterMap(p =>
|
|
159
|
+
switch p.region {
|
|
160
|
+
| Some(own) if own != region =>
|
|
161
|
+
Some(
|
|
162
|
+
`${p.label} deploys to ${own} in the manifest, and deploy-app deploys every stack to one region, ${region}. Deploy it with the deploy workflow, or from a manifest without that region.`,
|
|
163
|
+
)
|
|
164
|
+
| _ => None
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
|
|
153
168
|
let unbuilt = (projects: array<project>): array<project> =>
|
|
154
169
|
projects->Array.filter(p => !NodeFs.existsSync(p.program))
|
|
155
170
|
|
|
@@ -201,7 +216,7 @@ let check = async (~projects: array<project>): result<ready, array<string>> => {
|
|
|
201
216
|
}
|
|
202
217
|
| exception _ =>
|
|
203
218
|
problems->Array.push(
|
|
204
|
-
"Pulumi is not logged in. Run `pulumi login` (Pulumi Cloud) or `pulumi login --local`, and try again.",
|
|
219
|
+
"Pulumi is not logged in. Run `pulumi login` (Pulumi Cloud), `pulumi login s3://<bucket>` (a bucket of your own) or `pulumi login --local`, and try again.",
|
|
205
220
|
)
|
|
206
221
|
}
|
|
207
222
|
| exception _ =>
|
|
@@ -210,6 +225,9 @@ let check = async (~projects: array<project>): result<ready, array<string>> => {
|
|
|
210
225
|
)
|
|
211
226
|
}
|
|
212
227
|
}
|
|
228
|
+
region->Option.forEach(region =>
|
|
229
|
+
regionProblems(~projects, ~region)->Array.forEach(p => problems->Array.push(p))
|
|
230
|
+
)
|
|
213
231
|
unbuilt(projects)->Array.forEach(p =>
|
|
214
232
|
problems->Array.push(
|
|
215
233
|
`${p.label} is not built: ${p.program} is missing. Run \`pnpm run build\` and try again.`,
|
|
@@ -134,7 +134,8 @@ function projectOf(p) {
|
|
|
134
134
|
dir: p.dir,
|
|
135
135
|
projectName: projectName,
|
|
136
136
|
program: Nodepath.join(p.dir, Stdlib_Option.getOr(field("main"), "index.js")),
|
|
137
|
-
stackDefaults: p.stackDefaults
|
|
137
|
+
stackDefaults: p.stackDefaults,
|
|
138
|
+
region: p.region
|
|
138
139
|
}
|
|
139
140
|
};
|
|
140
141
|
} else {
|
|
@@ -183,6 +184,15 @@ function backendNeedsPassphrase(url, env) {
|
|
|
183
184
|
}
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
function regionProblems(projects, region) {
|
|
188
|
+
return Stdlib_Array.filterMap(projects, p => {
|
|
189
|
+
let own = p.region;
|
|
190
|
+
if (own !== undefined && own !== region) {
|
|
191
|
+
return p.label + ` deploys to ` + own + ` in the manifest, and deploy-app deploys every stack to one region, ` + region + `. Deploy it with the deploy workflow, or from a manifest without that region.`;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
186
196
|
function unbuilt(projects) {
|
|
187
197
|
return projects.filter(p => !Nodefs.existsSync(p.program));
|
|
188
198
|
}
|
|
@@ -243,7 +253,7 @@ async function check(projects) {
|
|
|
243
253
|
who = await workspace.whoAmI();
|
|
244
254
|
exit$3 = 2;
|
|
245
255
|
} catch (exn$3) {
|
|
246
|
-
problems.push("Pulumi is not logged in. Run `pulumi login` (Pulumi Cloud) or `pulumi login --local`, and try again.");
|
|
256
|
+
problems.push("Pulumi is not logged in. Run `pulumi login` (Pulumi Cloud), `pulumi login s3://<bucket>` (a bucket of your own) or `pulumi login --local`, and try again.");
|
|
247
257
|
}
|
|
248
258
|
if (exit$3 === 2) {
|
|
249
259
|
if (backendNeedsPassphrase(who.url, process.env)) {
|
|
@@ -252,6 +262,11 @@ async function check(projects) {
|
|
|
252
262
|
}
|
|
253
263
|
}
|
|
254
264
|
}
|
|
265
|
+
Stdlib_Option.forEach(region, region => {
|
|
266
|
+
regionProblems(projects, region).forEach(p => {
|
|
267
|
+
problems.push(p);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
255
270
|
unbuilt(projects).forEach(p => {
|
|
256
271
|
problems.push(p.label + ` is not built: ` + p.program + ` is missing. Run \`pnpm run build\` and try again.`);
|
|
257
272
|
});
|
|
@@ -917,6 +932,7 @@ export {
|
|
|
917
932
|
requiredNodeMajor,
|
|
918
933
|
checkNode,
|
|
919
934
|
backendNeedsPassphrase,
|
|
935
|
+
regionProblems,
|
|
920
936
|
unbuilt,
|
|
921
937
|
check,
|
|
922
938
|
layerName,
|
|
@@ -7,6 +7,8 @@ it here. Paths in the file are relative to the file's own folder.
|
|
|
7
7
|
|
|
8
8
|
`stack-defaults` on an entry are the settings `deploy-app` gives a stack it
|
|
9
9
|
creates for that folder — what a new stack of this app cannot deploy without.
|
|
10
|
+
A plugin's `region` overrides the file's for that stack; `review: false` keeps it
|
|
11
|
+
out of a pull request's review environment.
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
@module("yaml") external parseYaml: string => JSON.t = "parse"
|
|
@@ -24,6 +26,8 @@ type plugin = {
|
|
|
24
26
|
path: string,
|
|
25
27
|
@as("depends-on") dependsOn?: array<string>,
|
|
26
28
|
@as("stack-defaults") stackDefaults?: dict<string>,
|
|
29
|
+
region?: string,
|
|
30
|
+
review?: bool,
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
@schema
|
|
@@ -34,7 +38,7 @@ type t = {
|
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
/** One stack folder, with its path made absolute. */
|
|
37
|
-
type project = {name: string, dir: string, stackDefaults: dict<string>}
|
|
41
|
+
type project = {name: string, dir: string, stackDefaults: dict<string>, region: option<string>}
|
|
38
42
|
|
|
39
43
|
type resolved = {
|
|
40
44
|
file: string,
|
|
@@ -60,6 +64,7 @@ let resolve = (manifest: t, ~file: string): resolved => {
|
|
|
60
64
|
name: manifest.platform.name->Option.getOr("platform"),
|
|
61
65
|
dir: NodePath.resolve([base, manifest.platform.path]),
|
|
62
66
|
stackDefaults: manifest.platform.stackDefaults->Option.getOr(Dict.make()),
|
|
67
|
+
region: None,
|
|
63
68
|
},
|
|
64
69
|
plugins: manifest.plugins
|
|
65
70
|
->Option.getOr([])
|
|
@@ -67,6 +72,7 @@ let resolve = (manifest: t, ~file: string): resolved => {
|
|
|
67
72
|
name: p.name,
|
|
68
73
|
dir: NodePath.resolve([base, p.path]),
|
|
69
74
|
stackDefaults: p.stackDefaults->Option.getOr(Dict.make()),
|
|
75
|
+
region: p.region,
|
|
70
76
|
}),
|
|
71
77
|
}
|
|
72
78
|
}
|
|
@@ -18,7 +18,9 @@ let pluginSchema = Sury.$schema(s => ({
|
|
|
18
18
|
name: s.m(Sury.string),
|
|
19
19
|
path: s.m(Sury.string),
|
|
20
20
|
"depends-on": s.m(Sury.$option(Sury.array(Sury.string))),
|
|
21
|
-
"stack-defaults": s.m(Sury.$option(Sury.record(Sury.string)))
|
|
21
|
+
"stack-defaults": s.m(Sury.$option(Sury.record(Sury.string))),
|
|
22
|
+
region: s.m(Sury.$option(Sury.string)),
|
|
23
|
+
review: s.m(Sury.$option(Sury.boolean))
|
|
22
24
|
}));
|
|
23
25
|
|
|
24
26
|
let schema = Sury.$schema(s => ({
|
|
@@ -57,12 +59,14 @@ function resolve(manifest, file) {
|
|
|
57
59
|
platform: {
|
|
58
60
|
name: Stdlib_Option.getOr(manifest.platform.name, "platform"),
|
|
59
61
|
dir: Nodepath.resolve(base, manifest.platform.path),
|
|
60
|
-
stackDefaults: Stdlib_Option.getOr(manifest.platform["stack-defaults"], {})
|
|
62
|
+
stackDefaults: Stdlib_Option.getOr(manifest.platform["stack-defaults"], {}),
|
|
63
|
+
region: undefined
|
|
61
64
|
},
|
|
62
65
|
plugins: Stdlib_Option.getOr(manifest.plugins, []).map(p => ({
|
|
63
66
|
name: p.name,
|
|
64
67
|
dir: Nodepath.resolve(base, p.path),
|
|
65
|
-
stackDefaults: Stdlib_Option.getOr(p["stack-defaults"], {})
|
|
68
|
+
stackDefaults: Stdlib_Option.getOr(p["stack-defaults"], {}),
|
|
69
|
+
region: p.region
|
|
66
70
|
}))
|
|
67
71
|
};
|
|
68
72
|
}
|
package/src/Platform.res
CHANGED
|
@@ -112,11 +112,34 @@ let messagingEmailProviderRef: ref<Pulumi.Output.t<string>> = ref(Pulumi.Output.
|
|
|
112
112
|
resolves roles while it is built, and a mapping stated later is refused. */
|
|
113
113
|
let roleGroups = Reventless.Role.setGroups
|
|
114
114
|
|
|
115
|
-
/**
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
/** Groups a root says its identity provider has, for a provider the platform
|
|
116
|
+
cannot list. Where it can (`loadProvidedGroups`), the listing decides, and a
|
|
117
|
+
declared group the pool lacks fails the deploy. */
|
|
118
118
|
let providedGroups = Reventless.Role.provideGroups
|
|
119
119
|
|
|
120
|
+
/** The platform stack a plugin stack deploys against (`platform:stack`), or
|
|
121
|
+
`None` in a program that is its own platform. One reference per program:
|
|
122
|
+
a second with the same name is a duplicate resource. */
|
|
123
|
+
let platformStackRefCache: ref<option<option<Pulumi.StackReference.t>>> = ref(None)
|
|
124
|
+
|
|
125
|
+
let platformStackReference = (): option<Pulumi.StackReference.t> =>
|
|
126
|
+
switch platformStackRefCache.contents {
|
|
127
|
+
| Some(stackRef) => stackRef
|
|
128
|
+
| None =>
|
|
129
|
+
let stackRef =
|
|
130
|
+
Pulumi.Config.make(Some("platform"))
|
|
131
|
+
->Pulumi.Config.get("stack")
|
|
132
|
+
->Option.map(stack => Pulumi.StackReference.make(stack))
|
|
133
|
+
platformStackRefCache := Some(stackRef)
|
|
134
|
+
stackRef
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Ask the user pool which groups it has, so the role check compares each
|
|
138
|
+
plugin's roles with the pool. A plugin's deploy program calls `deployPlugin`
|
|
139
|
+
once this resolves; see [Platform_ProvidedGroups]. */
|
|
140
|
+
let loadProvidedGroups = (): promise<unit> =>
|
|
141
|
+
Platform_ProvidedGroups.load(~stackRef=platformStackReference())
|
|
142
|
+
|
|
120
143
|
/** The group a role resolves to — the name this platform writes into its
|
|
121
144
|
directives, its shell configuration and the elevated groups. For code that
|
|
122
145
|
grants access by group, so it never spells one itself. */
|
|
@@ -164,10 +187,10 @@ module MakeWithConfig = (
|
|
|
164
187
|
// [Reventless.OwnerScope.defaultElevatedGroups].
|
|
165
188
|
Reventless.OwnerScope.defaultElevatedRoles([Reventless.Role.admin])
|
|
166
189
|
|
|
167
|
-
// What this deployment provides for the roles its plugins need
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
// mapped its roles.
|
|
190
|
+
// What this deployment provides for the roles its plugins need, where the pool
|
|
191
|
+
// was not listed (`loadProvidedGroups`): the administrator group the stack
|
|
192
|
+
// declares, and the groups of the accounts provisioning creates. Read when each
|
|
193
|
+
// plugin is built, after the root has mapped its roles.
|
|
171
194
|
Reventless.Role.provideGroupsFrom(() =>
|
|
172
195
|
[Reventless.Role.adminGroup()]->Array.concat(Reventless.AccountsManifest.declaredGroups())
|
|
173
196
|
)
|
|
@@ -228,10 +251,7 @@ module MakeWithConfig = (
|
|
|
228
251
|
// Determine API source based on platform:stack config.
|
|
229
252
|
// - Platform/monolithic mode (no config): create a real AppSync API resource.
|
|
230
253
|
// - Plugin mode (config set): reference the platform's shared API via StackReference.
|
|
231
|
-
let platformStackRef =
|
|
232
|
-
Pulumi.Config.make(Some("platform"))
|
|
233
|
-
->Pulumi.Config.get("stack")
|
|
234
|
-
->Option.map(stack => Pulumi.StackReference.make(stack))
|
|
254
|
+
let platformStackRef = platformStackReference()
|
|
235
255
|
|
|
236
256
|
// The declared object stores, as the upload claimer needs them: qualified
|
|
237
257
|
// name, physical bucket, served prefix.
|