@reventlessdev/reventless-aws 3.0.0-alpha.263 → 3.0.0-alpha.265
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 +17 -0
- package/package.json +7 -7
- package/src/Platform.res +37 -6
- package/src/Platform.res.mjs +21 -6
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res +6 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res.mjs +4 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res +5 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res.mjs +3 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res +6 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res.mjs +4 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res +6 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs +4 -1
- package/src/adapter/CommandTopic/CommandTopicRegistry.res +50 -0
- package/src/adapter/CommandTopic/CommandTopicRegistry.res.mjs +26 -0
- package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +21 -3
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +230 -29
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +124 -9
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +188 -3
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +107 -6
- package/src/components/AutomationSlice_Builder.res.mjs +1 -1
- package/src/components/OutboundTranslationSlice_Builder.res +20 -0
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +12 -2
- package/src/plugin/runtime/PluginRuntime_Builder.res +26 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +26 -0
- package/tests/AutomationSliceEntryPoint_OpsTest.res +44 -3
- package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +65 -1
|
@@ -38,12 +38,44 @@ type sliceInfo = {
|
|
|
38
38
|
// slice that declares no sources (the default) and for any that names the log
|
|
39
39
|
// explicitly, which is why it is not derivable from `sourceTopics` being empty.
|
|
40
40
|
consumesDcbLog: bool,
|
|
41
|
+
// Which component's CommandTopic this slice publishes its inbound command to.
|
|
42
|
+
// `None` keeps the plugin's DCB queue — right for a slice targeting a DCB
|
|
43
|
+
// StateChangeSlice, and what the shared Lambda used unconditionally before an
|
|
44
|
+
// outbound slice could target an Aggregate, whose commands a different Lambda
|
|
45
|
+
// consumes.
|
|
46
|
+
//
|
|
47
|
+
// Resolved in the finalizer rather than here: an Aggregate's CommandTopic is
|
|
48
|
+
// created *after* the slices that target it, so a lookup at registration time
|
|
49
|
+
// finds nothing. Only the name is knowable this early.
|
|
50
|
+
commandTargetName: option<string>,
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
let bundledInfos: dict<sliceInfo> = Dict.make()
|
|
44
54
|
|
|
45
55
|
let dcbQueueUrlRef: ref<option<Pulumi.Output.t<string>>> = ref(None)
|
|
46
|
-
|
|
56
|
+
// The DCB command topic's own flavor, for the slices that publish to it. Set
|
|
57
|
+
// beside the URL rather than assumed: the runtime publisher used to hardcode
|
|
58
|
+
// FIFO, and these queues are standard, which SQS rejects.
|
|
59
|
+
let dcbQueueIsFifo = ref(false)
|
|
60
|
+
// The DCB command topic queues as deploy-time resources, so the finalizer can
|
|
61
|
+
// put them on the channel spec and `connectLambda`'s `sqs:SendMessage` grant
|
|
62
|
+
// covers the slices that publish to the DCB fallback. Accumulated rather than
|
|
63
|
+
// kept singular: the hook fires once per DCB command topic (sync, and async
|
|
64
|
+
// when async StateChangeSlices exist), and granting on both is correct.
|
|
65
|
+
let dcbQueueResources: array<ReventlessInfra.Adapter.resource> = []
|
|
66
|
+
// First call wins for the URL and flavor. The hook fires once per DCB command
|
|
67
|
+
// topic and a plugin with async StateChangeSlices fires it twice; the sync topic
|
|
68
|
+
// is created first and is the one a slice's command belongs on, so letting the
|
|
69
|
+
// async FIFO topic overwrite would send slice commands to a FIFO queue with no
|
|
70
|
+
// MessageGroupId — which SQS rejects. The resources keep accumulating: the grant
|
|
71
|
+
// is wanted on every DCB command topic.
|
|
72
|
+
let setDcbQueueUrl = (~isFifo=false, ~resource=?, url) => {
|
|
73
|
+
if dcbQueueUrlRef.contents->Option.isNone {
|
|
74
|
+
dcbQueueUrlRef := Some(url)
|
|
75
|
+
dcbQueueIsFifo := isFifo
|
|
76
|
+
}
|
|
77
|
+
resource->Option.forEach(r => dcbQueueResources->Array.push(r)->ignore)
|
|
78
|
+
}
|
|
47
79
|
// The admin/plugin DCB command-topic FIFO URL captured by the
|
|
48
80
|
// onDcbCommandTopicCreated hook. Read by the admin EventCollector's reactive
|
|
49
81
|
// ApiFragmentRegistry push (2e) to dispatch RecordApiFragmentPush.
|
|
@@ -62,6 +94,7 @@ let registerAutomationSlice = (
|
|
|
62
94
|
~context=?,
|
|
63
95
|
~sourceTopics=Dict.make(),
|
|
64
96
|
~consumesDcbLog=true,
|
|
97
|
+
~commandTargetName=?,
|
|
65
98
|
) =>
|
|
66
99
|
bundledInfos->Dict.set(
|
|
67
100
|
name,
|
|
@@ -74,6 +107,7 @@ let registerAutomationSlice = (
|
|
|
74
107
|
context,
|
|
75
108
|
sourceTopics,
|
|
76
109
|
consumesDcbLog,
|
|
110
|
+
commandTargetName,
|
|
77
111
|
},
|
|
78
112
|
)
|
|
79
113
|
|
|
@@ -147,6 +181,121 @@ let forEventCollector: ReventlessCore.Runtime.forEventCollector<
|
|
|
147
181
|
}
|
|
148
182
|
}
|
|
149
183
|
|
|
184
|
+
// How often the shared automation Lambda is invoked with no records, to work
|
|
185
|
+
// its slices' TODO backlogs.
|
|
186
|
+
//
|
|
187
|
+
// Without it a backlog only moves when the *next event* arrives, so a slice
|
|
188
|
+
// whose traffic stops holds its failed items indefinitely — and D4's "retried to
|
|
189
|
+
// maxRetries, then swept" is what an outbound slice is chosen for over a
|
|
190
|
+
// hand-rolled Task. Five minutes is short enough that a transient outage clears
|
|
191
|
+
// on its own without anyone watching, and long enough that the invocations are
|
|
192
|
+
// noise against the event traffic (~288/day per deployment, most of them finding
|
|
193
|
+
// an empty backlog and exiting).
|
|
194
|
+
let sweepIntervalMinutes = 5
|
|
195
|
+
|
|
196
|
+
// The scheduled trigger: a rule, permission for EventBridge to invoke, and a
|
|
197
|
+
// target carrying the constant payload the entry point's shell branches on. The
|
|
198
|
+
// payload is the whole event — a scheduled invocation has no `records`, which is
|
|
199
|
+
// exactly how the shell tells the two apart.
|
|
200
|
+
let makeSweepSchedule = (~runtime: ReventlessCore.Runtime.environment<runtimeParts>, ~opts) => {
|
|
201
|
+
let name = "AllAutomationSlicesSweep"
|
|
202
|
+
let customOpts = opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
|
|
203
|
+
|
|
204
|
+
let rule = {
|
|
205
|
+
open PulumiAws.Cloudwatch
|
|
206
|
+
EventRule.make(
|
|
207
|
+
~name=Pulumi.Pulumi.getStackName() ++ ("-" ++ name),
|
|
208
|
+
~args={
|
|
209
|
+
description: "Work the automation/outbound slices' TODO backlogs"->Pulumi.Input.make,
|
|
210
|
+
scheduleExpression: EventRule.ScheduleExpression.every(sweepIntervalMinutes->Minutes),
|
|
211
|
+
tags: AWS.Tags.make(
|
|
212
|
+
~name=Pulumi.Pulumi.getStackName() ++ ("-" ++ name),
|
|
213
|
+
~kind=ReventlessCore.ComponentType.AutomationSlice,
|
|
214
|
+
~role=Scheduler,
|
|
215
|
+
~component=name,
|
|
216
|
+
),
|
|
217
|
+
},
|
|
218
|
+
~opts=customOpts,
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// The permission and target need the Lambda's resolved arn/name, so they stay
|
|
223
|
+
// inside an apply — the same shape the plugin heartbeat's runner uses.
|
|
224
|
+
let _permissionAndTarget =
|
|
225
|
+
(
|
|
226
|
+
runtime.parts.lambda->Pulumi.Output.flatMap(l => l.arn),
|
|
227
|
+
runtime.parts.lambda->Pulumi.Output.flatMap(l => l.name),
|
|
228
|
+
)
|
|
229
|
+
->Pulumi.Output.all2
|
|
230
|
+
->Pulumi.Output.apply(((lambdaArn, lambdaName)) => {
|
|
231
|
+
let _permission = PulumiAws.Lambda.Permission.make(
|
|
232
|
+
~name,
|
|
233
|
+
~args={
|
|
234
|
+
action: "lambda:InvokeFunction",
|
|
235
|
+
function: lambdaName->Pulumi.Input.make,
|
|
236
|
+
principal: AWS.CloudwatchEventRule.principal,
|
|
237
|
+
},
|
|
238
|
+
~opts=customOpts,
|
|
239
|
+
)
|
|
240
|
+
let _target = {
|
|
241
|
+
open PulumiAws.Cloudwatch
|
|
242
|
+
EventTarget.make(
|
|
243
|
+
~name,
|
|
244
|
+
~args={
|
|
245
|
+
rule: EventTarget.Rule.ofEventRule(rule),
|
|
246
|
+
arn: lambdaArn->Pulumi.Input.make,
|
|
247
|
+
input: `{"reventlessSweep":true}`->Pulumi.Input.make,
|
|
248
|
+
},
|
|
249
|
+
~opts=customOpts,
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Least-privilege `geo:SearchPlaceIndexForText` on the platform's place index,
|
|
256
|
+
// for the slices that call the geocoder through the injected capability.
|
|
257
|
+
//
|
|
258
|
+
// Only attached when the platform actually provisioned an index — that bit is a
|
|
259
|
+
// plain bool and therefore known synchronously, while the index *name* is an
|
|
260
|
+
// Output. Without the split this would either grant on a nonsense ARN or need
|
|
261
|
+
// `option<Pulumi.Output.t<_>>`, which corrupts the Output proxy.
|
|
262
|
+
//
|
|
263
|
+
// The RolePolicy is created at top level with an Output-valued document, not
|
|
264
|
+
// inside an `apply`. A resource created in an apply callback does not reliably
|
|
265
|
+
// register with the engine — the same defect that intermittently left the
|
|
266
|
+
// heartbeat Lambda without its SQS grant.
|
|
267
|
+
let makeGeocoderGrant = (~runtime: ReventlessCore.Runtime.environment<runtimeParts>, ~opts) =>
|
|
268
|
+
if PluginRuntime_Builder.geocoderProvisioned() {
|
|
269
|
+
let customOpts =
|
|
270
|
+
opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
|
|
271
|
+
let policyJson =
|
|
272
|
+
PluginRuntime_Builder.geocoderPlaceIndex()->Pulumi.Output.apply(idx => {
|
|
273
|
+
open PulumiAws.PolicyDocument
|
|
274
|
+
PulumiAws.PolicyDocument.make(
|
|
275
|
+
~id="AllAutomationSlicesGeocode",
|
|
276
|
+
~statements=[
|
|
277
|
+
{
|
|
278
|
+
sid: "AllowGeocode",
|
|
279
|
+
effect: Allow,
|
|
280
|
+
actions: Action("geo:SearchPlaceIndexForText"),
|
|
281
|
+
// Account/region wildcarded, matching the Function URL handler's
|
|
282
|
+
// own policy: the index name is what identifies it, and the stack
|
|
283
|
+
// has no other account to reach.
|
|
284
|
+
resources: Resource(`arn:aws:geo:*:*:place-index/${idx}`),
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
)->PulumiAws.PolicyDocument.toJsonString
|
|
288
|
+
})
|
|
289
|
+
let _ = PulumiAws.IAM.RolePolicy.make(
|
|
290
|
+
~name="AllAutomationSlicesGeocode",
|
|
291
|
+
~args={
|
|
292
|
+
policy: policyJson->Pulumi.Output.asInput,
|
|
293
|
+
role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
|
|
294
|
+
},
|
|
295
|
+
~opts=customOpts,
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
150
299
|
let finished = ref(false)
|
|
151
300
|
|
|
152
301
|
// Legacy finalizer, kept only to satisfy `EventCollectorRuntime_Builder.T`.
|
|
@@ -358,9 +507,25 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
|
|
|
358
507
|
let handlerOutputs: array<Pulumi.Output.t<string>> = []
|
|
359
508
|
let packageDirs: dict<string> = Dict.make()
|
|
360
509
|
let allQueryDbResources: array<ReventlessInfra.Adapter.resource> = []
|
|
510
|
+
// Whether any slice publishes to the DCB fallback rather than a target
|
|
511
|
+
// aggregate's own topic — if so, that queue needs the SendMessage grant
|
|
512
|
+
// too, once, below the loop.
|
|
513
|
+
let anyDcbFallback = ref(false)
|
|
361
514
|
|
|
362
515
|
bundledInfos->Dict.forEachWithKey((info, _name) => {
|
|
363
516
|
info.queryDbResources->Array.forEach(r => allQueryDbResources->Array.push(r)->ignore)
|
|
517
|
+
// The queue this slice publishes its command to. An Aggregate registers
|
|
518
|
+
// its CommandTopic while plugin construction is still synchronous —
|
|
519
|
+
// before DCB construction registers the slices targeting it — so a
|
|
520
|
+
// named target resolves here; anything else keeps the DCB fallback.
|
|
521
|
+
let target = info.commandTargetName->Option.flatMap(CommandTopicRegistry.get)
|
|
522
|
+
switch target {
|
|
523
|
+
| Some({resource}) =>
|
|
524
|
+
// The target CommandTopic joins the channel's resources so
|
|
525
|
+
// `connectLambda`'s existing `sqs:SendMessage` grant covers it.
|
|
526
|
+
allQueryDbResources->Array.push(resource)->ignore
|
|
527
|
+
| None => anyDcbFallback := true
|
|
528
|
+
}
|
|
364
529
|
let pkg = Util_Bundle.extractPackageName(info.specModulePath)
|
|
365
530
|
packageDirs->Dict.set(pkg, Util_Bundle.resolvePackageRoot(pkg))
|
|
366
531
|
// Usually the same package as the spec, but bundle it explicitly —
|
|
@@ -380,8 +545,16 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
|
|
|
380
545
|
| None => ""
|
|
381
546
|
}
|
|
382
547
|
|
|
548
|
+
// Per slice, not plugin-wide: an outbound slice targeting an Aggregate
|
|
549
|
+
// publishes to that aggregate's CommandTopic. Everything else keeps the
|
|
550
|
+
// DCB topic, which is what the field has always meant — and with it the
|
|
551
|
+
// DCB queue's own flavor.
|
|
552
|
+
let (commandQueueUrl, commandQueueIsFifo) = switch target {
|
|
553
|
+
| Some({queueUrl, isFifo}) => (queueUrl, isFifo)
|
|
554
|
+
| None => (dcbQueueUrl, dcbQueueIsFifo.contents)
|
|
555
|
+
}
|
|
383
556
|
let handlerJson =
|
|
384
|
-
Pulumi.Output.all3((info.queryDbTableName,
|
|
557
|
+
Pulumi.Output.all3((info.queryDbTableName, commandQueueUrl, sourceUrnsFor(info)))
|
|
385
558
|
->Pulumi.Output.apply(((tableName, queueUrl, urns)) => {
|
|
386
559
|
let urnsJson =
|
|
387
560
|
urns->Array.map(JSON.Encode.string)->JSON.Encode.array->JSON.stringify
|
|
@@ -389,11 +562,20 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
|
|
|
389
562
|
// an entry point that predates multi-source still routes its first
|
|
390
563
|
// stream rather than none.
|
|
391
564
|
let firstUrn = urns->Array.get(0)->Option.getOr("")
|
|
392
|
-
|
|
565
|
+
let fifoJson = commandQueueIsFifo ? "true" : "false"
|
|
566
|
+
`{"specModule":${specModule},"bodyModule":${bodyModule},"callbackType":${callbackType},"queryDbTableName":"${tableName}","dcbQueueUrl":"${queueUrl}","commandQueueIsFifo":${fifoJson},"sourceUrn":"${firstUrn}","sourceUrns":${urnsJson}${contextFragment}}`
|
|
393
567
|
})
|
|
394
568
|
let _ = handlerOutputs->Array.push(handlerJson)
|
|
395
569
|
})
|
|
396
570
|
|
|
571
|
+
// The fallback publishers' grant. Every slice that resolved no target
|
|
572
|
+
// publishes to the plugin's DCB command topic, whose queue was captured
|
|
573
|
+
// by `setDcbQueueUrl` — without this the role has no `sqs:SendMessage`
|
|
574
|
+
// there and every such publish is rejected.
|
|
575
|
+
if anyDcbFallback.contents {
|
|
576
|
+
dcbQueueResources->Array.forEach(r => allQueryDbResources->Array.push(r)->ignore)
|
|
577
|
+
}
|
|
578
|
+
|
|
397
579
|
let handlerConfigOutput =
|
|
398
580
|
Pulumi.Output.all(handlerOutputs)
|
|
399
581
|
->Pulumi.Output.apply(handlers => `{"handlers":[${handlers->Array.join(",")}]}`)
|
|
@@ -434,6 +616,9 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
|
|
|
434
616
|
~runtime,
|
|
435
617
|
~opts,
|
|
436
618
|
)
|
|
619
|
+
|
|
620
|
+
makeSweepSchedule(~runtime, ~opts)
|
|
621
|
+
makeGeocoderGrant(~runtime, ~opts)
|
|
437
622
|
| None =>
|
|
438
623
|
log.warn(
|
|
439
624
|
~comp="AutomationSliceRuntime_Builder_Single",
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Aws from "@pulumi/aws";
|
|
3
4
|
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
4
5
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
|
+
import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
|
|
5
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
8
|
import * as Pulumi from "@pulumi/pulumi";
|
|
7
9
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
10
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
11
|
+
import * as AWS$ReventlessAws from "../AWS.res.mjs";
|
|
8
12
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
13
|
+
import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
|
|
9
14
|
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
15
|
+
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
10
16
|
import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
17
|
+
import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
|
|
18
|
+
import * as Cloudwatch_EventRule$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Cloudwatch/Cloudwatch_EventRule.res.mjs";
|
|
19
|
+
import * as Cloudwatch_EventTarget$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Cloudwatch/Cloudwatch_EventTarget.res.mjs";
|
|
20
|
+
import * as CommandTopicRegistry$ReventlessAws from "../CommandTopic/CommandTopicRegistry.res.mjs";
|
|
11
21
|
import * as PluginRuntime_Builder$ReventlessAws from "../../plugin/runtime/PluginRuntime_Builder.res.mjs";
|
|
12
22
|
import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
|
|
13
23
|
import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
|
|
@@ -20,15 +30,28 @@ let dcbQueueUrlRef = {
|
|
|
20
30
|
contents: undefined
|
|
21
31
|
};
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
let dcbQueueIsFifo = {
|
|
34
|
+
contents: false
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let dcbQueueResources = [];
|
|
38
|
+
|
|
39
|
+
function setDcbQueueUrl(isFifoOpt, resource, url) {
|
|
40
|
+
let isFifo = isFifoOpt !== undefined ? isFifoOpt : false;
|
|
41
|
+
if (Stdlib_Option.isNone(dcbQueueUrlRef.contents)) {
|
|
42
|
+
dcbQueueUrlRef.contents = url;
|
|
43
|
+
dcbQueueIsFifo.contents = isFifo;
|
|
44
|
+
}
|
|
45
|
+
Stdlib_Option.forEach(resource, r => {
|
|
46
|
+
dcbQueueResources.push(r);
|
|
47
|
+
});
|
|
25
48
|
}
|
|
26
49
|
|
|
27
50
|
function getDcbQueueUrl() {
|
|
28
51
|
return dcbQueueUrlRef.contents;
|
|
29
52
|
}
|
|
30
53
|
|
|
31
|
-
function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackTypeOpt, queryDbTableName, queryDbResourcesOpt, context, sourceTopicsOpt, consumesDcbLogOpt) {
|
|
54
|
+
function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackTypeOpt, queryDbTableName, queryDbResourcesOpt, context, sourceTopicsOpt, consumesDcbLogOpt, commandTargetName) {
|
|
32
55
|
let callbackType = callbackTypeOpt !== undefined ? callbackTypeOpt : "automation";
|
|
33
56
|
let queryDbResources = queryDbResourcesOpt !== undefined ? queryDbResourcesOpt : [];
|
|
34
57
|
let sourceTopics = sourceTopicsOpt !== undefined ? sourceTopicsOpt : ({});
|
|
@@ -41,7 +64,8 @@ function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackT
|
|
|
41
64
|
queryDbResources: queryDbResources,
|
|
42
65
|
context: context,
|
|
43
66
|
sourceTopics: sourceTopics,
|
|
44
|
-
consumesDcbLog: consumesDcbLog
|
|
67
|
+
consumesDcbLog: consumesDcbLog,
|
|
68
|
+
commandTargetName: commandTargetName
|
|
45
69
|
};
|
|
46
70
|
}
|
|
47
71
|
|
|
@@ -81,6 +105,51 @@ function forEventCollector(param, eventTopics, resources, memorySizeOpt, timeout
|
|
|
81
105
|
log.info("AutomationSliceRuntime_Builder_Single", undefined, `registered ` + eventCollectorName + ` for ` + parentName);
|
|
82
106
|
}
|
|
83
107
|
|
|
108
|
+
function makeSweepSchedule(runtime, opts) {
|
|
109
|
+
let name = "AllAutomationSlicesSweep";
|
|
110
|
+
let customOpts = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
|
|
111
|
+
let rule = new (Aws.cloudwatch.EventRule)(Pulumi.getStack() + ("-" + name), {
|
|
112
|
+
description: "Work the automation/outbound slices' TODO backlogs",
|
|
113
|
+
scheduleExpression: Primitive_option.some(Cloudwatch_EventRule$PulumiAws.ScheduleExpression.every({
|
|
114
|
+
TAG: "Minutes",
|
|
115
|
+
_0: 5
|
|
116
|
+
})),
|
|
117
|
+
tags: AWS_Tags$ReventlessAws.make(Pulumi.getStack() + ("-" + name), "AutomationSlice", "Scheduler", undefined, name, undefined, undefined, undefined)
|
|
118
|
+
}, customOpts);
|
|
119
|
+
Pulumi.all([
|
|
120
|
+
Output$Pulumi.flatMap(runtime.parts.lambda, l => l.arn),
|
|
121
|
+
Output$Pulumi.flatMap(runtime.parts.lambda, l => l.name)
|
|
122
|
+
]).apply(param => {
|
|
123
|
+
new (Aws.lambda.Permission)(name, {
|
|
124
|
+
action: "lambda:InvokeFunction",
|
|
125
|
+
function: param[1],
|
|
126
|
+
principal: AWS$ReventlessAws.CloudwatchEventRule.principal
|
|
127
|
+
}, customOpts);
|
|
128
|
+
new (Aws.cloudwatch.EventTarget)(name, {
|
|
129
|
+
rule: Cloudwatch_EventTarget$PulumiAws.Rule.ofEventRule(rule),
|
|
130
|
+
arn: param[0],
|
|
131
|
+
input: `{"reventlessSweep":true}`
|
|
132
|
+
}, customOpts);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function makeGeocoderGrant(runtime, opts) {
|
|
137
|
+
if (!PluginRuntime_Builder$ReventlessAws.geocoderProvisioned()) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
let customOpts = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
|
|
141
|
+
let policyJson = PluginRuntime_Builder$ReventlessAws.geocoderPlaceIndex().apply(idx => PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, "AllAutomationSlicesGeocode", [{
|
|
142
|
+
Sid: "AllowGeocode",
|
|
143
|
+
Effect: "Allow",
|
|
144
|
+
Action: "geo:SearchPlaceIndexForText",
|
|
145
|
+
Resource: `arn:aws:geo:*:*:place-index/` + idx
|
|
146
|
+
}])));
|
|
147
|
+
new (Aws.iam.RolePolicy)("AllAutomationSlicesGeocode", {
|
|
148
|
+
policy: policyJson,
|
|
149
|
+
role: runtime.parts.lambdaRole.id
|
|
150
|
+
}, customOpts);
|
|
151
|
+
}
|
|
152
|
+
|
|
84
153
|
let finished = {
|
|
85
154
|
contents: false
|
|
86
155
|
};
|
|
@@ -195,10 +264,19 @@ function finishWithDcbEventLog(dcbEventLog) {
|
|
|
195
264
|
let handlerOutputs = [];
|
|
196
265
|
let packageDirs = {};
|
|
197
266
|
let allQueryDbResources = [];
|
|
267
|
+
let anyDcbFallback = {
|
|
268
|
+
contents: false
|
|
269
|
+
};
|
|
198
270
|
Stdlib_Dict.forEachWithKey(bundledInfos, (info, _name) => {
|
|
199
271
|
info.queryDbResources.forEach(r => {
|
|
200
272
|
allQueryDbResources.push(r);
|
|
201
273
|
});
|
|
274
|
+
let target = Stdlib_Option.flatMap(info.commandTargetName, CommandTopicRegistry$ReventlessAws.get);
|
|
275
|
+
if (target !== undefined) {
|
|
276
|
+
allQueryDbResources.push(target.resource);
|
|
277
|
+
} else {
|
|
278
|
+
anyDcbFallback.contents = true;
|
|
279
|
+
}
|
|
202
280
|
let pkg = Util_Bundle$ReventlessAws.extractPackageName(info.specModulePath);
|
|
203
281
|
packageDirs[pkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, pkg);
|
|
204
282
|
let bodyPkg = Util_Bundle$ReventlessAws.extractPackageName(info.bodyModulePath);
|
|
@@ -208,18 +286,32 @@ function finishWithDcbEventLog(dcbEventLog) {
|
|
|
208
286
|
let callbackType = Stdlib_Option.getOr(JSON.stringify(info.callbackType), `""`);
|
|
209
287
|
let context = info.context;
|
|
210
288
|
let contextFragment = context !== undefined ? Stdlib_Option.getOr(Stdlib_Option.map(JSON.stringify(context), json => `,"context":` + json), "") : "";
|
|
289
|
+
let match = target !== undefined ? [
|
|
290
|
+
target.queueUrl,
|
|
291
|
+
target.isFifo
|
|
292
|
+
] : [
|
|
293
|
+
dcbQueueUrl,
|
|
294
|
+
dcbQueueIsFifo.contents
|
|
295
|
+
];
|
|
296
|
+
let commandQueueIsFifo = match[1];
|
|
211
297
|
let handlerJson = Pulumi.all([
|
|
212
298
|
info.queryDbTableName,
|
|
213
|
-
|
|
299
|
+
match[0],
|
|
214
300
|
sourceUrnsFor(info)
|
|
215
301
|
]).apply(param => {
|
|
216
302
|
let urns = param[2];
|
|
217
303
|
let urnsJson = JSON.stringify(urns.map(prim => prim));
|
|
218
304
|
let firstUrn = Stdlib_Option.getOr(urns[0], "");
|
|
219
|
-
|
|
305
|
+
let fifoJson = commandQueueIsFifo ? "true" : "false";
|
|
306
|
+
return `{"specModule":` + specModule + `,"bodyModule":` + bodyModule + `,"callbackType":` + callbackType + `,"queryDbTableName":"` + param[0] + `","dcbQueueUrl":"` + param[1] + `","commandQueueIsFifo":` + fifoJson + `,"sourceUrn":"` + firstUrn + `","sourceUrns":` + urnsJson + contextFragment + `}`;
|
|
220
307
|
});
|
|
221
308
|
handlerOutputs.push(handlerJson);
|
|
222
309
|
});
|
|
310
|
+
if (anyDcbFallback.contents) {
|
|
311
|
+
dcbQueueResources.forEach(r => {
|
|
312
|
+
allQueryDbResources.push(r);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
223
315
|
let handlerConfigOutput = Pulumi.all(handlerOutputs).apply(handlers => `{"handlers":[` + handlers.join(",") + `]}`);
|
|
224
316
|
let envVars = {};
|
|
225
317
|
envVars["HANDLER_CONFIG"] = handlerConfigOutput;
|
|
@@ -233,6 +325,8 @@ function finishWithDcbEventLog(dcbEventLog) {
|
|
|
233
325
|
eventTopics: eventTopics,
|
|
234
326
|
resources: allQueryDbResources
|
|
235
327
|
}], runtime, opts);
|
|
328
|
+
makeSweepSchedule(runtime, opts);
|
|
329
|
+
makeGeocoderGrant(runtime, opts);
|
|
236
330
|
} else {
|
|
237
331
|
log.warn("AutomationSliceRuntime_Builder_Single", undefined, "finishWithDcbEventLog: DCB EventLog has no parent");
|
|
238
332
|
}
|
|
@@ -244,18 +338,25 @@ let EventCollectorChannel;
|
|
|
244
338
|
|
|
245
339
|
let RuntimeEnvironment;
|
|
246
340
|
|
|
341
|
+
let sweepIntervalMinutes = 5;
|
|
342
|
+
|
|
247
343
|
export {
|
|
248
344
|
EventCollectorChannel,
|
|
249
345
|
RuntimeEnvironment,
|
|
250
346
|
log,
|
|
251
347
|
bundledInfos,
|
|
252
348
|
dcbQueueUrlRef,
|
|
349
|
+
dcbQueueIsFifo,
|
|
350
|
+
dcbQueueResources,
|
|
253
351
|
setDcbQueueUrl,
|
|
254
352
|
getDcbQueueUrl,
|
|
255
353
|
registerAutomationSlice,
|
|
256
354
|
storedSpecs,
|
|
257
355
|
grandParent,
|
|
258
356
|
forEventCollector,
|
|
357
|
+
sweepIntervalMinutes,
|
|
358
|
+
makeSweepSchedule,
|
|
359
|
+
makeGeocoderGrant,
|
|
259
360
|
finished,
|
|
260
361
|
finish,
|
|
261
362
|
finishWithDcbEventLog,
|
|
@@ -46,7 +46,7 @@ function Make(Api) {
|
|
|
46
46
|
let queryDbTableName = tableResource.name;
|
|
47
47
|
let sourceTopics = EventTopic$ReventlessCore.filter(allEventTopics, Belt_SetString.fromArray(sourceNames));
|
|
48
48
|
let consumesDcbLog = sourceNames.length === 0 || sourceNames.some(name => !(name in allEventTopics));
|
|
49
|
-
AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Automation.moduleUrl), "automation", queryDbTableName, queryDbOutputs.resources, context, sourceTopics, consumesDcbLog);
|
|
49
|
+
AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Automation.moduleUrl), "automation", queryDbTableName, queryDbOutputs.resources, context, sourceTopics, consumesDcbLog, undefined);
|
|
50
50
|
return as_;
|
|
51
51
|
};
|
|
52
52
|
return {
|
|
@@ -14,6 +14,20 @@ module EventCollectorRuntimeBuilder = {
|
|
|
14
14
|
let finish = Inner.finish
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// On AWS the core builder's in-process handler is never the thing that runs a
|
|
18
|
+
// translation: `forEventCollector` registers module paths, and the shared
|
|
19
|
+
// `AllAutomationSlices` Lambda rebuilds the callback from them and drives phase 2
|
|
20
|
+
// itself. So the capabilities that matter are the ones
|
|
21
|
+
// `AutomationSliceEntryPoint_Ops.capabilities` builds at runtime, from the
|
|
22
|
+
// Lambda's environment — where the place index name actually is.
|
|
23
|
+
//
|
|
24
|
+
// This deploy-time value therefore has no caller. `none` rather than a real
|
|
25
|
+
// geocoder because inventing one here would suggest a path that does not exist,
|
|
26
|
+
// and because a deploy-time module has no environment to read anyway.
|
|
27
|
+
module DeployTimeCapabilities = {
|
|
28
|
+
let capabilities = () => Reventless.Capabilities.none
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
module Make = (Api: {
|
|
18
32
|
let api: unit => Types.AppSync.api
|
|
19
33
|
let apiRole: unit => Types.AppSync.role
|
|
@@ -25,6 +39,7 @@ module Make = (Api: {
|
|
|
25
39
|
EventCollectorChannel,
|
|
26
40
|
EventCollectorRuntimeBuilder,
|
|
27
41
|
Api,
|
|
42
|
+
DeployTimeCapabilities,
|
|
28
43
|
)
|
|
29
44
|
|
|
30
45
|
module Make = (
|
|
@@ -65,9 +80,14 @@ module Make = (Api: {
|
|
|
65
80
|
~bodyModulePath=Util_Bundle.getModuleSpecifier(Translation.moduleUrl),
|
|
66
81
|
~callbackType="outbound",
|
|
67
82
|
~queryDbTableName,
|
|
83
|
+
// The target queue rides along so `connectLambda`'s existing
|
|
84
|
+
// `sqs:SendMessage` grant covers it. Without it the role can publish
|
|
85
|
+
// nowhere, and the command is lost after the geocoder has been paid for.
|
|
68
86
|
~queryDbResources=queryDbOutputs.resources,
|
|
69
87
|
~sourceTopics,
|
|
70
88
|
~consumesDcbLog,
|
|
89
|
+
// Only the name: the target Aggregate's CommandTopic does not exist yet.
|
|
90
|
+
~commandTargetName=?Spec.targetName,
|
|
71
91
|
)
|
|
72
92
|
|
|
73
93
|
ots
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
|
|
4
|
+
import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
|
|
4
5
|
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
5
6
|
import * as EventTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/EventTopic/EventTopic.res.mjs";
|
|
6
7
|
import * as Util_Bundle$ReventlessAws from "../util/Util_Bundle.res.mjs";
|
|
@@ -18,6 +19,14 @@ let EventCollectorRuntimeBuilder = {
|
|
|
18
19
|
finish: AutomationSliceRuntime_Builder_Single$ReventlessAws.finish
|
|
19
20
|
};
|
|
20
21
|
|
|
22
|
+
function capabilities() {
|
|
23
|
+
return Capabilities$Reventless.none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let DeployTimeCapabilities = {
|
|
27
|
+
capabilities: capabilities
|
|
28
|
+
};
|
|
29
|
+
|
|
21
30
|
function Make(Api) {
|
|
22
31
|
let Inner = OutboundTranslationSlice_Builder$ReventlessCore.Make({
|
|
23
32
|
make: RuntimeEnvironment_Lambda$ReventlessAws.make,
|
|
@@ -42,7 +51,7 @@ function Make(Api) {
|
|
|
42
51
|
},
|
|
43
52
|
forEventCollector: AutomationSliceRuntime_Builder_Single$ReventlessAws.forEventCollector,
|
|
44
53
|
finish: AutomationSliceRuntime_Builder_Single$ReventlessAws.finish
|
|
45
|
-
})(Api);
|
|
54
|
+
})(Api)(DeployTimeCapabilities);
|
|
46
55
|
let Make$1 = Spec => (Translation => {
|
|
47
56
|
let InnerMake = Inner.Make(Spec)(Translation);
|
|
48
57
|
let make = (dcbEventLog, allEventTopicsOpt, publishJsons, runtime, opts) => {
|
|
@@ -54,7 +63,7 @@ function Make(Api) {
|
|
|
54
63
|
let declaredSources = Spec.sourceNames;
|
|
55
64
|
let sourceTopics = EventTopic$ReventlessCore.filter(allEventTopics, Belt_SetString.fromArray(declaredSources));
|
|
56
65
|
let consumesDcbLog = declaredSources.length === 0 || declaredSources.some(name => !(name in allEventTopics));
|
|
57
|
-
AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Translation.moduleUrl), "outbound", queryDbTableName, queryDbOutputs.resources, undefined, sourceTopics, consumesDcbLog);
|
|
66
|
+
AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Translation.moduleUrl), "outbound", queryDbTableName, queryDbOutputs.resources, undefined, sourceTopics, consumesDcbLog, Spec.targetName);
|
|
58
67
|
return ots;
|
|
59
68
|
};
|
|
60
69
|
return {
|
|
@@ -80,6 +89,7 @@ export {
|
|
|
80
89
|
EventCollectorChannel,
|
|
81
90
|
RuntimeEnvironment,
|
|
82
91
|
EventCollectorRuntimeBuilder,
|
|
92
|
+
DeployTimeCapabilities,
|
|
83
93
|
Make,
|
|
84
94
|
}
|
|
85
95
|
/* Component-ReventlessCore Not a pure module */
|
|
@@ -51,6 +51,32 @@ let registerCapabilityEnv = (name: string, value: Pulumi.Output.t<string>) =>
|
|
|
51
51
|
capability, so this returns `[]` rather than throwing. */
|
|
52
52
|
let capabilityEnv = () => capabilityEnvRef->Dict.toArray
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
The geocoding place index, when the platform provisioned one.
|
|
56
|
+
|
|
57
|
+
Separate from `capabilityEnv` because an env dict cannot express a *grant*: a
|
|
58
|
+
slice Lambda calling Amazon Location needs `geo:SearchPlaceIndexForText` on this
|
|
59
|
+
index, and its role is created later, in the slice runtime's finalizer. So the
|
|
60
|
+
name travels twice — once as a variable the runtime reads, once as the resource
|
|
61
|
+
its role must be allowed to touch.
|
|
62
|
+
|
|
63
|
+
A plain Output with `""` for "not provisioned", plus a separate bool for "was it
|
|
64
|
+
registered at all", rather than `ref<option<Pulumi.Output.t<_>>>`: that shape
|
|
65
|
+
compiles to `Option.getOr`, whose nested-option probe corrupts the Output proxy.
|
|
66
|
+
The bool is an ordinary value and is known synchronously, which is what the
|
|
67
|
+
finalizer needs in order to decide whether to attach a policy at all.
|
|
68
|
+
*/
|
|
69
|
+
let geocoderPlaceIndexRef: ref<Pulumi.Output.t<string>> = ref(Pulumi.Output.make(""))
|
|
70
|
+
let geocoderProvisionedRef = ref(false)
|
|
71
|
+
|
|
72
|
+
let registerGeocoderPlaceIndex = (indexName: Pulumi.Output.t<string>) => {
|
|
73
|
+
geocoderPlaceIndexRef := indexName
|
|
74
|
+
geocoderProvisionedRef := true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let geocoderPlaceIndex = () => geocoderPlaceIndexRef.contents
|
|
78
|
+
let geocoderProvisioned = () => geocoderProvisionedRef.contents
|
|
79
|
+
|
|
54
80
|
type sliceModulePaths = {
|
|
55
81
|
specPath: string,
|
|
56
82
|
behaviorPath: string,
|
|
@@ -44,6 +44,27 @@ function capabilityEnv() {
|
|
|
44
44
|
return Object.entries(capabilityEnvRef);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
let geocoderPlaceIndexRef = {
|
|
48
|
+
contents: Pulumi.output("")
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
let geocoderProvisionedRef = {
|
|
52
|
+
contents: false
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function registerGeocoderPlaceIndex(indexName) {
|
|
56
|
+
geocoderPlaceIndexRef.contents = indexName;
|
|
57
|
+
geocoderProvisionedRef.contents = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function geocoderPlaceIndex() {
|
|
61
|
+
return geocoderPlaceIndexRef.contents;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function geocoderProvisioned() {
|
|
65
|
+
return geocoderProvisionedRef.contents;
|
|
66
|
+
}
|
|
67
|
+
|
|
47
68
|
let dcbConfigRef = {
|
|
48
69
|
contents: {
|
|
49
70
|
pluginName: "",
|
|
@@ -506,6 +527,11 @@ export {
|
|
|
506
527
|
capabilityEnvRef,
|
|
507
528
|
registerCapabilityEnv,
|
|
508
529
|
capabilityEnv,
|
|
530
|
+
geocoderPlaceIndexRef,
|
|
531
|
+
geocoderProvisionedRef,
|
|
532
|
+
registerGeocoderPlaceIndex,
|
|
533
|
+
geocoderPlaceIndex,
|
|
534
|
+
geocoderProvisioned,
|
|
509
535
|
dcbConfigRef,
|
|
510
536
|
registeredSliceModulePaths,
|
|
511
537
|
registerStateChangeSliceSpec,
|